Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -11,6 +11,7 @@ tb-frontend.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-ui.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "compile"
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use tb_frontend::forms::{FormObject, ObjectClass};
|
||||
use tb_frontend::hir::HEventProc;
|
||||
use tb_runtime::value::{TypeInit, UdtLayout};
|
||||
|
||||
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
||||
pub const TBC_VERSION: u16 = 1;
|
||||
pub const TBC_VERSION: u16 = 3;
|
||||
|
||||
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -52,7 +54,10 @@ impl fmt::Display for LoadError {
|
||||
match self {
|
||||
LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
|
||||
LoadError::Version(v) => {
|
||||
write!(f, "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})")
|
||||
write!(
|
||||
f,
|
||||
"Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})"
|
||||
)
|
||||
}
|
||||
LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
|
||||
}
|
||||
@@ -303,6 +308,14 @@ instrs! {
|
||||
0xB1 RetProc;
|
||||
0xB2 RetFn;
|
||||
0xB3 CallBuiltin(a: u16, b: u8);
|
||||
0xB4 LoadObjectProperty(a: u16, b: u16, c: bool);
|
||||
0xB5 StoreObjectProperty(a: u16, b: u16, c: bool);
|
||||
0xB6 PushObject(a: u16, b: bool);
|
||||
0xB7 TypeOf(a: u8);
|
||||
0xB8 ObjectMethod(a: u16, b: u16, c: u8);
|
||||
0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
|
||||
0xBA LoadDynamicObjectProperty(a: u16);
|
||||
0xBB StoreDynamicObjectProperty(a: u16);
|
||||
|
||||
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
|
||||
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel
|
||||
@@ -375,6 +388,8 @@ pub struct CompiledModule {
|
||||
pub data: Vec<DataItem>,
|
||||
/// Sprungtabellen für `ON n GOTO/GOSUB`.
|
||||
pub jump_tables: Vec<Vec<u32>>,
|
||||
pub objects: Vec<FormObject>,
|
||||
pub event_procs: Vec<HEventProc>,
|
||||
}
|
||||
|
||||
fn w_string(out: &mut Vec<u8>, s: &str) {
|
||||
@@ -458,6 +473,22 @@ impl CompiledModule {
|
||||
}
|
||||
sections.push((*b"JMPT", jmpt));
|
||||
|
||||
let mut objs = Vec::new();
|
||||
objs.extend_from_slice(&(self.objects.len() as u32).to_le_bytes());
|
||||
for o in &self.objects {
|
||||
w_string(&mut objs, &o.name);
|
||||
objs.push(o.class.id());
|
||||
w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
|
||||
objs.push(o.array as u8);
|
||||
}
|
||||
objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
|
||||
for e in &self.event_procs {
|
||||
objs.extend_from_slice(&e.object.to_le_bytes());
|
||||
w_string(&mut objs, &e.event);
|
||||
objs.extend_from_slice(&e.proc.to_le_bytes());
|
||||
}
|
||||
sections.push((*b"OBJS", objs));
|
||||
|
||||
// Header + Abschnittstabelle
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(TBC_MAGIC);
|
||||
@@ -567,7 +598,13 @@ impl CompiledModule {
|
||||
for _ in 0..n_instr {
|
||||
code.push(Instr::decode(&mut cr)?);
|
||||
}
|
||||
procs.push(ProcCode { name, n_params, locals_init, local_names, code });
|
||||
procs.push(ProcCode {
|
||||
name,
|
||||
n_params,
|
||||
locals_init,
|
||||
local_names,
|
||||
code,
|
||||
});
|
||||
}
|
||||
|
||||
let mut r = section(b"DATA")?;
|
||||
@@ -591,6 +628,31 @@ impl CompiledModule {
|
||||
jump_tables.push(t);
|
||||
}
|
||||
|
||||
let mut r = section(b"OBJS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut objects = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
|
||||
let parent = r.string()?;
|
||||
let array = r.u8()? != 0;
|
||||
objects.push(FormObject {
|
||||
name,
|
||||
class,
|
||||
parent_form: (!parent.is_empty()).then_some(parent),
|
||||
array,
|
||||
});
|
||||
}
|
||||
let n = r.u32()? as usize;
|
||||
let mut event_procs = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
event_procs.push(HEventProc {
|
||||
object: r.u16()?,
|
||||
event: r.string()?,
|
||||
proc: r.u16()?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CompiledModule {
|
||||
name,
|
||||
option_base,
|
||||
@@ -601,6 +663,8 @@ impl CompiledModule {
|
||||
procs,
|
||||
data,
|
||||
jump_tables,
|
||||
objects,
|
||||
event_procs,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -660,8 +724,13 @@ mod tests {
|
||||
local_names: vec![],
|
||||
code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
|
||||
}],
|
||||
data: vec![DataItem { text: "1.5".into(), line: 3 }],
|
||||
data: vec![DataItem {
|
||||
text: "1.5".into(),
|
||||
line: 3,
|
||||
}],
|
||||
jump_tables: vec![vec![4, 9]],
|
||||
objects: vec![],
|
||||
event_procs: vec![],
|
||||
};
|
||||
let bytes = m.to_tbc();
|
||||
let back = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
@@ -687,6 +756,8 @@ mod tests {
|
||||
procs: vec![],
|
||||
data: vec![],
|
||||
jump_tables: vec![],
|
||||
objects: vec![],
|
||||
event_procs: vec![],
|
||||
};
|
||||
let mut bytes = m.to_tbc();
|
||||
bytes[4] = 0xFF; // Version hochsetzen
|
||||
|
||||
@@ -43,9 +43,14 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
data: hir
|
||||
.data
|
||||
.iter()
|
||||
.map(|d| DataItem { text: d.text.clone(), line: d.line })
|
||||
.map(|d| DataItem {
|
||||
text: d.text.clone(),
|
||||
line: d.line,
|
||||
})
|
||||
.collect(),
|
||||
jump_tables: cg.jump_tables,
|
||||
objects: hir.objects.clone(),
|
||||
event_procs: hir.event_procs.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +73,7 @@ fn type_init(t: &HTy) -> TypeInit {
|
||||
HTy::Str => TypeInit::Str,
|
||||
HTy::FixedStr(n) => TypeInit::FixedStr(*n),
|
||||
HTy::Udt(id) => TypeInit::Udt(*id),
|
||||
HTy::Form | HTy::Control => TypeInit::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +175,8 @@ impl Codegen {
|
||||
for (idx, label) in std::mem::take(&mut ctx.fixups) {
|
||||
// Modulweites `ON ERROR GOTO` in einer Prozedur: das Label lebt
|
||||
// im Modulrumpf, nicht im eigenen.
|
||||
let modulweit = proc.kind != hir::HProcKind::Main
|
||||
&& matches!(ctx.code[idx], Instr::OnErrorGoto(_));
|
||||
let modulweit =
|
||||
proc.kind != hir::HProcKind::Main && matches!(ctx.code[idx], Instr::OnErrorGoto(_));
|
||||
let pc = if modulweit {
|
||||
self.modul_label_pc
|
||||
.get(label as usize)
|
||||
@@ -241,6 +247,52 @@ impl Codegen {
|
||||
HStmtKind::Assign { place, value } => {
|
||||
self.store_place(ctx, place, |cg, ctx| cg.expr(ctx, value));
|
||||
}
|
||||
HStmtKind::SetObjectProperty {
|
||||
object,
|
||||
index,
|
||||
property,
|
||||
value,
|
||||
} => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
self.expr(ctx, value);
|
||||
ctx.emit(Instr::StoreObjectProperty(
|
||||
*object,
|
||||
*property,
|
||||
index.is_some(),
|
||||
));
|
||||
}
|
||||
HStmtKind::SetDynamicObjectProperty {
|
||||
object,
|
||||
property,
|
||||
value,
|
||||
} => {
|
||||
self.expr(ctx, object);
|
||||
self.expr(ctx, value);
|
||||
let property = self.pool(property);
|
||||
ctx.emit(Instr::StoreDynamicObjectProperty(property));
|
||||
}
|
||||
HStmtKind::ObjectMethod {
|
||||
object,
|
||||
method,
|
||||
args,
|
||||
} => {
|
||||
for arg in args {
|
||||
self.expr(ctx, arg);
|
||||
}
|
||||
ctx.emit(Instr::ObjectMethod(*object, *method, args.len() as u8));
|
||||
}
|
||||
HStmtKind::ObjectLoad {
|
||||
object,
|
||||
index,
|
||||
unload,
|
||||
} => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
ctx.emit(Instr::ObjectLoad(*object, *unload, index.is_some()));
|
||||
}
|
||||
HStmtKind::Print { items, trailing } => {
|
||||
for item in items {
|
||||
match item {
|
||||
@@ -277,12 +329,21 @@ impl Codegen {
|
||||
}
|
||||
ctx.emit(Instr::Field(fields.len() as u8));
|
||||
}
|
||||
HStmtKind::LsetRset { rset, target, value } => {
|
||||
HStmtKind::LsetRset {
|
||||
rset,
|
||||
target,
|
||||
value,
|
||||
} => {
|
||||
self.make_ref(ctx, target);
|
||||
self.expr(ctx, value);
|
||||
ctx.emit(Instr::LsetRset(*rset));
|
||||
}
|
||||
HStmtKind::GetPut { put, file, recnum, var } => {
|
||||
HStmtKind::GetPut {
|
||||
put,
|
||||
file,
|
||||
recnum,
|
||||
var,
|
||||
} => {
|
||||
self.expr(ctx, file);
|
||||
if let Some(r) = recnum {
|
||||
self.expr(ctx, r);
|
||||
@@ -301,12 +362,19 @@ impl Codegen {
|
||||
HTy::Udt(i) => (7, *i),
|
||||
// Variable Strings: Länge erst zur Laufzeit.
|
||||
HTy::Str => (8, 0),
|
||||
HTy::Form | HTy::Control => (8, 0),
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.emit(Instr::GetPut(*put, recnum.is_some(), art, zusatz));
|
||||
}
|
||||
HStmtKind::Input { file, line_mode, prompt, question, targets } => {
|
||||
HStmtKind::Input {
|
||||
file,
|
||||
line_mode,
|
||||
prompt,
|
||||
question,
|
||||
targets,
|
||||
} => {
|
||||
if let Some(f) = file {
|
||||
// Dateinummer zuerst, dann die Referenzen darüber.
|
||||
self.expr(ctx, f);
|
||||
@@ -506,7 +574,12 @@ impl Codegen {
|
||||
}
|
||||
}
|
||||
HStmtKind::Restore(idx) => ctx.emit(Instr::Restore(*idx)),
|
||||
HStmtKind::Dim { slot, elem, dims, redim } => {
|
||||
HStmtKind::Dim {
|
||||
slot,
|
||||
elem,
|
||||
dims,
|
||||
redim,
|
||||
} => {
|
||||
for (lo, hi) in dims {
|
||||
self.expr(ctx, lo);
|
||||
self.expr(ctx, hi);
|
||||
@@ -637,6 +710,36 @@ impl Codegen {
|
||||
ctx.emit(Instr::PushStr(idx));
|
||||
}
|
||||
HExpr::Load(p) => self.load_place(ctx, p),
|
||||
HExpr::ObjectProperty {
|
||||
object,
|
||||
index,
|
||||
property,
|
||||
..
|
||||
} => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
ctx.emit(Instr::LoadObjectProperty(
|
||||
*object,
|
||||
*property,
|
||||
index.is_some(),
|
||||
));
|
||||
}
|
||||
HExpr::DynamicObjectProperty { object, property } => {
|
||||
self.expr(ctx, object);
|
||||
let property = self.pool(property);
|
||||
ctx.emit(Instr::LoadDynamicObjectProperty(property));
|
||||
}
|
||||
HExpr::ObjectRef { object, index, .. } => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
ctx.emit(Instr::PushObject(*object, index.is_some()));
|
||||
}
|
||||
HExpr::TypeOf { value, class } => {
|
||||
self.expr(ctx, value);
|
||||
ctx.emit(Instr::TypeOf(class.id()));
|
||||
}
|
||||
HExpr::Conv { from, to, arg } => {
|
||||
self.expr(ctx, arg);
|
||||
emit_conv(ctx, *from, *to);
|
||||
@@ -1276,7 +1379,12 @@ mod tests {
|
||||
let m = compile_src("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
|
||||
let code = main_code(&m);
|
||||
assert!(code.contains(&Instr::MakeRefGlobal(0)));
|
||||
assert!(code.iter().filter(|i| matches!(i, Instr::Call(1, 1))).count() == 2);
|
||||
assert!(
|
||||
code.iter()
|
||||
.filter(|i| matches!(i, Instr::Call(1, 1)))
|
||||
.count()
|
||||
== 2
|
||||
);
|
||||
// Prozedurrumpf liest/schreibt über Referenz
|
||||
let sub = &m.procs[1].code;
|
||||
assert!(sub.contains(&Instr::LoadRef(0)));
|
||||
@@ -1309,7 +1417,12 @@ mod tests {
|
||||
let m = compile_src("DATA 1, 2\nREAD a%, b%\nRESTORE\nREAD c%");
|
||||
let code = main_code(&m);
|
||||
assert_eq!(m.data.len(), 2);
|
||||
assert!(code.iter().filter(|i| matches!(i, Instr::ReadData(1))).count() == 3);
|
||||
assert!(
|
||||
code.iter()
|
||||
.filter(|i| matches!(i, Instr::ReadData(1)))
|
||||
.count()
|
||||
== 3
|
||||
);
|
||||
assert!(code.contains(&Instr::Restore(0)));
|
||||
assert!(code.contains(&Instr::ConvR8I2));
|
||||
}
|
||||
@@ -1318,7 +1431,9 @@ mod tests {
|
||||
fn fehlerbehandlung_emit() {
|
||||
let m = compile_src("ON ERROR GOTO H\nERROR 5\nEND\nH:\nRESUME NEXT");
|
||||
let code = main_code(&m);
|
||||
assert!(code.iter().any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
|
||||
assert!(code
|
||||
.iter()
|
||||
.any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
|
||||
assert!(code.contains(&Instr::RaiseError));
|
||||
assert!(code.contains(&Instr::ResumeNext));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use tb_runtime::errors::RuntimeError;
|
||||
use tb_runtime::host::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};
|
||||
|
||||
/// Warum die VM die Kontrolle abgibt.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -24,12 +25,24 @@ 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 },
|
||||
Stopped {
|
||||
line: u32,
|
||||
},
|
||||
Breakpoint {
|
||||
line: u32,
|
||||
},
|
||||
Stepped {
|
||||
line: u32,
|
||||
},
|
||||
Interrupted {
|
||||
line: u32,
|
||||
},
|
||||
/// Unbehandelter Laufzeitfehler.
|
||||
Error { code: u16, line: u32, message: String },
|
||||
Error {
|
||||
code: u16,
|
||||
line: u32,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
const F_STEP: u32 = 1;
|
||||
@@ -58,6 +71,15 @@ struct Frame {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FormEventReturn {
|
||||
Normal,
|
||||
Unload(u16),
|
||||
}
|
||||
|
||||
pub struct Vm {
|
||||
@@ -84,6 +106,7 @@ pub struct Vm {
|
||||
tick_zaehler: u32,
|
||||
breakpoints: HashSet<u32>,
|
||||
data_ptr: usize,
|
||||
pub forms: FormsModel,
|
||||
}
|
||||
|
||||
/// Quellenarten, wie der Codegenerator sie kodiert.
|
||||
@@ -126,6 +149,7 @@ impl Vm {
|
||||
.iter()
|
||||
.map(|t| default_value(t, &module.udts))
|
||||
.collect();
|
||||
let forms = FormsModel::new(module.objects.clone(), 80, 25);
|
||||
let mut vm = Vm {
|
||||
globals,
|
||||
locals: Vec::new(),
|
||||
@@ -142,6 +166,7 @@ impl Vm {
|
||||
tick_zaehler: 0,
|
||||
breakpoints: HashSet::new(),
|
||||
data_ptr: 0,
|
||||
forms,
|
||||
module,
|
||||
};
|
||||
// ISAM leitet das Satzlayout aus dem Typ der `OPEN`-Anweisung ab und
|
||||
@@ -173,9 +198,156 @@ impl Vm {
|
||||
last_stmt_pc: 0,
|
||||
line: 0,
|
||||
trap: None,
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
});
|
||||
}
|
||||
|
||||
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 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 {
|
||||
if 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);
|
||||
}
|
||||
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 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);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn dispatch_next_form_event(&mut self) -> bool {
|
||||
while let Some(event) = self.forms.next_event() {
|
||||
if self.dispatch_form_event(event, FormEventReturn::Normal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
|
||||
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 forms_zustellen(&mut self) -> 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.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 {
|
||||
@@ -243,6 +415,9 @@ impl Vm {
|
||||
last_stmt_pc: 0,
|
||||
line: 0,
|
||||
trap: Some(q),
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -358,6 +533,19 @@ impl Vm {
|
||||
|
||||
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();
|
||||
if self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
std::thread::yield_now();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let frame = self.frames.last_mut().expect("kein Frame");
|
||||
let proc = frame.proc;
|
||||
let pc = frame.pc;
|
||||
@@ -642,6 +830,9 @@ impl Vm {
|
||||
self.rt.tick(host);
|
||||
self.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
if self.forms_zustellen() {
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
|
||||
// ein Trap definiert ist — sonst kostet die Grenze nichts.
|
||||
if self.rt.traps.aktiv() {
|
||||
@@ -711,6 +902,7 @@ impl Vm {
|
||||
// lässt den Wert unberührt.
|
||||
self.stack.push(Value::Int(0));
|
||||
self.rt.tick(host);
|
||||
self.forms_zustellen();
|
||||
self.zustellen(host);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -757,6 +949,202 @@ impl Vm {
|
||||
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() {
|
||||
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() {
|
||||
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)?;
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
}
|
||||
let value = self.property_value(object, property, value)?;
|
||||
self.forms.set_at(object, index, property, value)?;
|
||||
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();
|
||||
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)?;
|
||||
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 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() {
|
||||
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();
|
||||
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::Screen, "SHOW") => {
|
||||
self.forms.screen_show(true)
|
||||
}
|
||||
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
|
||||
self.forms.screen_show(false)
|
||||
}
|
||||
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
|
||||
}
|
||||
self.dispatch_next_form_event();
|
||||
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();
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Dup => {
|
||||
let v = self.stack.last().cloned().ok_or(RuntimeError(51))?;
|
||||
self.push(v);
|
||||
@@ -906,8 +1294,7 @@ impl Vm {
|
||||
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();
|
||||
let copied: Vec<Value> = src.borrow().fields.iter().map(deep_copy).collect();
|
||||
dst.borrow_mut().fields = copied;
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
@@ -1334,7 +1721,25 @@ impl Vm {
|
||||
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 event.is_some() {
|
||||
self.dispatch_next_form_event();
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::RetFn => {
|
||||
@@ -1616,13 +2021,7 @@ impl Vm {
|
||||
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,
|
||||
);
|
||||
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())))?;
|
||||
}
|
||||
@@ -1918,7 +2317,10 @@ 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);
|
||||
let s = t
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(t);
|
||||
Some(Value::Str(Rc::from(s)))
|
||||
}
|
||||
Value::Int(_) => {
|
||||
@@ -1946,4 +2348,3 @@ fn strict_number(t: &str) -> Option<f64> {
|
||||
let cleaned = t.replace(['d', 'D'], "E").replace('e', "E");
|
||||
cleaned.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,3 +27,17 @@ pub fn compile_source(
|
||||
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
|
||||
Ok(codegen::compile(&hir))
|
||||
}
|
||||
|
||||
pub fn compile_source_with_forms(
|
||||
module_name: &str,
|
||||
source: &str,
|
||||
forms: &tb_frontend::forms::FormCatalog,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source_with_forms(module_name, source, forms);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
Ok(codegen::compile(
|
||||
&analysis.hir.expect("diagnose-frei, aber kein HIR"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -99,7 +99,10 @@ fn gemischte_arithmetik() {
|
||||
|
||||
#[test]
|
||||
fn logik_bitweise() {
|
||||
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
|
||||
assert_eq!(
|
||||
out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"),
|
||||
" 2 7 5 -1 \n"
|
||||
);
|
||||
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
|
||||
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
|
||||
}
|
||||
@@ -122,12 +125,18 @@ fn stringvergleich_und_verkettung() {
|
||||
#[test]
|
||||
fn for_ohne_durchlauf() {
|
||||
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
|
||||
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
|
||||
assert_eq!(
|
||||
out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""),
|
||||
"ende\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_mit_negativem_step() {
|
||||
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
|
||||
assert_eq!(
|
||||
out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"),
|
||||
" 3 2 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,11 +181,13 @@ fn exit_for_und_do() {
|
||||
fn kontrollfluss_korpusdatei() {
|
||||
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
|
||||
let src = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/compat/kontrollfluss.bas"),
|
||||
)
|
||||
.unwrap();
|
||||
let want = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/compat/kontrollfluss.out"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out(&src), want);
|
||||
@@ -268,7 +279,8 @@ fn data_read_restore() {
|
||||
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
|
||||
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
|
||||
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
|
||||
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
|
||||
let src =
|
||||
"DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
|
||||
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
|
||||
}
|
||||
|
||||
@@ -444,10 +456,7 @@ fn end_und_system() {
|
||||
|
||||
#[test]
|
||||
fn input_mit_redo() {
|
||||
let (ev, output) = run_with_input(
|
||||
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
|
||||
&["abc", "21"],
|
||||
);
|
||||
let (ev, output) = run_with_input("INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"]);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert!(output.contains("Redo from start"));
|
||||
assert!(output.ends_with(" 42 \n"), "{output}");
|
||||
@@ -464,7 +473,10 @@ fn line_input_liest_ganze_zeile() {
|
||||
|
||||
#[test]
|
||||
fn mid_anweisung_mutiert() {
|
||||
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
|
||||
assert_eq!(
|
||||
out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"),
|
||||
"hEYlo\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -495,12 +507,10 @@ fn unsupported_feature_fehler_73() {
|
||||
#[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\
|
||||
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
|
||||
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
|
||||
PRINT \"offen\"\n\
|
||||
CLOSE #1",
|
||||
);
|
||||
CLOSE #1");
|
||||
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
|
||||
assert!(ausgabe.contains("offen"), "{ausgabe}");
|
||||
}
|
||||
@@ -552,7 +562,10 @@ fn groessenaenderung_wirkt_zur_laufzeit() {
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
host.ereignis(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
|
||||
// Inhalt bleibt oben links erhalten.
|
||||
@@ -639,10 +652,7 @@ fn cls_setzt_cursor_zurueck() {
|
||||
|
||||
#[test]
|
||||
fn width_hebt_die_groesse_an() {
|
||||
assert_eq!(
|
||||
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
|
||||
" 32 \n"
|
||||
);
|
||||
assert_eq!(out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"), " 32 \n");
|
||||
}
|
||||
|
||||
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
|
||||
@@ -736,8 +746,14 @@ fn time_und_now_stimmen_ueberein() {
|
||||
#[test]
|
||||
fn fester_versatz_verschiebt_die_uhr() {
|
||||
let src = "PRINT INT(TIMER)";
|
||||
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
|
||||
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
|
||||
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600))
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap();
|
||||
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
|
||||
let diff = (mit - ohne).rem_euclid(86_400.0);
|
||||
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
|
||||
@@ -746,7 +762,10 @@ fn fester_versatz_verschiebt_die_uhr() {
|
||||
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
|
||||
#[test]
|
||||
fn timer_zaehlt_ab_lokaler_mitternacht() {
|
||||
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
|
||||
let t = out_mit_zone(
|
||||
"PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))",
|
||||
Zeitzone::Fest(-18000),
|
||||
);
|
||||
let zahlen: Vec<f64> = t
|
||||
.split_whitespace()
|
||||
.filter_map(|w| w.parse().ok())
|
||||
@@ -1093,3 +1112,169 @@ fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
|
||||
110 RESUME NEXT\n");
|
||||
assert_eq!(out.trim(), "20");
|
||||
}
|
||||
|
||||
fn forms_catalog() -> tb_frontend::forms::FormCatalog {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut c = tb_frontend::forms::FormCatalog::default();
|
||||
c.add("Form1", ObjectClass::Form, None, false);
|
||||
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
|
||||
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
|
||||
c
|
||||
}
|
||||
|
||||
fn form_vm(src: &str) -> Vm {
|
||||
let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog())
|
||||
.unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}"));
|
||||
Vm::new(module)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
|
||||
let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text");
|
||||
let ev = vm.run(&mut CaptureHost::default());
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
|
||||
let mut vm = form_vm(
|
||||
"LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\
|
||||
CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\
|
||||
SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\
|
||||
SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
|
||||
"drei\n-1 \n 0 \n-1 \nFormular"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modales_show_setzt_nach_unload_fort() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT 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",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis_nach(
|
||||
2,
|
||||
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
|
||||
art: tb_runtime::host::MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 0,
|
||||
zeile: 1,
|
||||
spalte: 1,
|
||||
}),
|
||||
);
|
||||
let ev = vm.run(&mut host);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1");
|
||||
assert!(host.presents >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
|
||||
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
|
||||
vm.queue_form_event(tb_ui::forms::FormEvent {
|
||||
object: 0,
|
||||
array_index: None,
|
||||
name: "CLICK".into(),
|
||||
args: vec![],
|
||||
});
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mausargumente_kommen_in_zellen_an() {
|
||||
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\
|
||||
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
|
||||
SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
host.ereignis(Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 2,
|
||||
zeile: 5,
|
||||
spalte: 7,
|
||||
}));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("b"), Some(Value::Int(1))));
|
||||
assert!(matches!(vm.inspect("s"), Some(Value::Int(2))));
|
||||
assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0))));
|
||||
assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_load_laeuft_vor_dem_sichtbarsetzen() {
|
||||
let mut vm = form_vm(
|
||||
"Form1.Show\nEND\n\
|
||||
SUB Form_Load()\nPRINT Form1.Visible\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steuerarray_ereignis_stellt_index_voran() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\
|
||||
SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
vm.forms.load_array(2, 3).unwrap();
|
||||
vm.queue_form_event(tb_ui::forms::FormEvent {
|
||||
object: 2,
|
||||
array_index: Some(3),
|
||||
name: "CLICK".into(),
|
||||
args: vec![],
|
||||
});
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("seen"), Some(Value::Int(3))));
|
||||
let active =
|
||||
tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL")
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(
|
||||
vm.forms.get(3, active).unwrap(),
|
||||
tb_ui::forms::PropertyValue::Object(Some((2, Some(3))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\
|
||||
Form1.Show\nUNLOAD Form1\nEND\n\
|
||||
SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\
|
||||
SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("loads"), Some(Value::Int(1))));
|
||||
assert!(vm.forms.objects[0].loaded);
|
||||
assert!(vm.forms.objects[0].visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption",
|
||||
&forms_catalog(),
|
||||
)
|
||||
.unwrap();
|
||||
let n = module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents))
|
||||
.count();
|
||||
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user