Phase 5: Ausführung und Output implementieren und archivieren
This commit is contained in:
@@ -704,7 +704,7 @@ impl Codegen {
|
||||
ctx.emit(Instr::EraseSlot(global, s));
|
||||
}
|
||||
}
|
||||
HStmtKind::End => ctx.emit(Instr::End),
|
||||
HStmtKind::End => ctx.emit(Instr::SystemInstr),
|
||||
HStmtKind::Stop => ctx.emit(Instr::StopInstr),
|
||||
HStmtKind::System => ctx.emit(Instr::SystemInstr),
|
||||
HStmtKind::Unsupported(name) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -258,6 +258,83 @@ pub struct ProjectSources {
|
||||
pub units: Vec<SourceUnit>,
|
||||
pub forms: Vec<FormFile>,
|
||||
}
|
||||
impl ProjectSources {
|
||||
/// One startup contract for IDE, CLI and serialized bytecode.
|
||||
pub fn compile(
|
||||
&self,
|
||||
compiler: &mut crate::project::ProjectCompiler,
|
||||
name: &str,
|
||||
) -> Result<crate::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &self.forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
let mut module = compiler.compile(name, &self.units, &catalog, &self.forms)?;
|
||||
if self
|
||||
.manifest
|
||||
.startup
|
||||
.as_ref()
|
||||
.is_some_and(|p| !has_extension(p, "frm"))
|
||||
{
|
||||
module.startup_form = None;
|
||||
}
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
|
||||
/// BASIC RUN resolves relative to the execution target, never to process cwd.
|
||||
pub fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
|
||||
let base = current.parent().unwrap_or(Path::new("."));
|
||||
if Path::new(program).extension().is_some() {
|
||||
return relative_case_insensitive(base, program)
|
||||
.and_then(|p| identity(&p))
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
for extension in ["bas", "frm", "mak", "tbc"] {
|
||||
if let Ok(path) = relative_case_insensitive(base, &format!("{program}.{extension}")) {
|
||||
return identity(&path).map_err(|e| e.to_string());
|
||||
}
|
||||
}
|
||||
Err(format!("{program}: Programm nicht gefunden"))
|
||||
}
|
||||
|
||||
pub fn load_program(path: &Path) -> Result<crate::bytecode::CompiledModule, String> {
|
||||
if has_extension(path, "tbc") {
|
||||
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
|
||||
return crate::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| e.to_string());
|
||||
}
|
||||
SourceLoader::default()
|
||||
.load(path)?
|
||||
.compile(&mut Default::default(), &module_name(path))
|
||||
.map_err(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
})
|
||||
}
|
||||
|
||||
/// Fresh runtime for every start, restart and RUN in both frontends.
|
||||
pub fn new_execution(
|
||||
module: crate::bytecode::CompiledModule,
|
||||
command: &str,
|
||||
size: Option<(usize, usize)>,
|
||||
line: Option<u32>,
|
||||
) -> Result<crate::interp::Vm, tb_runtime::errors::RuntimeError> {
|
||||
let mut vm = crate::interp::Vm::new(module);
|
||||
if let Some(line) = line {
|
||||
vm.start_at_line(line)?;
|
||||
}
|
||||
if let Some((cols, rows)) = size {
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
vm.forms.resize(cols, rows);
|
||||
}
|
||||
vm.rt.command = command.to_owned();
|
||||
vm.set_poll_interrupt(true);
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
impl SourceLoader {
|
||||
pub fn insert(&mut self, path: &Path, content: Content) -> Result<(), String> {
|
||||
let key = identity(path).map_err(|e| e.to_string())?;
|
||||
@@ -362,7 +439,16 @@ impl SourceLoader {
|
||||
pub fn load_manifest(&self, manifest: Manifest) -> Result<ProjectSources, String> {
|
||||
let mut units = Vec::new();
|
||||
let mut forms = Vec::new();
|
||||
for path in manifest.members() {
|
||||
let mut members: Vec<_> = manifest.members().collect();
|
||||
if let Some(startup) = &manifest.startup {
|
||||
let index = members
|
||||
.iter()
|
||||
.position(|p| *p == startup)
|
||||
.ok_or("Startdatei ist kein Projektmitglied")?;
|
||||
let selected = members.remove(index);
|
||||
members.insert(0, selected);
|
||||
}
|
||||
for path in members {
|
||||
let content = self.read(path)?;
|
||||
let (name, code, first_line) = match content {
|
||||
Content::Text(text) => (module_name(path), text, 1),
|
||||
|
||||
252
crates/tb-vm/tests/cooperative.rs
Normal file
252
crates/tb-vm/tests/cooperative.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use std::collections::VecDeque;
|
||||
use tb_runtime::{
|
||||
host::{Ereignis, Host},
|
||||
screen::TextScreen,
|
||||
snapshot,
|
||||
value::Value,
|
||||
};
|
||||
use tb_vm::interp::{PollResult, RunEvent, Vm};
|
||||
#[derive(Default)]
|
||||
struct HostStub {
|
||||
now: u64,
|
||||
clocks: usize,
|
||||
input: VecDeque<Ereignis>,
|
||||
}
|
||||
impl Host for HostStub {
|
||||
fn present(&mut self, _: &TextScreen) {}
|
||||
fn next_event(&mut self, blocking: bool) -> Option<Ereignis> {
|
||||
assert!(!blocking);
|
||||
self.input.pop_front()
|
||||
}
|
||||
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
|
||||
panic!("poll blocked")
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.clocks += 1;
|
||||
self.now
|
||||
}
|
||||
}
|
||||
fn vm(source: &str) -> Vm {
|
||||
Vm::new(tb_vm::compile_source("TEST", source).unwrap())
|
||||
}
|
||||
fn text(vm: &Vm, name: &str) -> String {
|
||||
match vm.inspect(name) {
|
||||
Some(Value::Str(s)) => s.to_string(),
|
||||
v => panic!("{v:?}"),
|
||||
}
|
||||
}
|
||||
fn input(host: &mut HostStub, chars: &str) {
|
||||
host.input
|
||||
.extend(chars.chars().map(|c| Ereignis::Taste(c.to_string(), 0)));
|
||||
}
|
||||
fn poll(vm: &mut Vm, host: &mut HostStub) -> PollResult {
|
||||
vm.poll(host, 200)
|
||||
}
|
||||
fn end(vm: &mut Vm, host: &mut HostStub) {
|
||||
for _ in 0..1000 {
|
||||
match poll(vm, host) {
|
||||
PollResult::Event(RunEvent::Ended) => return,
|
||||
PollResult::Yield | PollResult::Waiting { .. } => {}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
panic!("did not end");
|
||||
}
|
||||
fn pause(vm: &mut Vm, host: &mut HostStub) {
|
||||
vm.rt.abbruch = true;
|
||||
assert!(matches!(
|
||||
poll(vm, host),
|
||||
PollResult::Event(RunEvent::Interrupted { .. })
|
||||
));
|
||||
vm.rt.abbruch = false;
|
||||
}
|
||||
#[test]
|
||||
fn finite_slices_preserve_events_without_clock_queries() {
|
||||
for source in ["DO\nLOOP", "10 GOTO 10"] {
|
||||
let mut vm = vm(source);
|
||||
vm.set_poll_interrupt(true);
|
||||
let mut host = HostStub::default();
|
||||
for _ in 0..10 {
|
||||
assert_eq!(poll(&mut vm, &mut host), PollResult::Yield);
|
||||
}
|
||||
assert_eq!(host.clocks, 0);
|
||||
pause(&mut vm, &mut host);
|
||||
}
|
||||
let mut vm = vm("STOP\nPRINT 42\nEND");
|
||||
let mut host = HostStub::default();
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Event(RunEvent::Stopped { .. })
|
||||
));
|
||||
end(&mut vm, &mut host);
|
||||
assert!(snapshot::text(&vm.rt.screen).contains("42"));
|
||||
assert_eq!(host.clocks, 0);
|
||||
}
|
||||
#[test]
|
||||
fn partial_console_operations_keep_prompt_arguments_and_assignment() {
|
||||
for instruction in [
|
||||
"INPUT \"Name\"; s$",
|
||||
"LINE INPUT \"Name\"; s$",
|
||||
"s$=INPUT$(3)",
|
||||
] {
|
||||
let mut vm = vm(&format!("{instruction}\nn%=n%+1\nEND"));
|
||||
let mut host = HostStub::default();
|
||||
input(&mut host, "a");
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting { .. }
|
||||
));
|
||||
let before = snapshot::text(&vm.rt.screen);
|
||||
pause(&mut vm, &mut host);
|
||||
assert_eq!(snapshot::text(&vm.rt.screen), before);
|
||||
input(
|
||||
&mut host,
|
||||
if instruction.contains("INPUT$(") {
|
||||
"bc"
|
||||
} else {
|
||||
"bc\r"
|
||||
},
|
||||
);
|
||||
end(&mut vm, &mut host);
|
||||
assert_eq!(text(&vm, "s"), "abc");
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
|
||||
assert!(snapshot::text(&vm.rt.screen).matches("Name").count() <= 1);
|
||||
}
|
||||
let mut vm = vm("RANDOMIZE\nPRINT RND\nEND");
|
||||
let mut host = HostStub::default();
|
||||
input(&mut host, "12");
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting { .. }
|
||||
));
|
||||
pause(&mut vm, &mut host);
|
||||
input(&mut host, "3\r");
|
||||
end(&mut vm, &mut host);
|
||||
assert_eq!(
|
||||
snapshot::text(&vm.rt.screen)
|
||||
.matches("Random Number Seed")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn sleep_deadline_survives_pause_and_does_not_pop_argument_twice() {
|
||||
let mut vm = vm("SLEEP 2\nn%=n%+1\nEND");
|
||||
let mut host = HostStub::default();
|
||||
assert_eq!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting {
|
||||
deadline: Some(2000)
|
||||
}
|
||||
);
|
||||
host.now = 1000;
|
||||
pause(&mut vm, &mut host);
|
||||
assert_eq!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting {
|
||||
deadline: Some(2000)
|
||||
}
|
||||
);
|
||||
host.now = 2000;
|
||||
end(&mut vm, &mut host);
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
|
||||
}
|
||||
#[test]
|
||||
fn dialogs_keep_partial_text_focus_and_caller_arguments() {
|
||||
for (source, chars) in [
|
||||
("s$=INPUTBOX$(\"Prompt\",\"Title\")\nEND", "ab"),
|
||||
("n%=MSGBOX(\"Prompt\",256+1,\"Title\")\nEND", ""),
|
||||
] {
|
||||
let mut vm = vm(source);
|
||||
let mut host = HostStub::default();
|
||||
input(&mut host, chars);
|
||||
for _ in 0..5 {
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting { .. }
|
||||
));
|
||||
}
|
||||
pause(&mut vm, &mut host);
|
||||
input(&mut host, "\r");
|
||||
end(&mut vm, &mut host);
|
||||
if chars.is_empty() {
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(2))));
|
||||
} else {
|
||||
assert_eq!(text(&vm, "s"), "ab");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn modal_and_modeless_forms_resume_handlers_across_tiny_budgets() {
|
||||
use tb_frontend::forms::{FormCatalog, ObjectClass};
|
||||
let mut catalog = FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
|
||||
for modal in [false, true] {
|
||||
let source = format!("Timer1.Interval=100\nTimer1.Enabled=-1\nForm1.Show {}\n{}\nSUB Timer1_Timer\nFOR i%=1 TO 100\nNEXT\nForm1.Hide\nEND SUB", if modal {"1"} else {"0"}, if modal {"END"} else {""});
|
||||
let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", &source, &catalog).unwrap());
|
||||
let mut host = HostStub::default();
|
||||
let result = poll(&mut vm, &mut host);
|
||||
if modal {
|
||||
assert!(matches!(result, PollResult::Waiting { .. }));
|
||||
} else {
|
||||
assert_eq!(result, PollResult::Event(RunEvent::Ended));
|
||||
}
|
||||
pause(&mut vm, &mut host);
|
||||
host.now = 100;
|
||||
for _ in 0..1000 {
|
||||
let result = if modal {
|
||||
vm.poll(&mut host, 3)
|
||||
} else {
|
||||
vm.poll_visible_forms(&mut host, 3)
|
||||
};
|
||||
if result == PollResult::Event(RunEvent::Ended) && !vm.forms.has_visible_forms() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(!vm.forms.has_visible_forms());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn pending_dialog_keeps_physical_resize_and_modeless_handler_finishes_after_hide() {
|
||||
let mut vm = vm("s$=INPUTBOX$(\"Prompt\")\nEND");
|
||||
let mut host = HostStub::default();
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting { .. }
|
||||
));
|
||||
vm.rt.ereignis(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
input(&mut host, "\r");
|
||||
end(&mut vm, &mut host);
|
||||
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
|
||||
use tb_frontend::forms::{FormCatalog, ObjectClass};
|
||||
let mut catalog = FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
let module=tb_vm::compile_source_with_forms("FORM1","Form1.Show\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nForm1.Hide\nPRINT \"after hide\"\nEND SUB",&catalog).unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
assert_eq!(poll(&mut vm, &mut host), PollResult::Event(RunEvent::Ended));
|
||||
input(&mut host, "a");
|
||||
for _ in 0..100 {
|
||||
if vm.poll_visible_forms(&mut host, 1) == PollResult::Event(RunEvent::Ended) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(snapshot::text(&vm.rt.screen).contains("after hide"));
|
||||
}
|
||||
#[test]
|
||||
fn common_dialog_external_call_keeps_byref_arguments_until_completion() {
|
||||
let mut vm=vm("DECLARE SUB FindText (s AS STRING, flags AS INTEGER, cancelled AS INTEGER)\ns$=\"before\"\nFindText s$, flags%, cancelled%\nEND");
|
||||
let mut host = HostStub::default();
|
||||
assert!(matches!(
|
||||
poll(&mut vm, &mut host),
|
||||
PollResult::Waiting { .. }
|
||||
));
|
||||
pause(&mut vm, &mut host);
|
||||
input(&mut host, "\r");
|
||||
end(&mut vm, &mut host);
|
||||
assert_eq!(text(&vm, "s"), "before");
|
||||
assert!(matches!(vm.inspect("cancelled"), Some(Value::Int(0))));
|
||||
}
|
||||
Reference in New Issue
Block a user