Files
TerminalBasic/crates/tb-vm/src/interp.rs

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"
)
}