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