Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -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"),
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user