Phase 4: Steuerelemente implementieren
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user