Phase 5: Ausführung und Output implementieren und archivieren

This commit is contained in:
2026-09-06 18:56:47 +02:00
parent 9c85349a4d
commit e3dc9028bf
37 changed files with 2404 additions and 450 deletions

View File

@@ -49,6 +49,14 @@ pub enum RunEvent {
},
}
/// Scheduling result, deliberately separate from debugger events.
#[derive(Debug, Clone, PartialEq)]
pub enum PollResult {
Yield,
Waiting { deadline: Option<u64> },
Event(RunEvent),
}
const F_STEP: u32 = 1;
const F_BREAK: u32 = 2;
const F_POLL: u32 = 4;
@@ -82,6 +90,7 @@ struct Frame {
form_event: Option<FormEventReturn>,
handler_start: bool,
eingabe: Option<KonsolenEingabe>,
sleep: Option<Option<u64>>,
}
struct KonsolenEingabe {
@@ -130,6 +139,9 @@ pub struct Vm {
data_ptr: usize,
start_pc: Option<usize>,
pub forms: FormsModel,
waiting: Option<Option<u64>>,
dialog: Option<tb_ui::forms::Dialog>,
terminated: bool,
}
/// Quellenarten, wie der Codegenerator sie kodiert.
@@ -198,6 +210,9 @@ impl Vm {
breakpoints: HashSet::new(),
data_ptr: 0,
start_pc: None,
waiting: None,
dialog: None,
terminated: false,
forms,
module,
};
@@ -260,6 +275,7 @@ impl Vm {
form_event: None,
handler_start: false,
eingabe: None,
sleep: None,
});
}
@@ -316,56 +332,51 @@ impl Vm {
args: &[Value],
host: &mut dyn Host,
) -> Result<Option<Value>, RuntimeError> {
match id {
ids::MSGBOX => {
let text =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let kind = args
.get(1)
.map_or(Ok(0), |value| self.dialog_integer(value))?;
let title = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::msgbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&text,
kind,
&title,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Int(value)))
}
ids::INPUTBOX_S => {
let prompt =
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
let title = args
.get(1)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let initial = args
.get(2)
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
let position = match (args.get(3), args.get(4)) {
(None, None) => None,
(Some(x), Some(y)) => Some((self.dialog_integer(x)?, self.dialog_integer(y)?)),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let mut queued = self.take_dialog_input();
let result = tb_ui::forms::inputbox_dialog(
&mut self.rt.screen,
host,
&mut queued,
&prompt,
&title,
&initial,
position,
);
self.restore_dialog_input(queued);
result.map(|value| Some(Value::Str(Rc::from(value))))
}
_ => Err(RuntimeError::FEATURE_UNAVAILABLE),
if self.dialog.is_none() {
let text = self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
self.dialog = Some(match id {
ids::MSGBOX => {
let kind = args.get(1).map_or(Ok(0), |v| self.dialog_integer(v))?;
let title = args
.get(2)
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
tb_ui::forms::message_dialog(&self.rt.screen, &text, kind, &title)?
}
ids::INPUTBOX_S => {
let title = args
.get(1)
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
let initial = args
.get(2)
.map_or_else(|| Ok(String::new()), |v| self.external_string(v))?;
let position = match (args.get(3), args.get(4)) {
(None, None) => None,
(Some(x), Some(y)) => {
Some((self.dialog_integer(x)?, self.dialog_integer(y)?))
}
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
tb_ui::forms::input_dialog(&self.rt.screen, &text, &title, &initial, position)?
}
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
});
}
let mut queued = self.take_dialog_input();
let value = self
.dialog
.as_mut()
.unwrap()
.poll(&mut self.rt.screen, host, &mut queued);
self.restore_dialog_input(queued);
if let Some(value) = value {
self.dialog = None;
Ok(Some(match value {
tb_ui::forms::DialogValue::Number(n) => Value::Int(n),
tb_ui::forms::DialogValue::Text(s) => Value::Str(Rc::from(s)),
}))
} else {
self.waiting = Some(None);
Ok(None)
}
}
@@ -398,7 +409,7 @@ impl Vm {
) {
return Ok(false);
}
let mut args = self.stack.split_off(self.stack.len().saturating_sub(argc));
let args = self.stack[self.stack.len().saturating_sub(argc)..].to_vec();
match name.as_str() {
"CMNDLGREGISTER" => self.external_set(&args, 0, Value::Int(-1))?,
"CMNDLGCLOSE" => {}
@@ -408,7 +419,9 @@ impl Vm {
Value::Lng(0),
Value::Str(Rc::from("About")),
];
self.forms_dialog(ids::MSGBOX, &dialog, host)?;
if self.forms_dialog(ids::MSGBOX, &dialog, host)?.is_none() {
return Ok(true);
}
}
"FILEOPEN" | "FILESAVE" => {
let file = self.external_string(&args[0])?;
@@ -428,9 +441,9 @@ impl Vm {
Value::Str(Rc::from(title)),
Value::Str(Rc::from(default)),
];
let result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
let Some(result) = self.forms_dialog(ids::INPUTBOX_S, &dialog, host)? else {
return Ok(true);
};
let Value::Str(result) = result else {
return Err(RuntimeError::TYPE_MISMATCH);
};
@@ -460,9 +473,9 @@ impl Vm {
Value::Str(Rc::from("Find")),
self.external_arg(&args[usize::from(name == "CHANGETEXT")])?,
];
let result = self
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
.unwrap_or(Value::Str(Rc::from("")));
let Some(result) = self.forms_dialog(ids::INPUTBOX_S, &dialog, host)? else {
return Ok(true);
};
let target = usize::from(name == "CHANGETEXT");
self.external_set(&args, target, result.clone())?;
let empty = matches!(&result, Value::Str(text) if text.is_empty());
@@ -475,7 +488,7 @@ impl Vm {
}
_ => unreachable!(),
}
args.clear();
self.stack.truncate(self.stack.len() - argc);
Ok(true)
}
@@ -766,25 +779,6 @@ impl Vm {
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
let deadline =
(sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 1000.0) as u64));
loop {
self.tick(host);
if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
return;
}
if let Some(deadline) = deadline {
if host.jetzt_ms() >= deadline {
return;
}
} else if self.rt.naechste_taste(host, false).is_some() {
return;
}
self.warten(host, deadline);
}
}
/// Trap-Handler aufsetzen: ein Frame auf den Modulrumpf, der dessen
/// Locals mitbenutzt, mit eigenem `GOSUB`-Stapel und einer Rückmarke
/// auf die unterbrochene Stelle (design.md, D2).
@@ -808,6 +802,7 @@ impl Vm {
form_event: None,
handler_start: true,
eingabe: None,
sleep: None,
});
}
@@ -994,39 +989,114 @@ impl Vm {
// ---- Hauptschleife --------------------------------------------------------
pub fn is_terminated(&self) -> bool {
self.terminated
}
/// Hält modellose Formulare nach dem Ende des Modulrumpfs bedienbar.
/// Ereignisprozeduren laufen weiter auf derselben VM und können das
/// Formular schließen, ein anderes Programm starten oder einen Fehler
/// auslösen.
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
self.forms.sync_timers(|| host.jetzt_ms());
while self.forms.has_visible_forms() {
self.tick(host);
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if dispatched {
match self.run(host) {
RunEvent::Ended => {}
event => return event,
// Legacy explicit entry point: the caller deliberately starts a new
// forms pump, even after END. IDE/CLI only call it after natural end.
self.terminated = false;
loop {
match self.poll_visible_forms(host, usize::MAX) {
PollResult::Event(event) => return event,
PollResult::Waiting { deadline } => {
if (self.dialog.is_none() || self.rt.eingaben.is_empty())
&& !self.rt.abbruch
&& !self.rt.ende
{
self.warten(host, deadline);
}
}
}
if !dispatched && self.rt.ende {
return RunEvent::Ended;
}
if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
}
if !dispatched && self.forms.has_visible_forms() {
self.warten(host, None);
PollResult::Yield => {}
}
}
RunEvent::Ended
}
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
self.forms.sync_timers(|| host.jetzt_ms());
loop {
match self.poll(host, usize::MAX) {
PollResult::Event(e) => return e,
PollResult::Waiting { deadline } => {
if (self.dialog.is_none() || self.rt.eingaben.is_empty())
&& !self.rt.abbruch
&& !self.rt.ende
{
self.warten(host, deadline);
}
}
PollResult::Yield => {}
}
}
}
/// Polls modeless forms after the module body has ended.
pub fn poll_visible_forms(&mut self, host: &mut dyn Host, budget: usize) -> PollResult {
if self.terminated {
return PollResult::Event(RunEvent::Ended);
}
if self
.frames
.last()
.is_some_and(|f| f.pc < self.module.procs[f.proc].code.len())
{
return match self.poll(host, budget) {
PollResult::Event(RunEvent::Ended) if !self.terminated => PollResult::Yield,
result => result,
};
}
if !self.forms.has_visible_forms() {
return PollResult::Event(RunEvent::Ended);
}
self.tick(host);
if self.rt.abbruch {
return PollResult::Event(RunEvent::Interrupted {
line: self.current_line(),
});
}
if self.zustellen(host, Zustellpunkt::Kooperativ) {
match self.poll(host, budget) {
PollResult::Event(RunEvent::Ended) if !self.terminated => PollResult::Yield,
result => result,
}
} else if self.rt.ende || !self.forms.has_visible_forms() {
PollResult::Event(RunEvent::Ended)
} else {
PollResult::Waiting {
deadline: self.wait_deadline(None),
}
}
}
fn wait_deadline(&self, deadline: Option<u64>) -> Option<u64> {
deadline
.into_iter()
.chain(self.forms.next_deadline())
.chain(if self.forms.menu_is_open() {
None
} else {
self.rt.traps.naechste_frist()
})
.min()
}
/// Executes at most `budget` instructions without entering a host wait.
pub fn poll(&mut self, host: &mut dyn Host, budget: usize) -> PollResult {
self.waiting = None;
self.forms.sync_timers(|| host.jetzt_ms());
for _ in 0..budget {
if self.dialog.is_some() && self.rt.ende {
return PollResult::Event(RunEvent::Ended);
}
if self.rt.abbruch {
return PollResult::Event(RunEvent::Interrupted {
line: self.current_line(),
});
}
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;
@@ -1034,39 +1104,45 @@ impl Vm {
self.tick(host);
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if !dispatched && self.rt.ende {
return RunEvent::Ended;
}
if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
return PollResult::Event(RunEvent::Ended);
}
if !dispatched {
self.warten(host, None);
return PollResult::Waiting {
deadline: self.wait_deadline(None),
};
}
continue;
}
}
let frame = self.frames.last_mut().expect("kein Frame");
let proc = frame.proc;
let Some(frame) = self.frames.last_mut() else {
return PollResult::Event(RunEvent::Ended);
};
let pc = frame.pc;
let code = &self.module.procs[proc].code;
if pc >= code.len() {
// Sollte nicht passieren (Exit wird emittiert) — defensiv.
return RunEvent::Ended;
}
let instr = code[pc].clone();
let Some(instr) = self.module.procs[frame.proc].code.get(pc).cloned() else {
return PollResult::Event(RunEvent::Ended);
};
frame.pc = pc + 1;
match self.exec(instr, pc, host) {
Ok(Flow::Normal) => {}
Ok(Flow::Event(ev)) => return ev,
Ok(Flow::Event(ev)) => return PollResult::Event(ev),
Err(e) => {
self.dialog = None;
self.waiting = None;
if let Some(f) = self.frames.last_mut() {
f.sleep = None;
}
if let Some(ev) = self.handle_error(e.0, host) {
return ev;
return PollResult::Event(ev);
}
}
}
if let Some(deadline) = self.waiting {
return PollResult::Waiting {
deadline: self.wait_deadline(deadline),
};
}
}
PollResult::Yield
}
// ---- Fehlerbehandlung -------------------------------------------------------
@@ -1112,6 +1188,7 @@ impl Vm {
};
self.stack.truncate(stack_base);
self.frames.last_mut().unwrap().eingabe = None;
self.frames.last_mut().unwrap().sleep = None;
self.err = code;
match handler {
Handler::Goto(pc) => {
@@ -1421,8 +1498,27 @@ impl Vm {
Ok(Flow::Normal)
}
I::Sleep(mit_arg) => {
let sekunden = if mit_arg { self.pop_f64()? } else { 0.0 };
self.schlafen(host, sekunden);
let caller = self.frames.len() - 1;
if self.frames[caller].sleep.is_none() {
let seconds = if mit_arg { self.pop_f64()? } else { 0.0 };
self.frames[caller].sleep = Some(
(seconds > 0.0)
.then(|| host.jetzt_ms().saturating_add((seconds * 1000.0) as u64)),
);
}
let deadline = self.frames[caller].sleep.unwrap();
self.tick(host);
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if dispatched
|| self.rt.ende
|| deadline.is_some_and(|d| host.jetzt_ms() >= d)
|| (deadline.is_none() && self.rt.gepufferte_taste().is_some())
{
self.frames[caller].sleep = None;
} else {
self.frames[caller].pc = pc;
self.waiting = Some(deadline);
}
Ok(Flow::Normal)
}
I::EventSwitch(an) => {
@@ -1433,7 +1529,11 @@ impl Vm {
self.zeile_nr = n;
Ok(Flow::Normal)
}
I::End | I::SystemInstr => Ok(Flow::Event(RunEvent::Ended)),
I::End => Ok(Flow::Event(RunEvent::Ended)),
I::SystemInstr => {
self.terminated = true;
Ok(Flow::Event(RunEvent::Ended))
}
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped {
line: self.current_line(),
})),
@@ -2336,6 +2436,9 @@ impl Vm {
// ---- Prozeduren ----
I::Call(proc, argc) => {
if self.external_dialog(proc as usize, argc as usize, host)? {
if self.waiting.is_some() {
self.frames.last_mut().unwrap().pc = pc;
}
return Ok(Flow::Normal);
}
self.push_frame(proc as usize, argc as usize);
@@ -2412,6 +2515,26 @@ impl Vm {
if matches!(id, ids::MSGBOX | ids::INPUTBOX_S) {
if let Some(value) = self.forms_dialog(id, &args, host)? {
self.push(value);
} else {
self.stack.extend(args);
self.frames.last_mut().unwrap().pc = pc;
}
return Ok(Flow::Normal);
}
if matches!(id, ids::SHELL_STMT | ids::SHELL_FN) {
let command = args
.first()
.map(|v| self.external_string(v))
.transpose()?
.unwrap_or_default();
if let Some(code) = host.shell(&command)? {
if id == ids::SHELL_FN {
self.push(Value::Lng(code));
}
} else {
self.stack.extend(args);
self.frames.last_mut().unwrap().pc = pc;
self.waiting = Some(None);
}
return Ok(Flow::Normal);
}
@@ -2562,11 +2685,14 @@ impl Vm {
text: String::new(),
rest,
});
loop {
for _ in 0..64 {
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
}
self.tick(host);
if self.rt.abbruch {
return Ok(None);
}
if self.zustellen(host, Zustellpunkt::Konsole) {
return Ok(None);
}
@@ -2599,12 +2725,14 @@ impl Vm {
}
continue;
}
if self.rt.ende || self.rt.abbruch {
if self.rt.ende {
self.frames[caller].eingabe = None;
return Err(RuntimeError(62));
}
self.warten(host, None);
self.waiting = Some(None);
return Ok(None);
}
Ok(None)
}
fn do_input(