3297 lines
121 KiB
Rust
3297 lines
121 KiB
Rust
//! TBVM-Interpreter: zustandsbehaftete, vom Einbetter getriebene
|
|
//! Ausführung (Design: docs/tbvm-design.md).
|
|
//!
|
|
//! - Enge `match`-Schleife über dekodierte Instruktionen; Tick-Prüfung
|
|
//! (Breakpoints, Einzelschritt, Abbruch) nur an Anweisungsgrenzen
|
|
//! (`Stmt`-Instruktion) über ein einzelnes Flag-Wort.
|
|
//! - Fehlersemantik: Handler modulweit + Frame-lokal, Unwinding bis zum
|
|
//! Handler-Frame, `RESUME` über gemerkten Anweisungs-Offset; Fehler
|
|
//! während aktiver Behandlung ist fatal (keine Kaskaden).
|
|
//! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3.
|
|
|
|
use crate::bytecode::{CmpOp, CompiledModule, Instr};
|
|
use std::collections::{HashSet, VecDeque};
|
|
use std::rc::Rc;
|
|
use tb_runtime::builtins::{builtin_table, ids, RtState};
|
|
use tb_runtime::errors::RuntimeError;
|
|
use tb_runtime::host::{Ereignis, Host};
|
|
use tb_runtime::traps::Quelle;
|
|
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
|
|
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
|
|
|
|
#[path = "debugger.rs"]
|
|
mod debugger;
|
|
pub use debugger::*;
|
|
|
|
/// Warum die VM die Kontrolle abgibt.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum RunEvent {
|
|
/// `END`, `SYSTEM` oder Programmende.
|
|
Ended,
|
|
/// `STOP` — VM-Zustand bleibt fortsetzbar (IDE: CONT).
|
|
Stopped {
|
|
line: u32,
|
|
},
|
|
Breakpoint {
|
|
line: u32,
|
|
},
|
|
Stepped {
|
|
line: u32,
|
|
},
|
|
Interrupted {
|
|
line: u32,
|
|
},
|
|
Restart {
|
|
program: Option<String>,
|
|
line: Option<u32>,
|
|
},
|
|
/// Unbehandelter Laufzeitfehler.
|
|
Error {
|
|
code: u16,
|
|
line: u32,
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
/// Scheduling result, deliberately separate from debugger events.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum PollResult {
|
|
Yield,
|
|
Waiting { deadline: Option<u64> },
|
|
Event(RunEvent),
|
|
}
|
|
|
|
const F_STEP: u32 = 1;
|
|
const F_BREAK: u32 = 2;
|
|
const F_POLL: u32 = 4;
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Handler {
|
|
None,
|
|
Goto(u32),
|
|
ResumeNext,
|
|
}
|
|
|
|
struct Frame {
|
|
id: u64,
|
|
debug_line: Option<(u32, u32, usize)>,
|
|
proc: usize,
|
|
pc: usize,
|
|
locals_base: usize,
|
|
stack_base: usize,
|
|
gosub: Vec<usize>,
|
|
local_handler: Handler,
|
|
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
|
last_stmt_pc: usize,
|
|
line: u32,
|
|
source: u32,
|
|
column: u32,
|
|
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
|
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
|
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
|
/// beendet den Handler (design.md, D2).
|
|
trap: Option<Quelle>,
|
|
waiting_form: Option<u16>,
|
|
pending_show: Option<u16>,
|
|
form_event: Option<FormEventReturn>,
|
|
handler_start: bool,
|
|
eingabe: Option<KonsolenEingabe>,
|
|
sleep: Option<Option<u64>>,
|
|
}
|
|
|
|
struct KonsolenEingabe {
|
|
text: String,
|
|
/// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$.
|
|
rest: Option<usize>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Zustellpunkt {
|
|
Anweisung,
|
|
Kooperativ,
|
|
Konsole,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum FormEventReturn {
|
|
Normal,
|
|
Load,
|
|
Unload(u16),
|
|
}
|
|
|
|
pub struct Vm {
|
|
module: CompiledModule,
|
|
pub debug: Debugger,
|
|
globals: Vec<Value>,
|
|
locals: Vec<Value>,
|
|
stack: Vec<Value>,
|
|
frames: Vec<Frame>,
|
|
pub rt: RtState,
|
|
// Fehlerzustand
|
|
err: u16,
|
|
/// Wert von `ERL`: die Zeilennummer, in der der Fehler auftrat. Wird
|
|
/// **nur** beim Auslösen gesetzt — liefe sie mit `zeile_nr` mit, meldete
|
|
/// ein Handler auf einer nummerierten Zeile seine eigene Nummer.
|
|
erl: u32,
|
|
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
|
zeile_nr: u32,
|
|
module_handlers: Vec<Handler>,
|
|
in_handler: bool,
|
|
resume_pc: usize,
|
|
// Steuerung
|
|
flags: u32,
|
|
/// Zählt Anweisungsgrenzen für die regelmäßige Ereignisabholung.
|
|
tick_zaehler: u32,
|
|
breakpoints: HashSet<(u16, u32)>,
|
|
data_ptr: usize,
|
|
start_pc: Option<usize>,
|
|
pub forms: FormsModel,
|
|
waiting: Option<Option<u64>>,
|
|
dialog: Option<tb_ui::forms::Dialog>,
|
|
terminated: bool,
|
|
}
|
|
|
|
/// Quellenarten, wie der Codegenerator sie kodiert.
|
|
const ART_KEY: u8 = 0;
|
|
const ART_TIMER: u8 = 1;
|
|
const ART_UEVENT: u8 = 2;
|
|
const ART_SIGNAL: u8 = 3;
|
|
|
|
/// Art + Kennung → Quelle. Die Kennung kann ein Ausdruck sein, deshalb
|
|
/// wird der Wertebereich hier noch einmal geprüft (Fehler 5).
|
|
///
|
|
/// Das Zeitintervall gehört **nicht** hierher: `TIMER ON` trägt keine
|
|
/// Kennung, geprüft wird es nur an `ON TIMER(n&)`.
|
|
fn trap_quelle(art: u8, n: i32) -> Result<Quelle, RuntimeError> {
|
|
let ok = match art {
|
|
ART_KEY => matches!(n, 0..=25 | 30 | 31),
|
|
ART_SIGNAL => (1..=2).contains(&n),
|
|
_ => true,
|
|
};
|
|
if !ok {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
Ok(match art {
|
|
ART_KEY => Quelle::Key(n as u8),
|
|
ART_TIMER => Quelle::Timer,
|
|
ART_UEVENT => Quelle::UEvent,
|
|
_ => Quelle::Signal(n as u8),
|
|
})
|
|
}
|
|
|
|
enum Flow {
|
|
Normal,
|
|
Event(RunEvent),
|
|
}
|
|
|
|
impl Vm {
|
|
pub fn new(module: CompiledModule) -> Vm {
|
|
let globals = module
|
|
.globals_init
|
|
.iter()
|
|
.map(|t| default_value(t, &module.udts))
|
|
.collect();
|
|
let mut forms = FormsModel::new(module.objects.clone(), 80, 25);
|
|
for initial in &module.form_initial {
|
|
initial
|
|
.apply(&mut forms)
|
|
.expect("validierte Forms-Anfangsdaten");
|
|
}
|
|
if let Some(form) = module.startup_form {
|
|
forms.show(form, false).expect("validiertes Startformular");
|
|
}
|
|
let mut vm = Vm {
|
|
debug: Debugger::default(),
|
|
globals,
|
|
locals: Vec::new(),
|
|
stack: Vec::new(),
|
|
frames: Vec::new(),
|
|
rt: RtState::default(),
|
|
err: 0,
|
|
erl: 0,
|
|
zeile_nr: 0,
|
|
module_handlers: vec![Handler::None; module.modules.len()],
|
|
in_handler: false,
|
|
resume_pc: 0,
|
|
flags: 0,
|
|
tick_zaehler: 0,
|
|
breakpoints: HashSet::new(),
|
|
data_ptr: 0,
|
|
start_pc: None,
|
|
waiting: None,
|
|
dialog: None,
|
|
terminated: false,
|
|
forms,
|
|
module,
|
|
};
|
|
// ISAM leitet das Satzlayout aus dem Typ der `OPEN`-Anweisung ab und
|
|
// braucht dafür die UDT-Tabelle des Moduls.
|
|
vm.rt.isam.udts = vm.module.udts.clone();
|
|
vm.push_frame(0, 0);
|
|
vm
|
|
}
|
|
|
|
pub fn start_at_line(&mut self, line: u32) -> Result<(), RuntimeError> {
|
|
let target = self.module.procs[0]
|
|
.code
|
|
.iter()
|
|
.position(|instruction| matches!(instruction, Instr::SetErl(found) if *found == line))
|
|
.or_else(|| {
|
|
self.module.procs[0].code.iter().position(
|
|
|instruction| matches!(instruction, Instr::Stmt(found) if *found == line),
|
|
)
|
|
});
|
|
let target = target.ok_or(RuntimeError(8))?;
|
|
if self.module.procs[0]
|
|
.code
|
|
.iter()
|
|
.any(|instruction| matches!(instruction, Instr::Stmt(0)))
|
|
{
|
|
self.start_pc = Some(target);
|
|
} else {
|
|
self.frames[0].pc = target;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn push_frame(&mut self, proc: usize, argc: usize) {
|
|
// Argumente liegen zuoberst auf dem Stack (links → rechts).
|
|
let locals_base = self.locals.len();
|
|
let p = &self.module.procs[proc];
|
|
for init in &p.locals_init {
|
|
self.locals.push(default_value(init, &self.module.udts));
|
|
}
|
|
for i in (0..argc).rev() {
|
|
let v = self.stack.pop().unwrap_or(Value::Empty);
|
|
self.locals[locals_base + i] = v;
|
|
}
|
|
let stack_base = self.stack.len();
|
|
self.debug.next_frame += 1;
|
|
self.frames.push(Frame {
|
|
id: self.debug.next_frame,
|
|
debug_line: None,
|
|
source: 0,
|
|
column: 0,
|
|
proc,
|
|
pc: 0,
|
|
locals_base,
|
|
stack_base,
|
|
gosub: Vec::new(),
|
|
local_handler: Handler::None,
|
|
last_stmt_pc: 0,
|
|
line: 0,
|
|
trap: None,
|
|
waiting_form: None,
|
|
pending_show: None,
|
|
form_event: None,
|
|
handler_start: false,
|
|
eingabe: None,
|
|
sleep: None,
|
|
});
|
|
}
|
|
|
|
fn external_arg(&self, value: &Value) -> Result<Value, RuntimeError> {
|
|
match value {
|
|
Value::Ref(reference) => self.read_ref(reference),
|
|
value => Ok(value.clone()),
|
|
}
|
|
}
|
|
|
|
fn external_string(&self, value: &Value) -> Result<String, RuntimeError> {
|
|
match self.external_arg(value)? {
|
|
Value::Str(text) => Ok(text.to_string()),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn external_set(
|
|
&mut self,
|
|
args: &[Value],
|
|
index: usize,
|
|
value: Value,
|
|
) -> Result<(), RuntimeError> {
|
|
match args.get(index) {
|
|
Some(Value::Ref(reference)) => self.write_ref(reference, value),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn dialog_integer(&self, value: &Value) -> Result<i32, RuntimeError> {
|
|
match self.external_arg(value)? {
|
|
Value::Int(value) => Ok(value as i32),
|
|
Value::Lng(value) => Ok(value),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
|
|
std::mem::take(&mut self.rt.eingaben)
|
|
}
|
|
|
|
fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
|
|
for input in events {
|
|
match input.ereignis {
|
|
Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
|
|
event => self.rt.ereignis(event),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn forms_dialog(
|
|
&mut self,
|
|
id: u16,
|
|
args: &[Value],
|
|
host: &mut dyn Host,
|
|
) -> Result<Option<Value>, RuntimeError> {
|
|
if self.dialog.is_none() {
|
|
let text = self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
|
|
self.dialog = Some(match id {
|
|
ids::MSGBOX => {
|
|
let kind = args.get(1).map_or(Ok(0), |v| self.dialog_integer(v))?;
|
|
let title = args
|
|
.get(2)
|
|
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
|
|
tb_ui::forms::message_dialog(&self.rt.screen, &text, kind, &title)?
|
|
}
|
|
ids::INPUTBOX_S => {
|
|
let title = args
|
|
.get(1)
|
|
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
|
|
let initial = args
|
|
.get(2)
|
|
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
|
|
let position = match (args.get(3), args.get(4)) {
|
|
(None, None) => None,
|
|
(Some(x), Some(y)) => {
|
|
Some((self.dialog_integer(x)?, self.dialog_integer(y)?))
|
|
}
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
tb_ui::forms::input_dialog(&self.rt.screen, &text, &title, &initial, position)?
|
|
}
|
|
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
|
|
});
|
|
}
|
|
let mut queued = self.take_dialog_input();
|
|
let value = self
|
|
.dialog
|
|
.as_mut()
|
|
.unwrap()
|
|
.poll(&mut self.rt.screen, host, &mut queued);
|
|
self.restore_dialog_input(queued);
|
|
if let Some(value) = value {
|
|
self.dialog = None;
|
|
Ok(Some(match value {
|
|
tb_ui::forms::DialogValue::Number(n) => Value::Int(n),
|
|
tb_ui::forms::DialogValue::Text(s) => Value::Str(Rc::from(s)),
|
|
}))
|
|
} else {
|
|
self.waiting = Some(None);
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
fn external_dialog(
|
|
&mut self,
|
|
proc: usize,
|
|
argc: usize,
|
|
host: &mut dyn Host,
|
|
) -> Result<bool, RuntimeError> {
|
|
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
|
return Ok(false);
|
|
}
|
|
let name = self.module.procs[proc]
|
|
.name
|
|
.rsplit('!')
|
|
.next()
|
|
.unwrap()
|
|
.to_ascii_uppercase();
|
|
if !is_runtime_external(&name) {
|
|
return Ok(false);
|
|
}
|
|
let args = self.stack[self.stack.len().saturating_sub(argc)..].to_vec();
|
|
match name.as_str() {
|
|
"CMNDLGREGISTER" => self.external_set(&args, 0, Value::Int(-1))?,
|
|
"CMNDLGCLOSE" => {}
|
|
"ABOUT" => {
|
|
let dialog = vec![
|
|
self.external_arg(&args[0])?,
|
|
Value::Lng(0),
|
|
Value::Str(Rc::from("About")),
|
|
];
|
|
if self.forms_dialog(ids::MSGBOX, &dialog, host)?.is_none() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
"FILEOPEN" | "FILESAVE" => {
|
|
let file = self.external_string(&args[0])?;
|
|
let path = self.external_string(&args[1])?;
|
|
let title = if name == "FILEOPEN" {
|
|
"Open file"
|
|
} else {
|
|
"Save file"
|
|
};
|
|
let default = if path.is_empty() {
|
|
file
|
|
} else {
|
|
format!("{path}{sep}{file}", sep = std::path::MAIN_SEPARATOR)
|
|
};
|
|
let dialog = vec![
|
|
Value::Str(Rc::from(title)),
|
|
Value::Str(Rc::from(title)),
|
|
Value::Str(Rc::from(default)),
|
|
];
|
|
let Some(result) = self.forms_dialog(ids::INPUTBOX_S, &dialog, host)? else {
|
|
return Ok(true);
|
|
};
|
|
let Value::Str(result) = result else {
|
|
return Err(RuntimeError::TYPE_MISMATCH);
|
|
};
|
|
let result = result.to_string();
|
|
if result.is_empty() {
|
|
self.external_set(&args, 7, Value::Int(-1))?;
|
|
} else {
|
|
let (path, file) = result
|
|
.rsplit_once(['/', '\\'])
|
|
.map_or(("", result.as_str()), |(path, file)| (path, file));
|
|
self.external_set(&args, 0, Value::Str(Rc::from(file)))?;
|
|
self.external_set(&args, 1, Value::Str(Rc::from(path)))?;
|
|
self.external_set(&args, 7, Value::Int(0))?;
|
|
}
|
|
}
|
|
"FILEPRINT" => {
|
|
self.external_set(&args, 0, Value::Int(1))?;
|
|
self.external_set(&args, 3, Value::Int(0))?;
|
|
}
|
|
"FINDTEXT" | "CHANGETEXT" => {
|
|
let dialog = vec![
|
|
Value::Str(Rc::from(if name == "FINDTEXT" {
|
|
"Find text"
|
|
} else {
|
|
"Replacement text"
|
|
})),
|
|
Value::Str(Rc::from("Find")),
|
|
self.external_arg(&args[usize::from(name == "CHANGETEXT")])?,
|
|
];
|
|
let Some(result) = self.forms_dialog(ids::INPUTBOX_S, &dialog, host)? else {
|
|
return Ok(true);
|
|
};
|
|
let target = usize::from(name == "CHANGETEXT");
|
|
self.external_set(&args, target, result.clone())?;
|
|
let empty = matches!(&result, Value::Str(text) if text.is_empty());
|
|
self.external_set(&args, args.len() - 1, Value::Int(-i16::from(empty)))?;
|
|
}
|
|
"COLORPALETTE" => {
|
|
let color = self.external_arg(&args[0])?;
|
|
self.external_set(&args, 0, color)?;
|
|
self.external_set(&args, 3, Value::Int(0))?;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
self.stack.truncate(self.stack.len() - argc);
|
|
Ok(true)
|
|
}
|
|
|
|
pub fn queue_form_event(&mut self, event: FormEvent) {
|
|
self.forms.queue(event);
|
|
}
|
|
|
|
fn form_value(v: PropertyValue) -> Value {
|
|
match v {
|
|
PropertyValue::Integer(v) => Value::Int(v as i16),
|
|
PropertyValue::Single(v) => Value::Sng(v),
|
|
PropertyValue::String(v) => Value::Str(Rc::from(v)),
|
|
PropertyValue::Boolean(v) => Value::Int(if v { -1 } else { 0 }),
|
|
PropertyValue::Object(v) => v
|
|
.map(|(object, index)| Value::Obj(object, index))
|
|
.unwrap_or(Value::Empty),
|
|
PropertyValue::IntegerArray(_) => Value::Empty,
|
|
}
|
|
}
|
|
|
|
fn form_arg(v: Value) -> Result<PropertyValue, RuntimeError> {
|
|
Ok(match v {
|
|
Value::Int(v) => PropertyValue::Integer(v as i32),
|
|
Value::Lng(v) => PropertyValue::Integer(v),
|
|
Value::Sng(v) => PropertyValue::Single(v),
|
|
Value::Str(v) => PropertyValue::String(v.to_string()),
|
|
Value::Obj(object, index) => PropertyValue::Object(Some((object, index))),
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
})
|
|
}
|
|
|
|
fn property_value(
|
|
&self,
|
|
object: u16,
|
|
property: u16,
|
|
v: Value,
|
|
) -> Result<PropertyValue, RuntimeError> {
|
|
let class = self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.ok_or(RuntimeError(420))?
|
|
.class;
|
|
let spec = tb_frontend::forms::properties(class)
|
|
.get(property as usize)
|
|
.copied()
|
|
.ok_or(RuntimeError(422))?;
|
|
Ok(match (spec.ty, v) {
|
|
(tb_frontend::forms::PropertyType::Integer, Value::Int(v)) => {
|
|
PropertyValue::Integer(v as i32)
|
|
}
|
|
(tb_frontend::forms::PropertyType::Integer, Value::Lng(v)) => PropertyValue::Integer(v),
|
|
(tb_frontend::forms::PropertyType::Boolean, Value::Int(v)) => {
|
|
PropertyValue::Boolean(v != 0)
|
|
}
|
|
(tb_frontend::forms::PropertyType::Boolean, Value::Lng(v)) => {
|
|
PropertyValue::Boolean(v != 0)
|
|
}
|
|
(tb_frontend::forms::PropertyType::Single, Value::Sng(v)) => PropertyValue::Single(v),
|
|
(tb_frontend::forms::PropertyType::String, Value::Str(v)) => {
|
|
PropertyValue::String(v.to_string())
|
|
}
|
|
(tb_frontend::forms::PropertyType::Object, Value::Obj(object, index)) => {
|
|
PropertyValue::Object(Some((object, index)))
|
|
}
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
})
|
|
}
|
|
|
|
fn dispatch_form_event(&mut self, event: FormEvent, on_return: FormEventReturn) -> bool {
|
|
let Some(binding) = self
|
|
.module
|
|
.event_procs
|
|
.iter()
|
|
.find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name))
|
|
.cloned()
|
|
else {
|
|
return false;
|
|
};
|
|
if matches!(
|
|
event.name.as_str(),
|
|
"CLICK" | "DBLCLICK" | "GOTFOCUS" | "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN"
|
|
) && self
|
|
.module
|
|
.objects
|
|
.get(event.object as usize)
|
|
.is_some_and(|o| {
|
|
!matches!(
|
|
o.class,
|
|
tb_frontend::forms::ObjectClass::Form | tb_frontend::forms::ObjectClass::Screen
|
|
)
|
|
})
|
|
{
|
|
let _ = self
|
|
.forms
|
|
.set_active_control(event.object, event.array_index);
|
|
}
|
|
if let Some(index) = event.array_index {
|
|
self.push(Value::Int(index as i16));
|
|
}
|
|
for arg in event.args {
|
|
self.push(Self::form_value(arg));
|
|
}
|
|
self.push_frame(
|
|
binding.proc as usize,
|
|
self.module.procs[binding.proc as usize].n_params as usize,
|
|
);
|
|
if let Some(frame) = self.frames.last_mut() {
|
|
frame.form_event = Some(on_return);
|
|
frame.handler_start = true;
|
|
}
|
|
true
|
|
}
|
|
|
|
fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
|
|
let mut i = 0;
|
|
while let Some(event) = self.forms.events.get(i) {
|
|
if load_only && !event.name.eq_ignore_ascii_case("LOAD") {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
let laeuft = self
|
|
.module
|
|
.event_procs
|
|
.iter()
|
|
.find(|binding| {
|
|
binding.object == event.object
|
|
&& binding.event.eq_ignore_ascii_case(&event.name)
|
|
})
|
|
.is_some_and(|binding| {
|
|
self.frames.iter().any(|frame| {
|
|
frame.form_event.is_some() && frame.proc == binding.proc as usize
|
|
})
|
|
});
|
|
// Gesperrte Timer und laufende Prozeduren bleiben anstehend.
|
|
// Menübedienung und explizite modale Kooperation müssen trotzdem
|
|
// andere Handler erreichen können.
|
|
if laeuft || (self.forms.menu_is_open() && event.name.eq_ignore_ascii_case("TIMER")) {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
let event = self.forms.events.remove(i).unwrap();
|
|
if self.dispatch_form_event(
|
|
event,
|
|
if load_only {
|
|
FormEventReturn::Load
|
|
} else {
|
|
FormEventReturn::Normal
|
|
},
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
|
|
if self.frames.iter().any(|frame| {
|
|
matches!(frame.form_event,
|
|
Some(FormEventReturn::Unload(running)) if running == object)
|
|
}) {
|
|
return Ok(());
|
|
}
|
|
let event = FormEvent {
|
|
object,
|
|
array_index: None,
|
|
name: "UNLOAD".into(),
|
|
args: vec![PropertyValue::Integer(0)],
|
|
};
|
|
if !self.dispatch_form_event(event, FormEventReturn::Unload(object)) {
|
|
self.forms.unload_with(object, |_| {})?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn tick(&mut self, host: &mut dyn Host) {
|
|
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
|
self.forms.render(&mut self.rt.screen);
|
|
host.present(&self.rt.screen);
|
|
self.rt.pump(host, false);
|
|
if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
|
|
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
|
}
|
|
}
|
|
|
|
/// Ein gemeinsamer Zustellpfad für Statements, DOEVENTS, SLEEP und
|
|
/// blockierende Eingabe. Nur explizite Kooperation verschachtelt Forms.
|
|
fn zustellen(&mut self, host: &mut dyn Host, punkt: Zustellpunkt) -> bool {
|
|
let kooperativ = punkt != Zustellpunkt::Anweisung;
|
|
'zustellung: loop {
|
|
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
|
self.forms
|
|
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
|
if self.forms.next_deadline().is_some() {
|
|
self.forms.timers(host.jetzt_ms());
|
|
}
|
|
let forms_frei = kooperativ || !self.frames.iter().any(|f| f.form_event.is_some());
|
|
if forms_frei && self.dispatch_next_form_event(false) {
|
|
return true;
|
|
}
|
|
if self.rt.traps.aktiv() {
|
|
self.rt.tasten_traps_pruefen();
|
|
if (kooperativ || self.tick_zaehler.is_multiple_of(64))
|
|
&& self.rt.traps.naechste_frist().is_some()
|
|
{
|
|
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
|
}
|
|
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
|
self.trap_frame_aufsetzen(q, ziel);
|
|
return true;
|
|
}
|
|
}
|
|
// Unverbrauchte Konsolentasten bleiben liegen. Maus ohne Formular
|
|
// wird verworfen; mit Formular wird jede Eingabe samt ihrer Handler
|
|
// abgearbeitet, bevor die nächste Eingabe angenommen wird.
|
|
let mut i = 0;
|
|
while i < self.rt.eingaben.len() {
|
|
if !self.forms.menu_is_open() && self.rt.taste_trappen(i) {
|
|
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
|
self.trap_frame_aufsetzen(q, ziel);
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
if punkt == Zustellpunkt::Konsole
|
|
&& matches!(self.rt.eingaben[i].ereignis, Ereignis::Taste(..))
|
|
{
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if self.forms.active_form().is_some() {
|
|
if !forms_frei {
|
|
break;
|
|
}
|
|
match self.rt.eingaben.remove(i).unwrap().ereignis {
|
|
Ereignis::Taste(key, shift) => {
|
|
let menu_war_offen = self.forms.menu_is_open();
|
|
self.forms.handle_key(&key, shift);
|
|
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
|
if menu_war_offen && !self.forms.menu_is_open() {
|
|
continue 'zustellung;
|
|
}
|
|
}
|
|
Ereignis::Maus(m) => {
|
|
if m.zeile > 0
|
|
&& m.zeile <= self.rt.screen.rows()
|
|
&& m.spalte > 0
|
|
&& m.spalte <= self.rt.screen.cols()
|
|
{
|
|
let now = if self.forms.mouse_needs_time(m) {
|
|
host.jetzt_ms()
|
|
} else {
|
|
0
|
|
};
|
|
self.forms.handle_mouse_at(m, now);
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
if self.dispatch_next_form_event(false) {
|
|
return true;
|
|
}
|
|
} else if matches!(self.rt.eingaben[i].ereignis, Ereignis::Maus(_)) {
|
|
self.rt.eingaben.remove(i);
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
self.forms.render(&mut self.rt.screen);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
|
|
let deadline = deadline
|
|
.into_iter()
|
|
.chain(self.forms.next_deadline())
|
|
.chain(if self.forms.menu_is_open() {
|
|
None
|
|
} else {
|
|
self.rt.traps.naechste_frist()
|
|
})
|
|
.min();
|
|
if let Some(event) = host.warten(deadline) {
|
|
self.rt.ereignis(event);
|
|
}
|
|
}
|
|
|
|
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
|
|
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
|
|
/// Trap-Handler aufsetzen: ein Frame auf den Modulrumpf, der dessen
|
|
/// Locals mitbenutzt, mit eigenem `GOSUB`-Stapel und einer Rückmarke
|
|
/// auf die unterbrochene Stelle (design.md, D2).
|
|
fn trap_frame_aufsetzen(&mut self, q: Quelle, ziel: u32) {
|
|
let locals_base = self.frames[0].locals_base;
|
|
let stack_base = self.stack.len();
|
|
self.debug.next_frame += 1;
|
|
self.frames.push(Frame {
|
|
id: self.debug.next_frame,
|
|
debug_line: None,
|
|
source: 0,
|
|
column: 0,
|
|
proc: 0,
|
|
pc: ziel as usize,
|
|
locals_base,
|
|
stack_base,
|
|
gosub: Vec::new(),
|
|
local_handler: Handler::None,
|
|
last_stmt_pc: 0,
|
|
line: 0,
|
|
trap: Some(q),
|
|
waiting_form: None,
|
|
pending_show: None,
|
|
form_event: None,
|
|
handler_start: true,
|
|
eingabe: None,
|
|
sleep: None,
|
|
});
|
|
}
|
|
|
|
/// Trap-Frame abräumen. Anders als `pop_frame` bleiben die Locals
|
|
/// stehen — sie gehören dem Modulrumpf, nicht dem Handler.
|
|
fn trap_frame_abraeumen(&mut self) -> Option<Quelle> {
|
|
let f = self.frames.pop()?;
|
|
self.stack.truncate(f.stack_base);
|
|
f.trap
|
|
}
|
|
|
|
fn pop_frame(&mut self) {
|
|
let f = self.frames.pop().expect("Frame-Unterlauf");
|
|
if let Some(q) = f.trap {
|
|
self.rt.traps.handler_beendet(q);
|
|
} else {
|
|
self.locals.truncate(f.locals_base);
|
|
}
|
|
self.stack.truncate(f.stack_base);
|
|
}
|
|
|
|
// ---- Steuerung (Debugger/Runner) ----------------------------------------
|
|
|
|
pub fn set_step(&mut self, on: bool) {
|
|
if on {
|
|
self.flags |= F_STEP;
|
|
} else {
|
|
self.flags &= !F_STEP;
|
|
}
|
|
}
|
|
|
|
pub fn set_poll_interrupt(&mut self, on: bool) {
|
|
if on {
|
|
self.flags |= F_POLL;
|
|
} else {
|
|
self.flags &= !F_POLL;
|
|
}
|
|
}
|
|
|
|
pub fn add_breakpoint(&mut self, line: u32) {
|
|
self.breakpoints.insert((0, line));
|
|
self.flags |= F_BREAK;
|
|
}
|
|
|
|
pub fn remove_breakpoint(&mut self, line: u32) {
|
|
self.breakpoints.remove(&(0, line));
|
|
if self.breakpoints.is_empty() {
|
|
self.flags &= !F_BREAK;
|
|
}
|
|
}
|
|
|
|
pub fn add_module_breakpoint(&mut self, module: u16, line: u32) {
|
|
self.breakpoints.insert((module, line));
|
|
self.flags |= F_BREAK;
|
|
}
|
|
|
|
pub fn remove_module_breakpoint(&mut self, module: u16, line: u32) {
|
|
self.breakpoints.remove(&(module, line));
|
|
if self.breakpoints.is_empty() {
|
|
self.flags &= !F_BREAK;
|
|
}
|
|
}
|
|
|
|
pub fn current_source_pos(&self) -> tb_frontend::SourcePos {
|
|
self.frames
|
|
.last()
|
|
.map(|f| tb_frontend::SourcePos {
|
|
source: f.source,
|
|
line: f.line,
|
|
column: f.column,
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn current_module(&self) -> u16 {
|
|
self.module
|
|
.sources
|
|
.get(self.current_source_pos().source as usize)
|
|
.map_or(0, |s| s.module)
|
|
}
|
|
|
|
pub fn current_file(&self) -> &str {
|
|
self.module
|
|
.sources
|
|
.get(self.current_source_pos().source as usize)
|
|
.map_or(&self.module.name, |s| &s.path)
|
|
}
|
|
|
|
pub fn current_line(&self) -> u32 {
|
|
self.frames.last().map(|f| f.line).unwrap_or(0)
|
|
}
|
|
|
|
pub fn current_proc_name(&self) -> &str {
|
|
self.frames
|
|
.last()
|
|
.map(|f| self.module.procs[f.proc].name.as_str())
|
|
.unwrap_or("")
|
|
}
|
|
|
|
/// Variableninspektion: lokale Namen vor Modulvariablen, explizite
|
|
/// Typ-Suffixe vor einer eindeutigen Suche nach dem Basisnamen.
|
|
pub fn inspect(&self, name: &str) -> Option<Value> {
|
|
fn matches(stored: &str, requested: &str) -> bool {
|
|
let suffixes = ['%', '&', '!', '#', '$', '@'];
|
|
stored.eq_ignore_ascii_case(requested)
|
|
|| (!(stored.ends_with(suffixes) && requested.ends_with(suffixes))
|
|
&& stored
|
|
.trim_end_matches(suffixes)
|
|
.eq_ignore_ascii_case(requested.trim_end_matches(suffixes)))
|
|
}
|
|
fn unique(mut ids: impl Iterator<Item = usize>) -> Option<usize> {
|
|
let first = ids.next()?;
|
|
ids.next().is_none().then_some(first)
|
|
}
|
|
if let Some(f) = self.frames.last() {
|
|
let p = &self.module.procs[f.proc];
|
|
if let Some(i) = unique(
|
|
p.local_names
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, n)| matches(n, name))
|
|
.map(|(i, _)| i),
|
|
) {
|
|
return Some(self.deref_for_inspect(self.locals[f.locals_base + i].clone()));
|
|
}
|
|
}
|
|
let qualified = format!(
|
|
"{}!{name}",
|
|
self.module.modules[self.current_module() as usize].0
|
|
);
|
|
if let Some(id) = unique(
|
|
self.module
|
|
.global_names
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, n)| matches(n, name) || matches(n, &qualified))
|
|
.map(|(i, _)| i),
|
|
) {
|
|
return Some(self.globals[id].clone());
|
|
}
|
|
let id = unique(
|
|
self.module
|
|
.global_names
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, n)| {
|
|
matches(n.split_once('!').map_or(n.as_str(), |(_, name)| name), name)
|
|
})
|
|
.map(|(i, _)| i),
|
|
)?;
|
|
Some(self.globals[id].clone())
|
|
}
|
|
|
|
/// Arrayelement inspizieren.
|
|
pub fn inspect_element(&self, name: &str, idx: &[i32]) -> Option<Value> {
|
|
let v = self.inspect(name)?;
|
|
match v {
|
|
Value::Arr(a) => {
|
|
let a = a.borrow();
|
|
let flat = a.flat_index(idx).ok()?;
|
|
Some(a.data[flat as usize].clone())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// UDT-Feld inspizieren (Pfad über Feldindizes).
|
|
pub fn inspect_field(&self, name: &str, path: &[u16]) -> Option<Value> {
|
|
let mut v = self.inspect(name)?;
|
|
for f in path {
|
|
let Value::Rec(r) = v else { return None };
|
|
let r = r.borrow();
|
|
v = r.fields.get(*f as usize)?.clone();
|
|
}
|
|
Some(v)
|
|
}
|
|
|
|
fn deref_for_inspect(&self, v: Value) -> Value {
|
|
match v {
|
|
Value::Ref(r) => self.read_ref(&r).unwrap_or(Value::Empty),
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
// ---- Hauptschleife --------------------------------------------------------
|
|
|
|
pub fn is_terminated(&self) -> bool {
|
|
self.terminated
|
|
}
|
|
|
|
/// Hält modellose Formulare nach dem Ende des Modulrumpfs bedienbar.
|
|
/// Ereignisprozeduren laufen weiter auf derselben VM und können das
|
|
/// Formular schließen, ein anderes Programm starten oder einen Fehler
|
|
/// auslösen.
|
|
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
|
// Legacy explicit entry point: the caller deliberately starts a new
|
|
// forms pump, even after END. IDE/CLI only call it after natural end.
|
|
self.terminated = false;
|
|
loop {
|
|
match self.poll_visible_forms(host, usize::MAX) {
|
|
PollResult::Event(event) => return event,
|
|
PollResult::Waiting { deadline } => {
|
|
if (self.dialog.is_none() || self.rt.eingaben.is_empty())
|
|
&& !self.rt.abbruch
|
|
&& !self.rt.ende
|
|
{
|
|
self.warten(host, deadline);
|
|
}
|
|
}
|
|
PollResult::Yield => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
|
|
loop {
|
|
match self.poll(host, usize::MAX) {
|
|
PollResult::Event(e) => return e,
|
|
PollResult::Waiting { deadline } => {
|
|
if (self.dialog.is_none() || self.rt.eingaben.is_empty())
|
|
&& !self.rt.abbruch
|
|
&& !self.rt.ende
|
|
{
|
|
self.warten(host, deadline);
|
|
}
|
|
}
|
|
PollResult::Yield => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Polls modeless forms after the module body has ended.
|
|
pub fn poll_visible_forms(&mut self, host: &mut dyn Host, budget: usize) -> PollResult {
|
|
if self.terminated || (self.dialog.is_some() && self.rt.ende) {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
}
|
|
if self
|
|
.frames
|
|
.last()
|
|
.is_some_and(|f| f.pc < self.module.procs[f.proc].code.len())
|
|
{
|
|
return match self.poll(host, budget) {
|
|
PollResult::Event(RunEvent::Ended) if !self.terminated => PollResult::Yield,
|
|
result => result,
|
|
};
|
|
}
|
|
if !self.forms.has_visible_forms() {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
}
|
|
self.tick(host);
|
|
if self.rt.abbruch {
|
|
return PollResult::Event(RunEvent::Interrupted {
|
|
line: self.current_line(),
|
|
});
|
|
}
|
|
if self.zustellen(host, Zustellpunkt::Kooperativ) {
|
|
match self.poll(host, budget) {
|
|
PollResult::Event(RunEvent::Ended) if !self.terminated => PollResult::Yield,
|
|
result => result,
|
|
}
|
|
} else if self.rt.ende || !self.forms.has_visible_forms() {
|
|
PollResult::Event(RunEvent::Ended)
|
|
} else {
|
|
PollResult::Waiting {
|
|
deadline: self.wait_deadline(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wait_deadline(&self, deadline: Option<u64>) -> Option<u64> {
|
|
deadline
|
|
.into_iter()
|
|
.chain(self.forms.next_deadline())
|
|
.chain(if self.forms.menu_is_open() {
|
|
None
|
|
} else {
|
|
self.rt.traps.naechste_frist()
|
|
})
|
|
.min()
|
|
}
|
|
|
|
/// Executes at most `budget` instructions without entering a host wait.
|
|
pub fn poll(&mut self, host: &mut dyn Host, budget: usize) -> PollResult {
|
|
self.waiting = None;
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
for _ in 0..budget {
|
|
if self.dialog.is_some() && self.rt.ende {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
}
|
|
if self.rt.abbruch {
|
|
return PollResult::Event(RunEvent::Interrupted {
|
|
line: self.current_line(),
|
|
});
|
|
}
|
|
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
|
|
if !self.forms.is_visible(form) {
|
|
self.frames.last_mut().unwrap().waiting_form = None;
|
|
} else {
|
|
self.tick(host);
|
|
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
|
if !dispatched && self.rt.ende {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
}
|
|
if !dispatched {
|
|
return PollResult::Waiting {
|
|
deadline: self.wait_deadline(None),
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
if self.debug.enabled {
|
|
self.record_debug_execution();
|
|
}
|
|
let Some(frame) = self.frames.last_mut() else {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
};
|
|
let pc = frame.pc;
|
|
let Some(instr) = self.module.procs[frame.proc].code.get(pc).cloned() else {
|
|
return PollResult::Event(RunEvent::Ended);
|
|
};
|
|
if matches!(instr, Instr::RetProc | Instr::RetFn)
|
|
&& self
|
|
.debug
|
|
.immediate
|
|
.as_ref()
|
|
.is_some_and(|e| self.frames.len() == e.depth + 1)
|
|
{
|
|
self.finish_immediate();
|
|
return PollResult::Event(RunEvent::Stopped {
|
|
line: self.current_line(),
|
|
});
|
|
}
|
|
self.frames.last_mut().unwrap().pc = pc + 1;
|
|
match self.exec(instr, pc, host) {
|
|
Ok(Flow::Normal) => {}
|
|
Ok(Flow::Event(ev)) => return PollResult::Event(ev),
|
|
Err(e) => {
|
|
self.dialog = None;
|
|
self.waiting = None;
|
|
if let Some(f) = self.frames.last_mut() {
|
|
f.sleep = None;
|
|
}
|
|
if let Some(ev) = self.handle_error(e.0, host) {
|
|
if matches!(ev, RunEvent::Error { .. }) && self.immediate_active() {
|
|
self.debug.immediate_error = Some(format!("{ev:?}"));
|
|
self.finish_immediate();
|
|
return PollResult::Event(RunEvent::Stopped {
|
|
line: self.current_line(),
|
|
});
|
|
}
|
|
return PollResult::Event(ev);
|
|
}
|
|
}
|
|
}
|
|
if let Some(deadline) = self.waiting {
|
|
return PollResult::Waiting {
|
|
deadline: self.wait_deadline(deadline),
|
|
};
|
|
}
|
|
}
|
|
PollResult::Yield
|
|
}
|
|
|
|
// ---- Fehlerbehandlung -------------------------------------------------------
|
|
|
|
/// Fehler auslösen: Handler suchen, Stack abwickeln, springen.
|
|
/// `Some(event)` = unbehandelt (Programmabbruch).
|
|
fn handle_error(&mut self, code: u16, _host: &mut dyn Host) -> Option<RunEvent> {
|
|
let line = self.current_line();
|
|
let error_location = self.debug_location();
|
|
if self.in_handler {
|
|
// Fehler im Handler: fatal, keine Kaskade.
|
|
return Some(self.error_event(code, line));
|
|
}
|
|
// `ERL` friert auf der Zeile ein, in der es passiert ist.
|
|
self.erl = self.zeile_nr;
|
|
// Frame-Kette von innen nach außen nach LOCAL-Handlern absuchen.
|
|
let mut target: Option<(usize, Handler)> = None;
|
|
for (depth, f) in self.frames.iter().enumerate().rev() {
|
|
if f.local_handler != Handler::None {
|
|
target = Some((depth, f.local_handler));
|
|
break;
|
|
}
|
|
}
|
|
if target.is_none() {
|
|
for frame in self.frames.iter().rev() {
|
|
let module = self.module.sources[frame.source as usize].module as usize;
|
|
let handler = self.module_handlers[module];
|
|
if handler != Handler::None {
|
|
target = Some((0, handler));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let Some((mut depth, handler)) = target else {
|
|
return Some(self.error_event(code, line));
|
|
};
|
|
if let Some(entry) = &mut self.debug.immediate {
|
|
if depth < entry.depth {
|
|
let handler_proc = self.frames[depth].proc;
|
|
let handler_base = self.frames[depth].locals_base;
|
|
let f = &mut self.frames[entry.depth];
|
|
if handler == Handler::ResumeNext {
|
|
depth = entry.depth;
|
|
} else {
|
|
entry.handler_return = Some((f.last_stmt_pc, f.pc));
|
|
f.proc = handler_proc;
|
|
f.locals_base = handler_base;
|
|
depth = entry.depth;
|
|
}
|
|
}
|
|
}
|
|
// Unwinding bis zum Handler-Frame.
|
|
while self.frames.len() > depth + 1 {
|
|
self.pop_frame();
|
|
}
|
|
let (stack_base, last_stmt_pc, fproc) = {
|
|
let f = self.frames.last().unwrap();
|
|
(f.stack_base, f.last_stmt_pc, f.proc)
|
|
};
|
|
self.stack.truncate(stack_base);
|
|
self.frames.last_mut().unwrap().eingabe = None;
|
|
self.frames.last_mut().unwrap().sleep = None;
|
|
self.err = code;
|
|
match handler {
|
|
Handler::Goto(pc) => {
|
|
self.resume_pc = last_stmt_pc;
|
|
self.in_handler = true;
|
|
self.frames.last_mut().unwrap().pc = pc as usize;
|
|
}
|
|
Handler::ResumeNext => {
|
|
// Weiter mit der Anweisung nach der zuletzt begonnenen.
|
|
let next = self.next_stmt_pc(fproc, last_stmt_pc);
|
|
self.frames.last_mut().unwrap().pc = next;
|
|
}
|
|
Handler::None => unreachable!(),
|
|
}
|
|
if self.debug.break_errors {
|
|
self.debug.error = Some(DebugError {
|
|
code,
|
|
origin: error_location,
|
|
handler: self.next_debug_location(),
|
|
resume_pc: self.resume_pc,
|
|
});
|
|
self.debug.cancel_motion();
|
|
self.debug.error_pending = true;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn error_event(&self, code: u16, line: u32) -> RunEvent {
|
|
RunEvent::Error {
|
|
code,
|
|
line,
|
|
message: RuntimeError(code).message().to_string(),
|
|
}
|
|
}
|
|
|
|
/// Nächste Anweisungsgrenze nach `from` (Instruktionsindex).
|
|
fn next_stmt_pc(&self, proc: usize, from: usize) -> usize {
|
|
let code = &self.module.procs[proc].code;
|
|
let mut i = from + 1;
|
|
while i < code.len() {
|
|
if matches!(code[i], Instr::Stmt(_) | Instr::InitStmt(_)) {
|
|
return i;
|
|
}
|
|
i += 1;
|
|
}
|
|
code.len().saturating_sub(1) // läuft auf das Rumpfende (End/Ret)
|
|
}
|
|
|
|
// ---- Werte-Hilfen -------------------------------------------------------------
|
|
|
|
fn pop(&mut self) -> Result<Value, RuntimeError> {
|
|
self.stack.pop().ok_or(RuntimeError(51)) // Internal error
|
|
}
|
|
|
|
fn pop_i16(&mut self) -> Result<i16, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Int(v) => Ok(v),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_i32(&mut self) -> Result<i32, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Lng(v) => Ok(v),
|
|
Value::Int(v) => Ok(v as i32),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_f32(&mut self) -> Result<f32, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Sng(v) => Ok(v),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_f64(&mut self) -> Result<f64, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Dbl(v) => Ok(v),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_cur(&mut self) -> Result<i64, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Cur(v) => Ok(v),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_str(&mut self) -> Result<Rc<str>, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Str(s) => Ok(s),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_arr(&mut self) -> Result<Rc<std::cell::RefCell<ArrayObj>>, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Arr(a) => Ok(a),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_rec(&mut self) -> Result<Rc<std::cell::RefCell<RecordObj>>, RuntimeError> {
|
|
match self.pop()? {
|
|
Value::Rec(r) => Ok(r),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn pop_indices(&mut self, dims: u8) -> Result<Vec<i32>, RuntimeError> {
|
|
let mut idx = vec![0i32; dims as usize];
|
|
for i in (0..dims as usize).rev() {
|
|
idx[i] = self.pop_i32()?;
|
|
}
|
|
Ok(idx)
|
|
}
|
|
|
|
fn push(&mut self, v: Value) {
|
|
self.stack.push(v);
|
|
}
|
|
|
|
fn truthy(v: &Value) -> bool {
|
|
match v {
|
|
Value::Int(v) => *v != 0,
|
|
Value::Lng(v) => *v != 0,
|
|
Value::Sng(v) => *v != 0.0,
|
|
Value::Dbl(v) => *v != 0.0,
|
|
Value::Cur(v) => *v != 0,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn read_ref(&self, r: &VarRef) -> Result<Value, RuntimeError> {
|
|
Ok(match r {
|
|
VarRef::Global(i) => self.globals[*i as usize].clone(),
|
|
VarRef::Stack(i) => self.locals[*i as usize].clone(),
|
|
VarRef::Elem(a, flat) => a.borrow().data[*flat as usize].clone(),
|
|
VarRef::Field(rec, path) => {
|
|
let mut v = Value::Rec(rec.clone());
|
|
for f in path {
|
|
let Value::Rec(r) = v else {
|
|
return Err(RuntimeError::TYPE_MISMATCH);
|
|
};
|
|
let val = r.borrow().fields[*f as usize].clone();
|
|
v = val;
|
|
}
|
|
v
|
|
}
|
|
})
|
|
}
|
|
|
|
fn write_ref(&mut self, r: &VarRef, val: Value) -> Result<(), RuntimeError> {
|
|
match r {
|
|
VarRef::Global(i) => self.globals[*i as usize] = val,
|
|
VarRef::Stack(i) => self.locals[*i as usize] = val,
|
|
VarRef::Elem(a, flat) => a.borrow_mut().data[*flat as usize] = val,
|
|
VarRef::Field(rec, path) => {
|
|
let mut cur = rec.clone();
|
|
for f in &path[..path.len() - 1] {
|
|
let next = match &cur.borrow().fields[*f as usize] {
|
|
Value::Rec(r) => r.clone(),
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
cur = next;
|
|
}
|
|
cur.borrow_mut().fields[*path.last().unwrap() as usize] = val;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Array-Slot sicherstellen (Auto-DIM impliziter Arrays) und Handle liefern.
|
|
fn ensure_array(
|
|
&mut self,
|
|
global: bool,
|
|
slot: u16,
|
|
dims: u8,
|
|
elem: &TypeInit,
|
|
) -> Result<Rc<std::cell::RefCell<ArrayObj>>, RuntimeError> {
|
|
let frame_base = self.frames.last().map(|f| f.locals_base).unwrap_or(0);
|
|
let cell = if global {
|
|
&mut self.globals[slot as usize]
|
|
} else {
|
|
&mut self.locals[frame_base + slot as usize]
|
|
};
|
|
match cell {
|
|
Value::Arr(a) => Ok(a.clone()),
|
|
Value::Empty => {
|
|
let lo = self.module.modules[self.current_module() as usize].1 as i32;
|
|
let bounds = vec![(lo, 10); dims as usize];
|
|
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
|
let handle = Rc::new(std::cell::RefCell::new(arr));
|
|
let cell = if global {
|
|
&mut self.globals[slot as usize]
|
|
} else {
|
|
&mut self.locals[frame_base + slot as usize]
|
|
};
|
|
*cell = Value::Arr(handle.clone());
|
|
Ok(handle)
|
|
}
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
|
|
fn slot_value(&mut self, global: bool, slot: u16) -> &mut Value {
|
|
if global {
|
|
&mut self.globals[slot as usize]
|
|
} else {
|
|
let base = self.frames.last().map(|f| f.locals_base).unwrap_or(0);
|
|
&mut self.locals[base + slot as usize]
|
|
}
|
|
}
|
|
|
|
fn jump(&mut self, pc: u32) {
|
|
self.frames.last_mut().unwrap().pc = pc as usize;
|
|
}
|
|
|
|
// ---- Instruktionsausführung ------------------------------------------------
|
|
|
|
fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result<Flow, RuntimeError> {
|
|
use Instr as I;
|
|
match instr {
|
|
I::Source(_, _) => Ok(Flow::Normal),
|
|
I::Stmt(line) | I::InitStmt(line) => {
|
|
let initializing = matches!(instr, I::InitStmt(_));
|
|
if line == 0 {
|
|
if let Some(target) = self.start_pc.take() {
|
|
self.frames[0].pc = target;
|
|
}
|
|
}
|
|
let f = self.frames.last_mut().unwrap();
|
|
f.line = line;
|
|
if let Some(I::Source(source, column)) = pc
|
|
.checked_sub(1)
|
|
.and_then(|pc| self.module.procs[f.proc].code.get(pc))
|
|
{
|
|
f.source = *source;
|
|
f.column = *column;
|
|
}
|
|
f.last_stmt_pc = pc;
|
|
// Zustellpunkt: anzeigen, wenn sich der Bildschirm geändert
|
|
// hat, und regelmäßig Ereignisse abholen. Das ist keine
|
|
// Debugger-Funktion — ohne sie sähe niemand die Ausgabe und
|
|
// Größenänderungen kämen nie an.
|
|
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
|
self.forms.render(&mut self.rt.screen);
|
|
if !initializing
|
|
&& (self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024))
|
|
{
|
|
self.tick(host);
|
|
self.rt.screen.veraenderung_quittieren();
|
|
}
|
|
let erster_eintritt =
|
|
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
|
let caller = self.frames.len() - 1;
|
|
if !initializing
|
|
&& !erster_eintritt
|
|
&& !self.debug.error_pending
|
|
&& self.zustellen(host, Zustellpunkt::Anweisung)
|
|
{
|
|
if self.debug.enabled {
|
|
self.frames[caller].pc = pc;
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
if self.debug.enabled {
|
|
if let Some(event) = self.debug_boundary(pc, initializing) {
|
|
return Ok(Flow::Event(event));
|
|
}
|
|
}
|
|
if self.flags != 0 {
|
|
if self.flags & F_BREAK != 0
|
|
&& self.breakpoints.contains(&(self.current_module(), line))
|
|
{
|
|
self.debug.cancel_motion();
|
|
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
|
|
}
|
|
if self.flags & F_STEP != 0 && line > 0 {
|
|
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
|
}
|
|
if self.flags & F_POLL != 0 && self.rt.abbruch {
|
|
return Ok(Flow::Event(RunEvent::Interrupted { line }));
|
|
}
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
// ---- Ereignis-Traps (Sprachreferenz §8) ----
|
|
I::TrapDefine(art, ziel) => {
|
|
let n = self.pop_i32()?;
|
|
let q = trap_quelle(art, n)?;
|
|
if art == ART_TIMER {
|
|
if !(1..=86_400).contains(&n) {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
let jetzt = host.jetzt_ms();
|
|
self.rt.traps.timer_intervall(n as u64, jetzt);
|
|
}
|
|
self.rt.traps.definieren(q, Some(ziel));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::TrapDisable(art) => {
|
|
let n = self.pop_i32()?;
|
|
let q = trap_quelle(art, n)?;
|
|
self.rt.traps.definieren(q, None);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::TrapSet(art, zustand) => {
|
|
let n = self.pop_i32()?;
|
|
let q = trap_quelle(art, n)?;
|
|
let z = match zustand {
|
|
0 => tb_runtime::traps::Zustand::An,
|
|
1 => tb_runtime::traps::Zustand::Aus,
|
|
_ => tb_runtime::traps::Zustand::Gestoppt,
|
|
};
|
|
let jetzt = if art == ART_TIMER { host.jetzt_ms() } else { 0 };
|
|
self.rt.traps.setzen(q, z, jetzt);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Doevents => {
|
|
// Wert zuerst ablegen: ein hier zugestellter Handler
|
|
// bekommt seinen eigenen Stapelabschnitt darüber und
|
|
// lässt den Wert unberührt.
|
|
self.stack.push(Value::Int(0));
|
|
self.tick(host);
|
|
self.zustellen(host, Zustellpunkt::Kooperativ);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Sleep(mit_arg) => {
|
|
let caller = self.frames.len() - 1;
|
|
if self.frames[caller].sleep.is_none() {
|
|
let seconds = if mit_arg { self.pop_f64()? } else { 0.0 };
|
|
self.frames[caller].sleep = Some(
|
|
(seconds > 0.0)
|
|
.then(|| host.jetzt_ms().saturating_add((seconds * 1000.0) as u64)),
|
|
);
|
|
}
|
|
let deadline = self.frames[caller].sleep.unwrap();
|
|
self.tick(host);
|
|
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
|
if dispatched
|
|
|| self.rt.ende
|
|
|| deadline.is_some_and(|d| host.jetzt_ms() >= d)
|
|
|| (deadline.is_none() && self.rt.gepufferte_taste().is_some())
|
|
{
|
|
self.frames[caller].sleep = None;
|
|
} else {
|
|
self.frames[caller].pc = pc;
|
|
self.waiting = Some(deadline);
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::EventSwitch(an) => {
|
|
self.rt.traps.event(an);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::SetErl(n) => {
|
|
self.zeile_nr = n;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::End => Ok(Flow::Event(RunEvent::Ended)),
|
|
I::SystemInstr => {
|
|
self.terminated = true;
|
|
Ok(Flow::Event(RunEvent::Ended))
|
|
}
|
|
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped {
|
|
line: self.current_line(),
|
|
})),
|
|
I::Unsupported(_) => Err(RuntimeError(73)), // Advanced feature
|
|
|
|
I::PushInt(v) => {
|
|
self.push(Value::Int(v));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushLng(v) => {
|
|
self.push(Value::Lng(v));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushUdtId(id) => {
|
|
self.push(Value::Lng(id as i32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushSng(v) => {
|
|
self.push(Value::Sng(v));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushDbl(v) => {
|
|
self.push(Value::Dbl(v));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushCur(v) => {
|
|
self.push(Value::Cur(v));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushStr(i) => {
|
|
self.push(Value::Str(self.module.strings[i as usize].clone()));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadObjectProperty(object, property, has_index) => {
|
|
let index = if has_index {
|
|
Some(self.pop_i32()?)
|
|
} else {
|
|
None
|
|
};
|
|
if !self.forms.is_loaded_at(object, index) {
|
|
self.forms.ensure_loaded_at(object, index)?;
|
|
if index.is_none() && self.dispatch_next_form_event(true) {
|
|
let caller = self.frames.len() - 2;
|
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
|
return Ok(Flow::Normal);
|
|
}
|
|
}
|
|
let value = self.forms.get_at(object, index, property)?;
|
|
self.push(Self::form_value(value));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreObjectProperty(object, property, has_index) => {
|
|
let value = self.pop()?;
|
|
let index = if has_index {
|
|
Some(self.pop_i32()?)
|
|
} else {
|
|
None
|
|
};
|
|
if !self.forms.is_loaded_at(object, index) {
|
|
self.forms.ensure_loaded_at(object, index)?;
|
|
if index.is_none() {
|
|
self.push(value.clone());
|
|
}
|
|
if index.is_none() && self.dispatch_next_form_event(true) {
|
|
let caller = self.frames.len() - 2;
|
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
|
return Ok(Flow::Normal);
|
|
}
|
|
if index.is_none() {
|
|
let value = self.pop()?;
|
|
let value = self.property_value(object, property, value)?;
|
|
self.forms.set_at(object, index, property, value)?;
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
return Ok(Flow::Normal);
|
|
}
|
|
}
|
|
let value = self.property_value(object, property, value)?;
|
|
self.forms.set_at(object, index, property, value)?;
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PushObject(object, has_index) => {
|
|
let index = if has_index {
|
|
Some(self.pop_i32()?)
|
|
} else {
|
|
None
|
|
};
|
|
self.forms.ensure_loaded_at(object, index)?;
|
|
self.push(Value::Obj(object, index));
|
|
self.dispatch_next_form_event(true);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadDynamicObjectProperty(property) => {
|
|
let Value::Obj(object, index) = self.pop()? else {
|
|
return Err(RuntimeError::TYPE_MISMATCH);
|
|
};
|
|
let class = self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.ok_or(RuntimeError(420))?
|
|
.class;
|
|
let name = self
|
|
.module
|
|
.strings
|
|
.get(property as usize)
|
|
.ok_or(RuntimeError(422))?;
|
|
let property = tb_frontend::forms::property(class, name)
|
|
.map(|(property, _)| property)
|
|
.ok_or(RuntimeError(422))?;
|
|
let value = self.forms.get_at(object, index, property)?;
|
|
self.push(Self::form_value(value));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreDynamicObjectProperty(property) => {
|
|
let value = self.pop()?;
|
|
let Value::Obj(object, index) = self.pop()? else {
|
|
return Err(RuntimeError::TYPE_MISMATCH);
|
|
};
|
|
let class = self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.ok_or(RuntimeError(420))?
|
|
.class;
|
|
let name = self
|
|
.module
|
|
.strings
|
|
.get(property as usize)
|
|
.ok_or(RuntimeError(422))?;
|
|
let property = tb_frontend::forms::property(class, name)
|
|
.map(|(property, _)| property)
|
|
.ok_or(RuntimeError(422))?;
|
|
let value = self.property_value(object, property, value)?;
|
|
self.forms.set_at(object, index, property, value)?;
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadObjectIndexedProperty(object, property) => {
|
|
let index = self.pop_i32()?;
|
|
let object_index = if property & 0x8000 != 0 {
|
|
Some(self.pop_i32()?)
|
|
} else {
|
|
None
|
|
};
|
|
let value =
|
|
self.forms
|
|
.get_indexed_at(object, object_index, property & 0x7fff, index)?;
|
|
self.push(Self::form_value(value));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreObjectIndexedProperty(object, property) => {
|
|
let value = self.pop_i32()?;
|
|
let index = self.pop_i32()?;
|
|
let object_index = if property & 0x8000 != 0 {
|
|
Some(self.pop_i32()?)
|
|
} else {
|
|
None
|
|
};
|
|
self.forms
|
|
.set_indexed_at(object, object_index, property & 0x7fff, index, value)?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ObjectMethodFn(object, method, argc) => {
|
|
let indexed = argc & 0x80 != 0;
|
|
let argc = argc & 0x7f;
|
|
let class = self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.ok_or(RuntimeError(420))?
|
|
.class;
|
|
let name = *tb_frontend::forms::methods(class)
|
|
.get(method as usize)
|
|
.ok_or(RuntimeError(421))?;
|
|
let mut args = Vec::with_capacity(argc as usize);
|
|
for _ in 0..argc {
|
|
args.push(Self::form_arg(self.pop()?)?);
|
|
}
|
|
args.reverse();
|
|
let index = if indexed { Some(self.pop_i32()?) } else { None };
|
|
let value = self
|
|
.forms
|
|
.object_method_at(object, index, name, args)?
|
|
.ok_or(RuntimeError(421))?;
|
|
self.push(Self::form_value(value));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::TypeOf(class) => {
|
|
let matches = match self.pop()? {
|
|
Value::Obj(object, _) => self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.is_some_and(|o| o.class.id() == class),
|
|
_ => false,
|
|
};
|
|
self.push(Value::Int(if matches { -1 } else { 0 }));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ObjectMethod(object, method, argc) => {
|
|
let indexed = argc & 0x80 != 0;
|
|
let argc = argc & 0x7f;
|
|
let class = self
|
|
.module
|
|
.objects
|
|
.get(object as usize)
|
|
.ok_or(RuntimeError(420))?
|
|
.class;
|
|
let name = *tb_frontend::forms::methods(class)
|
|
.get(method as usize)
|
|
.ok_or(RuntimeError(421))?;
|
|
let resumed_show = self.frames.last().unwrap().pending_show == Some(object);
|
|
if resumed_show {
|
|
self.frames.last_mut().unwrap().pending_show = None;
|
|
} else if class == tb_frontend::forms::ObjectClass::Form
|
|
&& name == "SHOW"
|
|
&& !self.forms.is_loaded(object)
|
|
{
|
|
self.forms.ensure_loaded(object)?;
|
|
if self.dispatch_next_form_event(true) {
|
|
let caller = self.frames.len() - 2;
|
|
self.frames[caller].pending_show = Some(object);
|
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
|
return Ok(Flow::Normal);
|
|
}
|
|
}
|
|
let mut args = Vec::with_capacity(argc as usize);
|
|
for _ in 0..argc {
|
|
args.push(self.pop()?);
|
|
}
|
|
args.reverse();
|
|
let index = if indexed { Some(self.pop_i32()?) } else { None };
|
|
if resumed_show && !self.forms.is_loaded(object) {
|
|
return Ok(Flow::Normal);
|
|
}
|
|
match (class, name) {
|
|
(tb_frontend::forms::ObjectClass::Form, "SHOW") => {
|
|
let style = match args.first() {
|
|
None => 0,
|
|
Some(Value::Int(v)) => *v as i32,
|
|
Some(Value::Lng(v)) => *v,
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
if !matches!(style, 0 | 1) {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
if self.forms.show(object, style == 1)? == ShowResult::ModalWait {
|
|
self.frames.last_mut().unwrap().waiting_form = Some(object);
|
|
}
|
|
}
|
|
(tb_frontend::forms::ObjectClass::Form, "HIDE") => self.forms.hide(object)?,
|
|
(tb_frontend::forms::ObjectClass::Form, "LOAD") => {
|
|
self.forms.ensure_loaded(object)?
|
|
}
|
|
(tb_frontend::forms::ObjectClass::Form, "UNLOAD") => {
|
|
self.request_unload(object)?
|
|
}
|
|
(tb_frontend::forms::ObjectClass::Form, "PRINTFORM") => {
|
|
self.forms.render(&mut self.rt.screen);
|
|
self.rt
|
|
.print
|
|
.drucker
|
|
.push_str(&tb_runtime::snapshot::text(&self.rt.screen));
|
|
}
|
|
(tb_frontend::forms::ObjectClass::Screen, "SHOW") => {
|
|
self.forms.screen_show(true)
|
|
}
|
|
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
|
|
self.forms.screen_show(false)
|
|
}
|
|
_ => {
|
|
let args = args
|
|
.into_iter()
|
|
.map(Self::form_arg)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
self.forms.object_method_at(object, index, name, args)?;
|
|
}
|
|
}
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
self.dispatch_next_form_event(true);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ObjectLoad(object, unload, has_index) => {
|
|
if has_index {
|
|
let index = self.pop_i32()?;
|
|
if unload {
|
|
self.forms.unload_array(object, index)?;
|
|
} else {
|
|
self.forms.load_array(object, index)?;
|
|
}
|
|
} else if unload {
|
|
self.request_unload(object)?;
|
|
} else {
|
|
self.forms.ensure_loaded(object)?;
|
|
self.dispatch_next_form_event(true);
|
|
}
|
|
self.forms.sync_timers(|| host.jetzt_ms());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Dup => {
|
|
let v = self.stack.last().cloned().ok_or(RuntimeError(51))?;
|
|
self.push(v);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Pop => {
|
|
self.pop()?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
I::LoadGlobal(i) => {
|
|
self.push(self.globals[i as usize].clone());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreGlobal(i) => {
|
|
let v = self.pop()?;
|
|
self.globals[i as usize] = v;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadLocal(i) => {
|
|
let base = self.frames.last().unwrap().locals_base;
|
|
self.push(self.locals[base + i as usize].clone());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreLocal(i) => {
|
|
let v = self.pop()?;
|
|
let base = self.frames.last().unwrap().locals_base;
|
|
self.locals[base + i as usize] = v;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadRef(i) => {
|
|
let base = self.frames.last().unwrap().locals_base;
|
|
match &self.locals[base + i as usize] {
|
|
Value::Ref(r) => {
|
|
let r = r.clone();
|
|
let v = self.read_ref(&r)?;
|
|
self.push(v);
|
|
}
|
|
// BYVAL-übergebener Wert im selben Slot
|
|
other => {
|
|
let v = other.clone();
|
|
self.push(v);
|
|
}
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreRef(i) => {
|
|
let v = self.pop()?;
|
|
let base = self.frames.last().unwrap().locals_base;
|
|
match self.locals[base + i as usize].clone() {
|
|
Value::Ref(r) => self.write_ref(&r, v)?,
|
|
_ => self.locals[base + i as usize] = v,
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::MakeRefGlobal(i) => {
|
|
self.push(Value::Ref(VarRef::Global(i)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::MakeRefLocal(i) => {
|
|
let base = self.frames.last().unwrap().locals_base;
|
|
self.push(Value::Ref(VarRef::Stack((base + i as usize) as u32)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::MakeRefElem(dims) => {
|
|
let idx = self.pop_indices(dims)?;
|
|
let a = self.pop_arr()?;
|
|
let flat = a.borrow().flat_index(&idx)?;
|
|
self.push(Value::Ref(VarRef::Elem(a, flat)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::MakeRefField(f) => {
|
|
match self.pop()? {
|
|
Value::Rec(r) => self.push(Value::Ref(VarRef::Field(r, vec![f]))),
|
|
Value::Ref(VarRef::Field(r, mut path)) => {
|
|
path.push(f);
|
|
self.push(Value::Ref(VarRef::Field(r, path)));
|
|
}
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
I::LoadArr(global, slot, dims, elem) => {
|
|
let a = self.ensure_array(global, slot, dims, &elem)?;
|
|
self.push(Value::Arr(a));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadElem(dims) => {
|
|
let idx = self.pop_indices(dims)?;
|
|
let a = self.pop_arr()?;
|
|
let a = a.borrow();
|
|
let flat = a.flat_index(&idx)?;
|
|
self.push(a.data[flat as usize].clone());
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreElem(dims) => {
|
|
let v = self.pop()?;
|
|
let idx = self.pop_indices(dims)?;
|
|
let a = self.pop_arr()?;
|
|
let mut a = a.borrow_mut();
|
|
let flat = a.flat_index(&idx)?;
|
|
a.data[flat as usize] = v;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::DimArr(global, slot, dims, ref elem)
|
|
| I::CommonArr(global, slot, dims, ref elem) => {
|
|
let common = matches!(instr, I::CommonArr(..));
|
|
let bounds = self.pop_bounds(dims)?;
|
|
let cell = self.slot_value(global, slot);
|
|
if common {
|
|
if let Value::Arr(array) = cell {
|
|
let array = array.borrow();
|
|
return if &array.elem == elem && array.dims == bounds {
|
|
Ok(Flow::Normal)
|
|
} else {
|
|
Err(RuntimeError::TYPE_MISMATCH)
|
|
};
|
|
}
|
|
}
|
|
if !matches!(cell, Value::Empty) {
|
|
return Err(RuntimeError::DUPLICATE_DEFINITION);
|
|
}
|
|
let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?;
|
|
*self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::RedimArr(global, slot, dims, elem) => {
|
|
let bounds = self.pop_bounds(dims)?;
|
|
let arr = ArrayObj::new(elem, bounds, &self.module.udts)?;
|
|
let cell = self.slot_value(global, slot);
|
|
match cell {
|
|
Value::Arr(handle) => {
|
|
// In-place ersetzen: geteilte Handles (Parameter)
|
|
// sehen die neue Dimensionierung.
|
|
*handle.borrow_mut() = arr;
|
|
}
|
|
_ => *cell = Value::Arr(Rc::new(std::cell::RefCell::new(arr))),
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::EraseSlot(global, slot) => {
|
|
*self.slot_value(global, slot) = Value::Empty;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadField(f) => {
|
|
let r = self.pop_rec()?;
|
|
let v = r.borrow().fields[f as usize].clone();
|
|
self.push(v);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::StoreField(f) => {
|
|
let v = self.pop()?;
|
|
let r = self.pop_rec()?;
|
|
r.borrow_mut().fields[f as usize] = v;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CopyRec => {
|
|
let dst = self.pop_rec()?;
|
|
let src = self.pop_rec()?;
|
|
if !Rc::ptr_eq(&dst, &src) {
|
|
let copied: Vec<Value> = src.borrow().fields.iter().map(deep_copy).collect();
|
|
dst.borrow_mut().fields = copied;
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ArrBound(lower) => {
|
|
let dim = self.pop_i32()?;
|
|
let a = self.pop_arr()?;
|
|
let a = a.borrow();
|
|
if dim < 1 || dim as usize > a.dims.len() {
|
|
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
|
|
}
|
|
let (lo, hi) = a.dims[(dim - 1) as usize];
|
|
self.push(Value::Lng(if lower { lo } else { hi }));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::FixStr(n) => {
|
|
let s = self.pop_str()?;
|
|
let n = n as usize;
|
|
let len = s.chars().count();
|
|
let fixed: String = if len >= n {
|
|
s.chars().take(n).collect()
|
|
} else {
|
|
let mut t = s.to_string();
|
|
t.extend(std::iter::repeat_n(' ', n - len));
|
|
t
|
|
};
|
|
self.push(Value::Str(Rc::from(fixed.as_str())));
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
// ---- Arithmetik ----
|
|
I::AddI2 => self.bin_i16(|a, b| a.checked_add(b)),
|
|
I::AddI4 => self.bin_i32(|a, b| a.checked_add(b)),
|
|
I::AddR4 => self.bin_f32(|a, b| a + b),
|
|
I::AddR8 => self.bin_f64(|a, b| a + b),
|
|
I::AddCy => self.bin_cur(|a, b| a.checked_add(b)),
|
|
I::SubI2 => self.bin_i16(|a, b| a.checked_sub(b)),
|
|
I::SubI4 => self.bin_i32(|a, b| a.checked_sub(b)),
|
|
I::SubR4 => self.bin_f32(|a, b| a - b),
|
|
I::SubR8 => self.bin_f64(|a, b| a - b),
|
|
I::SubCy => self.bin_cur(|a, b| a.checked_sub(b)),
|
|
I::MulI2 => self.bin_i16(|a, b| a.checked_mul(b)),
|
|
I::MulI4 => self.bin_i32(|a, b| a.checked_mul(b)),
|
|
I::MulR4 => self.bin_f32(|a, b| a * b),
|
|
I::MulR8 => self.bin_f64(|a, b| a * b),
|
|
I::MulCy => {
|
|
let b = self.pop_cur()?;
|
|
let a = self.pop_cur()?;
|
|
let r = cur_mul(a, b)?;
|
|
self.push(Value::Cur(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NegI2 => {
|
|
let a = self.pop_i16()?;
|
|
self.push(Value::Int(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NegI4 => {
|
|
let a = self.pop_i32()?;
|
|
self.push(Value::Lng(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NegR4 => {
|
|
let a = self.pop_f32()?;
|
|
self.push(Value::Sng(-a));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NegR8 => {
|
|
let a = self.pop_f64()?;
|
|
self.push(Value::Dbl(-a));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NegCy => {
|
|
let a = self.pop_cur()?;
|
|
self.push(Value::Cur(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::DivR4 => {
|
|
let b = self.pop_f32()?;
|
|
let a = self.pop_f32()?;
|
|
if b == 0.0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
let r = a / b;
|
|
if r.is_infinite() {
|
|
return Err(RuntimeError::OVERFLOW);
|
|
}
|
|
self.push(Value::Sng(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::DivR8 => {
|
|
let b = self.pop_f64()?;
|
|
let a = self.pop_f64()?;
|
|
if b == 0.0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
let r = a / b;
|
|
if r.is_infinite() {
|
|
return Err(RuntimeError::OVERFLOW);
|
|
}
|
|
self.push(Value::Dbl(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::IDivI2 => {
|
|
let b = self.pop_i16()?;
|
|
let a = self.pop_i16()?;
|
|
if b == 0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
let r = a.checked_div(b).ok_or(RuntimeError::OVERFLOW)?;
|
|
self.push(Value::Int(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::IDivI4 => {
|
|
let b = self.pop_i32()?;
|
|
let a = self.pop_i32()?;
|
|
if b == 0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
let r = a.checked_div(b).ok_or(RuntimeError::OVERFLOW)?;
|
|
self.push(Value::Lng(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ModI2 => {
|
|
let b = self.pop_i16()?;
|
|
let a = self.pop_i16()?;
|
|
if b == 0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
// MIN MOD -1 ist mathematisch 0 (kein Überlauf).
|
|
let r = a.checked_rem(b).unwrap_or(0);
|
|
self.push(Value::Int(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ModI4 => {
|
|
let b = self.pop_i32()?;
|
|
let a = self.pop_i32()?;
|
|
if b == 0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
let r = a.checked_rem(b).unwrap_or(0);
|
|
self.push(Value::Lng(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::PowR8 => {
|
|
let b = self.pop_f64()?;
|
|
let a = self.pop_f64()?;
|
|
if a == 0.0 && b < 0.0 {
|
|
return Err(RuntimeError::DIVISION_BY_ZERO);
|
|
}
|
|
if a < 0.0 && b.fract() != 0.0 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
let r = a.powf(b);
|
|
if r.is_infinite() || r.is_nan() {
|
|
return Err(RuntimeError::OVERFLOW);
|
|
}
|
|
self.push(Value::Dbl(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Concat => {
|
|
let b = self.pop_str()?;
|
|
let a = self.pop_str()?;
|
|
if a.chars().count() + b.chars().count() > 32_767 {
|
|
return Err(RuntimeError::OUT_OF_STRING_SPACE);
|
|
}
|
|
let mut s = a.to_string();
|
|
s.push_str(&b);
|
|
self.push(Value::Str(Rc::from(s.as_str())));
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
// ---- Konvertierungen (Matrix) ----
|
|
I::ConvI2I4 => {
|
|
let v = self.pop_i16()?;
|
|
self.push(Value::Lng(v as i32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI2R4 => {
|
|
let v = self.pop_i16()?;
|
|
self.push(Value::Sng(v as f32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI2R8 => {
|
|
let v = self.pop_i16()?;
|
|
self.push(Value::Dbl(v as f64));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI2Cy => {
|
|
let v = self.pop_i16()?;
|
|
self.push(Value::Cur(v as i64 * 10_000));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI4I2 => {
|
|
let v = self.pop_i32()?;
|
|
self.push(Value::Int(value::i32_to_i16(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI4R4 => {
|
|
let v = self.pop_i32()?;
|
|
self.push(Value::Sng(v as f32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI4R8 => {
|
|
let v = self.pop_i32()?;
|
|
self.push(Value::Dbl(v as f64));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvI4Cy => {
|
|
let v = self.pop_i32()?;
|
|
self.push(Value::Cur(v as i64 * 10_000));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR4I2 => {
|
|
let v = self.pop_f32()?;
|
|
self.push(Value::Int(value::f64_to_i16(v as f64)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR4I4 => {
|
|
let v = self.pop_f32()?;
|
|
self.push(Value::Lng(value::f64_to_i32(v as f64)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR4R8 => {
|
|
let v = self.pop_f32()?;
|
|
self.push(Value::Dbl(v as f64));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR4Cy => {
|
|
let v = self.pop_f32()?;
|
|
self.push(Value::Cur(value::f64_to_cur(v as f64)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR8I2 => {
|
|
let v = self.pop_f64()?;
|
|
self.push(Value::Int(value::f64_to_i16(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR8I4 => {
|
|
let v = self.pop_f64()?;
|
|
self.push(Value::Lng(value::f64_to_i32(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR8R4 => {
|
|
let v = self.pop_f64()?;
|
|
self.push(Value::Sng(value::f64_to_f32(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvR8Cy => {
|
|
let v = self.pop_f64()?;
|
|
self.push(Value::Cur(value::f64_to_cur(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvCyI2 => {
|
|
let v = self.pop_cur()?;
|
|
self.push(Value::Int(value::cur_to_i16(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvCyI4 => {
|
|
let v = self.pop_cur()?;
|
|
self.push(Value::Lng(value::cur_to_i32(v)?));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvCyR4 => {
|
|
let v = self.pop_cur()?;
|
|
self.push(Value::Sng(value::cur_to_f64(v) as f32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::ConvCyR8 => {
|
|
let v = self.pop_cur()?;
|
|
self.push(Value::Dbl(value::cur_to_f64(v)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
// ---- Logik ----
|
|
I::NotI2 => {
|
|
let a = self.pop_i16()?;
|
|
self.push(Value::Int(!a));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::NotI4 => {
|
|
let a = self.pop_i32()?;
|
|
self.push(Value::Lng(!a));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::AndI2 => self.logic_i16(|a, b| a & b),
|
|
I::AndI4 => self.logic_i32(|a, b| a & b),
|
|
I::OrI2 => self.logic_i16(|a, b| a | b),
|
|
I::OrI4 => self.logic_i32(|a, b| a | b),
|
|
I::XorI2 => self.logic_i16(|a, b| a ^ b),
|
|
I::XorI4 => self.logic_i32(|a, b| a ^ b),
|
|
I::EqvI2 => self.logic_i16(|a, b| !(a ^ b)),
|
|
I::EqvI4 => self.logic_i32(|a, b| !(a ^ b)),
|
|
I::ImpI2 => self.logic_i16(|a, b| !a | b),
|
|
I::ImpI4 => self.logic_i32(|a, b| !a | b),
|
|
|
|
// ---- Vergleiche ----
|
|
I::CmpI2(op) => {
|
|
let b = self.pop_i16()?;
|
|
let a = self.pop_i16()?;
|
|
self.push(cmp_result(cmp_ord(a.cmp(&b), op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CmpI4(op) => {
|
|
let b = self.pop_i32()?;
|
|
let a = self.pop_i32()?;
|
|
self.push(cmp_result(cmp_ord(a.cmp(&b), op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CmpR4(op) => {
|
|
let b = self.pop_f32()?;
|
|
let a = self.pop_f32()?;
|
|
self.push(cmp_result(cmp_float(a as f64, b as f64, op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CmpR8(op) => {
|
|
let b = self.pop_f64()?;
|
|
let a = self.pop_f64()?;
|
|
self.push(cmp_result(cmp_float(a, b, op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CmpCy(op) => {
|
|
let b = self.pop_cur()?;
|
|
let a = self.pop_cur()?;
|
|
self.push(cmp_result(cmp_ord(a.cmp(&b), op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CmpStr(op) => {
|
|
let b = self.pop_str()?;
|
|
let a = self.pop_str()?;
|
|
self.push(cmp_result(cmp_ord(a.as_bytes().cmp(b.as_bytes()), op)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
// ---- Kontrollfluss ----
|
|
I::Jump(t) => {
|
|
self.jump(t);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::JumpIfFalse(t) => {
|
|
let v = self.pop()?;
|
|
if !Self::truthy(&v) {
|
|
self.jump(t);
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::JumpIfTrue(t) => {
|
|
let v = self.pop()?;
|
|
if Self::truthy(&v) {
|
|
self.jump(t);
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Gosub(t) => {
|
|
let f = self.frames.last_mut().unwrap();
|
|
let ret = f.pc;
|
|
f.gosub.push(ret);
|
|
f.pc = t as usize;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::RetGosub => {
|
|
let f = self.frames.last_mut().unwrap();
|
|
match f.gosub.pop() {
|
|
Some(ret) => {
|
|
f.pc = ret;
|
|
Ok(Flow::Normal)
|
|
}
|
|
// Leerer GOSUB-Stapel in einem Trap-Frame: das `RETURN`
|
|
// beendet den Handler und führt an der unterbrochenen
|
|
// Stelle weiter.
|
|
None if f.trap.is_some() => {
|
|
let q = self.trap_frame_abraeumen().unwrap();
|
|
self.rt.traps.handler_beendet(q);
|
|
Ok(Flow::Normal)
|
|
}
|
|
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
|
|
}
|
|
}
|
|
I::RetGosubTo(t) => {
|
|
let f = self.frames.last_mut().unwrap();
|
|
match f.gosub.pop() {
|
|
Some(_) => {
|
|
f.pc = t as usize;
|
|
Ok(Flow::Normal)
|
|
}
|
|
// `RETURN label` aus einem Trap-Handler: der Handler
|
|
// endet, die unterbrochene Aufrufkette wird verworfen
|
|
// und der Modulrumpf läuft am Label weiter. Das ist die
|
|
// klassische — und bewusst rabiate — Bedeutung.
|
|
None if f.trap.is_some() => {
|
|
let q = self.trap_frame_abraeumen().unwrap();
|
|
self.rt.traps.handler_beendet(q);
|
|
while self.frames.len() > 1 {
|
|
self.pop_frame();
|
|
}
|
|
let f = self.frames.last_mut().unwrap();
|
|
f.eingabe = None;
|
|
self.stack.truncate(f.stack_base);
|
|
f.pc = t as usize;
|
|
Ok(Flow::Normal)
|
|
}
|
|
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
|
|
}
|
|
}
|
|
I::OnJump(table, gosub) => {
|
|
let n = self.pop_i16()?;
|
|
if !(0..=255).contains(&n) {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
let t = &self.module.jump_tables[table as usize];
|
|
if n >= 1 && (n as usize) <= t.len() {
|
|
let target = t[(n - 1) as usize];
|
|
let f = self.frames.last_mut().unwrap();
|
|
if gosub {
|
|
let ret = f.pc;
|
|
f.gosub.push(ret);
|
|
}
|
|
f.pc = target as usize;
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Run(kind) => {
|
|
let (program, line) = match kind {
|
|
0 => (None, None),
|
|
1 => {
|
|
let line = self.pop_i32()?;
|
|
if line < 0 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
(None, Some(line as u32))
|
|
}
|
|
2 => (Some(self.pop_str()?.to_string()), None),
|
|
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
|
};
|
|
Ok(Flow::Event(RunEvent::Restart { program, line }))
|
|
}
|
|
|
|
// ---- Prozeduren ----
|
|
I::Call(proc, argc) => {
|
|
if self.external_dialog(proc as usize, argc as usize, host)? {
|
|
if self.waiting.is_some() {
|
|
self.frames.last_mut().unwrap().pc = pc;
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
self.push_frame(proc as usize, argc as usize);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::RetProc => {
|
|
let event = self.frames.last().and_then(|f| f.form_event);
|
|
let cancel = self
|
|
.frames
|
|
.last()
|
|
.and_then(|f| {
|
|
let base = f.locals_base;
|
|
match self.locals.get(base) {
|
|
Some(Value::Int(v)) => Some(*v),
|
|
_ => None,
|
|
}
|
|
})
|
|
.unwrap_or(0);
|
|
self.pop_frame();
|
|
if let Some(FormEventReturn::Unload(object)) = event {
|
|
self.forms.unload_with(object, |c| *c = cancel)?;
|
|
}
|
|
if matches!(event, Some(FormEventReturn::Normal))
|
|
&& !self.frames.iter().any(|f| f.form_event.is_some())
|
|
{
|
|
self.dispatch_next_form_event(false);
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::RetFn => {
|
|
let v = self.pop()?;
|
|
self.pop_frame();
|
|
self.push(v);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::CallBuiltin(id, argc) => {
|
|
if id == ids::RANDOMIZE && argc == 0 {
|
|
if self.frames.last().unwrap().eingabe.is_none() {
|
|
self.rt
|
|
.screen
|
|
.print("Random Number Seed (-32768 to 32767)? ");
|
|
}
|
|
let caller = self.frames.len() - 1;
|
|
if let Some(text) = self.konsoleneingabe(host, None)? {
|
|
self.rt.saat_setzen(tb_runtime::format::val(&text));
|
|
} else {
|
|
self.frames[caller].pc = pc;
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
if id == ids::INPUT_S && argc == 1 {
|
|
let n = match self.stack.last() {
|
|
Some(Value::Int(n)) => i32::from(*n),
|
|
Some(Value::Lng(n)) => *n,
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
if n < 0 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
let caller = self.frames.len() - 1;
|
|
if let Some(text) = self.konsoleneingabe(host, Some(n as usize))? {
|
|
self.pop()?;
|
|
self.push(Value::Str(Rc::from(text)));
|
|
} else {
|
|
self.frames[caller].pc = pc;
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
let mut args = Vec::with_capacity(argc as usize);
|
|
for _ in 0..argc {
|
|
args.push(self.pop()?);
|
|
}
|
|
args.reverse();
|
|
if matches!(id, ids::MSGBOX | ids::INPUTBOX_S) {
|
|
if let Some(value) = self.forms_dialog(id, &args, host)? {
|
|
self.push(value);
|
|
} else {
|
|
self.stack.extend(args);
|
|
self.frames.last_mut().unwrap().pc = pc;
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
if matches!(id, ids::SHELL_STMT | ids::SHELL_FN) {
|
|
let command = args
|
|
.first()
|
|
.map(|v| self.external_string(v))
|
|
.transpose()?
|
|
.unwrap_or_default();
|
|
if let Some(code) = host.shell(&command)? {
|
|
if id == ids::SHELL_FN {
|
|
self.push(Value::Lng(code));
|
|
}
|
|
} else {
|
|
self.stack.extend(args);
|
|
self.frames.last_mut().unwrap().pc = pc;
|
|
self.waiting = Some(None);
|
|
}
|
|
return Ok(Flow::Normal);
|
|
}
|
|
let f = builtin_table()[id as usize];
|
|
match f(&mut self.rt, host, &mut args) {
|
|
Ok(Some(v)) => {
|
|
self.push(v);
|
|
Ok(Flow::Normal)
|
|
}
|
|
Ok(None) => Ok(Flow::Normal),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
// ---- Fehlerbehandlung ----
|
|
I::OnErrorGoto(t) => {
|
|
let module = self.current_module() as usize;
|
|
self.module_handlers[module] = Handler::Goto(t);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::OnErrorLocal(t) => {
|
|
self.frames.last_mut().unwrap().local_handler = Handler::Goto(t);
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::OnErrorDisable => {
|
|
let module = self.current_module() as usize;
|
|
self.module_handlers[module] = Handler::None;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::OnErrorLocalDisable => {
|
|
self.frames.last_mut().unwrap().local_handler = Handler::None;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::OnErrorResumeNext(local) => {
|
|
if local {
|
|
self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext;
|
|
} else {
|
|
let module = self.current_module() as usize;
|
|
self.module_handlers[module] = Handler::ResumeNext;
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Resume0 => {
|
|
self.restore_debug_handler();
|
|
self.do_resume(|vm| vm.resume_pc)
|
|
}
|
|
I::ResumeNext => {
|
|
self.restore_debug_handler();
|
|
self.do_resume(|vm| {
|
|
let f = vm.frames.last().unwrap();
|
|
vm.next_stmt_pc(f.proc, vm.resume_pc)
|
|
})
|
|
}
|
|
I::ResumeLabel(t) => self.do_resume(move |_| t as usize),
|
|
I::RaiseError => {
|
|
let code = self.pop_i16()?;
|
|
if code < 1 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
Err(RuntimeError(code as u16))
|
|
}
|
|
I::LoadErr => {
|
|
self.push(Value::Lng(self.err as i32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LoadErl => {
|
|
self.push(Value::Lng(self.erl as i32));
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
// ---- DATA und Eingabe ----
|
|
I::ReadData(kind) => {
|
|
if self.data_ptr >= self.module.data.len() {
|
|
return Err(RuntimeError::OUT_OF_DATA);
|
|
}
|
|
let item = self.module.data[self.data_ptr].text.clone();
|
|
self.data_ptr += 1;
|
|
if kind == 0 {
|
|
self.push(Value::Str(Rc::from(item.as_str())));
|
|
} else {
|
|
let v = parse_data_number(&item)?;
|
|
self.push(Value::Dbl(v));
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Restore(idx) => {
|
|
self.data_ptr = idx as usize;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Input(argc, line_mode, prompt, question) => {
|
|
let caller = self.frames.len() - 1;
|
|
if !self.do_input(argc, line_mode, prompt, question, host)? {
|
|
self.frames[caller].pc = pc;
|
|
}
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::InputFile(argc, line_mode) => {
|
|
self.do_input_file(argc, line_mode)?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::GetPut(put, mit_nr, art, zusatz) => {
|
|
self.do_get_put(put, mit_nr, art, zusatz)?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::SetErr => {
|
|
let n = self.pop_i32()?;
|
|
if !(0..=32767).contains(&n) {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
self.err = n as u16;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::Field(anzahl) => {
|
|
self.do_field(anzahl)?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
I::LsetRset(rset) => {
|
|
self.do_lset_rset(rset)?;
|
|
Ok(Flow::Normal)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn pop_bounds(&mut self, dims: u8) -> Result<Vec<(i32, i32)>, RuntimeError> {
|
|
let mut flat = vec![0i32; dims as usize * 2];
|
|
for i in (0..flat.len()).rev() {
|
|
flat[i] = self.pop_i32()?;
|
|
}
|
|
Ok(flat.chunks(2).map(|c| (c[0], c[1])).collect())
|
|
}
|
|
|
|
fn do_resume(&mut self, target: impl FnOnce(&Vm) -> usize) -> Result<Flow, RuntimeError> {
|
|
if !self.in_handler {
|
|
return Err(RuntimeError(20)); // RESUME without error
|
|
}
|
|
self.in_handler = false;
|
|
self.err = 0;
|
|
let t = target(self);
|
|
self.frames.last_mut().unwrap().pc = t;
|
|
Ok(Flow::Normal)
|
|
}
|
|
|
|
/// Fortsetzbarer Leser: Text und Restlänge gehören zum unterbrochenen
|
|
/// Frame; Referenzen/Argumente bleiben bis zum Abschluss auf dessen Stack.
|
|
fn konsoleneingabe(
|
|
&mut self,
|
|
host: &mut dyn Host,
|
|
rest: Option<usize>,
|
|
) -> Result<Option<String>, RuntimeError> {
|
|
let caller = self.frames.len() - 1;
|
|
self.frames[caller]
|
|
.eingabe
|
|
.get_or_insert_with(|| KonsolenEingabe {
|
|
text: String::new(),
|
|
rest,
|
|
});
|
|
for _ in 0..64 {
|
|
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
|
|
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
|
}
|
|
self.tick(host);
|
|
if self.rt.abbruch {
|
|
return Ok(None);
|
|
}
|
|
if self.zustellen(host, Zustellpunkt::Konsole) {
|
|
return Ok(None);
|
|
}
|
|
if let Some(taste) = self.rt.gepufferte_taste() {
|
|
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
|
|
if let Some(rest) = &mut eingabe.rest {
|
|
let taste = self.rt.tastenanteil(taste, *rest);
|
|
*rest -= taste.chars().count();
|
|
eingabe.text.push_str(&taste);
|
|
} else {
|
|
match taste.as_str() {
|
|
tb_runtime::host::taste::ENTER => {
|
|
self.rt.screen.print("\n");
|
|
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
|
}
|
|
tb_runtime::host::taste::BACKSPACE => {
|
|
if eingabe.text.pop().is_some() {
|
|
self.rt.screen.rueckschritt();
|
|
}
|
|
}
|
|
_ => {
|
|
if taste.chars().count() == 1
|
|
&& taste.chars().next().is_some_and(|c| c as u32 >= 0x20)
|
|
{
|
|
eingabe.text.push_str(&taste);
|
|
self.rt.screen.print(&taste);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if self.rt.ende {
|
|
self.frames[caller].eingabe = None;
|
|
return Err(RuntimeError(62));
|
|
}
|
|
self.waiting = Some(None);
|
|
return Ok(None);
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
fn do_input(
|
|
&mut self,
|
|
argc: u8,
|
|
line_mode: bool,
|
|
prompt: u16,
|
|
question: bool,
|
|
host: &mut dyn Host,
|
|
) -> Result<bool, RuntimeError> {
|
|
let start = self
|
|
.stack
|
|
.len()
|
|
.checked_sub(argc as usize)
|
|
.ok_or(RuntimeError(51))?;
|
|
let refs = self.stack[start..]
|
|
.iter()
|
|
.map(|v| match v {
|
|
Value::Ref(r) => Ok(r.clone()),
|
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
loop {
|
|
if self.frames.last().unwrap().eingabe.is_none() {
|
|
if prompt != 0xFFFF {
|
|
self.rt.screen.print(&self.module.strings[prompt as usize]);
|
|
}
|
|
if question {
|
|
self.rt.screen.print("? ");
|
|
}
|
|
}
|
|
let Some(line) = self.konsoleneingabe(host, None)? else {
|
|
return Ok(false);
|
|
};
|
|
if line_mode {
|
|
let r = refs.first().ok_or(RuntimeError(51))?;
|
|
self.write_ref(r, Value::Str(Rc::from(line)))?;
|
|
self.stack.truncate(start);
|
|
return Ok(true);
|
|
}
|
|
// Felder trennen (Quotes respektieren).
|
|
let fields = split_input_fields(&line);
|
|
if fields.len() != refs.len() {
|
|
self.rt.screen.print("Redo from start\n");
|
|
continue;
|
|
}
|
|
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
|
|
let mut vals = Vec::with_capacity(refs.len());
|
|
let mut ok = true;
|
|
for (r, text) in refs.iter().zip(&fields) {
|
|
let target = self.read_ref(r)?;
|
|
match input_value(&target, text) {
|
|
Some(v) => vals.push(v),
|
|
None => {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if !ok {
|
|
self.rt.screen.print("Redo from start\n");
|
|
continue;
|
|
}
|
|
for (r, v) in refs.iter().zip(vals) {
|
|
self.write_ref(r, v)?;
|
|
}
|
|
self.stack.truncate(self.stack.len() - argc as usize);
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
/// `FIELD #n, laenge AS var$, …` — Recordpuffer aufteilen. Die
|
|
/// Feldvariablen werden gemerkt: `GET` frischt sie aus dem Puffer auf,
|
|
/// `LSET`/`RSET` schreiben zugleich in den Puffer.
|
|
fn do_field(&mut self, anzahl: u8) -> Result<(), RuntimeError> {
|
|
let mut paare = Vec::with_capacity(anzahl as usize);
|
|
for _ in 0..anzahl {
|
|
let r = match self.pop()? {
|
|
Value::Ref(r) => r,
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
let laenge = self.pop_i32()?;
|
|
paare.push((laenge, r));
|
|
}
|
|
paare.reverse();
|
|
let nummer = self.pop_i32()?;
|
|
let datei = self.rt.dateien.get(nummer)?;
|
|
datei.felder.clear();
|
|
let mut start = 0usize;
|
|
for (laenge, r) in paare {
|
|
if laenge < 0 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
let n = laenge as usize;
|
|
if start + n * tb_runtime::fileio::UTF32_BREITE > datei.puffer.len() {
|
|
return Err(RuntimeError(50)); // FIELD overflow
|
|
}
|
|
datei.felder.push(tb_runtime::fileio::Feld {
|
|
referenz: r,
|
|
start,
|
|
laenge: n,
|
|
});
|
|
start += n * tb_runtime::fileio::UTF32_BREITE;
|
|
}
|
|
// Feldvariablen sofort aus dem Puffer belegen.
|
|
self.felder_auffrischen(nummer)
|
|
}
|
|
|
|
/// Feldvariablen einer Datei aus dem Recordpuffer neu belegen.
|
|
fn felder_auffrischen(&mut self, nummer: i32) -> Result<(), RuntimeError> {
|
|
let datei = self.rt.dateien.get(nummer)?;
|
|
let werte: Vec<(VarRef, String)> = datei
|
|
.felder
|
|
.iter()
|
|
.map(|f| {
|
|
(
|
|
f.referenz.clone(),
|
|
tb_runtime::fileio::feld_lesen(&datei.puffer, f.start, f.laenge),
|
|
)
|
|
})
|
|
.collect();
|
|
for (r, t) in werte {
|
|
self.write_ref(&r, Value::Str(Rc::from(t.as_str())))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `LSET`/`RSET` — bündig zuweisen; bei einem `FIELD`-Feld zusätzlich in
|
|
/// den Recordpuffer schreiben.
|
|
fn do_lset_rset(&mut self, rset: bool) -> Result<(), RuntimeError> {
|
|
let wert = self.pop()?;
|
|
let ziel = match self.pop()? {
|
|
Value::Ref(r) => r,
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
let Value::Str(text) = &wert else {
|
|
// UDT-Zuweisung: unverändert durchreichen (Vorbild).
|
|
self.write_ref(&ziel, wert)?;
|
|
return Ok(());
|
|
};
|
|
|
|
// Gehört das Ziel zu einem FIELD-Puffer?
|
|
let treffer = self.rt.dateien.feld_finden(&ziel);
|
|
match treffer {
|
|
Some((nummer, start, laenge)) => {
|
|
let datei = self.rt.dateien.get(nummer)?;
|
|
tb_runtime::fileio::feld_setzen(&mut datei.puffer, start, laenge, text, rset);
|
|
let neu = tb_runtime::fileio::feld_lesen(&datei.puffer, start, laenge);
|
|
self.write_ref(&ziel, Value::Str(Rc::from(neu.as_str())))?;
|
|
}
|
|
None => {
|
|
// Ohne Feldbindung wirkt LSET/RSET auf die Stringlänge des
|
|
// bisherigen Werts (Vorbild).
|
|
let alt = match self.read_ref(&ziel)? {
|
|
Value::Str(s) => s.chars().count(),
|
|
_ => text.chars().count(),
|
|
};
|
|
let mut puffer = vec![0u8; alt * tb_runtime::fileio::UTF32_BREITE];
|
|
tb_runtime::fileio::feld_setzen(&mut puffer, 0, alt, text, rset);
|
|
let neu = tb_runtime::fileio::feld_lesen(&puffer, 0, alt);
|
|
self.write_ref(&ziel, Value::Str(Rc::from(neu.as_str())))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `GET`/`PUT` — Datensatz (RANDOM) bzw. Bytes (BINARY) übertragen.
|
|
fn do_get_put(
|
|
&mut self,
|
|
put: bool,
|
|
mit_nr: bool,
|
|
art: u8,
|
|
zusatz: u16,
|
|
) -> Result<(), RuntimeError> {
|
|
use tb_runtime::fileio as fio;
|
|
let ziel = if art != 0 {
|
|
match self.pop()? {
|
|
Value::Ref(r) => Some(r),
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let nr = if mit_nr { Some(self.pop_i32()?) } else { None };
|
|
let nummer = self.pop_i32()?;
|
|
|
|
let udts = self.module.udts.clone();
|
|
let datei = self.rt.dateien.get(nummer)?;
|
|
let binaer = datei.modus == fio::Modus::Binary;
|
|
// Ohne Nummer gilt die Position nach der letzten Operation.
|
|
let pos = match nr {
|
|
Some(n) if n >= 1 => n as u64,
|
|
Some(_) => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
|
None => datei.position,
|
|
};
|
|
|
|
// Typ der Zielvariablen bestimmt die übertragene Länge.
|
|
let typ = match art {
|
|
0 => None,
|
|
1 => Some(TypeInit::Int),
|
|
2 => Some(TypeInit::Lng),
|
|
3 => Some(TypeInit::Sng),
|
|
4 => Some(TypeInit::Dbl),
|
|
5 => Some(TypeInit::Cur),
|
|
6 => Some(TypeInit::FixedStr(zusatz as u32)),
|
|
7 => Some(TypeInit::Udt(zusatz)),
|
|
// Variabler String: aktuelle Zeichenzahl bestimmt die Länge.
|
|
_ => {
|
|
let laenge = match self.read_ref(ziel.as_ref().unwrap())? {
|
|
Value::Str(s) => s.chars().count() as u32,
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
Some(TypeInit::FixedStr(laenge))
|
|
}
|
|
};
|
|
|
|
if binaer {
|
|
let Some(t) = typ else {
|
|
// Ohne Variable überträgt BINARY nichts.
|
|
return Ok(());
|
|
};
|
|
let n = fio::breite(&t, &udts).ok_or(RuntimeError::TYPE_MISMATCH)?;
|
|
if put {
|
|
let wert = self.read_ref(ziel.as_ref().unwrap())?;
|
|
let mut puffer = vec![0u8; n];
|
|
fio::wert_schreiben(&mut puffer, 0, &wert, &t, &udts)?;
|
|
self.rt.dateien.get(nummer)?.bytes_schreiben(pos, &puffer)?;
|
|
} else {
|
|
let puffer = self.rt.dateien.get(nummer)?.bytes_lesen(pos, n)?;
|
|
let (v, _) = fio::wert_lesen(&puffer, 0, &t, &udts)?;
|
|
self.write_ref(ziel.as_ref().unwrap(), v)?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
// RANDOM: über den Recordpuffer.
|
|
if put {
|
|
if let (Some(r), Some(t)) = (&ziel, &typ) {
|
|
let wert = self.read_ref(r)?;
|
|
let datei = self.rt.dateien.get(nummer)?;
|
|
let mut puffer = std::mem::take(&mut datei.puffer);
|
|
puffer.iter_mut().for_each(|b| *b = b' ');
|
|
let ergebnis = fio::wert_schreiben(&mut puffer, 0, &wert, t, &udts);
|
|
self.rt.dateien.get(nummer)?.puffer = puffer;
|
|
ergebnis?;
|
|
}
|
|
self.rt.dateien.get(nummer)?.record_schreiben(pos)?;
|
|
} else {
|
|
self.rt.dateien.get(nummer)?.record_lesen(pos)?;
|
|
self.felder_auffrischen(nummer)?;
|
|
if let (Some(r), Some(t)) = (&ziel, &typ) {
|
|
let puffer = self.rt.dateien.get(nummer)?.puffer.clone();
|
|
let (v, _) = fio::wert_lesen(&puffer, 0, t, &udts)?;
|
|
self.write_ref(r, v)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// `INPUT #n` / `LINE INPUT #n` — Werte aus einer sequenziellen Datei.
|
|
fn do_input_file(&mut self, argc: u8, line_mode: bool) -> Result<(), RuntimeError> {
|
|
let mut refs = Vec::with_capacity(argc as usize);
|
|
for _ in 0..argc {
|
|
match self.pop()? {
|
|
Value::Ref(r) => refs.push(r),
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
}
|
|
refs.reverse();
|
|
let nummer = self.pop_i32()?;
|
|
|
|
if line_mode {
|
|
let zeile = self
|
|
.rt
|
|
.dateien
|
|
.get(nummer)?
|
|
.zeile_lesen()?
|
|
.ok_or(RuntimeError(62))?;
|
|
let r = refs.first().ok_or(RuntimeError(51))?;
|
|
self.write_ref(r, Value::Str(Rc::from(zeile.as_str())))?;
|
|
return Ok(());
|
|
}
|
|
for r in &refs {
|
|
let text = self.rt.dateien.get(nummer)?.feld_lesen()?;
|
|
let ziel = self.read_ref(r)?;
|
|
// Der Feldleser hat Quotes und äußeren Leerraum bereits ausgewertet.
|
|
let wert = if matches!(ziel, Value::Str(_)) {
|
|
Value::Str(Rc::from(text))
|
|
} else {
|
|
input_value(&ziel, &text).ok_or(RuntimeError(64))?
|
|
};
|
|
self.write_ref(r, wert)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Typisierte Binäroperationen mit Überlaufprüfung.
|
|
fn bin_i16(&mut self, f: impl Fn(i16, i16) -> Option<i16>) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_i16()?;
|
|
let a = self.pop_i16()?;
|
|
let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?;
|
|
self.push(Value::Int(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn bin_i32(&mut self, f: impl Fn(i32, i32) -> Option<i32>) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_i32()?;
|
|
let a = self.pop_i32()?;
|
|
let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?;
|
|
self.push(Value::Lng(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn bin_f32(&mut self, f: impl Fn(f32, f32) -> f32) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_f32()?;
|
|
let a = self.pop_f32()?;
|
|
let r = f(a, b);
|
|
if r.is_infinite() {
|
|
return Err(RuntimeError::OVERFLOW);
|
|
}
|
|
self.push(Value::Sng(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn bin_f64(&mut self, f: impl Fn(f64, f64) -> f64) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_f64()?;
|
|
let a = self.pop_f64()?;
|
|
let r = f(a, b);
|
|
if r.is_infinite() {
|
|
return Err(RuntimeError::OVERFLOW);
|
|
}
|
|
self.push(Value::Dbl(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn bin_cur(&mut self, f: impl Fn(i64, i64) -> Option<i64>) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_cur()?;
|
|
let a = self.pop_cur()?;
|
|
let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?;
|
|
self.push(Value::Cur(r));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn logic_i16(&mut self, f: impl Fn(i16, i16) -> i16) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_i16()?;
|
|
let a = self.pop_i16()?;
|
|
self.push(Value::Int(f(a, b)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
fn logic_i32(&mut self, f: impl Fn(i32, i32) -> i32) -> Result<Flow, RuntimeError> {
|
|
let b = self.pop_i32()?;
|
|
let a = self.pop_i32()?;
|
|
self.push(Value::Lng(f(a, b)));
|
|
Ok(Flow::Normal)
|
|
}
|
|
}
|
|
|
|
/// CURRENCY-Multiplikation: exakt in i128, Skalierung ÷10 000 mit
|
|
/// Banker's Rounding.
|
|
fn cur_mul(a: i64, b: i64) -> Result<i64, RuntimeError> {
|
|
let p = a as i128 * b as i128;
|
|
let q = p.div_euclid(10_000);
|
|
let r = p.rem_euclid(10_000);
|
|
let rounded = if r > 5_000 {
|
|
q + 1
|
|
} else if r < 5_000 || q % 2 == 0 {
|
|
q
|
|
} else {
|
|
q + 1
|
|
};
|
|
i64::try_from(rounded).map_err(|_| RuntimeError::OVERFLOW)
|
|
}
|
|
|
|
fn deep_copy(v: &Value) -> Value {
|
|
match v {
|
|
Value::Rec(r) => {
|
|
let fields = r.borrow().fields.iter().map(deep_copy).collect();
|
|
Value::Rec(Rc::new(std::cell::RefCell::new(RecordObj { fields })))
|
|
}
|
|
other => other.clone(),
|
|
}
|
|
}
|
|
|
|
fn cmp_ord(o: std::cmp::Ordering, op: CmpOp) -> bool {
|
|
use std::cmp::Ordering::*;
|
|
match op {
|
|
CmpOp::Eq => o == Equal,
|
|
CmpOp::Ne => o != Equal,
|
|
CmpOp::Lt => o == Less,
|
|
CmpOp::Le => o != Greater,
|
|
CmpOp::Gt => o == Greater,
|
|
CmpOp::Ge => o != Less,
|
|
}
|
|
}
|
|
|
|
fn cmp_float(a: f64, b: f64, op: CmpOp) -> bool {
|
|
match op {
|
|
CmpOp::Eq => a == b,
|
|
CmpOp::Ne => a != b,
|
|
CmpOp::Lt => a < b,
|
|
CmpOp::Le => a <= b,
|
|
CmpOp::Gt => a > b,
|
|
CmpOp::Ge => a >= b,
|
|
}
|
|
}
|
|
|
|
fn cmp_result(b: bool) -> Value {
|
|
Value::Int(if b { -1 } else { 0 })
|
|
}
|
|
|
|
/// DATA-Element als Zahl: der komplette Eintrag muss numerisch sein
|
|
/// (leer = 0), sonst Fehler 13.
|
|
fn parse_data_number(s: &str) -> Result<f64, RuntimeError> {
|
|
let t = s.trim();
|
|
if t.is_empty() {
|
|
return Ok(0.0);
|
|
}
|
|
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
|
|
cleaned
|
|
.parse::<f64>()
|
|
.map_err(|_| RuntimeError::TYPE_MISMATCH)
|
|
}
|
|
|
|
/// INPUT-Zeile in Felder trennen: Kommas trennen, Anführungszeichen
|
|
/// schützen; Leerraum um unquotierte Felder wird entfernt.
|
|
fn split_input_fields(line: &str) -> Vec<String> {
|
|
let mut fields = Vec::new();
|
|
let mut cur = String::new();
|
|
let mut in_quotes = false;
|
|
for c in line.chars() {
|
|
match c {
|
|
'"' => {
|
|
in_quotes = !in_quotes;
|
|
cur.push(c);
|
|
}
|
|
',' if !in_quotes => {
|
|
fields.push(cur.clone());
|
|
cur.clear();
|
|
}
|
|
_ => cur.push(c),
|
|
}
|
|
}
|
|
fields.push(cur);
|
|
fields
|
|
}
|
|
|
|
/// Eingabefeld in den Typ des Ziels konvertieren (Tag des aktuellen
|
|
/// Werts bestimmt den Typ); `None` = „Redo from start".
|
|
fn input_value(target: &Value, text: &str) -> Option<Value> {
|
|
let t = text.trim();
|
|
match target {
|
|
Value::Str(_) => {
|
|
let s = t
|
|
.strip_prefix('"')
|
|
.and_then(|s| s.strip_suffix('"'))
|
|
.unwrap_or(t);
|
|
Some(Value::Str(Rc::from(s)))
|
|
}
|
|
Value::Int(_) => {
|
|
let v = strict_number(t)?;
|
|
value::f64_to_i16(v).ok().map(Value::Int)
|
|
}
|
|
Value::Lng(_) => {
|
|
let v = strict_number(t)?;
|
|
value::f64_to_i32(v).ok().map(Value::Lng)
|
|
}
|
|
Value::Sng(_) => strict_number(t).map(|v| Value::Sng(v as f32)),
|
|
Value::Dbl(_) => strict_number(t).map(Value::Dbl),
|
|
Value::Cur(_) => {
|
|
let v = strict_number(t)?;
|
|
value::f64_to_cur(v).ok().map(Value::Cur)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn strict_number(t: &str) -> Option<f64> {
|
|
if t.is_empty() {
|
|
return Some(0.0);
|
|
}
|
|
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
|
|
cleaned.parse::<f64>().ok()
|
|
}
|
|
|
|
pub(crate) fn is_runtime_external(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"CMNDLGREGISTER"
|
|
| "CMNDLGCLOSE"
|
|
| "ABOUT"
|
|
| "FILEOPEN"
|
|
| "FILESAVE"
|
|
| "FILEPRINT"
|
|
| "FINDTEXT"
|
|
| "CHANGETEXT"
|
|
| "COLORPALETTE"
|
|
)
|
|
}
|