Phase 4: Ereignisschleife und klassische Traps

This commit is contained in:
2026-09-04 15:56:47 +02:00
parent 060674b466
commit 5cf5a6582c
40 changed files with 4024 additions and 176 deletions

View File

@@ -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");
}