//! 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, line: Option, }, /// 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 }, 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, 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, waiting_form: Option, pending_show: Option, form_event: Option, handler_start: bool, eingabe: Option, sleep: Option>, } struct KonsolenEingabe { text: String, /// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$. rest: Option, } #[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, locals: Vec, stack: Vec, frames: Vec, 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, 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, pub forms: FormsModel, waiting: Option>, dialog: Option, 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 { 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 { match value { Value::Ref(reference) => self.read_ref(reference), value => Ok(value.clone()), } } fn external_string(&self, value: &Value) -> Result { 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 { 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 { std::mem::take(&mut self.rt.eingaben) } fn restore_dialog_input(&mut self, events: VecDeque) { 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, 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 { 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 { 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 { 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) { 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 { 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 { 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) -> Option { 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 { 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 { 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) -> Option { 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 { 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 { self.stack.pop().ok_or(RuntimeError(51)) // Internal error } fn pop_i16(&mut self) -> Result { match self.pop()? { Value::Int(v) => Ok(v), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_i32(&mut self) -> Result { 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 { match self.pop()? { Value::Sng(v) => Ok(v), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_f64(&mut self) -> Result { match self.pop()? { Value::Dbl(v) => Ok(v), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_cur(&mut self) -> Result { match self.pop()? { Value::Cur(v) => Ok(v), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_str(&mut self) -> Result, RuntimeError> { match self.pop()? { Value::Str(s) => Ok(s), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_arr(&mut self) -> Result>, RuntimeError> { match self.pop()? { Value::Arr(a) => Ok(a), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_rec(&mut self) -> Result>, RuntimeError> { match self.pop()? { Value::Rec(r) => Ok(r), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn pop_indices(&mut self, dims: u8) -> Result, 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 { 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>, 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 { 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::, _>>()?; 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 = 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, 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 { 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, ) -> Result, 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 { 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::, _>>()?; 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) -> Result { 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) -> Result { 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 { 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 { 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) -> Result { 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 { 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 { 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 { 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 { let t = s.trim(); if t.is_empty() { return Ok(0.0); } let cleaned = t.replace(['d', 'D'], "E").replace('e', "E"); cleaned .parse::() .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 { 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 { 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 { if t.is_empty() { return Some(0.0); } let cleaned = t.replace(['d', 'D'], "E").replace('e', "E"); cleaned.parse::().ok() } pub(crate) fn is_runtime_external(name: &str) -> bool { matches!( name, "CMNDLGREGISTER" | "CMNDLGCLOSE" | "ABOUT" | "FILEOPEN" | "FILESAVE" | "FILEPRINT" | "FINDTEXT" | "CHANGETEXT" | "COLORPALETTE" ) }