Phase 4: Steuerelemente implementieren

This commit is contained in:
2026-09-05 12:49:46 +02:00
parent 1cb6ae8fd9
commit 19804e0e2d
36 changed files with 6472 additions and 689 deletions

View File

@@ -302,6 +302,7 @@ instrs! {
0xA4 RetGosub;
0xA5 RetGosubTo(a: u32);
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
0xA7 Run(a: u8); // 0 = ohne Ziel, 1 = Zeile, 2 = Datei
// 0xB0 — Prozeduren und Builtins
0xB0 Call(a: u16, b: u8);
@@ -316,6 +317,9 @@ instrs! {
0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
0xBA LoadDynamicObjectProperty(a: u16);
0xBB StoreDynamicObjectProperty(a: u16);
0xBC LoadObjectIndexedProperty(a: u16, b: u16);
0xBD ObjectMethodFn(a: u16, b: u16, c: u8);
0xBE StoreObjectIndexedProperty(a: u16, b: u16);
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel

View File

@@ -20,6 +20,10 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
string_ids: HashMap::new(),
jump_tables: Vec::new(),
modul_label_pc: Vec::new(),
initialize_main: hir
.objects
.iter()
.any(|object| object.class == tb_frontend::forms::ObjectClass::Form),
};
let mut procs = Vec::new();
for proc in &hir.procs {
@@ -29,7 +33,7 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
name: hir.name.clone(),
option_base: hir.option_base,
strings: cg.strings,
globals_init: hir.globals.iter().map(|g| slot_init(g)).collect(),
globals_init: hir.globals.iter().map(slot_init).collect(),
global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
udts: hir
.udts
@@ -85,6 +89,9 @@ struct Codegen {
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
modul_label_pc: Vec<Option<u32>>,
/// Formulare müssen erst nach den globalen DIM-Anweisungen Ereignisse
/// zustellen; reine Textprogramme behalten ihre bisherigen Grenzen.
initialize_main: bool,
}
struct ProcCtx {
@@ -160,7 +167,30 @@ impl Codegen {
fixups: Vec::new(),
table_fixups: Vec::new(),
};
if proc.kind == hir::HProcKind::Main {
let declarations = proc
.body
.iter()
.filter(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
.collect::<Vec<_>>();
for stmt in declarations {
self.stmt_inner(&mut ctx, proc, stmt, false);
}
if self.initialize_main
|| proc
.body
.iter()
.any(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
{
ctx.emit(Instr::Stmt(0));
}
}
for stmt in &proc.body {
if proc.kind == hir::HProcKind::Main
&& matches!(stmt.kind, HStmtKind::Dim { redim: false, .. })
{
continue;
}
self.stmt(&mut ctx, proc, stmt);
}
// Rumpfende
@@ -234,12 +264,17 @@ impl Codegen {
// ---- Anweisungen -------------------------------------------------------
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) {
self.stmt_inner(ctx, proc, stmt, true);
}
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
match &stmt.kind {
HStmtKind::Label(l) => {
ctx.bind(*l);
return;
}
_ => ctx.emit(Instr::Stmt(stmt.line)),
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
_ => {}
}
match &stmt.kind {
HStmtKind::Label(_) => unreachable!(),
@@ -263,6 +298,23 @@ impl Codegen {
index.is_some(),
));
}
HStmtKind::SetObjectIndexedProperty {
object,
object_index,
property,
index,
value,
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
self.expr(ctx, value);
ctx.emit(Instr::StoreObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HStmtKind::SetDynamicObjectProperty {
object,
property,
@@ -275,13 +327,21 @@ impl Codegen {
}
HStmtKind::ObjectMethod {
object,
index,
method,
args,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethod(*object, *method, args.len() as u8));
ctx.emit(Instr::ObjectMethod(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HStmtKind::ObjectLoad {
object,
@@ -509,6 +569,18 @@ impl Codegen {
None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
},
HStmtKind::Run { target, string } => {
if let Some(target) = target {
self.expr(ctx, target);
}
ctx.emit(Instr::Run(if target.is_none() {
0
} else if *string {
2
} else {
1
}));
}
HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc),
HStmtKind::CallSub { proc: id, args } => {
for a in args {
@@ -533,6 +605,9 @@ impl Codegen {
}
let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if matches!(b, Builtin::MsgBox) {
ctx.emit(Instr::Pop);
}
if builtin_returns_value(*b) {
ctx.emit(Instr::Pop);
}
@@ -725,6 +800,41 @@ impl Codegen {
index.is_some(),
));
}
HExpr::ObjectIndexedProperty {
object,
object_index,
property,
index,
..
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
ctx.emit(Instr::LoadObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HExpr::ObjectMethodCall {
object,
index,
method,
args,
..
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethodFn(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HExpr::DynamicObjectProperty { object, property } => {
self.expr(ctx, object);
let property = self.pool(property);
@@ -1173,6 +1283,9 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::Width => ids::WIDTH,
Builtin::ViewPrint => ids::VIEW_PRINT,
Builtin::ScreenStmt => ids::SCREEN_STMT,
Builtin::GraphicsLine => ids::GRAPHICS_LINE,
Builtin::GraphicsPaint => ids::GRAPHICS_PAINT,
Builtin::GraphicsView => ids::GRAPHICS_VIEW,
Builtin::KeyAssign => ids::KEY_ASSIGN,
Builtin::KeyList => ids::KEY_LIST,
Builtin::KeyDisplay => ids::KEY_DISPLAY,
@@ -1277,6 +1390,10 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
Builtin::IsamSetmem => ids::ISAM_SETMEM,
Builtin::IsamBof => ids::ISAM_BOF,
Builtin::MsgBox => ids::MSGBOX,
Builtin::InputBoxS => ids::INPUTBOX_S,
Builtin::ClipboardAdd => ids::CLIPBOARD_ADD,
Builtin::ClipboardGet => ids::CLIPBOARD_GET,
}
}

View File

@@ -10,11 +10,11 @@
//! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3.
use crate::bytecode::{CmpOp, CompiledModule, Instr};
use std::collections::HashSet;
use std::collections::{HashSet, VecDeque};
use std::rc::Rc;
use tb_runtime::builtins::{builtin_table, RtState};
use tb_runtime::builtins::{builtin_table, ids, RtState};
use tb_runtime::errors::RuntimeError;
use tb_runtime::host::Host;
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};
@@ -37,6 +37,10 @@ pub enum RunEvent {
Interrupted {
line: u32,
},
Restart {
program: Option<String>,
line: Option<u32>,
},
/// Unbehandelter Laufzeitfehler.
Error {
code: u16,
@@ -106,6 +110,7 @@ pub struct Vm {
tick_zaehler: u32,
breakpoints: HashSet<u32>,
data_ptr: usize,
start_pc: Option<usize>,
pub forms: FormsModel,
}
@@ -166,6 +171,7 @@ impl Vm {
tick_zaehler: 0,
breakpoints: HashSet::new(),
data_ptr: 0,
start_pc: None,
forms,
module,
};
@@ -176,6 +182,24 @@ impl Vm {
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::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();
@@ -204,6 +228,238 @@ impl Vm {
});
}
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<Ereignis> {
let mut events = VecDeque::new();
events.extend(
self.rt
.tasten
.drain(..)
.map(|(key, shift)| Ereignis::Taste(key, shift)),
);
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
events
}
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
for event in events {
match event {
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
Ereignis::Maus(event) => self.rt.maus.push_back(event),
Ereignis::Abbruch => self.rt.abbruch = true,
Ereignis::Ende => self.rt.ende = true,
Ereignis::Signal(number) => {
if number == 1
&& !self
.rt
.traps
.melden(tb_runtime::traps::Quelle::Signal(number))
{
self.rt.abbruch = true;
}
}
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
}
}
}
fn forms_dialog(
&mut self,
id: u16,
args: &[Value],
host: &mut dyn Host,
) -> Result<Option<Value>, RuntimeError> {
match id {
ids::MSGBOX => {
let text =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let kind = args
.get(1)
.map_or(Ok(0), |value| self.dialog_integer(value))?;
let title = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::msgbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&text,
kind,
&title,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Int(value)))
}
ids::INPUTBOX_S => {
let prompt =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let title = args
.get(1)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let initial = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
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),
};
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::inputbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&prompt,
&title,
&initial,
position,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Str(Rc::from(value))))
}
_ => Err(RuntimeError::FEATURE_UNAVAILABLE),
}
}
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.to_ascii_uppercase();
if !matches!(
name.as_str(),
"CMNDLGREGISTER"
| "CMNDLGCLOSE"
| "ABOUT"
| "FILEOPEN"
| "FILESAVE"
| "FILEPRINT"
| "FINDTEXT"
| "CHANGETEXT"
| "COLORPALETTE"
) {
return Ok(false);
}
let mut args = self.stack.split_off(self.stack.len().saturating_sub(argc));
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")),
];
self.forms_dialog(ids::MSGBOX, &dialog, host)?;
}
"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 result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
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 result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
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!(),
}
args.clear();
Ok(true)
}
pub fn queue_form_event(&mut self, event: FormEvent) {
self.forms.queue(event);
}
@@ -221,6 +477,17 @@ impl Vm {
}
}
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,
@@ -322,36 +589,37 @@ impl Vm {
Ok(())
}
fn forms_zustellen(&mut self) -> bool {
fn tick(&mut self, host: &mut dyn Host) {
self.forms.render(&mut self.rt.screen);
if self.forms.menu_is_open() {
host.present(&self.rt.screen);
self.rt.pump(host, false);
} else {
self.rt.tick(host);
}
}
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows());
while let Some(m) = self.rt.maus.pop_front() {
if let Some(object) = self.forms.active_form() {
let name = match m.art {
tb_runtime::host::MausArt::Druck => "MOUSEDOWN",
tb_runtime::host::MausArt::Loslassen => "MOUSEUP",
tb_runtime::host::MausArt::Bewegung => "MOUSEMOVE",
};
self.forms.queue(FormEvent {
object,
array_index: None,
name: name.into(),
args: vec![
PropertyValue::Integer(m.taste as i32),
PropertyValue::Integer(m.shift as i32),
PropertyValue::Single(m.spalte as f32),
PropertyValue::Single(m.zeile as f32),
],
});
self.forms.timers(host.jetzt_ms());
if self.forms.active_form().is_some() {
while let Some((key, shift)) = self.rt.tasten.pop_front() {
self.forms.handle_key(&key, shift);
}
}
let now_ms = host.jetzt_ms();
while let Some(m) = self.rt.maus.pop_front() {
self.forms.handle_mouse_at(m, now_ms);
}
self.forms.render(&mut self.rt.screen);
self.dispatch_next_form_event()
}
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
if !self.rt.traps.aktiv() {
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
return false;
}
let jetzt = host.jetzt_ms();
@@ -377,7 +645,7 @@ impl Vm {
// virtuelle nie vorrückt.
let real_start = std::time::Instant::now();
loop {
self.rt.tick(host);
self.tick(host);
if self.zustellen(host) {
return;
}
@@ -531,14 +799,41 @@ impl Vm {
// ---- Hauptschleife --------------------------------------------------------
/// 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 {
while self.forms.has_visible_forms() {
self.tick(host);
let dispatched = self.forms_zustellen(host);
if dispatched {
match self.run(host) {
RunEvent::Ended => {}
event => return event,
}
}
if self.rt.ende {
return RunEvent::Ended;
}
if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
RunEvent::Ended
}
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
loop {
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.rt.tick(host);
self.forms_zustellen();
self.tick(host);
self.forms_zustellen(host);
if self.rt.ende {
return RunEvent::Ended;
}
@@ -818,6 +1113,11 @@ impl Vm {
use Instr as I;
match instr {
I::Stmt(line) => {
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;
f.last_stmt_pc = pc;
@@ -826,11 +1126,12 @@ impl Vm {
// 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);
if self.rt.screen.ist_veraendert() || self.tick_zaehler % 1024 == 0 {
self.rt.tick(host);
self.forms.render(&mut self.rt.screen);
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
self.tick(host);
self.rt.screen.veraenderung_quittieren();
}
if self.forms_zustellen() {
if self.forms_zustellen(host) {
return Ok(Flow::Normal);
}
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
@@ -841,7 +1142,7 @@ impl Vm {
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
// reicht das nicht, wird daraus ein eigener Zähler je
// Trap.
if self.tick_zaehler % 64 == 0 {
if self.tick_zaehler.is_multiple_of(64) {
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
}
@@ -901,8 +1202,8 @@ impl Vm {
// bekommt seinen eigenen Stapelabschnitt darüber und
// lässt den Wert unberührt.
self.stack.push(Value::Int(0));
self.rt.tick(host);
self.forms_zustellen();
self.tick(host);
self.forms_zustellen(host);
self.zustellen(host);
Ok(Flow::Normal)
}
@@ -1051,6 +1352,56 @@ impl Vm {
self.forms.set_at(object, index, property, value)?;
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
@@ -1064,6 +1415,8 @@ impl Vm {
Ok(Flow::Normal)
}
I::ObjectMethod(object, method, argc) => {
let indexed = argc & 0x80 != 0;
let argc = argc & 0x7f;
let class = self
.module
.objects
@@ -1093,6 +1446,7 @@ impl Vm {
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);
}
@@ -1118,13 +1472,26 @@ impl Vm {
(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)
}
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
_ => {
let args = args
.into_iter()
.map(Self::form_arg)
.collect::<Result<Vec<_>, _>>()?;
self.forms.object_method_at(object, index, name, args)?;
}
}
self.dispatch_next_form_event();
Ok(Flow::Normal)
@@ -1318,7 +1685,7 @@ impl Vm {
s.chars().take(n).collect()
} else {
let mut t = s.to_string();
t.extend(std::iter::repeat(' ').take(n - len));
t.extend(std::iter::repeat_n(' ', n - len));
t
};
self.push(Value::Str(Rc::from(fixed.as_str())));
@@ -1714,9 +2081,27 @@ impl Vm {
}
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)? {
return Ok(Flow::Normal);
}
self.push_frame(proc as usize, argc as usize);
Ok(Flow::Normal)
}
@@ -1754,6 +2139,12 @@ impl Vm {
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);
}
return Ok(Flow::Normal);
}
let f = builtin_table()[id as usize];
match f(&mut self.rt, host, &mut args) {
Ok(Some(v)) => {
@@ -2228,9 +2619,7 @@ fn cur_mul(a: i64, b: i64) -> Result<i64, RuntimeError> {
let r = p.rem_euclid(10_000);
let rounded = if r > 5_000 {
q + 1
} else if r < 5_000 {
q
} else if q % 2 == 0 {
} else if r < 5_000 || q % 2 == 0 {
q
} else {
q + 1

View File

@@ -38,13 +38,11 @@ fn err_code(src: &str) -> u16 {
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis {
vorher: std::path::PathBuf,
dir: std::path::PathBuf,
}
impl TempVerzeichnis {
fn neu(name: &str) -> TempVerzeichnis {
let vorher = std::env::current_dir().unwrap();
let dir = std::env::temp_dir().join(format!(
"tb_vm_{name}_{}_{:?}",
std::process::id(),
@@ -52,14 +50,16 @@ impl TempVerzeichnis {
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
TempVerzeichnis { vorher, dir }
TempVerzeichnis { dir }
}
fn pfad(&self, name: &str) -> String {
self.dir.join(name).to_string_lossy().replace('"', "\"\"")
}
}
impl Drop for TempVerzeichnis {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.vorher);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
@@ -71,6 +71,48 @@ fn print_hallo_welt() {
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
}
#[test]
fn grafik_line_paint_und_view_zeichnen_in_den_zellenpuffer() {
assert_eq!(
out("SCREEN 2\nVIEW (0,0)-(31,15),0,1\nLINE (0,0)-(15,7),1,BF\nPAINT (24,8),1\nEND"),
"████\n████\n"
);
}
#[test]
fn run_gibt_datei_oder_zeile_an_den_runner_weiter() {
assert_eq!(
run("RUN \"next\"").0,
RunEvent::Restart {
program: Some("next".into()),
line: None
}
);
assert_eq!(
run("RUN 100").0,
RunEvent::Restart {
program: None,
line: Some(100)
}
);
}
#[test]
fn common_dialog_registrierung_setzt_byref_erfolg() {
assert_eq!(
out("DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nCmnDlgRegister ok%\nPRINT ok%\nEND"),
"-1 \n"
);
}
#[test]
fn len_liefert_die_feste_satzbreite_eines_udt() {
assert_eq!(
out("TYPE Satz\nText AS STRING * 3\nZahl AS INTEGER\nEND TYPE\nDIM Wert AS Satz\nPRINT LEN(Wert)"),
" 14 \n"
);
}
#[test]
fn banker_rounding_cint() {
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
@@ -486,31 +528,19 @@ fn print_zonen_und_tab() {
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
}
#[test]
fn unsupported_feature_fehler_73() {
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
let (ev, _) = run("RUN");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73);
assert_eq!(message, "Feature unavailable");
}
other => panic!("{other:?}"),
}
}
/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
let dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(&format!(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1");
CLOSE #1",
dir.pfad("db.isam")
));
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
@@ -594,17 +624,10 @@ fn color_prueft_wertebereich() {
}
#[test]
fn screen_anweisung_nur_textmodus() {
// Literaler Grafikmodus schon zur Compile-Zeit.
let d = tb_vm::compile_source("T", "SCREEN 1").unwrap_err();
assert!(
d.iter().any(|x| x.message.contains("Feature unavailable")),
"{d:?}"
);
// Textmodus ist folgenlos zulässig.
fn screen_anweisung_bildet_grafikmodi_auf_den_zellenpuffer_ab() {
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
// Berechneter Modus erst zur Laufzeit.
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73);
assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n");
assert_eq!(err_code("m% = 14\nSCREEN m%"), 5);
}
#[test]
@@ -644,6 +667,32 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
}
#[test]
fn input_s_liest_aus_einer_binaerdatei() {
let tmp = TempVerzeichnis::neu("input_s_datei");
let pfad = tmp.pfad("daten.bin");
std::fs::write(&pfad, b"abcdef").unwrap();
assert_eq!(
out(&format!(
"OPEN \"{pfad}\" FOR BINARY AS #1\na$ = INPUT$(3, #1)\nPRINT a$\nCLOSE #1"
)),
"abc\n"
);
}
#[test]
fn clipboard_und_printer_objekte_haben_laufzeitwirkung() {
let module = tb_vm::compile_source(
"T",
"CLIPBOARD.ADDITEM \"abc\"\nPRINT CLIPBOARD.GETTEXT\nPRINTER.PRINT \"Seite\"\nPRINTER.NEWPAGE\nPRINTER.ENDDOC",
)
.unwrap();
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "abc\n");
assert_eq!(vm.rt.print.drucker, "Seite\n\u{c}");
}
#[test]
fn cls_setzt_cursor_zurueck() {
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
@@ -1136,6 +1185,14 @@ fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
}
#[test]
fn screen_controlpanel_hat_einen_indexierten_laufzeitwert() {
assert_eq!(
out("SCREEN.CONTROLPANEL(5) = 3\nPRINT SCREEN.CONTROLPANEL(5)"),
" 3 \n"
);
}
#[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
let mut vm = form_vm(
@@ -1175,6 +1232,31 @@ fn modales_show_setzt_nach_unload_fort() {
assert!(host.presents >= 2);
}
#[test]
fn modelloses_formular_bleibt_nach_programmende_bedienbar() {
let mut vm = form_vm(
"DIM SHARED gesehen%\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
);
vm.forms.show(0, false).unwrap();
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
let mut host = CaptureHost::default();
host.ereignis_nach(
1,
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
art: tb_runtime::host::MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}),
);
assert_eq!(vm.run_visible_forms(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
assert!(!vm.forms.has_visible_forms());
}
#[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
@@ -1278,3 +1360,153 @@ fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
.count();
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
}
#[test]
fn listenmethoden_indexeigenschaft_und_picture_messung_laufen_in_der_vm() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), false);
catalog.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"List1.Sorted = -1\nList1.ADDITEM \"b\"\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT List1.ListCount\nPRINT Picture1.TEXTWIDTH(\"abc\")",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
"a\n 2 \n 3"
);
}
#[test]
fn listenmethoden_und_indexeigenschaft_laufen_auf_control_arrays() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), true);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"LOAD List1(1)\nList1(1).ADDITEM \"x\"\nPRINT List1(1).List(0)",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "x");
}
#[test]
fn msgbox_und_inputbox_sind_keine_unsupported_opcodes_mehr() {
use tb_runtime::host::{taste, Ereignis};
let module = tb_vm::compile_source(
"DIALOG",
"r% = MSGBOX(\"Weiter?\", 4, \"Frage\")\ns$ = INPUTBOX$(\"Name\")\nPRINT r%; s$",
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
assert!(!module.procs[0]
.code
.iter()
.any(|instruction| matches!(instruction, tb_vm::bytecode::Instr::Unsupported(_))));
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for key in [taste::ENTER, "A", taste::ENTER] {
host.ereignis(Ereignis::Taste(key.into(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "6 A");
}
#[test]
fn default_button_hat_vorrang_und_beendet_modales_formular() {
use tb_frontend::forms::ObjectClass;
use tb_runtime::host::{taste, Ereignis};
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Command1", ObjectClass::CommandButton, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\nSUB Command1_Click()\nSHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
let default = tb_frontend::forms::property(ObjectClass::CommandButton, "DEFAULT")
.unwrap()
.0;
vm.forms
.set(1, default, tb_ui::forms::PropertyValue::Boolean(true))
.unwrap();
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste(taste::ENTER.into(), 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
}
#[test]
fn printform_schreibt_das_textformular_in_den_druckerkanal() {
use tb_frontend::forms::ObjectClass;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
let module = tb_vm::compile_source_with_forms("FORM1", "Form1.Show\nForm1.PRINTFORM", &catalog)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
for (name, value) in [("WIDTH", 12), ("HEIGHT", 4)] {
let property = tb_frontend::forms::property(ObjectClass::Form, name)
.unwrap()
.0;
vm.forms
.set_initial(0, property, tb_ui::forms::PropertyValue::Integer(value))
.unwrap();
}
let caption = tb_frontend::forms::property(ObjectClass::Form, "CAPTION")
.unwrap()
.0;
vm.forms
.set_initial(
0,
caption,
tb_ui::forms::PropertyValue::String("Druck".into()),
)
.unwrap();
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert!(vm.rt.print.drucker.contains("Druck"));
}
#[test]
fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
use tb_frontend::forms::ObjectClass;
use tb_runtime::host::umschalt;
let mut catalog = tb_frontend::forms::FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("mnuDatei", ObjectClass::Menu, Some("Form1"), false);
let module = tb_vm::compile_source_with_forms(
"FORM1",
"DIM SHARED n%\nON TIMER(1) GOSUB Tick\nTIMER ON\nForm1.Show\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nPRINT n%\nEND\nTick:\nn% = n% + 1\nRETURN",
&catalog,
)
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
let mut vm = Vm::new(module);
let caption = tb_frontend::forms::property(ObjectClass::Menu, "CAPTION")
.unwrap()
.0;
vm.forms
.set_initial(
1,
caption,
tb_ui::forms::PropertyValue::String("&Datei".into()),
)
.unwrap();
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(vm.forms.handle_key("d", umschalt::ALT));
host.uhr_vorruecken(1_500);
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("n"), Some(Value::Int(0))));
assert!(vm.forms.handle_key(tb_runtime::host::taste::ESC, 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
}