Phase 4: Formularmodell und Objektsprache

This commit is contained in:
2026-09-04 17:30:32 +02:00
parent 5cf5a6582c
commit 54488b5b67
35 changed files with 7195 additions and 627 deletions

View File

@@ -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()
}