Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -304,6 +304,14 @@ instrs! {
|
||||
0xB2 RetFn;
|
||||
0xB3 CallBuiltin(a: u16, b: u8);
|
||||
|
||||
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
|
||||
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel
|
||||
0xE1 TrapDisable(a: u8); // GOSUB 0
|
||||
0xE2 TrapSet(a: u8, b: u8); // Quellenart, Zustand
|
||||
0xE3 EventSwitch(a: bool);
|
||||
0xE4 Doevents; // Zustellpunkt; legt 0 ab
|
||||
0xE5 Sleep(a: bool); // hat Argument? Dauer vom Stack (Sekunden)
|
||||
|
||||
// 0xC0 — Fehlerbehandlung
|
||||
0xC0 OnErrorGoto(a: u32);
|
||||
0xC1 OnErrorLocal(a: u32);
|
||||
@@ -614,6 +622,12 @@ mod tests {
|
||||
Instr::Call(2, 3),
|
||||
Instr::Input(2, false, 0xFFFF, true),
|
||||
Instr::ConvCyR8,
|
||||
Instr::TrapDefine(1, 4242),
|
||||
Instr::TrapDisable(3),
|
||||
Instr::TrapSet(0, 2),
|
||||
Instr::EventSwitch(false),
|
||||
Instr::Doevents,
|
||||
Instr::Sleep(true),
|
||||
Instr::RetFn,
|
||||
];
|
||||
let mut buf = Vec::new();
|
||||
|
||||
@@ -95,6 +95,24 @@ impl ProcCtx {
|
||||
fn here(&self) -> u32 {
|
||||
self.code.len() as u32
|
||||
}
|
||||
/// Label auf die unmittelbar zuvor emittierte `Stmt`-Grenze binden.
|
||||
///
|
||||
/// Schleifen brauchen im Kreis eine Anweisungsgrenze — sonst wird dort
|
||||
/// nie ein Ereignis zugestellt, kein Breakpoint erreicht und kein
|
||||
/// Abbruch bemerkt (`FOR i = 1 TO 10000: NEXT` als Warteschleife).
|
||||
/// Wo die Grenze ohnehin direkt vor dem Schleifenkopf liegt, zeigt der
|
||||
/// Rücksprung einfach auf sie, statt eine zweite zu emittieren.
|
||||
fn bind_auf_stmt(&mut self, label: u16) {
|
||||
let pc = self.code.len().saturating_sub(1) as u32;
|
||||
debug_assert!(
|
||||
matches!(self.code.last(), Some(Instr::Stmt(_))),
|
||||
"bind_auf_stmt ohne vorangehende Anweisungsgrenze"
|
||||
);
|
||||
if (label as usize) >= self.label_pc.len() {
|
||||
self.label_pc.resize(label as usize + 1, None);
|
||||
}
|
||||
self.label_pc[label as usize] = Some(pc);
|
||||
}
|
||||
fn bind(&mut self, label: u16) {
|
||||
let pc = self.here();
|
||||
if (label as usize) >= self.label_pc.len() {
|
||||
@@ -170,7 +188,8 @@ impl Codegen {
|
||||
| Instr::RetGosubTo(t)
|
||||
| Instr::ResumeLabel(t)
|
||||
| Instr::OnErrorGoto(t)
|
||||
| Instr::OnErrorLocal(t) => *t = pc,
|
||||
| Instr::OnErrorLocal(t)
|
||||
| Instr::TrapDefine(_, t) => *t = pc,
|
||||
other => unreachable!("Fixup auf {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -330,9 +349,14 @@ impl Codegen {
|
||||
ctx.bind(l_end);
|
||||
}
|
||||
}
|
||||
HStmtKind::Loop { pre, post, body, exit_label } => {
|
||||
HStmtKind::Loop {
|
||||
pre,
|
||||
post,
|
||||
body,
|
||||
exit_label,
|
||||
} => {
|
||||
let l_start = ctx.new_label();
|
||||
ctx.bind(l_start);
|
||||
ctx.bind_auf_stmt(l_start);
|
||||
if let Some((is_until, cond)) = pre {
|
||||
self.expr(ctx, cond);
|
||||
if *is_until {
|
||||
@@ -370,19 +394,49 @@ impl Codegen {
|
||||
exit_label,
|
||||
} => {
|
||||
self.gen_for(
|
||||
ctx, proc, var, *ty, from, to, step.as_ref(), *limit_slot, *step_slot, body,
|
||||
ctx,
|
||||
proc,
|
||||
var,
|
||||
*ty,
|
||||
from,
|
||||
to,
|
||||
step.as_ref(),
|
||||
*limit_slot,
|
||||
*step_slot,
|
||||
body,
|
||||
*exit_label,
|
||||
stmt.line,
|
||||
);
|
||||
}
|
||||
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
||||
HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(0), *l),
|
||||
HStmtKind::OnGoto { sel, gosub, targets } => {
|
||||
HStmtKind::OnGoto {
|
||||
sel,
|
||||
gosub,
|
||||
targets,
|
||||
} => {
|
||||
self.expr(ctx, sel);
|
||||
let table = self.jump_tables.len();
|
||||
self.jump_tables.push(Vec::new());
|
||||
ctx.table_fixups.push((table, targets.clone()));
|
||||
ctx.emit(Instr::OnJump(table as u16, *gosub));
|
||||
}
|
||||
HStmtKind::TrapDef { art, index, ziel } => {
|
||||
self.expr(ctx, index);
|
||||
match ziel {
|
||||
Some(l) => ctx.emit_jump(Instr::TrapDefine(*art, 0), *l),
|
||||
None => ctx.emit(Instr::TrapDisable(*art)),
|
||||
}
|
||||
}
|
||||
HStmtKind::TrapSet {
|
||||
art,
|
||||
index,
|
||||
zustand,
|
||||
} => {
|
||||
self.expr(ctx, index);
|
||||
ctx.emit(Instr::TrapSet(*art, *zustand));
|
||||
}
|
||||
HStmtKind::EventSwitch(an) => ctx.emit(Instr::EventSwitch(*an)),
|
||||
HStmtKind::ReturnGosub(target) => match target {
|
||||
None => ctx.emit(Instr::RetGosub),
|
||||
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
|
||||
@@ -398,6 +452,17 @@ impl Codegen {
|
||||
for a in args {
|
||||
self.expr(ctx, a);
|
||||
}
|
||||
// `DOEVENTS` und `SLEEP` sind Zustellpunkte und damit Sache
|
||||
// der Ausführung, nicht der Bibliothek: ein Builtin kann
|
||||
// keine Ereignisprozedur des Programms aufrufen
|
||||
// (design.md, D1).
|
||||
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
|
||||
ctx.emit(i);
|
||||
if matches!(b, Builtin::Doevents) {
|
||||
ctx.emit(Instr::Pop);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let id = builtin_id(*b);
|
||||
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
|
||||
if builtin_returns_value(*b) {
|
||||
@@ -484,6 +549,7 @@ impl Codegen {
|
||||
step_slot: Option<VarSlot>,
|
||||
body: &[HStmt],
|
||||
exit_label: u16,
|
||||
line: u32,
|
||||
) {
|
||||
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
||||
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
||||
@@ -501,6 +567,15 @@ impl Codegen {
|
||||
let l_test = ctx.new_label();
|
||||
let l_body = ctx.new_label();
|
||||
ctx.bind(l_test);
|
||||
// Anweisungsgrenze für den Rücksprung von `NEXT`. Sie kann nicht
|
||||
// auf die Grenze des `FOR` zeigen — dort stünde die Initialisierung
|
||||
// noch einmal.
|
||||
//
|
||||
// ponytail: gemeldet wird die Zeile des `FOR`, nicht die des
|
||||
// `NEXT` — die trägt das HIR nicht. Ceiling: braucht der Debugger
|
||||
// in Phase 5 die genaue Zeile, bekommt `HStmtKind::For` ein Feld
|
||||
// `next_line`.
|
||||
ctx.emit(Instr::Stmt(line));
|
||||
match const_step {
|
||||
Some(s) => {
|
||||
// Vergleichsrichtung zur Compilezeit.
|
||||
@@ -625,6 +700,10 @@ impl Codegen {
|
||||
for a in args {
|
||||
self.expr(ctx, a);
|
||||
}
|
||||
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
|
||||
ctx.emit(i);
|
||||
return;
|
||||
}
|
||||
ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8));
|
||||
}
|
||||
HExpr::ArrayBound { lower, place, dim } => {
|
||||
@@ -934,6 +1013,15 @@ fn emit_conv(ctx: &mut ProcCtx, from: NumTy, to: NumTy) {
|
||||
|
||||
/// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit.
|
||||
/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege.
|
||||
/// `DOEVENTS`/`SLEEP` → eigene Instruktion statt Builtin-Aufruf.
|
||||
fn zustellpunkt_instr(b: Builtin, argc: usize) -> Option<Instr> {
|
||||
match b {
|
||||
Builtin::Doevents => Some(Instr::Doevents),
|
||||
Builtin::Sleep => Some(Instr::Sleep(argc > 0)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn builtin_id(b: Builtin) -> u16 {
|
||||
match b {
|
||||
Builtin::Len => ids::LEN,
|
||||
@@ -1060,6 +1148,7 @@ fn builtin_id(b: Builtin) -> u16 {
|
||||
Builtin::CommandS => ids::COMMAND_S,
|
||||
Builtin::Doevents => ids::DOEVENTS,
|
||||
Builtin::Sleep => ids::SLEEP,
|
||||
Builtin::SetUEvent => ids::SETUEVENT,
|
||||
Builtin::Beep => ids::BEEP,
|
||||
// ISAM
|
||||
Builtin::IsamOpen => ids::ISAM_OPEN,
|
||||
|
||||
@@ -15,9 +15,8 @@ use std::rc::Rc;
|
||||
use tb_runtime::builtins::{builtin_table, RtState};
|
||||
use tb_runtime::errors::RuntimeError;
|
||||
use tb_runtime::host::Host;
|
||||
use tb_runtime::value::{
|
||||
self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef,
|
||||
};
|
||||
use tb_runtime::traps::Quelle;
|
||||
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
|
||||
|
||||
/// Warum die VM die Kontrolle abgibt.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -54,6 +53,11 @@ struct Frame {
|
||||
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
|
||||
last_stmt_pc: usize,
|
||||
line: u32,
|
||||
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
|
||||
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
|
||||
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
|
||||
/// beendet den Handler (design.md, D2).
|
||||
trap: Option<Quelle>,
|
||||
}
|
||||
|
||||
pub struct Vm {
|
||||
@@ -65,7 +69,12 @@ pub struct Vm {
|
||||
pub rt: RtState,
|
||||
// Fehlerzustand
|
||||
err: u16,
|
||||
/// Wert von `ERL`: die Zeilennummer, in der der Fehler auftrat. Wird
|
||||
/// **nur** beim Auslösen gesetzt — liefe sie mit `zeile_nr` mit, meldete
|
||||
/// ein Handler auf einer nummerierten Zeile seine eigene Nummer.
|
||||
erl: u32,
|
||||
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
|
||||
zeile_nr: u32,
|
||||
module_handler: Handler,
|
||||
in_handler: bool,
|
||||
resume_pc: usize,
|
||||
@@ -77,6 +86,34 @@ pub struct Vm {
|
||||
data_ptr: usize,
|
||||
}
|
||||
|
||||
/// Quellenarten, wie der Codegenerator sie kodiert.
|
||||
const ART_KEY: u8 = 0;
|
||||
const ART_TIMER: u8 = 1;
|
||||
const ART_UEVENT: u8 = 2;
|
||||
const ART_SIGNAL: u8 = 3;
|
||||
|
||||
/// Art + Kennung → Quelle. Die Kennung kann ein Ausdruck sein, deshalb
|
||||
/// wird der Wertebereich hier noch einmal geprüft (Fehler 5).
|
||||
///
|
||||
/// Das Zeitintervall gehört **nicht** hierher: `TIMER ON` trägt keine
|
||||
/// Kennung, geprüft wird es nur an `ON TIMER(n&)`.
|
||||
fn trap_quelle(art: u8, n: i32) -> Result<Quelle, RuntimeError> {
|
||||
let ok = match art {
|
||||
ART_KEY => matches!(n, 0..=25 | 30 | 31),
|
||||
ART_SIGNAL => (1..=2).contains(&n),
|
||||
_ => true,
|
||||
};
|
||||
if !ok {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
Ok(match art {
|
||||
ART_KEY => Quelle::Key(n as u8),
|
||||
ART_TIMER => Quelle::Timer,
|
||||
ART_UEVENT => Quelle::UEvent,
|
||||
_ => Quelle::Signal(n as u8),
|
||||
})
|
||||
}
|
||||
|
||||
enum Flow {
|
||||
Normal,
|
||||
Event(RunEvent),
|
||||
@@ -97,6 +134,7 @@ impl Vm {
|
||||
rt: RtState::default(),
|
||||
err: 0,
|
||||
erl: 0,
|
||||
zeile_nr: 0,
|
||||
module_handler: Handler::None,
|
||||
in_handler: false,
|
||||
resume_pc: 0,
|
||||
@@ -134,9 +172,88 @@ impl Vm {
|
||||
local_handler: Handler::None,
|
||||
last_stmt_pc: 0,
|
||||
line: 0,
|
||||
trap: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
|
||||
/// `true`, wenn ein Handler aufgesetzt wurde.
|
||||
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
if !self.rt.traps.aktiv() {
|
||||
return false;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
|
||||
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
|
||||
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
|
||||
/// dem `SLEEP` weiter.
|
||||
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
|
||||
let start = host.jetzt_ms();
|
||||
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
|
||||
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
|
||||
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
|
||||
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
|
||||
// virtuelle nie vorrückt.
|
||||
let real_start = std::time::Instant::now();
|
||||
loop {
|
||||
self.rt.tick(host);
|
||||
if self.zustellen(host) {
|
||||
return;
|
||||
}
|
||||
if self.rt.ende || self.rt.abbruch {
|
||||
return;
|
||||
}
|
||||
if sekunden <= 0.0 {
|
||||
// Ohne Argument: bis zum Tastendruck.
|
||||
if self.rt.naechste_taste(host, false).is_some() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let vergangen = host.jetzt_ms().saturating_sub(start);
|
||||
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
fn trap_frame_aufsetzen(&mut self, q: Quelle, ziel: u32) {
|
||||
let locals_base = self.frames[0].locals_base;
|
||||
let stack_base = self.stack.len();
|
||||
self.frames.push(Frame {
|
||||
proc: 0,
|
||||
pc: ziel as usize,
|
||||
locals_base,
|
||||
stack_base,
|
||||
gosub: Vec::new(),
|
||||
local_handler: Handler::None,
|
||||
last_stmt_pc: 0,
|
||||
line: 0,
|
||||
trap: Some(q),
|
||||
});
|
||||
}
|
||||
|
||||
/// Trap-Frame abräumen. Anders als `pop_frame` bleiben die Locals
|
||||
/// stehen — sie gehören dem Modulrumpf, nicht dem Handler.
|
||||
fn trap_frame_abraeumen(&mut self) -> Option<Quelle> {
|
||||
let f = self.frames.pop()?;
|
||||
self.stack.truncate(f.stack_base);
|
||||
f.trap
|
||||
}
|
||||
|
||||
fn pop_frame(&mut self) {
|
||||
let f = self.frames.pop().expect("Frame-Unterlauf");
|
||||
self.locals.truncate(f.locals_base);
|
||||
@@ -273,6 +390,8 @@ impl Vm {
|
||||
// Fehler im Handler: fatal, keine Kaskade.
|
||||
return Some(self.error_event(code, line));
|
||||
}
|
||||
// `ERL` friert auf der Zeile ein, in der es passiert ist.
|
||||
self.erl = self.zeile_nr;
|
||||
// Frame-Kette von innen nach außen nach LOCAL-Handlern absuchen.
|
||||
let mut target: Option<(usize, Handler)> = None;
|
||||
for (depth, f) in self.frames.iter().enumerate().rev() {
|
||||
@@ -523,6 +642,24 @@ impl Vm {
|
||||
self.rt.tick(host);
|
||||
self.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
|
||||
// ein Trap definiert ist — sonst kostet die Grenze nichts.
|
||||
if self.rt.traps.aktiv() {
|
||||
// ponytail: die Uhr nur alle 64 Anweisungen lesen, die
|
||||
// Tastenschlange dagegen jedes Mal. Ceiling: die
|
||||
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
|
||||
// reicht das nicht, wird daraus ein eigener Zähler je
|
||||
// Trap.
|
||||
if self.tick_zaehler % 64 == 0 {
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
}
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
}
|
||||
if self.flags != 0 {
|
||||
if self.flags & F_STEP != 0 {
|
||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||
@@ -536,12 +673,64 @@ impl Vm {
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
// ---- Ereignis-Traps (Sprachreferenz §8) ----
|
||||
I::TrapDefine(art, ziel) => {
|
||||
let n = self.pop_i32()?;
|
||||
let q = trap_quelle(art, n)?;
|
||||
if art == ART_TIMER {
|
||||
if !(1..=86_400).contains(&n) {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.timer_intervall(n as u64, jetzt);
|
||||
}
|
||||
self.rt.traps.definieren(q, Some(ziel));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::TrapDisable(art) => {
|
||||
let n = self.pop_i32()?;
|
||||
let q = trap_quelle(art, n)?;
|
||||
self.rt.traps.definieren(q, None);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::TrapSet(art, zustand) => {
|
||||
let n = self.pop_i32()?;
|
||||
let q = trap_quelle(art, n)?;
|
||||
let z = match zustand {
|
||||
0 => tb_runtime::traps::Zustand::An,
|
||||
1 => tb_runtime::traps::Zustand::Aus,
|
||||
_ => tb_runtime::traps::Zustand::Gestoppt,
|
||||
};
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.setzen(q, z, jetzt);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Doevents => {
|
||||
// Wert zuerst ablegen: ein hier zugestellter Handler
|
||||
// bekommt seinen eigenen Stapelabschnitt darüber und
|
||||
// lässt den Wert unberührt.
|
||||
self.stack.push(Value::Int(0));
|
||||
self.rt.tick(host);
|
||||
self.zustellen(host);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Sleep(mit_arg) => {
|
||||
let sekunden = if mit_arg { self.pop_f64()? } else { 0.0 };
|
||||
self.schlafen(host, sekunden);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::EventSwitch(an) => {
|
||||
self.rt.traps.event(an);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::SetErl(n) => {
|
||||
self.erl = n;
|
||||
self.zeile_nr = n;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::End | I::SystemInstr => Ok(Flow::Event(RunEvent::Ended)),
|
||||
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped { line: self.current_line() })),
|
||||
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped {
|
||||
line: self.current_line(),
|
||||
})),
|
||||
I::Unsupported(_) => Err(RuntimeError(73)), // Advanced feature
|
||||
|
||||
I::PushInt(v) => {
|
||||
@@ -1086,6 +1275,14 @@ impl Vm {
|
||||
f.pc = ret;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
// Leerer GOSUB-Stapel in einem Trap-Frame: das `RETURN`
|
||||
// beendet den Handler und führt an der unterbrochenen
|
||||
// Stelle weiter.
|
||||
None if f.trap.is_some() => {
|
||||
let q = self.trap_frame_abraeumen().unwrap();
|
||||
self.rt.traps.handler_beendet(q);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
|
||||
}
|
||||
}
|
||||
@@ -1096,6 +1293,20 @@ impl Vm {
|
||||
f.pc = t as usize;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
// `RETURN label` aus einem Trap-Handler: der Handler
|
||||
// endet, die unterbrochene Aufrufkette wird verworfen
|
||||
// und der Modulrumpf läuft am Label weiter. Das ist die
|
||||
// klassische — und bewusst rabiate — Bedeutung.
|
||||
None if f.trap.is_some() => {
|
||||
let q = self.trap_frame_abraeumen().unwrap();
|
||||
self.rt.traps.handler_beendet(q);
|
||||
while self.frames.len() > 1 {
|
||||
self.pop_frame();
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
f.pc = t as usize;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,9 +476,10 @@ fn print_zonen_und_tab() {
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
|
||||
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
|
||||
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
|
||||
let (ev, _) = run("SETUEVENT");
|
||||
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
|
||||
let (ev, _) = run("RUN");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
@@ -510,9 +511,8 @@ fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
|
||||
/// die Eingabeaufforderung unsichtbar da.
|
||||
#[test]
|
||||
fn ausgabe_erscheint_vor_der_eingabe() {
|
||||
use tb_runtime::host::{CaptureHost, Host};
|
||||
let module =
|
||||
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
|
||||
use tb_runtime::host::CaptureHost;
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::with_input(&["7"]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
@@ -534,10 +534,10 @@ fn eingabe_wird_geechot_und_rueckschritt_loescht() {
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['a', 'b', 'x'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
host.ereignis(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string()));
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.to_string()));
|
||||
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string(), 0));
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let text = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// Erste Zeile: das Echo ohne das gelöschte Zeichen.
|
||||
@@ -609,8 +609,8 @@ fn inkey_liefert_zeichen_und_sondertaste() {
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("q".into()));
|
||||
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1
|
||||
host.ereignis(Ereignis::Taste("q".into(), 0));
|
||||
host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // F1
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let t = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// "q", dann Länge 2 und führendes Nullzeichen.
|
||||
@@ -624,7 +624,7 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['x', 'y', 'z', 'w'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
host.ereignis(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
// Nur das PRINT erscheint — INPUT$ echot nicht.
|
||||
@@ -774,3 +774,322 @@ fn rueckfall_auf_utc_laeuft_fehlerfrei() {
|
||||
);
|
||||
assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
|
||||
}
|
||||
|
||||
// ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) ---------------------
|
||||
|
||||
/// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem
|
||||
/// Zustellpunkt um `schritt_ms` vor (design.md, D6).
|
||||
fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
|
||||
struct UhrHost {
|
||||
inner: CaptureHost,
|
||||
schritt: u64,
|
||||
}
|
||||
impl tb_runtime::host::Host for UhrHost {
|
||||
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
|
||||
self.inner.present(s);
|
||||
}
|
||||
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
self.inner.next_event(b)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
let j = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt);
|
||||
j
|
||||
}
|
||||
}
|
||||
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
|
||||
panic!("Compile-Fehler: {d:?}");
|
||||
});
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = UhrHost {
|
||||
inner: CaptureHost::default(),
|
||||
schritt: schritt_ms,
|
||||
};
|
||||
let ev = vm.run(&mut host);
|
||||
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeit_trap_laeuft_und_kehrt_zurueck() {
|
||||
// Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die
|
||||
// Schleife unveraendert weiter — der unterbrochene Zustand bleibt.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"i=\"; i%\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("i= 201"),
|
||||
"Schleife lief nicht sauber weiter: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_timer_on_kein_trap() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"fertig\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}");
|
||||
assert!(out.contains("fertig"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
CALL Rechne\n\
|
||||
PRINT \"ende\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n\
|
||||
SUB Rechne\n\
|
||||
FOR j% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"sub\"; j%\n\
|
||||
END SUB\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("sub 201"),
|
||||
"SUB lief nicht sauber weiter: {out:?}"
|
||||
);
|
||||
assert!(out.contains("ende"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_off_klammert_die_zustellung() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
EVENT OFF\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
EVENT ON\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
let text = out.trim();
|
||||
let strich = text.find('|').expect("Trennzeichen fehlt");
|
||||
assert!(
|
||||
!text[..strich].contains('T'),
|
||||
"waehrend EVENT OFF wurde zugestellt: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
text[strich..].contains('T'),
|
||||
"nach EVENT ON wurde nicht zugestellt: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_off_im_handler_ueberlebt_das_return() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 400\n\
|
||||
NEXT\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
TIMER OFF\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert_eq!(
|
||||
out.trim().matches('T').count(),
|
||||
1,
|
||||
"RETURN hat den Trap wieder eingeschaltet: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gosub_null_schaltet_den_trap_ab() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
ON TIMER(1) GOSUB 0\n\
|
||||
FOR i% = 1 TO 300\n\
|
||||
NEXT\n\
|
||||
PRINT \"fertig\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setuevent_loest_den_uevent_trap_aus() {
|
||||
let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\
|
||||
UEVENT ON\n\
|
||||
CALL SetUEvent\n\
|
||||
FOR i% = 1 TO 20\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
END\n\
|
||||
Nutzer:\n\
|
||||
PRINT \"U\";\n\
|
||||
RETURN\n");
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kennung_ausserhalb_des_bereichs_ist_fehler_5() {
|
||||
// Nicht konstant, also erst zur Laufzeit pruefbar.
|
||||
let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n";
|
||||
assert_eq!(err_code(src), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leere_warteschleife_hat_eine_anweisungsgrenze() {
|
||||
// `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne
|
||||
// Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt,
|
||||
// kein Breakpoint erreicht und kein Abbruch bemerkt.
|
||||
use tb_vm::bytecode::Instr;
|
||||
for src in [
|
||||
"FOR i% = 1 TO 3\nNEXT\n",
|
||||
"WHILE i% < 3\nWEND\n",
|
||||
"DO WHILE i% < 3\nLOOP\n",
|
||||
"DO\nLOOP UNTIL i% > 3\n",
|
||||
] {
|
||||
let m = tb_vm::compile_source("T", src).unwrap();
|
||||
let code = &m.procs[0].code;
|
||||
let mut kreise = 0;
|
||||
for (j, i) in code.iter().enumerate() {
|
||||
let ziel = match i {
|
||||
Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize,
|
||||
_ => continue,
|
||||
};
|
||||
if ziel >= j {
|
||||
continue;
|
||||
}
|
||||
kreise += 1;
|
||||
assert!(
|
||||
code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))),
|
||||
"Schleife ohne Anweisungsgrenze im Kreis: {src:?}"
|
||||
);
|
||||
}
|
||||
assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doevents_stellt_zu() {
|
||||
// Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 5\n\
|
||||
x% = DOEVENTS\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
let t = out.trim();
|
||||
let strich = t.find('|').expect("Trennzeichen fehlt");
|
||||
assert!(
|
||||
t[..strich].contains('T'),
|
||||
"DOEVENTS stellte nicht zu: {t:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doevents_liefert_null() {
|
||||
assert_eq!(out("PRINT DOEVENTS").trim(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_endet_durch_ein_ereignis() {
|
||||
// SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige
|
||||
// Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter.
|
||||
let t = std::time::Instant::now();
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
SLEEP 60\n\
|
||||
PRINT \"danach\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("danach"),
|
||||
"SLEEP kehrte nicht zurueck: {out:?}"
|
||||
);
|
||||
assert!(
|
||||
t.elapsed().as_secs() < 5,
|
||||
"SLEEP wartete die vollen 60 Sekunden ab"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() {
|
||||
// `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist
|
||||
// das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im
|
||||
// Handler), nicht an der unterbrochenen Stelle.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"10 ON ERROR GOTO 200\n 20 ON TIMER(1) GOSUB 90\n 30 TIMER ON\n 40 FOR i% = 1 TO 300\n 50 NEXT\n 60 PRINT \"|\";\n 70 END\n 90 ERROR 7\n 100 RETURN\n 200 PRINT \"E\"; ERR; ERL;\n 210 RESUME NEXT\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains(" 90"),
|
||||
"ERL nennt nicht die Zeile im Handler: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
|
||||
// Regression: der Handler steht selbst auf einer nummerierten Zeile.
|
||||
// `ERL` muss trotzdem die Zeile des Fehlers melden (Spec
|
||||
// vm-fehlerbehandlung, „ERR und ERL").
|
||||
let out = out("10 ON ERROR GOTO 100\n\
|
||||
20 ERROR 7\n\
|
||||
30 END\n\
|
||||
100 PRINT ERL;\n\
|
||||
110 RESUME NEXT\n");
|
||||
assert_eq!(out.trim(), "20");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user