From 5cf5a6582cd3a95a0afe65de73217b53ab6feea9 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 4 Sep 2026 15:56:47 +0200 Subject: [PATCH] Phase 4: Ereignisschleife und klassische Traps --- Cargo.lock | 1 + Cargo.toml | 4 + PLAN.md | 8 +- crates/tb-cli/src/main.rs | 19 +- crates/tb-cli/tests/compat.rs | 163 +++++- crates/tb-frontend/src/ast.rs | 22 +- crates/tb-frontend/src/hir.rs | 18 + crates/tb-frontend/src/parser.rs | 97 +++- crates/tb-frontend/src/sema.rs | 292 ++++++++-- crates/tb-runtime/src/builtins.rs | 254 ++++++++- crates/tb-runtime/src/host.rs | 221 +++++++- crates/tb-runtime/src/lib.rs | 25 +- crates/tb-runtime/src/traps.rs | 520 ++++++++++++++++++ crates/tb-ui/Cargo.toml | 1 + crates/tb-ui/src/host.rs | 221 +++++++- crates/tb-ui/src/lib.rs | 7 +- crates/tb-ui/src/signale.rs | 112 ++++ crates/tb-vm/src/bytecode.rs | 14 + crates/tb-vm/src/codegen.rs | 99 +++- crates/tb-vm/src/interp.rs | 221 +++++++- crates/tb-vm/tests/vm.rs | 341 +++++++++++- docs/bibliothek.md | 16 +- docs/inventar.md | 22 +- docs/sprachreferenz.md | 75 ++- .../.openspec.yaml | 2 + .../design.md | 228 ++++++++ .../messung.md | 29 + .../proposal.md | 125 +++++ .../specs/ereignissteuerung/spec.md | 208 +++++++ .../specs/kompat-testkorpus/spec.md | 19 + .../specs/sprach-frontend/spec.md | 49 ++ .../specs/vm-ausfuehrung/spec.md | 42 ++ .../tasks.md | 60 ++ .../umfang-und-form.md | 261 +++++++++ openspec/specs/ereignissteuerung/spec.md | 210 +++++++ openspec/specs/kompat-testkorpus/spec.md | 18 + openspec/specs/sprach-frontend/spec.md | 48 ++ openspec/specs/vm-ausfuehrung/spec.md | 41 ++ tests/compat/traps.bas | 76 +++ tests/compat/traps.out | 11 + 40 files changed, 4024 insertions(+), 176 deletions(-) create mode 100644 crates/tb-runtime/src/traps.rs create mode 100644 crates/tb-ui/src/signale.rs create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/design.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/messung.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/proposal.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/ereignissteuerung/spec.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/kompat-testkorpus/spec.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/sprach-frontend/spec.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/vm-ausfuehrung/spec.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/tasks.md create mode 100644 openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/umfang-und-form.md create mode 100644 openspec/specs/ereignissteuerung/spec.md create mode 100644 tests/compat/traps.bas create mode 100644 tests/compat/traps.out diff --git a/Cargo.lock b/Cargo.lock index 468b714..5ce653a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -636,6 +636,7 @@ dependencies = [ "crossterm", "log", "ratatui", + "signal-hook", "tb-runtime", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 1b43fbc..a9f82fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ authors = ["Georg Bauer"] ratatui = "0.29" crossterm = "0.28" +# Betriebssystemsignale als Ereignisquelle (SIGNAL, design.md D8). +# Liegt ohnehin im Baum: crossterm nutzt es unter Unix fuer SIGWINCH. +signal-hook = "0.3" + # Text und Zeit unicode-width = "0.2" # Nur fuer den UTC-Versatz zu einem Zeitpunkt (siehe design.md, D1/D2). diff --git a/PLAN.md b/PLAN.md index 3006dc3..daf8974 100644 --- a/PLAN.md +++ b/PLAN.md @@ -393,10 +393,12 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. Transaktion verfällt. ## Phase 4 — Forms-Engine (`tb-ui::forms`) -- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen- +- [x] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen- Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer Ereignisprozedur) + Befund: Die VM stellt klassische Traps über einen eigenen Frame zu; + Forms-Ereignisprozeduren verwenden später denselben Rückrufweg. - [ ] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE` (modal/nicht-modal) - [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox, @@ -415,13 +417,15 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. https://github.com/cout/vbdos ohne Non-Features kompilieren und sind nutzbar (Konsolenprogramme bereits ab Phase 3) - [ ] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`) -- [ ] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) — +- [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) — `ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit `GOSUB label`, dazu die Steueranweisungen `TIMER`, `KEY(n)`, `UEVENT`, `SIGNAL(n)` mit `ON`/`OFF`/`STOP`, das `EVENT`-Statement und `SetUEvent`. Setzt die Ereignisschleife (erste Aufgabe dieser Phase) voraus; `COM`/`PEN`/`PLAY`/`STRIG` bleiben Non-Feature (Fehler 73). **10 Inventareinträge** + Befund: `EVENT` besitzt nur `ON`/`OFF`; `SIGNAL(1/2)` bildet + plattformübergreifend `SIGINT`/`SIGTERM` ab. - [ ] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`. Sie hängen am Formularmodell, nicht an der Steuerelementliste, und diff --git a/crates/tb-cli/src/main.rs b/crates/tb-cli/src/main.rs index 0f377e7..c5c8b01 100644 --- a/crates/tb-cli/src/main.rs +++ b/crates/tb-cli/src/main.rs @@ -153,11 +153,16 @@ fn cmd_run(args: &[String]) -> ExitCode { struct PipeHost { puffer: std::collections::VecDeque, eof: bool, + start: std::time::Instant, } impl PipeHost { fn new() -> Self { - PipeHost { puffer: std::collections::VecDeque::new(), eof: false } + PipeHost { + puffer: std::collections::VecDeque::new(), + eof: false, + start: std::time::Instant::now(), + } } /// Eine Zeile von stdin in Tastendrücke zerlegen. @@ -177,10 +182,12 @@ impl PipeHost { zeile.pop(); } for c in zeile.chars() { - self.puffer.push_back(Ereignis::Taste(c.to_string())); + self.puffer.push_back(Ereignis::Taste(c.to_string(), 0)); } - self.puffer - .push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string())); + self.puffer.push_back(Ereignis::Taste( + tb_runtime::host::taste::ENTER.to_string(), + 0, + )); } } } @@ -195,4 +202,8 @@ impl Host for PipeHost { } self.puffer.pop_front() } + + fn jetzt_ms(&mut self) -> u64 { + self.start.elapsed().as_millis() as u64 + } } diff --git a/crates/tb-cli/tests/compat.rs b/crates/tb-cli/tests/compat.rs index c20290f..08dcb37 100644 --- a/crates/tb-cli/tests/compat.rs +++ b/crates/tb-cli/tests/compat.rs @@ -39,7 +39,9 @@ fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) { /// Wert einer Kopfzeilen-Direktive `' : ` aus den ersten Zeilen. fn direktive(src: &str, marke: &str) -> Option { src.lines() - .take(4) + // Kopfzeilenfenster: Groesse, Zeitzone, Uhr, Tasten, Tempdir, + // Resize passen zusammen in die ersten Zeilen. + .take(8) .find(|l| l.contains(marke)) .map(|l| l.split(marke).nth(1).unwrap().trim().to_string()) } @@ -94,6 +96,120 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> { } } +/// Vorbereitete Tastendrücke: `' tb-keys: `. Sondertasten als +/// ``…``, sonst je ein Zeichen; `+` davor bedeutet Umschalt, +/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten. +fn tasten_direktive(name: &str, src: &str) -> Vec { + use tb_runtime::host::{taste, umschalt, Ereignis}; + let Some(wert) = direktive(src, "tb-keys:") else { + return Vec::new(); + }; + let mut out = Vec::new(); + let zeichen: Vec = wert.chars().collect(); + let mut i = 0; + while i < zeichen.len() { + let mut shift = 0u8; + while i < zeichen.len() && matches!(zeichen[i], '+' | '^' | '%') { + shift |= match zeichen[i] { + '+' => umschalt::SHIFT, + '^' => umschalt::CTRL, + _ => umschalt::ALT, + }; + i += 1; + } + if i >= zeichen.len() { + panic!("{name}: tb-keys endet mit einem Umschaltzeichen"); + } + if zeichen[i] == '<' { + let ende = zeichen[i..] + .iter() + .position(|c| *c == '>') + .unwrap_or_else(|| panic!("{name}: tb-keys: `<` ohne `>`")); + let nm: String = zeichen[i + 1..i + ende].iter().collect(); + let code = match nm.to_uppercase().as_str() { + "F1" => 0x3B, + "F2" => 0x3C, + "F3" => 0x3D, + "F4" => 0x3E, + "F5" => 0x3F, + "F6" => 0x40, + "F7" => 0x41, + "F8" => 0x42, + "F9" => 0x43, + "F10" => 0x44, + "F11" => 0x85, + "F12" => 0x86, + "UP" => 0x48, + "LEFT" => 0x4B, + "RIGHT" => 0x4D, + "DOWN" => 0x50, + other => panic!("{name}: tb-keys kennt `<{other}>` nicht"), + }; + out.push(Ereignis::Taste(taste::sonder(code), shift)); + i += ende + 1; + } else { + out.push(Ereignis::Taste(zeichen[i].to_string(), shift)); + i += 1; + } + } + out +} + +/// Virtuelle Uhr: `' tb-clock: ms/Zustellpunkt`. Ohne die Direktive +/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe +/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr +/// auszuweichen (design.md, D6). +fn clock_direktive(name: &str, src: &str) -> Option { + let wert = direktive(src, "tb-clock:")?; + let ms = wert + .trim() + .strip_suffix("ms/Zustellpunkt") + .unwrap_or_else(|| panic!("{name}: tb-clock erwartet `ms/Zustellpunkt`, fand {wert:?}")) + .trim(); + Some( + ms.parse() + .unwrap_or_else(|_| panic!("{name}: {ms:?} ist keine Zahl")), + ) +} + +/// Nutzt das Programm zeitgesteuerte Traps? Grobe, aber ausreichende +/// Prüfung: ohne `ON TIMER` gibt es keine Fälligkeit zu rechnen. +fn braucht_uhr(src: &str) -> bool { + src.to_uppercase().contains("ON TIMER") +} + +fn clock_schritt(name: &str, src: &str) -> Option { + let schritt = clock_direktive(name, src); + assert!( + schritt.is_some() || !braucht_uhr(src), + "{name}: nutzt zeitgesteuerte Traps, deklariert aber keinen Zeitverlauf — \ + erste Zeilen brauchen `' tb-clock: ms/Zustellpunkt`" + ); + schritt +} + +/// Testhost mit virtueller Uhr: sie rückt bei jedem Blick auf die Zeit um +/// einen festen Schritt vor. Damit ist der Lauf reproduzierbar und +/// braucht keine Wartezeit. +struct UhrHost { + inner: CaptureHost, + schritt_ms: 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, blockierend: bool) -> Option { + self.inner.next_event(blockierend) + } + fn jetzt_ms(&mut self) -> u64 { + let jetzt = self.inner.jetzt_ms(); + self.inner.uhr_vorruecken(self.schritt_ms); + jetzt + } +} + /// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit /// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen /// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt. @@ -146,6 +262,24 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String { tb_runtime::host::Ereignis::Groesse { cols: c, rows: r }, ); } + for t in tasten_direktive(&name, &src) { + host.ereignis(t); + } + let schritt = clock_schritt(&name, &src); + if let Some(schritt_ms) = schritt { + let mut host = UhrHost { + inner: host, + schritt_ms, + }; + return match vm.run(&mut host) { + RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen), + other => panic!( + "{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}", + path.display(), + tb_runtime::snapshot::snapshot(&vm.rt.screen) + ), + }; + } match vm.run(&mut host) { RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen), other => panic!( @@ -207,12 +341,8 @@ fn assert_output_matches(file: &str, want: &str, got: &str) { .position(|(a, b)| a != b) .map(|i| i / 2 + 1) .unwrap_or(0); - let paar = |s: &str| { - s.chars() - .skip((spalte - 1) * 2) - .take(2) - .collect::() - }; + let paar = + |s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::(); panic!( "{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \ Soll: {}\n Ist: {}", @@ -257,12 +387,31 @@ fn korpus_laeuft_mit_korrekter_ausgabe() { let got = run_corpus_file(&path, Some((*cols, *rows))); let etikett = format!("{name} @ {cols}x{rows}"); assert_output_matches(&etikett, &want, &got); + let wiederholt = run_corpus_file(&path, Some((*cols, *rows))); + assert_eq!( + got, wiederholt, + "{etikett}: zwei Läufe weichen voneinander ab" + ); checked += 1; } } assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}"); } +#[test] +fn uhr_direktive_wird_gelesen() { + assert_eq!( + clock_schritt("uhr.bas", "' tb-clock: 25ms/Zustellpunkt\nON TIMER(1)"), + Some(25) + ); +} + +#[test] +#[should_panic(expected = "deklariert aber keinen Zeitverlauf")] +fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() { + clock_schritt("uhr.bas", "ON TIMER(1) GOSUB Tick"); +} + // ---- tbc-Binary (Exit-Codes nach D6) ---------------------------------------- #[test] diff --git a/crates/tb-frontend/src/ast.rs b/crates/tb-frontend/src/ast.rs index 10b9917..19a8d52 100644 --- a/crates/tb-frontend/src/ast.rs +++ b/crates/tb-frontend/src/ast.rs @@ -331,10 +331,28 @@ pub enum Stmt { to: Option, pos: SourcePos, }, - NameStmt { old: Expr, new: Expr, pos: SourcePos }, + NameStmt { + old: Expr, + new: Expr, + pos: SourcePos, + }, // ---- Bildschirm/Ereignisse ---- /// `VIEW PRINT [oben TO unten]`. - ViewPrint { top: Option, bottom: Option, pos: SourcePos }, + ViewPrint { + top: Option, + bottom: Option, + pos: SourcePos, + }, + /// `ON TIMER(n&) GOSUB ziel`, `ON KEY(n%) GOSUB ziel`, + /// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die + /// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet + /// den Trap ab. + TrapDef { + device: String, + index: Option, + target: LabelRef, + pos: SourcePos, + }, /// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … EventControl { device: String, diff --git a/crates/tb-frontend/src/hir.rs b/crates/tb-frontend/src/hir.rs index 2d8f298..68159b0 100644 --- a/crates/tb-frontend/src/hir.rs +++ b/crates/tb-frontend/src/hir.rs @@ -329,6 +329,8 @@ pub enum Builtin { CommandS, Doevents, Sleep, + /// `SetUEvent` — löst das benutzerdefinierte Ereignis aus (§8). + SetUEvent, Beep, // ISAM (Change `phase-3-isam`) IsamOpen, @@ -537,6 +539,22 @@ pub enum HStmtKind { targets: Vec, }, ReturnGosub(Option), + /// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle + /// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei + /// `TIMER` das Intervall in Sekunden. `ziel = None` = `GOSUB 0`. + TrapDef { + art: u8, + index: HExpr, + ziel: Option, + }, + /// `(n) ON|OFF|STOP` — `zustand` 0 An, 1 Aus, 2 Gestoppt. + TrapSet { + art: u8, + index: HExpr, + zustand: u8, + }, + /// `EVENT ON` / `EVENT OFF`. + EventSwitch(bool), /// EXIT SUB/FUNCTION/DEF bzw. Rumpfende. ExitProc, CallSub { diff --git a/crates/tb-frontend/src/parser.rs b/crates/tb-frontend/src/parser.rs index aaedced..dedc8d1 100644 --- a/crates/tb-frontend/src/parser.rs +++ b/crates/tb-frontend/src/parser.rs @@ -666,12 +666,11 @@ impl<'a> P<'a> { let decls = self.parse_var_decls(); Some(Stmt::StaticDecl { decls, pos }) } - TokenKind::Ident { ref name, .. } if name == "LPRINT" => { - self.parse_print(pos, true) - } - TokenKind::Ident { ref name, suffix: None } - if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) => - { + TokenKind::Ident { ref name, .. } if name == "LPRINT" => self.parse_print(pos, true), + TokenKind::Ident { + ref name, + suffix: None, + } if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) => { self.advance(); // VIEW self.advance(); // PRINT let (mut top, mut bottom) = (None, None); @@ -972,9 +971,7 @@ impl<'a> P<'a> { } if matches!(self.k(), TokenKind::Eol) { // Blockform - let stop = |p: &P| { - p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If) - }; + let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If); let then_body = self.parse_stmt_list(stop); let mut elseifs = Vec::new(); while self.eat_kw(Kw::ElseIf) { @@ -1071,10 +1068,12 @@ impl<'a> P<'a> { } } } - let body = self.parse_stmt_list(|p: &P| { - p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select) + let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select)); + arms.push(CaseArm { + specs, + body, + pos: arm_pos, }); - arms.push(CaseArm { specs, body, pos: arm_pos }); } if self.at_end_pair(Kw::Select) { self.advance(); @@ -1149,6 +1148,46 @@ impl<'a> P<'a> { Some(Stmt::While { cond, body, pos }) } + /// `ON [(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor + /// unberührt, wenn hier kein Trap steht. + fn parse_trap_def(&mut self, pos: SourcePos) -> Option { + let start = self.i; + let TokenKind::Ident { name, suffix: None } = self.k() else { + return None; + }; + if !matches!( + name.as_str(), + "TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "COM" | "PEN" | "PLAY" | "STRIG" + ) { + return None; + } + let device = name.clone(); + self.advance(); + let index = if self.eat(&TokenKind::LParen) { + let e = self.parse_expr(); + if e.is_none() || !self.eat(&TokenKind::RParen) { + self.i = start; + return None; + } + e + } else { + None + }; + if !self.eat_kw(Kw::Gosub) { + // Doch kein Trap — z. B. `ON TIMER GOTO …` gibt es nicht, aber + // die Diagnose dafür gehört an die allgemeine Stelle. + self.i = start; + return None; + } + let target = self.parse_label_ref()?; + Some(Stmt::TrapDef { + device, + index, + target, + pos, + }) + } + fn parse_on(&mut self, pos: SourcePos) -> Option { self.advance(); // ON let local = self.eat_kw(Kw::Local); @@ -1186,7 +1225,13 @@ impl<'a> P<'a> { self.sync(); return None; } - // ON ausdruck GOTO/GOSUB liste (Event-Traps: Phase 3/4) + // ON [(n)] GOSUB ziel — klassische Ereignis-Traps (§8). + // Muss vor dem berechneten `ON ausdruck GOSUB` stehen, sonst + // verschwindet `ON TIMER(5) GOSUB Marke` still darin. + if let Some(st) = self.parse_trap_def(pos) { + return Some(st); + } + // ON ausdruck GOTO/GOSUB liste let expr = self.parse_expr()?; let gosub = if self.eat_kw(Kw::Goto) { false @@ -1528,26 +1573,34 @@ impl<'a> P<'a> { return None; } // Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … - if let Expr::Name { ref name, suffix: None, ref args, .. } = target { + if let Expr::Name { + ref name, + suffix: None, + ref args, + .. + } = target + { if matches!( name.as_str(), - "TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" - | "EVENT" + "TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT" ) { let action = match self.k() { TokenKind::Kw(Kw::On) => Some(EventAction::On), TokenKind::Kw(Kw::Stop) => Some(EventAction::Stop), - TokenKind::Ident { ref name, suffix: None } - if name == "OFF" => - { - Some(EventAction::Off) - } + TokenKind::Ident { + ref name, + suffix: None, + } if name == "OFF" => Some(EventAction::Off), _ => None, }; if let Some(action) = action { self.advance(); let index = args.clone().and_then(|mut a| { - if a.is_empty() { None } else { Some(a.remove(0)) } + if a.is_empty() { + None + } else { + Some(a.remove(0)) + } }); return Some(Stmt::EventControl { device: name.clone(), diff --git a/crates/tb-frontend/src/sema.rs b/crates/tb-frontend/src/sema.rs index 8f50e30..e5c91ee 100644 --- a/crates/tb-frontend/src/sema.rs +++ b/crates/tb-frontend/src/sema.rs @@ -233,9 +233,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> { "NOW" => (0, 0, &[], Db), "DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db), "DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db), - "DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => { - (1, 1, &[N], I) - } + "DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I), "FORMAT$" => (1, 2, &[A, S], St), "ERR" | "ERL" => (0, 0, &[], L), "FRE" => (1, 1, &[A], L), @@ -253,9 +251,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> { "FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => { (5, 5, &[N, N, N, N, N], Db) } - "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => { - (6, 6, &[N, N, N, N, N, N], Db) - } + "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db), "NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db), "MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db), "SLN" | "SLN#" => (3, 3, &[N, N, N], Db), @@ -636,9 +632,9 @@ impl Sema { self.prescan(&a.body, scope, module_data); } } - Stmt::For { body, .. } - | Stmt::DoLoop { body, .. } - | Stmt::While { body, .. } => self.prescan(body, scope, module_data), + Stmt::For { body, .. } | Stmt::DoLoop { body, .. } | Stmt::While { body, .. } => { + self.prescan(body, scope, module_data) + } _ => {} } } @@ -880,21 +876,25 @@ impl Sema { Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)), Expr::StrLit(s) => Some(ConstVal::Str(s.clone())), Expr::Paren(e) => self.fold_const(e), - Expr::Name { name, args: None, .. } => { - self.consts.get(name).and_then(|(_, v)| v.clone()) - } - Expr::Unary { op: UnOp::Neg, operand, .. } => { - match self.fold_const(operand)? { - ConstVal::Num(n) => Some(ConstVal::Num(-n)), - ConstVal::Str(_) => None, - } - } - Expr::Unary { op: UnOp::Not, operand, .. } => { - match self.fold_const(operand)? { - ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)), - ConstVal::Str(_) => None, - } - } + Expr::Name { + name, args: None, .. + } => self.consts.get(name).and_then(|(_, v)| v.clone()), + Expr::Unary { + op: UnOp::Neg, + operand, + .. + } => match self.fold_const(operand)? { + ConstVal::Num(n) => Some(ConstVal::Num(-n)), + ConstVal::Str(_) => None, + }, + Expr::Unary { + op: UnOp::Not, + operand, + .. + } => match self.fold_const(operand)? { + ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)), + ConstVal::Str(_) => None, + }, Expr::Binary { op, lhs, rhs, .. } => { let l = self.fold_const(lhs)?; let r = self.fold_const(rhs)?; @@ -1513,25 +1513,41 @@ impl Sema { Some(t) => { self.check_label_exists(t, scope, *pos); match t { - LabelRef::Name(n) => { - self.data_marks_name.get(n).copied().unwrap_or(0) - } - LabelRef::Line(n) => { - self.data_marks_line.get(n).copied().unwrap_or(0) - } + LabelRef::Name(n) => self.data_marks_name.get(n).copied().unwrap_or(0), + LabelRef::Line(n) => self.data_marks_line.get(n).copied().unwrap_or(0), } } }; self.push(out, scope, HStmtKind::Restore(idx)); } - Stmt::DefFn { name, suffix, params, body, pos } => { + Stmt::DefFn { + name, + suffix, + params, + body, + pos, + } => { self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos); } - Stmt::DefFnBlock { name, suffix, params, body, pos } => { + Stmt::DefFnBlock { + name, + suffix, + params, + body, + pos, + } => { self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos); } // ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ---- - Stmt::Open { file, mode, isam, number, len, pos, .. } => { + Stmt::Open { + file, + mode, + isam, + number, + len, + pos, + .. + } => { let (fe, _) = self.want_str(file, scope); self.reject_com_device(file, *pos); let (ne, nt) = self.want_num(number, scope); @@ -1779,12 +1795,67 @@ impl Sema { self.push( out, scope, - HStmtKind::BuiltinStmt { b: Builtin::ViewPrint, args: a }, + HStmtKind::BuiltinStmt { + b: Builtin::ViewPrint, + args: a, + }, ); } - Stmt::EventControl { device, index, action, pos } => { + Stmt::TrapDef { + device, + index, + target, + pos, + } => { + let Some(art) = trap_art(device) else { + // COM/PEN/PLAY/STRIG sind Non-Feature — namentlich. + self.err(*pos, "Feature unavailable"); + return; + }; + // Wertebereich prüfen, soweit die Kennung konstant ist. + if let Some(i) = index { + if let Some(n) = const_zahl(i) { + if !trap_bereich_ok(art, n) { + self.err(*pos, trap_bereich_text(art)); + } + } + } else if art != TRAP_UEVENT { + self.err(*pos, trap_bereich_text(art)); + } + if index.is_some() && art == TRAP_UEVENT { + self.err(*pos, "ON UEVENT nimmt keine Kennung"); + } + let hindex = match index { + Some(i) => self.lower_num_as(i, scope, NumTy::Lng), + None => HExpr::Lng(0), + }; + // `GOSUB 0` schaltet den Trap ab und meint nicht Zeile 0. + let ziel = if matches!(target, LabelRef::Line(0)) { + None + } else { + match self.label_id(target, scope, *pos) { + Some(id) => Some(id), + None => return, + } + }; + self.push( + out, + scope, + HStmtKind::TrapDef { + art, + index: hindex, + ziel, + }, + ); + } + Stmt::EventControl { + device, + index, + action, + pos, + } => { match device.as_str() { - "TIMER" | "KEY" | "UEVENT" | "EVENT" => {} + "TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {} _ => self.err(*pos, "Feature unavailable"), } if let Some(i) = index { @@ -1805,7 +1876,45 @@ impl Sema { ); return; } - self.push(out, scope, HStmtKind::Unsupported("Ereignissteuerung")); + if device == "EVENT" { + // Das Vorbild kennt nur ON und OFF — kein STOP. + if *action == EventAction::Stop { + self.err(*pos, "EVENT kennt nur ON und OFF"); + return; + } + let an = *action == EventAction::On; + self.push(out, scope, HStmtKind::EventSwitch(an)); + return; + } + let Some(art) = trap_art(device) else { + self.err(*pos, "Feature unavailable"); + return; + }; + if let Some(i) = index { + if let Some(n) = const_zahl(i) { + if !trap_bereich_ok(art, n) { + self.err(*pos, trap_bereich_text(art)); + } + } + } + let hindex = match index { + Some(i) => self.lower_num_as(i, scope, NumTy::Lng), + None => HExpr::Lng(0), + }; + let zustand = match action { + EventAction::On => 0, + EventAction::Off => 1, + EventAction::Stop => 2, + }; + self.push( + out, + scope, + HStmtKind::TrapSet { + art, + index: hindex, + zustand, + }, + ); } } } @@ -2343,10 +2452,22 @@ impl Sema { let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos); match name { - "BEEP" => self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Beep, args: vec![] }), - "DOEVENTS" => { - self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Doevents, args: vec![] }) - } + "BEEP" => self.push( + out, + scope, + HStmtKind::BuiltinStmt { + b: Builtin::Beep, + args: vec![], + }, + ), + "DOEVENTS" => self.push( + out, + scope, + HStmtKind::BuiltinStmt { + b: Builtin::Doevents, + args: vec![], + }, + ), "RANDOMIZE" => { let a = lowered .into_iter() @@ -2494,19 +2615,29 @@ impl Sema { self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a }); } // ---- ISAM ------------------------------------------------------ - "CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" - | "UPDATE" | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" - | "MOVENEXT" | "MOVEPREVIOUS" | "SEEKEQ" | "SEEKGT" | "SEEKGE" - | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => { + "CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE" + | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS" + | "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => { self.lower_isam_stmt(name, args, lowered, scope, out) } + "SETUEVENT" => self.push( + out, + scope, + HStmtKind::BuiltinStmt { + b: Builtin::SetUEvent, + args: vec![], + }, + ), // Bildschirm-/Datei-/System-Anweisungen späterer Phasen. - _ => self.push(out, scope, HStmtKind::Unsupported(match name { - "RUN" => "RUN", - "MSGBOX" => "MSGBOX", - "SETUEVENT" => "SETUEVENT", - _ => "Anweisung", - })), + _ => self.push( + out, + scope, + HStmtKind::Unsupported(match name { + "RUN" => "RUN", + "MSGBOX" => "MSGBOX", + _ => "Anweisung", + }), + ), } } @@ -2897,9 +3028,12 @@ impl Sema { let (re, rt) = self.lower_expr(rhs, scope); self.lower_binary(*op, le, lt, re, rt, *pos) } - Expr::Name { name, suffix, args, pos } => { - self.lower_name_expr(name, suffix, args, *pos, scope) - } + Expr::Name { + name, + suffix, + args, + pos, + } => self.lower_name_expr(name, suffix, args, *pos, scope), } } @@ -3742,6 +3876,7 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos { | Stmt::LockStmt { pos, .. } | Stmt::NameStmt { pos, .. } | Stmt::ViewPrint { pos, .. } + | Stmt::TrapDef { pos, .. } | Stmt::EventControl { pos, .. } | Stmt::Include { pos, .. } | Stmt::MetaArrays { pos, .. } @@ -4082,3 +4217,52 @@ mod tests { ); } } + +// ---- Ereignis-Traps (Sprachreferenz §8) ------------------------------------- + +pub const TRAP_KEY: u8 = 0; +pub const TRAP_TIMER: u8 = 1; +pub const TRAP_UEVENT: u8 = 2; +pub const TRAP_SIGNAL: u8 = 3; + +/// Quellenname → Art. `None` für die Non-Features. +fn trap_art(device: &str) -> Option { + Some(match device { + "KEY" => TRAP_KEY, + "TIMER" => TRAP_TIMER, + "UEVENT" => TRAP_UEVENT, + "SIGNAL" => TRAP_SIGNAL, + _ => return None, + }) +} + +/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar. +fn const_zahl(e: &Expr) -> Option { + match e { + Expr::IntLit(n) => Some(*n as i64), + Expr::LongLit(n) => Some(*n as i64), + Expr::SingleLit(n) => Some(*n as i64), + Expr::DoubleLit(n) => Some(*n as i64), + Expr::Paren(inner) => const_zahl(inner), + _ => None, + } +} + +/// Wertebereiche nach der Original-Hilfe (Belege in `umfang-und-form.md`). +fn trap_bereich_ok(art: u8, n: i64) -> bool { + match art { + TRAP_KEY => matches!(n, 0..=25 | 30 | 31), + TRAP_TIMER => (1..=86_400).contains(&n), + TRAP_SIGNAL => (1..=2).contains(&n), + _ => true, + } +} + +fn trap_bereich_text(art: u8) -> &'static str { + match art { + TRAP_KEY => "ON KEY: Kennung 0, 1-25 oder 30-31 erwartet", + TRAP_TIMER => "ON TIMER: Intervall 1 bis 86400 Sekunden erwartet", + TRAP_SIGNAL => "ON SIGNAL: Kennung 1 (SIGINT) oder 2 (SIGTERM) erwartet", + _ => "Kennung erwartet", + } +} diff --git a/crates/tb-runtime/src/builtins.rs b/crates/tb-runtime/src/builtins.rs index 49fcd6a..8a574d3 100644 --- a/crates/tb-runtime/src/builtins.rs +++ b/crates/tb-runtime/src/builtins.rs @@ -7,13 +7,13 @@ use crate::console::PrintState; use crate::datetime as dt; -use crate::finanz as fin; -use crate::fileio as fio; -use crate::value; use crate::errors::RuntimeError; +use crate::fileio as fio; +use crate::finanz as fin; use crate::format; use crate::host::{Ereignis, Host}; use crate::screen::TextScreen; +use crate::value; use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value}; use std::collections::VecDeque; use std::rc::Rc; @@ -28,8 +28,11 @@ pub struct RtState { /// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion. pub isam: crate::isam::Isam, pub print: PrintState, - /// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form. - pub tasten: VecDeque, + /// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit + /// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand + /// wird nur für die benutzerdefinierten Trap-Tasten gebraucht; + /// `INKEY$` und `INPUT` sehen ihn nicht. + pub tasten: VecDeque<(String, u8)>, /// Abbruchwunsch (Strg+Untbr) wurde gemeldet. pub abbruch: bool, /// Eingabeende wurde gemeldet. @@ -42,6 +45,8 @@ pub struct RtState { pub waehrung: crate::using::Waehrung, /// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet? pub trace: bool, + /// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8). + pub traps: crate::traps::Traps, /// Trefferliste und Zeiger für `DIR$`. pub dir_treffer: Vec, pub dir_index: usize, @@ -73,6 +78,7 @@ impl Default for RtState { key_zeile: false, waehrung: Default::default(), trace: false, + traps: crate::traps::Traps::neu(), dir_treffer: Vec::new(), dir_index: 0, uhr_offset: 0, @@ -100,10 +106,19 @@ impl RtState { return; }; match e { - Ereignis::Taste(t) => self.tasten.push_back(t), + Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)), Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows), Ereignis::Abbruch => self.abbruch = true, Ereignis::Ende => self.ende = true, + Ereignis::Signal(n) => { + if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) { + self.abbruch = true; + } + } + // ponytail: Mausereignisse bekommen ihren Verbraucher erst + // mit dem Forms-Objektmodell. Bis dahin werden sie hier + // verworfen, damit die Warteschlange nicht wächst. + Ereignis::Maus(_) => {} } if blockierend { return; @@ -116,12 +131,46 @@ impl RtState { pub fn tick(&mut self, host: &mut dyn Host) { host.present(&self.screen); self.pump(host, false); + self.traps_pruefen(host); + } + + /// Was am Zustellpunkt vor der eigentlichen Zustellung zu tun ist: + /// Zeit prüfen und getrappte Tasten aus der Schlange nehmen. + /// + /// Die Tasten werden **hier** aussortiert, nicht beim Einsortieren + /// (design.md, D5): so wirkt ein `KEY(n) OFF` sofort und auch auf + /// Tasten, die schon in der Schlange liegen. + pub fn traps_pruefen(&mut self, host: &mut dyn Host) { + if !self.traps.aktiv() { + return; + } + let jetzt = host.jetzt_ms(); + self.traps.zeit_pruefen(jetzt); + self.tasten_traps_pruefen(); + } + + /// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM + /// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen. + pub fn tasten_traps_pruefen(&mut self) { + // Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt + // erhalten, und eine getrappte Taste hinter einer ungetrappten + // wartet, bis diese gelesen ist. + while let Some((s, shift)) = self.tasten.front() { + let Some(code) = crate::host::taste::scancode_von(s) else { + break; + }; + let Some(n) = self.traps.taste_faengt(code, *shift) else { + break; + }; + self.tasten.pop_front(); + self.traps.melden(crate::traps::Quelle::Key(n)); + } } /// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`. pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option { loop { - if let Some(t) = self.tasten.pop_front() { + if let Some((t, _)) = self.tasten.pop_front() { return Some(t); } if self.ende { @@ -386,7 +435,9 @@ pub mod ids { pub const ISAM_SAVEPOINT: u16 = 145; pub const ISAM_SETMEM: u16 = 146; pub const ISAM_BOF: u16 = 147; - pub const COUNT: u16 = 148; + /// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8). + pub const SETUEVENT: u16 = 148; + pub const COUNT: u16 = 149; } /// Dispatch-Tabelle in Index-Reihenfolge. @@ -541,6 +592,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] { bi_isam_savepoint, bi_isam_setmem, bi_isam_bof, + bi_setuevent, ]; debug_assert_eq!(TABLE.len(), ids::COUNT as usize); TABLE @@ -1439,9 +1491,26 @@ fn key_index(n: i32) -> Option { } } -fn bi_key_assign(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { +fn bi_key_assign( + st: &mut RtState, + _: &mut dyn Host, + a: &mut [Value], +) -> Result, RuntimeError> { let n = arg_i32(a, 0)?; let text = arg_str(a, 1)?; + // 15–25 weisen kein Makro zu, sondern erklären eine Trap-Taste über + // Tastaturflagbyte und Scancode (Sprachreferenz §8). + if (15..=25).contains(&n) { + let mut it = text.chars(); + let (Some(flag), Some(scan)) = (it.next(), it.next()) else { + return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); + }; + if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 { + return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); + } + st.traps.benutzertaste(n as u8, flag as u8, scan as u8); + return Ok(None); + } let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?; // Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild). st.key_makros[i] = text.chars().take(15).collect(); @@ -1651,7 +1720,21 @@ fn bi_sleep(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result Result, RuntimeError> { +fn bi_setuevent( + st: &mut RtState, + _: &mut dyn Host, + _: &mut [Value], +) -> Result, RuntimeError> { + // „sets a flag that BASIC checks before executing the next statement" + st.traps.melden(crate::traps::Quelle::UEvent); + Ok(None) +} + +fn bi_beep( + _: &mut RtState, + _: &mut dyn Host, + _: &mut [Value], +) -> Result, RuntimeError> { // ponytail: Der Signalton geht direkt an das Terminal, nicht durch den // Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host // mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf. @@ -1885,4 +1968,155 @@ mod tests { }; assert_eq!(n, 3); } + #[test] + fn setuevent_meldet_das_benutzerereignis() { + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = crate::host::CaptureHost::default(); + st.traps.definieren(Quelle::UEvent, Some(11)); + st.traps.setzen(Quelle::UEvent, Zustand::An, 0); + assert_eq!(st.traps.naechstes(), None); + bi_setuevent(&mut st, &mut h, &mut []).unwrap(); + assert_eq!(st.traps.naechstes(), Some((Quelle::UEvent, 11))); + } + + #[test] + fn signal_aus_dem_host_meldet_den_trap() { + use crate::host::{Ereignis, Host}; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = crate::host::CaptureHost::default(); + st.traps.definieren(Quelle::Signal(1), Some(22)); + st.traps.setzen(Quelle::Signal(1), Zustand::An, 0); + h.ereignis(Ereignis::Signal(1)); + st.pump(&mut h, false); + assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22))); + assert!(matches!(h.next_event(false), None)); + } + + #[test] + fn sigint_ohne_aktiven_trap_bleibt_abbruch() { + use crate::host::Ereignis; + let mut st = RtState::default(); + let mut h = crate::host::CaptureHost::default(); + h.ereignis(Ereignis::Signal(1)); + st.pump(&mut h, false); + assert!(st.abbruch); + } + + #[test] + fn mausereignisse_haeufen_sich_nicht_an() { + use crate::host::{Ereignis, MausArt, MausEreignis}; + let mut st = RtState::default(); + let mut h = crate::host::CaptureHost::default(); + for i in 0..500 { + h.ereignis(Ereignis::Maus(MausEreignis { + art: MausArt::Bewegung, + taste: 0, + shift: 0, + zeile: 1 + i % 20, + spalte: 1 + i % 70, + })); + } + st.pump(&mut h, false); + assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer"); + } + + #[test] + fn getrappte_taste_verlaesst_den_eingabestrom() { + use crate::host::{taste, CaptureHost, Ereignis}; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = CaptureHost::default(); + st.traps.definieren(Quelle::Key(1), Some(77)); + st.traps.setzen(Quelle::Key(1), Zustand::An, 0); + h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); // F1 + st.pump(&mut h, false); + st.traps_pruefen(&mut h); + assert_eq!(st.traps.naechstes(), Some((Quelle::Key(1), 77))); + assert_eq!(st.naechste_taste(&mut h, false), None, "F1 ist weg"); + } + + #[test] + fn nicht_getrappte_taste_bleibt_im_strom() { + use crate::host::{taste, CaptureHost, Ereignis}; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = CaptureHost::default(); + st.traps.definieren(Quelle::Key(1), Some(77)); + st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0); + h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); + st.pump(&mut h, false); + st.traps_pruefen(&mut h); + assert_eq!(st.traps.naechstes(), None); + assert_eq!( + st.naechste_taste(&mut h, false), + Some(taste::sonder(0x3B)), + "bei OFF bleibt F1 lesbar" + ); + } + + #[test] + fn off_wirkt_auf_bereits_wartende_tasten() { + use crate::host::{taste, CaptureHost, Ereignis}; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = CaptureHost::default(); + st.traps.definieren(Quelle::Key(1), Some(77)); + st.traps.setzen(Quelle::Key(1), Zustand::An, 0); + h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); + st.pump(&mut h, false); // Taste liegt in der Schlange + st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0); + st.traps_pruefen(&mut h); + assert_eq!( + st.naechste_taste(&mut h, false), + Some(taste::sonder(0x3B)), + "das OFF wirkt auch auf schon Wartendes (design.md D5)" + ); + } + + #[test] + fn benutzertaste_ueber_key_anweisung() { + use crate::host::{umschalt, CaptureHost}; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = CaptureHost::default(); + st.traps.definieren(Quelle::Key(15), Some(88)); + st.traps.setzen(Quelle::Key(15), Zustand::An, 0); + // KEY 15, CHR$(4) + CHR$(30) — Strg+A + let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char); + bi_key_assign( + &mut st, + &mut h, + &mut [Value::Int(15), Value::Str(def.into())], + ) + .unwrap(); + h.taste_mit("a", umschalt::CTRL); + st.pump(&mut h, false); + st.traps_pruefen(&mut h); + assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 88))); + assert_eq!(st.naechste_taste(&mut h, false), None); + } + + #[test] + fn benutzertaste_ohne_umschalt_bleibt_im_strom() { + use crate::host::CaptureHost; + use crate::traps::{Quelle, Zustand}; + let mut st = RtState::default(); + let mut h = CaptureHost::default(); + st.traps.definieren(Quelle::Key(15), Some(88)); + st.traps.setzen(Quelle::Key(15), Zustand::An, 0); + let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char); + bi_key_assign( + &mut st, + &mut h, + &mut [Value::Int(15), Value::Str(def.into())], + ) + .unwrap(); + h.taste_mit("a", 0); // ohne Strg + st.pump(&mut h, false); + st.traps_pruefen(&mut h); + assert_eq!(st.traps.naechstes(), None); + assert_eq!(st.naechste_taste(&mut h, false), Some("a".to_string())); + } } diff --git a/crates/tb-runtime/src/host.rs b/crates/tb-runtime/src/host.rs index de5a946..6cc0c85 100644 --- a/crates/tb-runtime/src/host.rs +++ b/crates/tb-runtime/src/host.rs @@ -16,17 +16,56 @@ use crate::screen::TextScreen; /// Was vom Terminal hereinkommt. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Ereignis { - /// Tastendruck in `INKEY$`-Form: ein Zeichen für Zeichentasten, - /// Nullzeichen + Kennung für Sondertasten. - Taste(String), + /// Tastendruck in `INKEY$`-Form (ein Zeichen für Zeichentasten, + /// Nullzeichen + Kennung für Sondertasten) samt Umschaltzustand. + /// + /// Der Umschaltzustand ist ein Bitfeld wie bei der Maus: 1 Umschalt, + /// 2 Strg, 4 Alt. Er wird für `ON KEY(15)`–`ON KEY(25)` gebraucht — + /// benutzerdefinierte Tasten sind über Umschaltzustand und Scancode + /// erklärt, nicht über das erzeugte Zeichen. + Taste(String, u8), /// Die Darstellungsfläche hat ihre Größe geändert. Groesse { cols: usize, rows: usize }, + /// Mausereignis in Zellenkoordinaten, 1-basiert wie `LOCATE`. + Maus(MausEreignis), + /// Betriebssystemsignal: 1 = SIGINT, 2 = SIGTERM (Sprachreferenz, + /// Abweichung 8). Andere Signale werden nicht abgebildet. + Signal(u8), /// Abbruchwunsch (Strg+Untbr). Abbruch, /// Eingabeende — es kommt nichts mehr. Ende, } +/// Was die Maus getan hat. Die Position ist 1-basiert und in Zellen des +/// Textbildschirms — dieselbe Zählweise wie `LOCATE`, `CSRLIN` und `POS`; +/// die Umrechnung aus der 0-basierten Zählung des Terminals macht die +/// Darstellungsschicht (design.md, D7). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MausEreignis { + pub art: MausArt, + /// Gedrückte Taste: 1 links, 2 rechts, 4 Mitte; 0 bei reiner Bewegung. + pub taste: u8, + /// Umschaltzustand als Bitfeld: 1 Umschalt, 2 Strg, 4 Alt. + pub shift: u8, + pub zeile: usize, + pub spalte: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MausArt { + Druck, + Loslassen, + Bewegung, +} + +/// Umschaltzustand: Bitfeld 1 Umschalt, 2 Strg, 4 Alt. +pub mod umschalt { + pub const SHIFT: u8 = 1; + pub const CTRL: u8 = 2; + pub const ALT: u8 = 4; +} + /// Steuerzeichen in `INKEY$`-Form (Kodierung des Vorbilds). pub mod taste { pub const ENTER: &str = "\r"; @@ -38,6 +77,84 @@ pub mod taste { pub fn sonder(code: u8) -> String { format!("\0{}", code as char) } + + /// Zeichen → Scancode der Tastatur (Tabelle aus `qb45advr.hlp`, + /// „Keyboard Scan Codes"). Gebraucht für die benutzerdefinierten + /// Trap-Tasten `KEY 15`–`KEY 25`, die über Scancode erklärt sind. + /// + /// ponytail: nur die Tasten der Original-Tabelle, keine Ziffernblock- + /// Doppelbelegungen. Ceiling: ein hier nicht geführtes Zeichen kann + /// keinen benutzerdefinierten Trap auslösen; die Tabelle wächst, wenn + /// jemand eine solche Taste braucht. + pub fn scancode(c: char) -> Option { + let c = c.to_ascii_uppercase(); + Some(match c { + '\u{1b}' => 0x01, + '1' | '!' => 0x02, + '2' | '@' => 0x03, + '3' | '#' => 0x04, + '4' | '$' => 0x05, + '5' | '%' => 0x06, + '6' | '^' => 0x07, + '7' | '&' => 0x08, + '8' | '*' => 0x09, + '9' | '(' => 0x0A, + '0' | ')' => 0x0B, + '-' | '_' => 0x0C, + '=' | '+' => 0x0D, + '\u{8}' => 0x0E, + '\t' => 0x0F, + 'Q' => 0x10, + 'W' => 0x11, + 'E' => 0x12, + 'R' => 0x13, + 'T' => 0x14, + 'Y' => 0x15, + 'U' => 0x16, + 'I' => 0x17, + 'O' => 0x18, + 'P' => 0x19, + '[' | '{' => 0x1A, + ']' | '}' => 0x1B, + '\r' => 0x1C, + 'A' => 0x1E, + 'S' => 0x1F, + 'D' => 0x20, + 'F' => 0x21, + 'G' => 0x22, + 'H' => 0x23, + 'J' => 0x24, + 'K' => 0x25, + 'L' => 0x26, + ';' | ':' => 0x27, + '\'' | '"' => 0x28, + '`' | '~' => 0x29, + '\\' | '|' => 0x2B, + 'Z' => 0x2C, + 'X' => 0x2D, + 'C' => 0x2E, + 'V' => 0x2F, + 'B' => 0x30, + 'N' => 0x31, + 'M' => 0x32, + ',' | '<' => 0x33, + '.' | '>' => 0x34, + '/' | '?' => 0x35, + ' ' => 0x39, + _ => return None, + }) + } + + /// Scancode eines Tastendrucks in `INKEY$`-Form: bei Sondertasten die + /// Kennung hinter dem Nullzeichen, sonst aus der Zeichentabelle. + pub fn scancode_von(s: &str) -> Option { + let mut it = s.chars(); + match (it.next(), it.next()) { + (Some('\0'), Some(k)) => Some(k as u8), + (Some(c), None) => scancode(c), + _ => None, + } + } } pub trait Host { @@ -47,6 +164,16 @@ pub trait Host { /// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`, /// wenn nichts anliegt (Grundlage von `INKEY$`). fn next_event(&mut self, blockierend: bool) -> Option; + + /// Monotone Zeit in Millisekunden seit Programmstart. + /// + /// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie + /// greift dafür **nicht** selbst auf die Uhr zu (design.md, D3). Nur so + /// kann ein Host ohne Terminal die Zeit stellen und ein zeitgesteuertes + /// Programm ohne Wartezeit und reproduzierbar prüfen. Von der Ortszeit + /// hinter `TIME$`/`TIMER` ist das unabhängig — hier zählt Dauer, nicht + /// Uhrzeit. + fn jetzt_ms(&mut self) -> u64; } /// Host ohne Terminal: hält den Bildschirmzustand fest und speist @@ -61,6 +188,9 @@ pub struct CaptureHost { /// Ereignisse, die erst nach der n-ten Anzeige eingereiht werden — /// so lässt sich eine Größenänderung mitten im Lauf nachstellen. verzoegert: Vec<(usize, Ereignis)>, + /// Virtuelle Uhr in Millisekunden. Sie läuft nur, wenn der Test sie + /// stellt — von allein vergeht hier keine Zeit. + uhr_ms: u64, } impl CaptureHost { @@ -77,10 +207,16 @@ impl CaptureHost { /// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter. pub fn tippe(&mut self, text: &str) { for c in text.chars() { - self.ereignisse.push_back(Ereignis::Taste(c.to_string())); + self.ereignisse.push_back(Ereignis::Taste(c.to_string(), 0)); } self.ereignisse - .push_back(Ereignis::Taste(taste::ENTER.to_string())); + .push_back(Ereignis::Taste(taste::ENTER.to_string(), 0)); + } + + /// Tastendruck mit Umschaltzustand einreihen (Strg+A, Alt+F1 …). + pub fn taste_mit(&mut self, s: &str, shift: u8) { + self.ereignisse + .push_back(Ereignis::Taste(s.to_string(), shift)); } /// Beliebiges Ereignis einreihen (Größenänderung, Abbruch, Sondertaste). @@ -92,6 +228,16 @@ impl CaptureHost { pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) { self.verzoegert.push((nach, e)); } + + /// Virtuelle Uhr vorrücken. + pub fn uhr_vorruecken(&mut self, ms: u64) { + self.uhr_ms += ms; + } + + /// Virtuelle Uhr auf einen Wert setzen. + pub fn uhr_setzen(&mut self, ms: u64) { + self.uhr_ms = ms; + } } impl Host for CaptureHost { @@ -113,6 +259,10 @@ impl Host for CaptureHost { fn next_event(&mut self, _blockierend: bool) -> Option { self.ereignisse.pop_front() } + + fn jetzt_ms(&mut self) -> u64 { + self.uhr_ms + } } #[cfg(test)] @@ -137,37 +287,84 @@ mod tests { #[test] fn verzoegertes_ereignis_kommt_nach_der_anzeige() { let mut h = CaptureHost::default(); - h.ereignis_nach(2, Ereignis::Groesse { cols: 120, rows: 40 }); + h.ereignis_nach( + 2, + Ereignis::Groesse { + cols: 120, + rows: 40, + }, + ); let s = TextScreen::new(); h.present(&s); assert_eq!(h.next_event(false), None); h.present(&s); assert_eq!( h.next_event(false), - Some(Ereignis::Groesse { cols: 120, rows: 40 }) + Some(Ereignis::Groesse { + cols: 120, + rows: 40 + }) ); } #[test] fn groessenaenderung_und_abbruch_als_ereignis() { let mut h = CaptureHost::default(); - h.ereignis(Ereignis::Groesse { cols: 120, rows: 40 }); + h.ereignis(Ereignis::Groesse { + cols: 120, + rows: 40, + }); h.ereignis(Ereignis::Abbruch); assert_eq!( h.next_event(true), - Some(Ereignis::Groesse { cols: 120, rows: 40 }) + Some(Ereignis::Groesse { + cols: 120, + rows: 40 + }) ); assert_eq!(h.next_event(true), Some(Ereignis::Abbruch)); } + #[test] + fn maus_und_tasten_behalten_ihre_reihenfolge() { + let mut h = CaptureHost::default(); + let klick = Ereignis::Maus(MausEreignis { + art: MausArt::Druck, + taste: 1, + shift: 0, + zeile: 3, + spalte: 7, + }); + h.ereignis(Ereignis::Taste("a".into(), 0)); + h.ereignis(klick.clone()); + h.ereignis(Ereignis::Taste("b".into(), 0)); + assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0))); + assert_eq!(h.next_event(true), Some(klick)); + assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0))); + } + + #[test] + fn virtuelle_uhr_laeuft_nur_wenn_gestellt() { + let mut h = CaptureHost::default(); + assert_eq!(h.jetzt_ms(), 0); + h.uhr_vorruecken(250); + assert_eq!(h.jetzt_ms(), 250); + h.uhr_vorruecken(250); + assert_eq!(h.jetzt_ms(), 500); + h.uhr_setzen(42); + assert_eq!(h.jetzt_ms(), 42); + // Ohne Zutun vergeht keine Zeit — das traegt die Reproduzierbarkeit. + assert_eq!(h.jetzt_ms(), 42); + } + #[test] fn vorbereitete_zeile_wird_zu_tasten() { let mut h = CaptureHost::with_input(&["ab"]); - assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into()))); - assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into()))); + assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0))); + assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0))); assert_eq!( h.next_event(true), - Some(Ereignis::Taste(taste::ENTER.into())) + Some(Ereignis::Taste(taste::ENTER.into(), 0)) ); assert_eq!(h.next_event(false), None); } diff --git a/crates/tb-runtime/src/lib.rs b/crates/tb-runtime/src/lib.rs index 36566dc..421c43d 100644 --- a/crates/tb-runtime/src/lib.rs +++ b/crates/tb-runtime/src/lib.rs @@ -4,18 +4,19 @@ //! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive //! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3). -pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe) -pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe -pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds -pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär -pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb) -pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL +pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe) +pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe +pub mod datetime; // DATE$, TIME$, TIMER +pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds +pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär +pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …) +pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL pub mod host; +pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb) +pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE … pub mod screen; -pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …) -pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten pub mod snapshot; // Bildschirmzustand als vergleichbarer Text (Korpus) // Zellenpuffer des Textbildschirms (ohne Terminalbezug) // Host-Trait (Konsole, Capture) — Entscheidung D3 -pub mod datetime; // DATE$, TIME$, TIMER -pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE … -pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING … -pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix +pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING … +pub mod traps; // Zustaende der klassischen Ereignis-Traps (Sprachreferenz §8) +pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten +pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix diff --git a/crates/tb-runtime/src/traps.rs b/crates/tb-runtime/src/traps.rs new file mode 100644 index 0000000..3bdeb83 --- /dev/null +++ b/crates/tb-runtime/src/traps.rs @@ -0,0 +1,520 @@ +//! Zustände der klassischen Ereignis-Traps (Sprachreferenz §8). +//! +//! Die Fachlogik liegt hier, ohne VM und ohne Host: welche Quelle in +//! welchem Zustand ist, was ansteht und was als Nächstes zuzustellen wäre. +//! Wer das Sprungziel anspringt, entscheidet die VM. +//! +//! Belege und Festlegungen: `umfang-und-form.md` des Changes +//! `phase-4-ereignisschleife`. + +/// Eine Ereignisquelle. Die Reihenfolge der Varianten ist zugleich die +/// Zustellreihenfolge bei gleichzeitiger Fälligkeit (Festlegung, weil das +/// Vorbild dazu nichts hergibt): Tasten zuerst, weil sie an einer +/// Benutzereingabe hängen. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Quelle { + /// `ON KEY(n)` — n aus 0, 1–25, 30–31. + Key(u8), + /// `ON TIMER(n)`. + Timer, + /// `ON UEVENT`. + UEvent, + /// `ON SIGNAL(n)` — n aus 1–2. + Signal(u8), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Zustand { + /// Zustellen. + An, + /// Verwerfen — auch das bereits Gemerkte. + #[default] + Aus, + /// Höchstens eines merken und beim nächsten `ON` zustellen. + Gestoppt, +} + +/// Ein definierter Trap: Ziel, Zustand, ob etwas ansteht. +#[derive(Debug, Clone, Copy)] +struct Trap { + quelle: Quelle, + /// Sprungziel im Modulrumpf. `None` = `GOSUB 0`, also abgeschaltet. + ziel: Option, + zustand: Zustand, + /// Höchstens ein gemerktes Ereignis (Vorbild: „remembered"). + anstehend: bool, + /// Läuft der Handler dieser Quelle gerade? + laeuft: bool, + /// Wurde im laufenden Handler ausdrücklich `OFF` ausgeführt? Dann + /// unterbleibt das selbsttätige `ON` beim `RETURN`. + off_im_handler: bool, +} + +/// Alle Traps eines Moduls. +#[derive(Debug, Default)] +pub struct Traps { + traps: Vec, + /// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein + /// dritter Zustand — den kennt das Vorbild nicht. + event_an: bool, + /// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er + /// das nächste Mal fällig wird (beides in Millisekunden). + timer_intervall_ms: u64, + timer_faellig_ms: u64, + /// Benutzerdefinierte Tasten 15–25: Umschaltzustand und Scancode aus + /// `KEY n, CHR$(flag) + CHR$(scancode)`. + benutzertasten: [Option<(u8, u8)>; 11], + /// Gibt es überhaupt einen nicht abgeschalteten Trap? Gepflegt statt + /// gerechnet: die VM fragt das an jeder Anweisungsgrenze (design.md, D4). + aktiv: bool, +} + +impl Traps { + pub fn neu() -> Self { + Traps { + event_an: true, + ..Default::default() + } + } + + fn finde(&mut self, q: Quelle) -> Option<&mut Trap> { + self.traps.iter_mut().find(|t| t.quelle == q) + } + + /// `ON GOSUB ziel`. `ziel = None` schaltet den Trap ab + /// (`GOSUB 0`). Der Zustand bleibt, was er war — das Einschalten ist + /// Sache der Steueranweisung. + pub fn definieren(&mut self, q: Quelle, ziel: Option) { + if let Some(t) = self.finde(q) { + t.ziel = ziel; + if ziel.is_none() { + t.anstehend = false; + } + self.aktiv = self.traps.iter().any(|t| t.ziel.is_some()); + return; + } + self.aktiv = self.aktiv || ziel.is_some(); + self.traps.push(Trap { + quelle: q, + ziel, + zustand: Zustand::Aus, + anstehend: false, + laeuft: false, + off_im_handler: false, + }); + } + + /// Intervall des Zeit-Traps setzen (Sekunden, 1–86 400) und die erste + /// Fälligkeit von `jetzt_ms` aus rechnen. + pub fn timer_intervall(&mut self, sekunden: u64, jetzt_ms: u64) { + self.timer_intervall_ms = sekunden.saturating_mul(1000); + self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms); + } + + /// Steueranweisung ` ON|OFF|STOP`. Ohne vorangehende + /// Trap-Definition bleibt sie wirkungslos, aber fehlerfrei. + pub fn setzen(&mut self, q: Quelle, z: Zustand, jetzt_ms: u64) { + let neu_faellig = matches!(q, Quelle::Timer) && z == Zustand::An; + let intervall = self.timer_intervall_ms; + if let Some(t) = self.finde(q) { + if z == Zustand::Aus { + // „OFF … Events occurring while trapping is off are + // ignored" und verwirft auch das unter STOP Gemerkte. + t.anstehend = false; + if t.laeuft { + t.off_im_handler = true; + } + } + t.zustand = z; + } + // Ein neu eingeschalteter Zeit-Trap zählt ab jetzt. + if neu_faellig && intervall > 0 { + self.timer_faellig_ms = jetzt_ms.saturating_add(intervall); + } + } + + /// `EVENT ON` / `EVENT OFF`. + pub fn event(&mut self, an: bool) { + if !an { + for trap in &mut self.traps { + trap.anstehend = false; + } + } + self.event_an = an; + } + + /// Ein Ereignis ist eingetreten. Es wird nur gemerkt, wenn die Quelle + /// einen Trap hat und nicht auf `OFF` steht. + pub fn melden(&mut self, q: Quelle) -> bool { + if !self.event_an { + return false; + } + if let Some(t) = self.finde(q) { + if t.ziel.is_some() && t.zustand != Zustand::Aus { + t.anstehend = true; + return true; + } + } + false + } + + /// Zeit prüfen und den Zeit-Trap melden, wenn die Frist verstrichen ist. + /// Übersprungene Fristen zählen als eine — das Vorbild merkt sich nur + /// eines. + pub fn zeit_pruefen(&mut self, jetzt_ms: u64) { + if self.timer_intervall_ms == 0 || jetzt_ms < self.timer_faellig_ms { + return; + } + // Nächste Fälligkeit vom jetzigen Zeitpunkt aus, nicht kumulativ: + // sonst holt ein langer Handler die verpassten Fristen nach. + self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms); + self.melden(Quelle::Timer); + } + + /// Gibt es einen definierten, nicht abgeschalteten Trap? Solange nicht, + /// kostet die Prüfung an der Anweisungsgrenze nichts (design.md, D4). + #[inline] + pub fn aktiv(&self) -> bool { + self.aktiv + } + + /// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`, + /// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds. + pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) { + if (15..=25).contains(&n) { + self.benutzertasten[n as usize - 15] = Some((flag_zu_umschalt(flag), scancode)); + } + } + + /// Welcher `KEY`-Trap fängt diesen Tastendruck? Geprüft wird gegen den + /// Scancode und — bei den benutzerdefinierten Tasten — zusätzlich gegen + /// den Umschaltzustand. `KEY(0)` fängt alle Tasten der Tabelle. + /// + /// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss + /// wissen, welche Quelle er melden soll. + pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option { + if !self.event_an { + return None; + } + let treffer = |k: u8| -> bool { + match k { + 0 => vordefinierte_taste(scancode).is_some(), + 1..=14 | 30 | 31 => vordefinierte_taste(scancode) == Some(k), + 15..=25 => self.benutzertasten[k as usize - 15] == Some((shift, scancode)), + _ => false, + } + }; + self.traps + .iter() + .filter(|t| t.ziel.is_some() && t.zustand != Zustand::Aus && !t.laeuft) + .filter_map(|t| match t.quelle { + Quelle::Key(k) if treffer(k) => Some(k), + _ => None, + }) + // Ein genauer Trap geht dem Sammeltrap `KEY(0)` vor. + .min_by_key(|&k| if k == 0 { u8::MAX } else { k }) + } + + /// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert + /// höchstens eines je Aufruf; die übrigen bleiben anstehend. + pub fn naechstes(&mut self) -> Option<(Quelle, u32)> { + if !self.event_an { + return None; + } + let mut kandidaten: Vec = (0..self.traps.len()) + .filter(|&i| { + let t = &self.traps[i]; + t.anstehend && t.zustand == Zustand::An && !t.laeuft && t.ziel.is_some() + }) + .collect(); + kandidaten.sort_by_key(|&i| self.traps[i].quelle); + let i = *kandidaten.first()?; + let t = &mut self.traps[i]; + t.anstehend = false; + // „an automatic TIMER STOP is executed so that recursive traps + // cannot take place" — gilt für jede Quelle. + t.laeuft = true; + t.off_im_handler = false; + Some((t.quelle, t.ziel.unwrap())) + } + + /// `RETURN` aus dem Handler: „automatically performs a TIMER ON + /// statement unless an explicit TIMER OFF was performed inside". + pub fn handler_beendet(&mut self, q: Quelle) { + if let Some(t) = self.finde(q) { + t.laeuft = false; + if t.off_im_handler { + t.zustand = Zustand::Aus; + t.anstehend = false; + } else { + t.zustand = Zustand::An; + } + t.off_im_handler = false; + } + } +} + +/// Tastaturflagbyte des Vorbilds → unser Bitfeld (1 Umschalt, 2 Strg, +/// 4 Alt). Links und rechts sind im Terminal nicht unterscheidbar, beide +/// Umschaltbits gelten deshalb gleich; NumLock und CapsLock spielen für +/// einen Trap keine Rolle (Festlegung, siehe Sprachreferenz §8). +fn flag_zu_umschalt(flag: u8) -> u8 { + let mut u = 0; + if flag & 0b11 != 0 { + u |= 1; + } + if flag & 0b100 != 0 { + u |= 2; + } + if flag & 0b1000 != 0 { + u |= 4; + } + u +} + +/// Scancode → Nummer des vordefinierten Traps (`ON KEY(1)`–`ON KEY(14)`, +/// `ON KEY(30)`/`ON KEY(31)`), Tabelle aus der Original-Hilfe. +fn vordefinierte_taste(scancode: u8) -> Option { + Some(match scancode { + 0x3B..=0x44 => scancode - 0x3A, // F1–F10 → 1–10 + 0x48 => 11, // Pfeil hoch + 0x4B => 12, // Pfeil links + 0x4D => 13, // Pfeil rechts + 0x50 => 14, // Pfeil runter + 0x85 => 30, // F11 + 0x86 => 31, // F12 + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mit_timer() -> Traps { + let mut t = Traps::neu(); + t.definieren(Quelle::Timer, Some(100)); + t.timer_intervall(1, 0); + t + } + + #[test] + fn ohne_trap_kostet_nichts() { + let mut t = Traps::neu(); + assert!(!t.aktiv()); + assert_eq!(t.naechstes(), None); + t.definieren(Quelle::Timer, Some(7)); + assert!(t.aktiv()); + } + + #[test] + fn aus_verwirft_auch_das_gemerkte() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::Gestoppt, 0); + t.zeit_pruefen(1000); + t.setzen(Quelle::Timer, Zustand::Aus, 1000); + t.setzen(Quelle::Timer, Zustand::An, 1000); + assert_eq!(t.naechstes(), None, "OFF verwirft das unter STOP Gemerkte"); + } + + #[test] + fn gestoppt_merkt_genau_eines() { + let mut t = Traps::neu(); + t.definieren(Quelle::Key(1), Some(42)); + t.setzen(Quelle::Key(1), Zustand::Gestoppt, 0); + t.melden(Quelle::Key(1)); + t.melden(Quelle::Key(1)); + t.melden(Quelle::Key(1)); + t.setzen(Quelle::Key(1), Zustand::An, 0); + assert_eq!(t.naechstes(), Some((Quelle::Key(1), 42))); + t.handler_beendet(Quelle::Key(1)); + assert_eq!(t.naechstes(), None, "gemerkt wird hoechstens eines"); + } + + #[test] + fn aus_verwirft_kuenftige() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::Aus, 0); + t.zeit_pruefen(5000); + t.setzen(Quelle::Timer, Zustand::An, 5000); + assert_eq!(t.naechstes(), None); + } + + #[test] + fn zeit_wird_erst_nach_der_frist_faellig() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.zeit_pruefen(999); + assert_eq!(t.naechstes(), None); + t.zeit_pruefen(1000); + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100))); + } + + #[test] + fn uebersprungene_fristen_zaehlen_als_eine() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.zeit_pruefen(10_000); // zehn Fristen auf einmal + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100))); + t.handler_beendet(Quelle::Timer); + t.zeit_pruefen(10_000); + assert_eq!(t.naechstes(), None, "nicht nachgeholt"); + } + + #[test] + fn kein_wiedereintritt_waehrend_der_handler_laeuft() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.zeit_pruefen(1000); + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100))); + t.zeit_pruefen(2000); + assert_eq!(t.naechstes(), None, "kein Wiedereintritt"); + t.handler_beendet(Quelle::Timer); + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)), "danach einmal"); + } + + #[test] + fn off_im_handler_ueberlebt_das_return() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.zeit_pruefen(1000); + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100))); + t.setzen(Quelle::Timer, Zustand::Aus, 1000); // OFF im Handler + t.handler_beendet(Quelle::Timer); + t.zeit_pruefen(9000); + assert_eq!(t.naechstes(), None, "RETURN schaltet nicht wieder ein"); + } + + #[test] + fn return_schaltet_sonst_selbsttaetig_ein() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.zeit_pruefen(1000); + t.naechstes().unwrap(); + t.handler_beendet(Quelle::Timer); + t.zeit_pruefen(2000); + assert_eq!(t.naechstes(), Some((Quelle::Timer, 100))); + } + + #[test] + fn event_off_verwirft_ereignisse_und_laesst_die_zustaende() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.event(false); + t.zeit_pruefen(1000); + assert_eq!(t.naechstes(), None); + t.event(true); + assert_eq!( + t.naechstes(), + None, + "EVENT OFF muss Eingetretenes verwerfen" + ); + t.zeit_pruefen(2000); + assert_eq!( + t.naechstes(), + Some((Quelle::Timer, 100)), + "TIMER ON blieb erhalten" + ); + } + + #[test] + fn reihenfolge_key_vor_timer_vor_uevent_vor_signal() { + let mut t = Traps::neu(); + for (q, ziel) in [ + (Quelle::Signal(1), 4), + (Quelle::UEvent, 3), + (Quelle::Timer, 2), + (Quelle::Key(5), 1), + ] { + t.definieren(q, Some(ziel)); + t.setzen(q, Zustand::An, 0); + t.melden(q); + } + let mut reihe = Vec::new(); + while let Some((q, _)) = t.naechstes() { + reihe.push(q); + t.handler_beendet(q); + } + assert_eq!( + reihe, + vec![ + Quelle::Key(5), + Quelle::Timer, + Quelle::UEvent, + Quelle::Signal(1) + ] + ); + } + + #[test] + fn key_null_faengt_alle_tasten_der_tabelle() { + let mut t = Traps::neu(); + t.definieren(Quelle::Key(0), Some(9)); + t.setzen(Quelle::Key(0), Zustand::An, 0); + assert_eq!(t.taste_faengt(0x3B, 0), Some(0), "F1"); + assert_eq!(t.taste_faengt(0x48, 0), Some(0), "Pfeil hoch"); + assert_eq!(t.taste_faengt(0x1E, 0), None, "A ist keine Trap-Taste"); + t.setzen(Quelle::Key(0), Zustand::Aus, 0); + assert_eq!(t.taste_faengt(0x3B, 0), None, "OFF gibt die Taste frei"); + } + + #[test] + fn vordefinierte_taste_wird_ueber_den_scancode_erkannt() { + let mut t = Traps::neu(); + t.definieren(Quelle::Key(1), Some(1)); + t.setzen(Quelle::Key(1), Zustand::An, 0); + assert_eq!(t.taste_faengt(0x3B, 0), Some(1), "F1"); + assert_eq!(t.taste_faengt(0x3C, 0), None, "F2 hat keinen Trap"); + } + + #[test] + fn benutzertaste_braucht_umschaltzustand_und_scancode() { + let mut t = Traps::neu(); + t.definieren(Quelle::Key(15), Some(5)); + t.setzen(Quelle::Key(15), Zustand::An, 0); + // KEY 15, CHR$(4) + CHR$(30) — Strg+A + t.benutzertaste(15, 4, 0x1E); + assert_eq!(t.taste_faengt(0x1E, 2), Some(15), "Strg+A"); + assert_eq!(t.taste_faengt(0x1E, 0), None, "A ohne Strg nicht"); + assert_eq!(t.taste_faengt(0x1F, 2), None, "Strg+S nicht"); + } + + #[test] + fn beide_umschaltbits_gelten_gleich() { + let mut t = Traps::neu(); + t.definieren(Quelle::Key(16), Some(6)); + t.setzen(Quelle::Key(16), Zustand::An, 0); + t.benutzertaste(16, 1, 0x1E); // rechte Umschalttaste + assert_eq!(t.taste_faengt(0x1E, 1), Some(16)); + let mut t2 = Traps::neu(); + t2.definieren(Quelle::Key(16), Some(6)); + t2.setzen(Quelle::Key(16), Zustand::An, 0); + t2.benutzertaste(16, 2, 0x1E); // linke Umschalttaste + assert_eq!(t2.taste_faengt(0x1E, 1), Some(16)); + } + + #[test] + fn genauer_trap_geht_dem_sammeltrap_vor() { + let mut t = Traps::neu(); + for (q, z) in [(Quelle::Key(0), 9), (Quelle::Key(1), 1)] { + t.definieren(q, Some(z)); + t.setzen(q, Zustand::An, 0); + } + assert_eq!( + t.taste_faengt(0x3B, 0), + Some(1), + "F1 hat einen eigenen Trap" + ); + assert_eq!(t.taste_faengt(0x3C, 0), Some(0), "F2 faellt an KEY(0)"); + } + + #[test] + fn gosub_null_schaltet_den_trap_ab() { + let mut t = mit_timer(); + t.setzen(Quelle::Timer, Zustand::An, 0); + t.definieren(Quelle::Timer, None); + t.zeit_pruefen(5000); + assert_eq!(t.naechstes(), None); + assert!(!t.aktiv()); + } +} diff --git a/crates/tb-ui/Cargo.toml b/crates/tb-ui/Cargo.toml index d73de55..0a1cf1d 100644 --- a/crates/tb-ui/Cargo.toml +++ b/crates/tb-ui/Cargo.toml @@ -10,6 +10,7 @@ authors.workspace = true tb-runtime.workspace = true ratatui.workspace = true crossterm.workspace = true +signal-hook.workspace = true thiserror.workspace = true log.workspace = true diff --git a/crates/tb-ui/src/host.rs b/crates/tb-ui/src/host.rs index 43bbd8d..39a4912 100644 --- a/crates/tb-ui/src/host.rs +++ b/crates/tb-ui/src/host.rs @@ -5,11 +5,11 @@ //! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2). use std::io::{self, Stdout}; -use std::time::Duration; +use std::time::{Duration, Instant}; use crossterm::event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, - KeyModifiers, + KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }; use crossterm::execute; use crossterm::terminal::{ @@ -18,10 +18,11 @@ use crossterm::terminal::{ use ratatui::backend::CrosstermBackend; use ratatui::Terminal; -use tb_runtime::host::{taste, Ereignis, Host}; +use tb_runtime::host::{taste, umschalt, Ereignis, Host, MausArt, MausEreignis}; use tb_runtime::screen::TextScreen; use crate::screen::ScreenWidget; +use crate::signale::Signalquelle; /// Host für laufende Programme im Terminal. /// @@ -29,6 +30,12 @@ use crate::screen::ScreenWidget; /// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler. pub struct TerminalHost { terminal: Terminal>, + /// Nullpunkt der monotonen Zeit (`Host::jetzt_ms`). + start: Instant, + signale: Signalquelle, + /// Zuletzt bekannte Darstellungsgröße in Zellen. Mausereignisse + /// außerhalb dieser Fläche werden verworfen (design.md, D7). + flaeche: (usize, usize), } impl TerminalHost { @@ -37,7 +44,16 @@ impl TerminalHost { let mut out = io::stdout(); execute!(out, EnterAlternateScreen, EnableMouseCapture)?; let terminal = Terminal::new(CrosstermBackend::new(out))?; - Ok(TerminalHost { terminal }) + let flaeche = terminal + .size() + .map(|s| (s.width as usize, s.height as usize)) + .unwrap_or((80, 25)); + Ok(TerminalHost { + terminal, + start: Instant::now(), + signale: Signalquelle::neu(), + flaeche, + }) } /// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt. @@ -66,8 +82,17 @@ impl Host for TerminalHost { .draw(|f| f.render_widget(ScreenWidget(screen), f.area())); } + fn jetzt_ms(&mut self) -> u64 { + self.start.elapsed().as_millis() as u64 + } + fn next_event(&mut self, blockierend: bool) -> Option { loop { + // Signale vor dem Terminal: sie liegen schon an, wenn wir hier + // ankommen, und ein blockierendes `read` wuerde sie liegenlassen. + if let Some(n) = self.signale.abholen() { + return Some(Ereignis::Signal(n)); + } if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) { return None; } @@ -78,10 +103,16 @@ impl Host for TerminalHost { } } Ok(Event::Resize(cols, rows)) => { + self.flaeche = (cols as usize, rows as usize); return Some(Ereignis::Groesse { cols: cols as usize, rows: rows as usize, - }) + }); + } + Ok(Event::Mouse(m)) => { + if let Some(e) = maus_zu_ereignis(m, self.flaeche) { + return Some(e); + } } Ok(_) => {} Err(_) => return Some(Ereignis::Ende), @@ -90,6 +121,53 @@ impl Host for TerminalHost { } } +/// crossterm-Maus → Ereignis in Zellenkoordinaten (design.md, D7). +/// +/// crossterm zählt ab 0, der Dialekt ab 1 — umgerechnet wird hier, damit es +/// in der Laufzeit nur eine Zählweise gibt. Ereignisse außerhalb der +/// Darstellungsfläche und Ereignisse ohne Entsprechung liefern `None`. +fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option { + let (cols, rows) = flaeche; + let spalte = m.column as usize + 1; + let zeile = m.row as usize + 1; + if spalte > cols || zeile > rows { + return None; + } + let knopf = |b: MouseButton| match b { + MouseButton::Left => 1, + MouseButton::Right => 2, + MouseButton::Middle => 4, + }; + let (art, taste) = match m.kind { + MouseEventKind::Down(b) => (MausArt::Druck, knopf(b)), + MouseEventKind::Up(b) => (MausArt::Loslassen, knopf(b)), + MouseEventKind::Drag(b) => (MausArt::Bewegung, knopf(b)), + MouseEventKind::Moved => (MausArt::Bewegung, 0), + // Rollrad kennt das Vorbild nicht — Stufe 2 (PLAN.md). + MouseEventKind::ScrollDown + | MouseEventKind::ScrollUp + | MouseEventKind::ScrollLeft + | MouseEventKind::ScrollRight => return None, + }; + let mut shift = 0u8; + if m.modifiers.contains(KeyModifiers::SHIFT) { + shift |= 1; + } + if m.modifiers.contains(KeyModifiers::CONTROL) { + shift |= 2; + } + if m.modifiers.contains(KeyModifiers::ALT) { + shift |= 4; + } + Some(Ereignis::Maus(MausEreignis { + art, + taste, + shift, + zeile, + spalte, + })) +} + /// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung. fn taste_zu_ereignis(k: KeyEvent) -> Option { // Strg+Untbr bzw. Strg+C: Abbruchwunsch. @@ -118,7 +196,17 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option { KeyCode::Delete => taste::sonder(83), _ => return None, }; - Some(Ereignis::Taste(s)) + let mut shift = 0u8; + if k.modifiers.contains(KeyModifiers::SHIFT) { + shift |= umschalt::SHIFT; + } + if k.modifiers.contains(KeyModifiers::CONTROL) { + shift |= umschalt::CTRL; + } + if k.modifiers.contains(KeyModifiers::ALT) { + shift |= umschalt::ALT; + } + Some(Ereignis::Taste(s, shift)) } #[cfg(test)] @@ -128,13 +216,13 @@ mod tests { #[test] fn zeichentaste_wird_zu_inkey_form() { let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); - assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into()))); + assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into(), 0))); } #[test] fn sondertaste_traegt_fuehrendes_nullzeichen() { let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE); - let Some(Ereignis::Taste(s)) = taste_zu_ereignis(k) else { + let Some(Ereignis::Taste(s, _)) = taste_zu_ereignis(k) else { panic!("keine Taste"); }; assert_eq!(s.chars().count(), 2); @@ -142,6 +230,123 @@ mod tests { assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59 } + fn maus(kind: MouseEventKind, col: u16, row: u16, mods: KeyModifiers) -> MouseEvent { + MouseEvent { + kind, + column: col, + row, + modifiers: mods, + } + } + + #[test] + fn maus_zaehlt_ab_eins() { + let e = maus_zu_ereignis( + maus( + MouseEventKind::Down(MouseButton::Left), + 0, + 0, + KeyModifiers::NONE, + ), + (80, 25), + ); + assert_eq!( + e, + Some(Ereignis::Maus(MausEreignis { + art: MausArt::Druck, + taste: 1, + shift: 0, + zeile: 1, + spalte: 1, + })) + ); + } + + #[test] + fn letzte_zelle_liegt_noch_drin() { + let e = maus_zu_ereignis( + maus( + MouseEventKind::Up(MouseButton::Right), + 79, + 24, + KeyModifiers::NONE, + ), + (80, 25), + ); + let Some(Ereignis::Maus(m)) = e else { + panic!("kein Mausereignis") + }; + assert_eq!((m.zeile, m.spalte), (25, 80)); + assert_eq!((m.art, m.taste), (MausArt::Loslassen, 2)); + } + + #[test] + fn ausserhalb_der_flaeche_wird_verworfen() { + let e = maus_zu_ereignis( + maus(MouseEventKind::Moved, 80, 0, KeyModifiers::NONE), + (80, 25), + ); + assert_eq!(e, None); + let e = maus_zu_ereignis( + maus(MouseEventKind::Moved, 0, 25, KeyModifiers::NONE), + (80, 25), + ); + assert_eq!(e, None); + } + + #[test] + fn nach_groessenaenderung_gilt_die_neue_flaeche() { + // Dieselbe Position: bei 80x25 draussen, bei 120x40 drin. + let p = maus(MouseEventKind::Moved, 100, 30, KeyModifiers::NONE); + assert_eq!(maus_zu_ereignis(p, (80, 25)), None); + let Some(Ereignis::Maus(m)) = maus_zu_ereignis(p, (120, 40)) else { + panic!("kein Mausereignis") + }; + assert_eq!((m.zeile, m.spalte), (31, 101)); + } + + #[test] + fn umschaltzustand_als_bitfeld() { + let e = maus_zu_ereignis( + maus( + MouseEventKind::Down(MouseButton::Middle), + 5, + 5, + KeyModifiers::SHIFT | KeyModifiers::ALT, + ), + (80, 25), + ); + let Some(Ereignis::Maus(m)) = e else { + panic!("kein Mausereignis") + }; + assert_eq!(m.shift, 1 | 4); + assert_eq!(m.taste, 4); + } + + #[test] + fn rollrad_hat_keine_entsprechung() { + let e = maus_zu_ereignis( + maus(MouseEventKind::ScrollUp, 1, 1, KeyModifiers::NONE), + (80, 25), + ); + assert_eq!(e, None); + } + + #[test] + fn umschaltzustand_der_taste_wird_mitgefuehrt() { + // Strg+A: Strg allein ist kein Abbruch, nur Strg+C ist es. + let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); + assert_eq!( + taste_zu_ereignis(k), + Some(Ereignis::Taste("a".into(), umschalt::CTRL)) + ); + let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::SHIFT); + let Some(Ereignis::Taste(_, shift)) = taste_zu_ereignis(k) else { + panic!("keine Taste") + }; + assert_eq!(shift, umschalt::ALT | umschalt::SHIFT); + } + #[test] fn strg_c_ist_abbruch() { let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); diff --git a/crates/tb-ui/src/lib.rs b/crates/tb-ui/src/lib.rs index 2ab6e9b..b71df18 100644 --- a/crates/tb-ui/src/lib.rs +++ b/crates/tb-ui/src/lib.rs @@ -8,7 +8,8 @@ //! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge, //! Tastatur- und Mausereignisse. -pub mod screen; -pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse -pub mod forms; pub mod events; +pub mod forms; +pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse +pub mod screen; +pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL) diff --git a/crates/tb-ui/src/signale.rs b/crates/tb-ui/src/signale.rs new file mode 100644 index 0000000..a38fc25 --- /dev/null +++ b/crates/tb-ui/src/signale.rs @@ -0,0 +1,112 @@ +//! Betriebssystemsignale als Ereignisquelle (design.md, D8). +//! +//! Der Signalhandler tut nur eines: er setzt eine Marke. Übersetzt wird sie +//! erst beim Abholen durch den Host, an einem gewöhnlichen Zustellpunkt — +//! im Signalkontext läuft damit kein Programmcode und nichts von der +//! Laufzeit. +//! +//! Abgebildet wird nur, was auf allen Zielplattformen besteht: +//! `SIGNAL(1)` = `SIGINT`, `SIGNAL(2)` = `SIGTERM` (Sprachreferenz, +//! Abweichung 8). Unter Windows nutzt `signal-hook` das `signal` der +//! C-Laufzeit; `SIGTERM` ist dort von außen nicht zustellbar. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use signal_hook::consts::{SIGINT, SIGTERM}; + +/// Kennung von `SIGNAL(n%)`. +pub const SIGNAL_INT: u8 = 1; +pub const SIGNAL_TERM: u8 = 2; + +pub struct Signalquelle { + int: Arc, + term: Arc, +} + +impl Signalquelle { + /// Handler eintragen. Schlägt das fehl, bleibt die Quelle stumm — ein + /// Programm ohne `SIGNAL`-Trap merkt davon nichts, und eines mit Trap + /// verhält sich wie unter `SIGNAL(n) OFF`. + pub fn neu() -> Self { + let int = Arc::new(AtomicBool::new(false)); + let term = Arc::new(AtomicBool::new(false)); + let _ = signal_hook::flag::register(SIGINT, Arc::clone(&int)); + let _ = signal_hook::flag::register(SIGTERM, Arc::clone(&term)); + Signalquelle { int, term } + } + + /// Höchstens ein anstehendes Signal abholen und die Marke zurücksetzen. + /// `SIGINT` hat Vorrang: es ist das dringendere der beiden. + pub fn abholen(&self) -> Option { + if self.int.swap(false, Ordering::SeqCst) { + return Some(SIGNAL_INT); + } + if self.term.swap(false, Ordering::SeqCst) { + return Some(SIGNAL_TERM); + } + None + } +} + +#[cfg(test)] +impl Signalquelle { + /// Quelle ohne eingetragene Handler. Ein echtes Signal erreicht **jede** + /// im Prozess registrierte Quelle; die reinen Übersetzungstests müssen + /// deshalb aus dem Weg bleiben. + fn ohne_registrierung() -> Self { + Signalquelle { + int: Arc::new(AtomicBool::new(false)), + term: Arc::new(AtomicBool::new(false)), + } + } +} + +impl Default for Signalquelle { + fn default() -> Self { + Signalquelle::neu() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Marke → Ereignis, ohne echtes Signal: prüft die Übersetzung. + #[test] + fn marke_wird_zu_kennung_und_verfaellt_danach() { + let q = Signalquelle::ohne_registrierung(); + assert_eq!(q.abholen(), None); + q.int.store(true, Ordering::SeqCst); + assert_eq!(q.abholen(), Some(SIGNAL_INT)); + assert_eq!(q.abholen(), None, "eine Marke gilt genau einmal"); + q.term.store(true, Ordering::SeqCst); + assert_eq!(q.abholen(), Some(SIGNAL_TERM)); + } + + #[test] + fn sigint_hat_vorrang_vor_sigterm() { + let q = Signalquelle::ohne_registrierung(); + q.int.store(true, Ordering::SeqCst); + q.term.store(true, Ordering::SeqCst); + assert_eq!(q.abholen(), Some(SIGNAL_INT)); + assert_eq!(q.abholen(), Some(SIGNAL_TERM)); + } + + /// Ganze Kette: echtes Signal an den eigenen Prozess. Der eingetragene + /// Handler ersetzt die Vorgabewirkung, der Test überlebt es also. + #[test] + fn echtes_signal_setzt_die_marke() { + let q = Signalquelle::neu(); + signal_hook::low_level::raise(SIGINT).expect("raise"); + // Zustellung ist asynchron; ein paar Versuche genuegen. + for _ in 0..1000 { + if let Some(n) = q.abholen() { + assert_eq!(n, SIGNAL_INT); + return; + } + std::thread::yield_now(); + } + panic!("SIGINT kam nicht an"); + } +} diff --git a/crates/tb-vm/src/bytecode.rs b/crates/tb-vm/src/bytecode.rs index aac3847..f51337f 100644 --- a/crates/tb-vm/src/bytecode.rs +++ b/crates/tb-vm/src/bytecode.rs @@ -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(); diff --git a/crates/tb-vm/src/codegen.rs b/crates/tb-vm/src/codegen.rs index 8cfe26e..10e7ee1 100644 --- a/crates/tb-vm/src/codegen.rs +++ b/crates/tb-vm/src/codegen.rs @@ -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, 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 { + 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, diff --git a/crates/tb-vm/src/interp.rs b/crates/tb-vm/src/interp.rs index 4f6d835..d7c8d2a 100644 --- a/crates/tb-vm/src/interp.rs +++ b/crates/tb-vm/src/interp.rs @@ -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, } 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 { + 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 { + 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), } } diff --git a/crates/tb-vm/tests/vm.rs b/crates/tb-vm/tests/vm.rs index 84b0bc0..3aa06c2 100644 --- a/crates/tb-vm/tests/vm.rs +++ b/crates/tb-vm/tests/vm.rs @@ -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 { + 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"); +} diff --git a/docs/bibliothek.md b/docs/bibliothek.md index 4581754..33f82d3 100644 --- a/docs/bibliothek.md +++ b/docs/bibliothek.md @@ -103,9 +103,19 @@ nicht um. ## Ereignisse -| Element | Art | -|---|---| -| `DOEVENTS` | Funktion | +| Element | Art | Syntax und Wirkung | +|---|---|---| +| `DOEVENTS` | Funktion | `DOEVENTS` stellt ein anstehendes Ereignis zu und liefert 0. | +| `EVENT` | Anweisung | `EVENT ON\|OFF` schaltet die Erkennung aller Ereignis-Traps ein oder aus. | +| `KEY (Ereignis)` | Anweisung | `KEY(n) ON\|OFF\|STOP` steuert einen Tasten-Trap. | +| `ON KEY` | Anweisung | `ON KEY(n) GOSUB ziel` bindet eine Taste an einen Handler. | +| `ON SIGNAL` | Anweisung | `ON SIGNAL(n) GOSUB ziel` bindet `SIGINT` oder `SIGTERM` an einen Handler. | +| `ON TIMER` | Anweisung | `ON TIMER(n) GOSUB ziel` bindet ein Intervall von 1–86 400 Sekunden. | +| `ON UEVENT` | Anweisung | `ON UEVENT GOSUB ziel` bindet das benutzerdefinierte Ereignis. | +| `SetUEvent` | Routine | `CALL SetUEvent` löst ein benutzerdefiniertes Ereignis aus. | +| `SIGNAL` | Anweisung | `SIGNAL(n) ON\|OFF\|STOP` steuert einen Signal-Trap. | +| `TIMER` | Anweisung | `TIMER ON\|OFF\|STOP` steuert den Zeit-Trap. | +| `UEVENT` | Anweisung | `UEVENT ON\|OFF\|STOP` steuert den benutzerdefinierten Trap. | ## Fehlerbehandlung diff --git a/docs/inventar.md b/docs/inventar.md index 5e20da1..d6198aa 100644 --- a/docs/inventar.md +++ b/docs/inventar.md @@ -21,7 +21,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter **Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der Abschnitt der Sprachreferenz, bei `offen` ein Strich. -**Abdeckung.** implementiert 217 · offen 15 · Non-Feature 53 · gesamt 285 +**Abdeckung.** implementiert 227 · offen 5 · Non-Feature 53 · gesamt 285 | Name | Art | Gruppe | Status | Fundstelle | Quelle | |---|---|---|---|---|---| @@ -125,22 +125,22 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich. | `TYPE` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr | | `UBOUND` | Funktion | Deklaration | implementiert | tb-frontend::sema | bas7advr | | `DOEVENTS` | Funktion | Ereignisse | implementiert | tb-runtime::builtins | sprachreferenz | -| `EVENT` | Anweisung | Ereignisse | offen | - | bas7advr | -| `KEY (Ereignis)` | Anweisung | Ereignisse | offen | - | bas7advr | -| `ON KEY` | Anweisung | Ereignisse | offen | - | bas7advr | +| `EVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | +| `KEY (Ereignis)` | Anweisung | Ereignisse | implementiert | tb-runtime::traps | bas7advr | +| `ON KEY` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | | `ON PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `ON PLAY` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | -| `ON SIGNAL` | Anweisung | Ereignisse | offen | - | bas7advr | +| `ON SIGNAL` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | | `ON STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | -| `ON TIMER` | Anweisung | Ereignisse | offen | - | bas7advr | -| `ON UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr | +| `ON TIMER` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | +| `ON UEVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | | `PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `PLAY (Ereignis)` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | -| `SetUEvent` | Routine | Ereignisse | offen | - | bas7advr | -| `SIGNAL` | Anweisung | Ereignisse | offen | - | bas7advr | +| `SetUEvent` | Routine | Ereignisse | implementiert | tb-runtime::builtins | bas7advr | +| `SIGNAL` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | | `STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | -| `TIMER` | Anweisung | Ereignisse | offen | - | bas7advr | -| `UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr | +| `TIMER` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | +| `UEVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr | | `ERL` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | | `ERR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | | `ERR` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | diff --git a/docs/sprachreferenz.md b/docs/sprachreferenz.md index 083a13b..6aac2cf 100644 --- a/docs/sprachreferenz.md +++ b/docs/sprachreferenz.md @@ -184,11 +184,63 @@ INTEGER −1 (wahr) / 0 (falsch). ## 8. Ereignis-Traps (klassisch, ohne Forms) -Vorhanden im Vorbild: `ON COM/KEY/PEN/PLAY/STRIG/TIMER/UEVENT(…) GOSUB label` -mit Steueranweisungen `… ON/OFF/STOP`, dazu das `EVENT`-Statement (auch für -Forms-Ereignisse) und `SetUEvent`. Timer-Events und Trapping sind ausgesetzt, -solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant: -`TIMER`, `KEY`, `UEVENT`; `COM/PEN/PLAY/STRIG`: Nicht-Ziel (Fehler 73). +Vorhanden im Vorbild: `ON COM/KEY/PEN/PLAY/SIGNAL/STRIG/TIMER/UEVENT(…) +GOSUB label` mit Steueranweisungen `… ON/OFF/STOP`, dazu das +`EVENT`-Statement (auch für Forms-Ereignisse) und `SetUEvent`. +Timer-Events und Trapping sind ausgesetzt, solange ein Menü-Control den +Fokus hat. Für Terminal Basic relevant: `TIMER`, `KEY`, `UEVENT`, +`SIGNAL`; `COM/PEN/PLAY/STRIG`: Nicht-Ziel (Fehler 73). + +**Traps und Wertebereiche** + +| Anweisung | Wertebereich | Quelle | +|---|---|---| +| `ON TIMER(n&) GOSUB ziel` | `n&` = 1–86 400 Sekunden | verstrichene Zeit | +| `ON KEY(n%) GOSUB ziel` | `n%` = 0 (alle), 1–10 (F1–F10), 11 ↑, 12 ←, 13 →, 14 ↓, 15–25 (benutzerdefiniert), 30–31 (F11–F12) | Tastendruck | +| `ON UEVENT GOSUB ziel` | — | `SetUEvent` | +| `ON SIGNAL(n%) GOSUB ziel` | `n%` = 1 (`SIGINT`), 2 (`SIGTERM`) | Betriebssystem | + +`GOSUB 0` schaltet den jeweiligen Trap ab. Ein Wert außerhalb des +Bereichs wird namentlich abgewiesen — zur Übersetzungszeit, wenn er +konstant ist, sonst mit Fehler 5. + +**Maskierung.** Je Quelle `ON` (zustellen), `OFF` (verwerfen — auch ein +unter `STOP` bereits gemerktes Ereignis) und `STOP` (höchstens eines +merken und beim nächsten `ON` zustellen). `EVENT ON`/`EVENT OFF` +klammern einen Abschnitt, in dem keine Ereignisse erkannt werden müssen; +ein `EVENT STOP` gibt es nicht. + +**Zustellpunkte.** Anweisungsgrenze, `DOEVENTS`, während `SLEEP` und vor +blockierender Eingabe — sonst nirgends. Ein zugestelltes Ereignis +beendet ein laufendes `SLEEP`. + +**Wiedereintritt.** Der Eintritt in einen Handler führt selbsttätig +`STOP` seiner Quelle aus, das `RETURN` selbsttätig `ON` — außer im +Handler stand ein ausdrückliches `OFF`, dann bleibt die Quelle aus. + +**Benutzerdefinierte Trap-Tasten.** `KEY n, CHR$(flag) + CHR$(scancode)` +erklärt für `n` = 15–25, welche Taste der zugehörige `ON KEY(n)`-Trap +fängt — anders als für 1–10 und 30–31, wo dieselbe Anweisung ein Makro +zuweist. `flag` ist das Tastaturflagbyte (1 rechte Umschalttaste, 2 linke, +4 Strg, 8 Alt), `scancode` der Scancode der Taste. Beispiel: `KEY 15, +CHR$(4) + CHR$(30)` fängt Strg+A. + +**Festlegungen ohne Beleg im Vorbild** (2026-09-04; die Original-Hilfe +verweist dazu auf ein nicht vorliegendes Kapitel des Programmer's Guide): + +- Eine Taste mit aktivem `ON KEY`-Trap erscheint **nicht** zusätzlich bei + `INKEY$`/`INPUT`; sonst sähe ein Programm dieselbe Taste zweimal. +- Sind mehrere Ereignisse zugleich fällig, gilt die Reihenfolge `KEY` + (aufsteigend nach `n`), `TIMER`, `UEVENT`, `SIGNAL`; zugestellt wird + eines je Zustellpunkt. +- Zugestellt wird an jeder Anweisungsgrenze, auch innerhalb von + `SUB`/`FUNCTION`; das Sprungziel liegt im Modulrumpf — dieselbe Regel + wie beim modulweiten `ON ERROR`. +- Die Bits des Tastaturflagbytes folgen der BIOS-Belegung. Linke und + rechte Umschalttaste sind im Terminal nicht unterscheidbar; beide Bits + gelten gleich. NumLock und CapsLock werden ignoriert. +- Zeichen ohne Eintrag in der Scancode-Tabelle des Vorbilds können keinen + benutzerdefinierten Trap auslösen. ## 9. Konsolen-E/A @@ -482,3 +534,16 @@ Dialoge: links erhalten, der Cursor wird in die neuen Grenzen geklemmt, und ein `VIEW PRINT`-Bereich wird am unteren Rand geklemmt bzw. — wenn er ganz vom Schirm fällt — auf den vollen Bildschirm zurückgesetzt. + +8. **`SIGNAL` liegt auf Betriebssystemsignalen** (2026-09-04): Im Vorbild + trappte `ON SIGNAL(n%)` OS/2-Signale — Prozessflags A/B/C, ausgelöst + über `DOSFLAGPROCESS` aus einem anderen Prozess; einen Wertebereich + für `n%` dokumentierte die Hilfe nicht. Diese Quelle existiert auf + keiner Zielplattform. Abgebildet wird deshalb die minimale Menge, die + auf allen dreien besteht: `SIGNAL(1)` = `SIGINT`, `SIGNAL(2)` = + `SIGTERM`. Die vier übrigen Signale des C-Standards (`SIGABRT`, + `SIGFPE`, `SIGILL`, `SIGSEGV`) entstehen aus dem eigenen + Programmzustand und sind nach einem `RETURN` nicht fortsetzbar; sie + werden nicht abgebildet. Unter Windows ist `SIGTERM` von außen nicht + zustellbar — `SIGNAL(2)` bleibt dort wirkungslos. Trifft `SIGINT` ohne aktiven Trap + ein, bleibt es beim bisherigen Abbruch. diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/.openspec.yaml b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/.openspec.yaml new file mode 100644 index 0000000..1d9aeef --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-04 diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/design.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/design.md new file mode 100644 index 0000000..5f4d404 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/design.md @@ -0,0 +1,228 @@ +# Design — Ereignisschleife und klassische Ereignis-Traps + +## Context + +Siehe proposal.md — Why. Technisch maßgeblich ist der Bestand aus Phase 3: + +``` + crossterm TerminalHost RtState Vm + --------- ------------ ------- -- + Key --> next_event() --> Ereignis::Taste --> tasten[] <-- INKEY$ + Resize --> next_event() --> Ereignis::Groesse -> screen.resize() + Ctrl+C --> next_event() --> Ereignis::Abbruch -> abbruch + Maus --> x verworfen (`Ok(_) => {}` im TerminalHost) <-- fehlt + Zeit --> x keine Quelle <-- fehlt + + Zustellpunkte heute: Instr::Stmt (screen dirty oder alle 1024 Ticks), + vor blockierender Eingabe. +``` + +Zwei Randbedingungen schneiden den Entwurf zu: + +1. **Builtins sehen die VM nicht.** Ihre Signatur ist + `(&mut RtState, &mut dyn Host, &mut [Value])`. `DOEVENTS` ist heute + ein Builtin und kann deshalb prinzipiell keine Prozedur des laufenden + Programms ausführen. +2. **`Vm::run()` ist nicht wiedereintrittsfähig.** Es gibt keinen + Einstiegspunkt „führe Ziel X aus und komm zurück". + +## Goals / Non-Goals + +**Goals:** + +- Eine Zustellung, die den unterbrochenen Zustand nicht verliert und + ohne aktiven Trap nichts kostet. +- Zeitgesteuerte Programme im Korpus ohne echte Wartezeit und ohne + Flackern zwischen zwei Läufen. +- Ein Ereignismodell, das die Forms-Ereignisse des nächsten Changes + trägt, ohne dann umgebaut zu werden. + +**Non-Goals:** + +- Nebenläufigkeit. Es gibt weiterhin genau einen Ausführungsstrang; die + Zeitquelle ist kein Thread, sondern eine Abfrage am Zustellpunkt. +- Priorisierbare oder verschachtelte Handler. + +## Decisions + +### D1 — Zustellung ist ein Vorgang der VM, kein Builtin + +Die Prüfung „ist etwas fällig?" sitzt in der bestehenden Tick-Prüfung an +`Instr::Stmt`. Ist etwas fällig, springt die VM das Trap-Ziel an, als +hätte das Programm dort ein `GOSUB` ausgeführt. `DOEVENTS` und `SLEEP` +werden zu Anweisungen, die denselben Weg auslösen, statt zu Builtins, +die ihn nicht auslösen können. + +| Alternative | Warum nicht | +|---|---| +| `&mut Vm` an alle Builtins durchreichen | Signaturänderung an ~150 Builtins; echte Rekursion in `run()` mit unbegrenzter Stapeltiefe | +| Trampolin: Builtin liefert „bitte Ziel X rufen" zurück | Trägt die klassischen Traps, aber modales `SHOW` des nächsten Changes wird zur Zustandsmaschine — der Umbau käme dann doch | + +### D2 — Der Handler läuft in einem eigenen Frame, es wird nicht abgewickelt + +`ON ERROR` wickelt die Frame-Kette bis zum Handler-Frame ab. Für einen +Trap ist das falsch: das Programm muss danach weiterlaufen. Die +Zustellung **legt** deshalb einen Frame auf den Modulrumpf **auf**, mit +einer Rückmarke auf die unterbrochene Stelle; `RETURN` nimmt ihn wieder +herunter. + +``` + vor der Zustellung waehrend des Handlers nach RETURN + ------------------ --------------------- ----------- + [ SUB Rechne ] <- top [ Trap-Frame @Marke ] [ SUB Rechne ] + [ Modulrumpf ] [ SUB Rechne ] [ Modulrumpf ] + [ Modulrumpf ] +``` + +Der Trap-Frame teilt die Modulvariablen (sie sind Globals), hat aber +einen eigenen `GOSUB`-Stapel — damit kann ein Handler selbst `GOSUB` +verwenden, ohne den unterbrochenen Stapel zu berühren. + +### D3 — Die Zeit kommt vom Host + +Der `Host`-Trait bekommt eine monotone Zeitabfrage (**BREAKING**). Die +Laufzeit rechnet daraus die Fälligkeit der Zeit-Traps; der Host weiß +nichts von Traps. + +Grund ist die Prüfbarkeit: der `CaptureHost` stellt die Zeit selbst und +ein Korpusprogramm mit `ON TIMER(5)` läuft in Mikrosekunden und immer +gleich. Ein direkter Uhrzugriff in der Laufzeit hätte beides verhindert +und wäre zudem die zweite Uhr neben der von `TIMER`/`TIME$` — die +bleibt, wo sie ist, denn sie liefert Ortszeit, keine monotone Dauer. + +### D4 — Maskierung als Tabelle, Prüfung über das vorhandene Flagwort + +Je Quelle ein Zustand (`ON`/`OFF`/`STOP`) und höchstens ein anstehendes +Ereignis. Belegt aus der Original-Hilfe (`umfang-und-form.md`): der +Eintritt in den Handler führt selbsttätig `STOP` aus, das `RETURN` +selbsttätig `ON` — außer im Handler stand ein ausdrückliches `OFF`; und +`OFF` verwirft auch ein unter `STOP` gemerktes Ereignis. `EVENT` ist +keine dritte Zustandsebene, sondern eine Klammer mit nur `ON` und `OFF`. Die Tick-Prüfung bekommt ein weiteres Bit im vorhandenen +Flagwort neben `F_STEP`/`F_BREAK`/`F_POLL`. Ohne definierten Trap ist +das Bit aus, und die heiße Schleife prüft weiterhin genau ein Wort — +das trägt die Zusicherung „ohne aktiven Trap kein messbarer Aufwand" +aus der Spec. + +### D5 — Getrappte Tasten werden erst am Zustellpunkt aussortiert + +Die Tastenschlange bleibt, wie sie ist. Am Zustellpunkt vergleicht die +Laufzeit den Kopf der Schlange mit den aktiven `KEY`-Traps und nimmt die +Taste nur dann heraus. Damit wirkt ein `KEY(1) OFF` sofort und auch auf +Tasten, die schon in der Schlange liegen — Filtern beim Einsortieren +hätte den Zustand zum Zeitpunkt des Tastendrucks eingefroren. + +### D6 — Virtuelle Zeit im Korpus über eine Kopfzeilen-Direktive + +Der Harness kennt bereits `' tb-screen:` und `' tb-tz:`. Dazu kommt +`' tb-clock: ms/Zustellpunkt`: die virtuelle Uhr des `CaptureHost` +rückt an jedem Zustellpunkt um `n` Millisekunden vor. Ein Programm mit +`ON TIMER(1)` und `' tb-clock: 100ms/Zustellpunkt` feuert damit +reproduzierbar nach zehn Zustellpunkten. Fehlt die Direktive und nutzt +das Programm Zeit-Traps, bricht der Harness ab, statt auf die Systemuhr +auszuweichen. + +### D7 — Maus: Quelle jetzt, Auswertung mit den Steuerelementen + +Die Maus kommt als Quelle in diesem Change herein, weil die +Eingangsaufgabe der Phase die Terminal-Events in die Warteschlange +führt. Zwei Festlegungen fallen dabei an: + +- **Koordinaten in Zellen, 1-basiert.** `crossterm` zählt ab 0, der + Bildschirm des Dialekts ab 1 (`LOCATE`, `CSRLIN`, `SCREEN(z, s)`). + Umgerechnet wird an der Stelle, an der die Ereignisse entstehen — in + der Darstellungsschicht —, damit es in der Laufzeit nur eine Zählweise + gibt. Die Forms-Referenz nennt Mauskoordinaten SINGLE, aber in Zellen; + die Umrechnung in Formularkoordinaten macht später das Objektmodell. +- **Ohne Verbraucher verworfen.** Bis die Steuerelemente da sind, nimmt + niemand ein Mausereignis an. Es am Zustellpunkt zu verwerfen ist die + einzige Variante, die keine unbegrenzt wachsende Warteschlange + hinterlässt; Bewegungsereignisse kommen bei gedrückter Taste in hoher + Frequenz. + +### D8 — `SIGNAL` liegt auf Betriebssystemsignalen + +Entscheidung des Projektinhabers vom 2026-09-04, Beleg und Begründung in +`umfang-und-form.md`. Abgebildet wird nur, was auf **allen** +Zielplattformen besteht: + +| `n%` | Signal | Bemerkung | +|---|---|---| +| 1 | `SIGINT` | überall zustellbar | +| 2 | `SIGTERM` | unter Windows von außen nicht zustellbar (Abweichung) | + +Der Signalhandler tut nichts weiter, als eine Marke zu setzen; der Host +liest sie beim nächsten Abholen und macht ein Ereignis daraus. Damit +läuft kein Programmcode im Signalkontext, und die Zustellung folgt +denselben Punkten wie jede andere Quelle. Liegen beide Marken an, hat +`SIGINT` Vorrang. + +Ein Codepfad für alle drei Plattformen: `signal-hook` gilt sein +`flag`-Modul nicht nach Plattform ab und bildet unter Windows auf das +`signal` der C-Laufzeit ab. Die Abhängigkeit liegt ohnehin im Baum — +`crossterm` nutzt sie unter Unix für `SIGWINCH` —, sie wird für `tb-ui` +nur ausdrücklich deklariert, nicht neu hereingeholt. Damit entfällt der +sonst nötige zweite Pfad über den Konsolen-Steuerhandler von Windows. + +### D9 — Umfang und Form aus den Einzelseiten der Original-Hilfe + +Befund aus `phase-3-isam`: die Themenliste klärt den *Umfang*, die +Einzelseiten klären die *Form*. Für diesen Change heißt das: die +Wertebereiche von `ON KEY(n)`, die Reihenfolge gleichzeitig fälliger +Ereignisse und die Frage, ob Trapping in Prozeduren ruht, werden vor der +Implementierung aus den Einzelseiten belegt (erste Aufgabe in tasks.md), +nicht aus der Erinnerung nachgebaut. Ergibt die Hilfe nichts, gilt eine +dokumentierte Festlegung, die in der Sprachreferenz als Abweichung +steht. + +### D10 — Schleifen bekommen eine Anweisungsgrenze im Rücksprung + +Befund bei der Umsetzung von D1: der Codegenerator übersetzte eine +Schleife **ohne Rumpf** in reine Sprünge — in `FOR i = 1 TO 10000: NEXT` +gab es keine Anweisungsgrenze und damit keinen Zustellpunkt. Das trifft +alle vier Schleifenformen und betraf auch Breakpoints und den Abbruch, +war also schon vor diesem Change eine Lücke. + +Der Rücksprung führt jetzt in jedem Fall über eine Grenze. Bei +`DO`/`WHILE` zeigt er auf die ohnehin vorhandene Grenze des +Schleifenkopfs — das kostet keine Instruktion. Nur `FOR` braucht eine +eigene, weil vor dem Schleifentest die Initialisierung steht. + +## Risks / Trade-offs + +- **Der Trap-Frame sieht einen halbfertigen Zustand** → Zustellung nur + an Anweisungsgrenzen; die Liste der Zustellpunkte ist in der Spec + abschließend und wird nicht „für den Komfort" erweitert. +- **Ereignisse feuern schneller, als der Handler zurückkehrt** → + höchstens ein anstehendes Ereignis je Quelle, und die eigene Quelle + ist während des Handlers gestoppt. Ein Zeit-Trap kann damit Fristen + überspringen; das entspricht dem Vorbild. +- **Tick-Kosten** → `benches/vm.rs` misst vorher und nachher; die + Zusicherung aus der Spec ist ein Messwert, keine Behauptung. Die + zusätzliche Grenze je `FOR`-Durchlauf (D10) geht in dieselbe Messung + ein. +- **`SIGNAL(2)` ist unter Windows wirkungslos** — es gibt dort kein + `kill` → als Abweichung dokumentiert, statt ein Ersatzereignis zu + erfinden, das es im Vorbild nicht gab. +- **`SIGINT` kollidiert mit dem Abbruch** → im Rohmodus liefert das + Terminal Strg+C als Taste, nicht als Signal; ein von außen gesendetes + `SIGINT` geht an den Trap, wenn einer aktiv ist, sonst bleibt es beim + Abbruch. Dieselbe Regel wie bei getrappten Tasten. +- **`Host`-Trait ist BREAKING** → betrifft `TerminalHost`, `CaptureHost` + und den Test-Host in `tb-cli`; alle drei liegen im Repo, kein externer + Verbraucher. +- **Mausereignisse ohne Verbraucher könnten stumm falsch sein** — ein + Fehler im Koordinatenmodell fiele erst im nächsten Change auf → + Unit-Tests der Umrechnung an den Rändern (Zelle 1,1 und die letzte + Zelle) und nach einer Größenänderung, nicht erst über Steuerelemente. + +## Open Questions + +- Ruht das Trapping in Prozeduren eines *anderen* Moduls? Mehrere Module + gibt es erst mit der Projektverwaltung in Phase 5; die Antwort ändert + hier weder Spec noch Aufgaben. +- Ob die Zeitquelle später auch Forms-`Timer`-Controls speist oder diese + eine eigene Fälligkeitsliste bekommen, entscheidet der Change zum + Objektmodell. +- Ob Mausbewegungen ohne gedrückte Taste zusammengefasst werden müssen + (`MouseMove` kommt sehr häufig), zeigt sich erst mit dem ersten + Verbraucher; das Ereignismodell bleibt davon unberührt. diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/messung.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/messung.md new file mode 100644 index 0000000..431c1a7 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/messung.md @@ -0,0 +1,29 @@ +# Messung: Kosten der Ereigniszustellung (Aufgabe 8.1) + +`cargo bench -p tb-vm --bench vm`, Release, dieselbe Maschine. „Vorher" ist +der Stand von `060674b` in einem eigenen Worktree, „nachher" der Median aus +drei Läufen dieses Changes. + +| Last | vorher | nachher | Abweichung | +|---|---|---|---| +| INTEGER-Schleife (10 Mio) | 898 ms | 901 ms | +0,3 % | +| DOUBLE-Schleife (5 Mio) | 419 ms | 429 ms | +2,4 % | +| SUB-Aufrufe (1 Mio, BYREF) | 94 ms | 96 ms | +2,1 % | +| String-Funktionen (200k) | 86 ms | 83 ms | −3,5 % | + +**Deutung.** Zwei Änderungen wirken hier zusammen, und nur die zweite kostet: + +1. **Die Zustellprüfung** an der Anweisungsgrenze ist ein einzelner + Bool-Test (`traps.aktiv()`, ein gepflegtes Feld). Ohne definierten Trap + fällt sie sofort durch; die Uhr wird gar nicht erst gelesen. Das ist im + Rauschen nicht auffindbar. +2. **Die zusätzliche Anweisungsgrenze je `FOR`-Durchlauf** (design.md, D10) + ist eine Instruktion mehr pro Iteration. Sie erklärt die ~2 % auf den + schleifenlastigen Lasten und ist bewusst bezahlt: ohne sie ist + `FOR i = 1 TO n: NEXT` für Ereignisse, Breakpoints und Abbruch taub. + `DO`/`WHILE` zahlen nichts, dort zeigt der Rücksprung auf eine ohnehin + vorhandene Grenze. + +Die Streuung zwischen den Läufen liegt bei ±1,5 % (String-Last sogar +schneller als vorher), die 2 % sind also am Rand der Auflösung dieses +Harness — als obere Schranke aber belastbar. diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/proposal.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/proposal.md new file mode 100644 index 0000000..cd0d435 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/proposal.md @@ -0,0 +1,125 @@ +# Phase 4 (Eingang) — Ereignisschleife und klassische Ereignis-Traps + +## Why + +Phase 4 beginnt laut PLAN mit der Ereignisschleifen-Architektur. Diese +Architektur lässt sich an den **klassischen Ereignis-Traps** vollständig +bauen und beweisen, ohne eine Zeile Forms-Objektmodell: Sie brauchen +dieselbe Warteschlange, dieselben Zustellpunkte, dieselbe Maskierung und +denselben Rückruf in die VM, aber keine Formulare, keine Steuerelemente +und kein `.FRM`. Umgekehrt müsste der Entwurf der Schleife mitten im +Objektmodell blind bezahlt werden. + +Phase 3 hat die halbe Maschinerie bereits nebenbei gebaut: die +`Ereignis`-Warteschlange, das Einsortieren in `RtState::pump`, die +Zustellpunkte an Anweisungsgrenzen und vor blockierender Eingabe sowie +den `CaptureHost` für Tests ohne Terminal. Es fehlen drei Dinge: eine +**Zeitquelle**, die **Maskierung** einzelner Ereignisquellen und vor +allem der **Rückruf in die VM** — heute gibt es keinen Weg, aus der +Laufzeit heraus eine Prozedur oder ein `GOSUB`-Ziel des laufenden +Programms auszuführen. `DOEVENTS()` liefert deshalb konstant 0. + +Zugleich sind die zehn Ereignis-Elemente die letzte Lücke des Inventars +außerhalb von Forms: von den 15 offenen Einträgen entfallen zehn auf +diese Gruppe, drei auf die Forms-Dialoge und zwei auf Phase 5. + +## What Changes + +- **Ereignisquellen und Warteschlange**: Das Ereignismodell der Laufzeit + wird um eine **Zeitquelle** erweitert. Die Zeit kommt vom Host, nicht + aus einem direkten Uhrzugriff der Laufzeit — nur so kann der + `CaptureHost` virtuelle Zeit stellen und ein Korpusprogramm mit + `ON TIMER` deterministisch prüfen. **BREAKING** für den `Host`-Trait. +- **Mausereignisse als vierte Quelle**: Der `TerminalHost` verwirft die + Mausereignisse von `crossterm` heute. Sie werden aufgenommen und in + Zellenkoordinaten geführt — Tastendruck, Loslassen und Bewegung mit + Taste und Umschaltzustand. Die Eingangsaufgabe der Phase lautet + „Terminal-Events → Event-Queue → VM-Ticks"; die Maus ist ein + Terminal-Event und damit Teil der Schleife, auch wenn sie erst die + Steuerelemente auswerten. +- **Rückruf in die VM**: Ereigniszustellung wird ein Vorgang der VM, kein + Builtin. Ein zugestelltes Ereignis führt das `GOSUB`-Ziel seines Traps + aus und kehrt danach an die unterbrochene Stelle zurück. Damit + bekommt auch `DOEVENTS()` seine Wirkung. +- **Zustellpunkte**: Die vorhandenen Punkte (Anweisungsgrenze, + blockierende Eingabe) werden zu einer geschlossenen, dokumentierten + Liste ausgebaut: Anweisungsgrenze, `DOEVENTS`, `SLEEP` (wird durch ein + zugestelltes Ereignis beendet) und blockierende Eingabe. Zwischen + Zustellpunkten unterbricht nichts das Programm. +- **Klassische Ereignis-Traps** (Sprachreferenz §8, **10 + Inventareinträge**): `ON TIMER(n&)`, `ON KEY(n%)`, `ON UEVENT` und + `ON SIGNAL(n%)` je mit `GOSUB label`; die Steueranweisungen `TIMER`, + `KEY(n)`, `UEVENT` und `SIGNAL(n)` je mit `ON`/`OFF`/`STOP` sowie die + Klammer `EVENT` mit `ON`/`OFF`; dazu `SetUEvent`. Wertebereiche nach + Beleg: `KEY` 0, 1–25, 30–31; `TIMER` 1–86 400 Sekunden; `SIGNAL` 1–2. + Ihr Status im Inventar wechselt + auf `implementiert`. +- **Maskierung mit drei Zuständen**: `ON` stellt zu, `OFF` verwirft — + auch ein unter `STOP` bereits gemerktes Ereignis —, `STOP` merkt eines + und stellt es beim nächsten `ON` zu. Der Eintritt in einen Handler + führt selbsttätig `STOP` seiner Quelle aus, das `RETURN` selbsttätig + `ON`, außer im Handler stand ein ausdrückliches `OFF`. +- **`SIGNAL` auf Betriebssystemsignale** (Entscheidung 2026-09-04): Die + Quelle des Vorbilds war OS/2-gebunden (Prozessflags über + `DOSFLAGPROCESS`) und existiert nicht. Abgebildet wird die minimale + Menge, die auf allen Zielplattformen besteht: `SIGNAL(1)` auf `SIGINT`, + `SIGNAL(2)` auf `SIGTERM`. Signale erreichen die Laufzeit über den + Host, nicht im Signalkontext. +- **Getrappte Tasten verlassen den Eingabestrom**: Eine Taste, für die + ein aktiver `ON KEY`-Trap besteht, wird zugestellt und MUST NOT + zusätzlich bei `INKEY$`, `INPUT` oder `INPUT$` erscheinen. +- **Namentliche Abweisung im Frontend**: Die zehn Elemente erhalten + Grammatik und Signaturen. Heute rutscht `ON TIMER(5) GOSUB marke` + stillschweigend in das berechnete `ON ausdruck GOSUB` — genau der + Fall, den der Guiding Principle als Bug definiert. + +**Non-Goals:** + +- Die **Auswertung** von Mausereignissen durch Steuerelemente + (Trefferprüfung, Fokuswechsel, Ziehen) — sie setzt das Objektmodell + voraus und gehört zum Change der Steuerelemente. Quelle, + Warteschlange und Koordinatenmodell entstehen hier. +- Das Forms-Objektmodell, Steuerelemente, `.FRM` und die Dialoge + (`MSGBOX`, `INPUTBOX$`) — eigene Changes derselben Phase. +- `COM`, `PEN`, `PLAY`, `STRIG` bleiben Non-Feature (Fehler 73). + +## Capabilities + +### New Capabilities + +- `ereignissteuerung`: Ereignisquellen (Tastatur, Zeit, + benutzerdefiniert), Warteschlange und Reihenfolge, Zustellpunkte, + Maskierung mit `ON`/`OFF`/`STOP` je Quelle und global, Zustellung als + `GOSUB` in das laufende Programm, Zusammenspiel mit Fehlerbehandlung + und blockierender Eingabe. + +### Modified Capabilities + +- `sprach-frontend`: Grammatik und Signaturprüfung SHALL die + Trap-Anweisungen und `SetUEvent` umfassen; `ON (n) GOSUB` + MUST NOT als berechnetes `ON ausdruck GOSUB` behandelt werden. +- `vm-ausfuehrung`: Die VM SHALL an Anweisungsgrenzen anstehende + Ereignisse zustellen und dafür das `GOSUB`-Ziel des Traps ausführen, + ohne den unterbrochenen Zustand zu verlieren. +- `kompat-testkorpus`: Korpusabdeckung der Ereignis-Traps mit + deterministischer Zeit. + +## Impact + +- `crates/tb-runtime`: `Ereignis` und der `Host`-Trait um die Zeitquelle + erweitert (**BREAKING**); Trap-Zustände und Warteschlange in `RtState`; + `INKEY$`-Puffer filtert getrappte Tasten; `SLEEP` endet bei Zustellung. +- `crates/tb-vm`: Zustellung als Vorgang der Ausführung (neue + Instruktion und Frame-Behandlung); `DOEVENTS` erhält Wirkung. +- `crates/tb-frontend`: Grammatik und Signaturen der zehn Elemente; + `Unsupported`-Absenkung von `SETUEVENT` entfällt. +- `crates/tb-ui`: `TerminalHost` liefert die Zeit, reicht die + Mausereignisse von `crossterm` durch, statt sie zu verwerfen, und + meldet Betriebssystemsignale als Ereignis. +- `tests/compat`: neue Korpusprogramme für Zeit-, Tasten- und + UEvent-Traps sowie für `ON`/`OFF`/`STOP`. +- `docs/`: `sprachreferenz.md` §8 ausformuliert; `inventar.md` setzt die + zehn Einträge auf `implementiert`; `bibliothek.md` um die Elemente + erweitert. +- PLAN.md: die Eingangsaufgabe von Phase 4 und der Punkt „Klassische + Ereignis-Traps" sind damit erledigt. diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/ereignissteuerung/spec.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/ereignissteuerung/spec.md new file mode 100644 index 0000000..ac5a1b9 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/ereignissteuerung/spec.md @@ -0,0 +1,208 @@ +## Purpose + +Die Ereignissteuerung bestimmt, woher Ereignisse kommen, wann ein +laufendes Programm sie zu sehen bekommt und wie es darauf reagiert: +Ereignisquellen (Tastatur, Maus, Zeit, benutzerdefiniert) und ihre +Warteschlange, die abschließende Liste der +Zustellpunkte, die Maskierung je Quelle und global, sowie die Zustellung +als `GOSUB` in das laufende Programm. Sie ist die gemeinsame Grundlage +der klassischen Ereignis-Traps und späterer Forms-Ereignisse. + +## ADDED Requirements + +### Requirement: Ereignisquellen mit Zeit vom Host +Die Laufzeit SHALL Ereignisse aus vier Quellen führen: Tastatur, Maus, +Zeit und benutzerdefinierte Ereignisse. Die Zeit SHALL vom Host bezogen +werden; +die Laufzeit MUST NOT für die Ereigniszustellung selbst auf die +Systemuhr zugreifen. Ein Host ohne Terminal MUST die Zeit frei stellen +können, sodass zeitgesteuerte Programme ohne Wartezeit und mit +reproduzierbarem Ergebnis prüfbar sind. + +#### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit +- **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat +- **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet + +#### Scenario: Wiederholter Lauf liefert dasselbe Ergebnis +- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird +- **THEN** ist die Ausgabe beide Male identisch + +### Requirement: Mausereignisse in Zellenkoordinaten +Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an +die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit +gedrückter Taste, Umschaltzustand und Position. Die Position SHALL in +Zellen des Textbildschirms angegeben werden, gezählt wie bei `LOCATE`, +also 1-basiert. Ereignisse außerhalb der aktuellen Bildschirmfläche MUST +verworfen werden. Die Reihenfolge zwischen Maus- und Tastenereignissen +SHALL der Reihenfolge ihres Eintreffens entsprechen. Solange kein +Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen +werden, damit die Warteschlange nicht unbegrenzt wächst. + +#### Scenario: Position als Zelle +- **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird +- **THEN** trägt das Ereignis Zeile 1 und Spalte 1 + +#### Scenario: Reihenfolge bleibt erhalten +- **WHEN** eine Taste gedrückt und danach die Maus geklickt wird +- **THEN** liefert die Warteschlange erst das Tasten-, dann das Mausereignis + +#### Scenario: Ereignis außerhalb der Fläche +- **WHEN** ein Mausereignis eine Position außerhalb der aktuellen Bildschirmgröße nennt +- **THEN** wird es verworfen und erreicht die Warteschlange nicht + +#### Scenario: Kein Verbraucher, keine Anhäufung +- **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird +- **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter + +### Requirement: Abschließende Liste der Zustellpunkte +Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden +— das Vorbild prüft die Ereignismarke „before executing the next +statement": +an einer Anweisungsgrenze, bei `DOEVENTS`, während `SLEEP` und vor einer +blockierenden Eingabe. Zwischen zwei Zustellpunkten MUST NOT ein +Trap-Handler beginnen; insbesondere MUST NOT eine Anweisung in ihrer +Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes +`SLEEP` beenden. + +#### Scenario: Keine Zustellung innerhalb einer Anweisung +- **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird +- **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze + +#### Scenario: SLEEP endet durch Ereignis +- **WHEN** ein Programm `SLEEP 60` ausführt und nach 2 Sekunden ein aktiver Trap fällig wird +- **THEN** wird der Handler ausgeführt und `SLEEP` kehrt danach zurück, ohne die vollen 60 Sekunden abzuwarten + +#### Scenario: DOEVENTS stellt zu +- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird +- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert + +### Requirement: Maskierung je Quelle mit drei Zuständen +Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse +zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST +NOT nachträglich zugestellt werden, und ein zuvor unter `STOP` gemerktes +Ereignis MUST von `OFF` verworfen werden. `STOP` merkt höchstens ein +anstehendes Ereignis und stellt es beim nächsten `ON` zu. Ohne +vorangehende `ON …`-Trap-Definition MUST eine Steueranweisung wirkungslos +bleiben, aber keinen Fehler auslösen. + +#### Scenario: OFF verwirft +- **WHEN** `TIMER OFF` gilt, die Frist verstreicht und danach `TIMER ON` ausgeführt wird +- **THEN** wird kein Handler ausgeführt + +#### Scenario: STOP merkt genau eines +- **WHEN** `KEY(1) STOP` gilt, F1 dreimal gedrückt wird und danach `KEY(1) ON` ausgeführt wird +- **THEN** läuft der Handler genau einmal + +#### Scenario: OFF verwirft das unter STOP Gemerkte +- **WHEN** `TIMER STOP` gilt, die Frist verstreicht, danach `TIMER OFF` und dann `TIMER ON` ausgeführt werden +- **THEN** läuft kein Handler + +### Requirement: EVENT klammert Abschnitte ohne Ereignisprüfung +`EVENT OFF` SHALL die Ereignisprüfung für den folgenden Abschnitt +abschalten, `EVENT ON` sie wieder einschalten; die beiden klammern einen +Abschnitt, in dem keine Ereignisse erkannt werden müssen. Ein dritter +Zustand MUST NOT bestehen: `EVENT STOP` ist im Vorbild nicht +dokumentiert und MUST namentlich abgewiesen werden. Der Schalter SHALL +die Zustände der einzelnen Quellen überlagern, ohne sie zu verändern: +nach `EVENT ON` gilt für jede Quelle wieder ihr eigener Zustand. +Ereignisse, die während `EVENT OFF` eintreten, MUST NOT nachträglich +zugestellt werden. + +#### Scenario: Klammer überlagert Einzelzustand +- **WHEN** `TIMER ON` gilt und danach `EVENT OFF` ausgeführt wird +- **THEN** wird kein Zeit-Trap zugestellt, und nach `EVENT ON` wird wieder zugestellt, ohne dass `TIMER ON` erneut nötig ist + +#### Scenario: EVENT STOP gibt es nicht +- **WHEN** ein Modul `EVENT STOP` enthält +- **THEN** wird es namentlich abgewiesen + +### Requirement: Zustellung als GOSUB ohne Wiedereintritt +Ein zugestelltes Ereignis SHALL das `GOSUB`-Ziel seines Traps ausführen +und danach an die unterbrochene Stelle zurückkehren; der unterbrochene +Zustand (aufrufende Prozedur, Schleifen, Variablen) MUST erhalten +bleiben. Beim Eintritt in den Handler SHALL für seine eigene Quelle +selbsttätig `STOP` wirken, sodass keine rekursiven Traps entstehen; das +`RETURN` SHALL für sie selbsttätig `ON` ausführen — es sei denn, im +Handler wurde ausdrücklich `OFF` für diese Quelle ausgeführt, dann bleibt +sie aus. Ein `RETURN label` aus einem Handler MUST erlaubt sein. + +#### Scenario: Rückkehr in die unterbrochene Schleife +- **WHEN** ein Trap während einer `FOR`-Schleife zugestellt wird +- **THEN** läuft die Schleife nach `RETURN` mit unverändertem Zählerstand weiter + +#### Scenario: Kein Wiedereintritt +- **WHEN** ein Zeit-Trap läuft und die nächste Frist während des Handlers verstreicht +- **THEN** wird der Handler nicht erneut betreten, sondern das Ereignis nach `RETURN` einmal zugestellt + +#### Scenario: OFF im Handler überlebt das RETURN +- **WHEN** ein Handler `TIMER OFF` ausführt und danach `RETURN` erreicht +- **THEN** bleibt die Quelle aus und kein weiterer Zeit-Trap wird zugestellt + +### Requirement: Getrappte Tasten verlassen den Eingabestrom +Eine Taste, für die ein aktiver `ON KEY`-Trap besteht, SHALL dem Trap +zugestellt werden und MUST NOT zusätzlich über `INKEY$`, `INPUT`, +`LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der +Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben. + +#### Scenario: Getrappte Taste erscheint nicht bei INKEY$ +- **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird +- **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String + +#### Scenario: Nicht getrappte Taste bleibt im Strom +- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird +- **THEN** liefert `INKEY$` die Sondertastenfolge für F1 + +### Requirement: Benutzerdefiniertes Ereignis +`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über +`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für +Maskierung und Zustellung unterliegt wie die übrigen Quellen. + +#### Scenario: SetUEvent löst den Handler aus +- **WHEN** `ON UEVENT GOSUB Marke` mit `UEVENT ON` gilt und `SetUEvent` aufgerufen wird +- **THEN** läuft der Handler am nächsten Zustellpunkt genau einmal + +### Requirement: Reihenfolge bei mehreren fälligen Ereignissen +Sind an einem Zustellpunkt mehrere Ereignisse fällig, SHALL genau eines +zugestellt werden; die übrigen bleiben anstehend und werden an +folgenden Zustellpunkten zugestellt. Die Reihenfolge SHALL festgelegt +und dokumentiert sein, sodass sie über Läufe hinweg gleich bleibt. + +#### Scenario: Zwei gleichzeitig fällige Traps +- **WHEN** an einem Zustellpunkt ein Zeit- und ein Tasten-Trap zugleich fällig sind +- **THEN** läuft zuerst genau ein Handler und der andere am nächsten Zustellpunkt + +### Requirement: Fehler in einem Handler +Ein Laufzeitfehler in einem Trap-Handler SHALL der geltenden +Fehlerbehandlung unterliegen. `RESUME` und `RESUME NEXT` SHALL sich auf +die Anweisung im Handler beziehen, nicht auf die unterbrochene +Anweisung. + +#### Scenario: Fehler im Handler erreicht den Handler des Moduls +- **WHEN** ein Trap-Handler eine Anweisung mit Laufzeitfehler ausführt und ein modulweiter `ON ERROR GOTO`-Handler gesetzt ist +- **THEN** wird dieser Handler betreten und `ERL` nennt die Zeile im Trap-Handler + +### Requirement: Signal-Trap auf Betriebssystemsignalen +`ON SIGNAL(n%) GOSUB` und die Steueranweisung `SIGNAL(n%)` SHALL +denselben Regeln für Maskierung, Zustellung und Wiedereintritt +unterliegen wie die übrigen Quellen. `n%` SHALL auf +Betriebssystemsignale abgebildet werden, und zwar ausschließlich auf die +Menge, die auf allen Zielplattformen besteht: 1 auf den +Unterbrechungswunsch (`SIGINT`), 2 auf den Beendigungswunsch +(`SIGTERM`). Ein anderer Wert MUST namentlich abgewiesen werden — zur +Übersetzungszeit, wenn er konstant ist, sonst mit Laufzeitfehler 5. Ein +Signal MUST NOT im Signalkontext verarbeitet werden; es SHALL über den +Host als Ereignis in die Warteschlange gelangen. Die Abweichung zur +OS/2-gebundenen Quelle des Vorbilds SHALL in der Sprachreferenz +ausgewiesen sein. + +#### Scenario: Signal-Trap folgt der Maskierung +- **WHEN** `ON SIGNAL(1) GOSUB Marke` mit `SIGNAL(1) STOP` gilt und ein `SIGINT` eintrifft +- **THEN** läuft der Handler erst nach `SIGNAL(1) ON`, und zwar genau einmal + +#### Scenario: Unzulässige Signalnummer +- **WHEN** ein Modul `ON SIGNAL(7) GOSUB Marke` enthält +- **THEN** wird es namentlich abgewiesen und die Meldung nennt den zulässigen Bereich + +#### Scenario: Signal ohne Trap bleibt Abbruch +- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist +- **THEN** bleibt es beim bisherigen Abbruchverhalten diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/kompat-testkorpus/spec.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/kompat-testkorpus/spec.md new file mode 100644 index 0000000..2946ffb --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/kompat-testkorpus/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Korpusabdeckung der Ereignis-Traps +Der Testkorpus SHALL die Ereignissteuerung mit Programmen abdecken, die +ohne echte Wartezeit und mit reproduzierbarer Ausgabe laufen: Zeit-Trap, +Tasten-Trap einschließlich der Frage, ob die Taste im Eingabestrom +verbleibt, benutzerdefiniertes Ereignis, die drei Maskierungszustände je +Quelle, der Hauptschalter `EVENT` sowie Rückkehr und Nicht-Wiedereintritt +eines Handlers. Ein Korpusprogramm mit Zeitbezug SHALL seinen +Zeitverlauf im Programmkopf deklarieren; ohne Deklaration MUST der +Harness den Lauf abweisen statt auf die Systemuhr auszuweichen. + +#### Scenario: Zeit-Trap im Korpus +- **WHEN** ein Korpusprogramm mit deklariertem Zeitverlauf und `ON TIMER` ausgeführt wird +- **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe und der Lauf benötigt keine messbare Wartezeit + +#### Scenario: Fehlende Zeitdeklaration +- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert +- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/sprach-frontend/spec.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/sprach-frontend/spec.md new file mode 100644 index 0000000..f6a7432 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/sprach-frontend/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: Ereignis-Traps in Grammatik und Signaturprüfung +Der Parser SHALL die Trap-Anweisungen des Vorbilds als eigene Konstrukte +erkennen: `ON TIMER(n) GOSUB ziel`, `ON KEY(n) GOSUB ziel`, +`ON UEVENT GOSUB ziel` und `ON SIGNAL(n) GOSUB ziel`. Diese Formen MUST +NOT als berechnetes `ON ausdruck GOSUB liste` behandelt werden. Die +Steueranweisungen `TIMER`, `KEY(n)`, `UEVENT` und `SIGNAL(n)` mit `ON`, +`OFF` oder `STOP` SHALL erkannt und nur für diese Quellen zugelassen +werden; `EVENT` SHALL nur `ON` und `OFF` zulassen, weil das Vorbild +keinen dritten Zustand kennt. `COM`, `PEN`, `PLAY` und `STRIG` MUST als +Non-Feature namentlich abgewiesen werden. `SetUEvent` SHALL eine +Signatur besitzen. Die Wertebereiche SHALL geprüft werden: `ON KEY(n%)` +mit `n%` aus 0, 1–25 oder 30–31, `ON TIMER(n&)` mit `n&` aus 1–86 400 +und `ON SIGNAL(n%)` mit `n%` aus 1–2. Eine fehlerhafte Verwendung MUST +eine Diagnose erzeugen, die das Element namentlich nennt. + +#### Scenario: ON TIMER ist kein berechnetes GOSUB +- **WHEN** ein Modul `ON TIMER(5) GOSUB Marke` enthält +- **THEN** wird die Anweisung als Zeit-Trap erfasst und nicht als `ON ausdruck GOSUB` mit einer einelementigen Zielliste + +#### Scenario: Ungültige Kennung wird namentlich gemeldet +- **WHEN** ein Modul `ON KEY(99) GOSUB Marke` enthält +- **THEN** nennt die Diagnose `ON KEY` und den zulässigen Wertebereich + +#### Scenario: EVENT kennt kein STOP +- **WHEN** ein Modul `EVENT STOP` enthält +- **THEN** nennt die Diagnose `EVENT` und die zulässigen Formen `ON` und `OFF` + +#### Scenario: Zeitintervall außerhalb des Bereichs +- **WHEN** ein Modul `ON TIMER(90000) GOSUB Marke` enthält +- **THEN** nennt die Diagnose `ON TIMER` und den Bereich 1 bis 86400 + +#### Scenario: Non-Feature-Quelle wird abgewiesen +- **WHEN** ein Modul `ON PEN GOSUB Marke` enthält +- **THEN** meldet die Übersetzung „Feature unavailable" an der Fundstelle der Anweisung — wie bei jedem anderen Non-Feature — und nicht einen generischen Syntaxfehler + +#### Scenario: SetUEvent besitzt eine Signatur +- **WHEN** ein Modul `CALL SetUEvent` enthält +- **THEN** entsteht keine Diagnose „Subprogram not defined" + +### Requirement: Trap-Ziele sind Sprungziele des Moduls +Das Ziel eines Ereignis-Traps SHALL wie ein `GOSUB`-Ziel aufgelöst +werden. Ein unbekanntes Ziel MUST zur Übersetzungszeit gemeldet werden +und nennt dabei das Label. + +#### Scenario: Unbekanntes Trap-Ziel +- **WHEN** ein Modul `ON TIMER(1) GOSUB Fehlt` ohne Label `Fehlt` enthält +- **THEN** meldet die Übersetzung ein undefiniertes Sprungziel und nennt `Fehlt` diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/vm-ausfuehrung/spec.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/vm-ausfuehrung/spec.md new file mode 100644 index 0000000..6890ee5 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/specs/vm-ausfuehrung/spec.md @@ -0,0 +1,42 @@ +## ADDED Requirements + +### Requirement: Ereigniszustellung an Anweisungsgrenzen +Die VM SHALL an Anweisungsgrenzen prüfen, ob ein Ereignis zuzustellen +ist, und in diesem Fall das `GOSUB`-Ziel des zugehörigen Traps +ausführen. Der unterbrochene Zustand — Aufrufkette, Schleifen, Locals, +`GOSUB`-Stapel und Fehlerzustand — MUST dabei erhalten bleiben; mit +`RETURN` SHALL die Ausführung an der unterbrochenen Stelle fortfahren. +Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist, +MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen. + +#### Scenario: Trap aus einer Prozedur heraus +- **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt +- **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort + +#### Scenario: Ohne aktiven Trap wird die Uhr nicht gelesen +- **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird +- **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert + +### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis +Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`, +`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine +Anweisungsgrenze führen, auch wenn der Schleifenrumpf leer ist. Andernfalls +gäbe es in einer Warteschleife keinen Zustellpunkt: kein Ereignis, kein +Breakpoint und kein Abbruch würden dort wirken. + +#### Scenario: Leere Warteschleife +- **WHEN** `FOR i% = 1 TO 10000` unmittelbar von `NEXT` gefolgt wird +- **THEN** führt der Rücksprung über eine Anweisungsgrenze und ein aktiver Zeit-Trap wird darin zugestellt + +#### Scenario: Leere Schleife in jeder Form +- **WHEN** eine `WHILE`/`WEND`-, `DO WHILE`/`LOOP`- oder `DO`/`LOOP UNTIL`-Schleife ohne Rumpf übersetzt wird +- **THEN** liegt zwischen Rücksprungziel und Rücksprung eine Anweisungsgrenze + +### Requirement: DOEVENTS gibt die Kontrolle ab +`DOEVENTS` SHALL anstehende Ereignisse zustellen und danach einen Wert +liefern. Solange keine Formulare geladen werden können, SHALL der Wert 0 +sein. + +#### Scenario: DOEVENTS in einer Warteschleife +- **WHEN** ein Programm in einer Schleife `DOEVENTS` aufruft und ein Trap fällig wird +- **THEN** läuft der Handler innerhalb dieser Schleife diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/tasks.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/tasks.md new file mode 100644 index 0000000..30940b0 --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/tasks.md @@ -0,0 +1,60 @@ +## 1. Belege aus der Original-Hilfe + +- [x] 1.1 Einzelseiten zu `ON TIMER`, `ON KEY`, `ON UEVENT`, `ON SIGNAL`, `TIMER`, `KEY(n)`, `UEVENT`, `SIGNAL(n)`, `EVENT` und `SetUEvent` erfassen und in `umfang-und-form.md` dieses Changes wortgetreu festhalten (Syntaxzeilen, Wertebereiche von `n`, Reihenfolge gleichzeitig fälliger Ereignisse, Verhalten in Prozeduren); verifiziert durch die vorhandene Datei mit Quellenangabe je Element +- [x] 1.2 Aus 1.1 die Festlegungen ableiten, die die Hilfe **nicht** hergibt, und sie als Abweichung in `docs/sprachreferenz.md` §8 eintragen — insbesondere die Quellenzuordnung von `SIGNAL(n)`; verifiziert durch den Abschnitt „Abweichungen" mit je einem Satz Begründung +- [x] 1.3 Delta-Specs an die Belege aus 1.1 angleichen (Leitplanke „Referenzverhalten schlägt Eleganz"): `EVENT` ohne `STOP`, automatischer `STOP`/`ON` um den Handler, `OFF` verwirft Gemerktes, Wertebereiche von `KEY`/`TIMER`/`SIGNAL`; verifiziert durch `openspec validate --strict` und die geänderten Requirements + +## 2. Zeit- und Mausquelle im Host (BREAKING) + +- [x] 2.1 `Host`-Trait um eine monotone Zeitabfrage erweitern und `TerminalHost` daran anschließen; verifiziert durch `cargo build`, das alle drei Host-Implementierungen ohne Warnung übersetzt +- [x] 2.2 `CaptureHost` um eine stellbare virtuelle Uhr erweitern (setzen und vorrücken); verifiziert durch einen Unit-Test, der die Uhr vorrückt und den gelesenen Wert prüft +- [x] 2.3 Signalquelle: Marke im Signalhandler setzen, Übersetzung in ein Ereignis beim Abholen (D8), `SIGINT` und `SIGTERM` plattformübergreifend über `signal-hook`; verifiziert durch einen Unit-Test, der eine gesetzte Marke als Ereignis wiederfindet +- [x] 2.4 Mausereignis in das Ereignismodell aufnehmen (Drücken, Loslassen, Bewegung, Taste, Umschaltzustand, Position) und `TerminalHost` die `crossterm`-Mausereignisse durchreichen lassen, statt sie zu verwerfen; verifiziert durch Unit-Tests der Abbildung je Ereignisart +- [x] 2.5 Umrechnung auf 1-basierte Zellenkoordinaten in der Darstellungsschicht, Ereignisse außerhalb der Fläche verwerfen (D7); verifiziert durch Unit-Tests an den Rändern — Zelle 1,1, letzte Zelle, Position außerhalb, und dieselben Fälle nach einer Größenänderung +- [x] 2.6 `CaptureHost` kann Mausereignisse einspeisen und die Reihenfolge gegenüber Tastenereignissen bleibt erhalten; verifiziert durch einen Unit-Test mit gemischter Folge + +## 3. Trap-Zustände in der Laufzeit + +- [x] 3.1 Trap-Tabelle in `RtState`: je Quelle Zustand `ON`/`OFF`/`STOP`, höchstens ein anstehendes Ereignis, Zielkennung; verifiziert durch Unit-Tests für die drei Zustandsübergänge inklusive „STOP merkt genau eines" +- [x] 3.2 Hauptschalter `EVENT` als Überlagerung der Einzelzustände; verifiziert durch einen Unit-Test, der nach `EVENT ON` den vorherigen Einzelzustand wiederfindet +- [x] 3.3 Fälligkeit der Zeit-Traps aus der Host-Zeit berechnen; verifiziert durch einen Unit-Test mit virtueller Uhr über mehrere Fristen inklusive übersprungener Frist +- [x] 3.4 `SetUEvent` als Auslöser des benutzerdefinierten Ereignisses; verifiziert durch einen Unit-Test, der genau eine Zustellung sieht +- [x] 3.5 Getrappte Tasten am Zustellpunkt aus der Tastenschlange nehmen (D5); verifiziert durch Unit-Tests für beide Richtungen — getrappte Taste erscheint nicht bei `INKEY$`, nicht getrappte bleibt erhalten +- [x] 3.7 Umschaltzustand im Tastenereignis mitführen und `KEY n, CHR$(flag)+CHR$(scan)` für n = 15–25 als Erklärung einer Trap-Taste (statt Makrozuweisung); verifiziert durch Unit-Tests für Strg+A mit und ohne Strg sowie durch die Scancode-Tabelle aus der Original-Hilfe +- [x] 3.6 Mausereignisse ohne Verbraucher am Zustellpunkt verwerfen und die Warteschlange begrenzen (D7); verifiziert durch einen Unit-Test, der viele Bewegungsereignisse einspeist und die Warteschlangenlänge prüft + +## 4. Zustellung in der VM + +- [x] 4.1 Zustellbit im Flagwort und Fälligkeitsprüfung an `Instr::Stmt`; verifiziert durch einen VM-Test, der ohne aktiven Trap keine Zustellung auslöst +- [x] 4.2 Trap-Frame auf den Modulrumpf legen und mit `RETURN` abräumen (D2), inklusive eigenem `GOSUB`-Stapel und `RETURN label`; verifiziert durch VM-Tests für Zustellung aus dem Modulrumpf und aus einer `SUB` heraus, jeweils mit unverändertem Zustand danach +- [x] 4.3 Eigene Quelle während des Handlers stoppen und mit `RETURN` freigeben; verifiziert durch einen VM-Test, der den Handler nicht erneut betritt +- [x] 4.7 Anweisungsgrenze in den Rücksprung jeder Schleife legen — ohne sie ist `FOR i = 1 TO n: NEXT` für Ereignisse, Breakpoints und Abbruch taub (Befund bei 4.2); verifiziert durch einen Test, der für alle vier Schleifenformen eine Grenze im Kreis nachweist, und dadurch, dass die Trap-Tests mit leerem Schleifenrumpf laufen +- [x] 4.4 Reihenfolge bei mehreren fälligen Ereignissen nach Beleg aus 1.1 festlegen; verifiziert durch einen VM-Test mit zwei gleichzeitig fälligen Quellen +- [x] 4.5 `DOEVENTS` und `SLEEP` von Builtins auf Anweisungen mit Zustellung umstellen; verifiziert durch VM-Tests — Handler läuft innerhalb einer `DOEVENTS`-Schleife, und `SLEEP` endet vorzeitig durch eine Zustellung +- [x] 4.8 Befund bei 4.6: `ERL` meldete die Zeile des Fehlerhandlers statt der Fehlerzeile, sobald der Handler selbst nummeriert war — laufende Zeilennummer und `ERL` getrennt; verifiziert durch einen Regressionstest mit nummeriertem Handler +- [x] 4.6 Fehler in einem Handler an die geltende Fehlerbehandlung geben, `RESUME` bezieht sich auf den Handler; verifiziert durch einen VM-Test, dessen `ERL` die Zeile im Handler nennt + +## 5. Frontend + +- [x] 5.1 Grammatik für `ON TIMER(n)/KEY(n)/UEVENT/SIGNAL(n) GOSUB ziel` als eigene Konstrukte; verifiziert durch einen Parser-Test, der zeigt, dass daraus kein berechnetes `ON ausdruck GOSUB` entsteht +- [x] 5.2 Signaturen und Wertebereichsprüfung der Steueranweisungen und von `SetUEvent`; verifiziert durch Sema-Tests, deren Diagnosen das Element namentlich nennen (`ON KEY(99)`, `ON PEN`, `CALL SetUEvent`) +- [x] 5.3 Trap-Ziele wie `GOSUB`-Ziele auflösen; verifiziert durch einen Sema-Test, der ein fehlendes Label namentlich meldet +- [x] 5.4 Absenkung ins HIR/Bytecode, `Unsupported`-Absenkung von `SETUEVENT` entfernen; verifiziert durch `cargo test -p tb-frontend`, inklusive des Inventartests + +## 6. Korpus und Harness + +- [x] 6.1 Kopfzeilen-Direktive `' tb-clock: ms/Zustellpunkt` im Harness auswerten und ohne Direktive den Lauf mit zeitgesteuerten Traps abweisen (D6); verifiziert durch einen Harness-Test für beide Fälle +- [x] 6.2 Korpusprogramme mit Sollausgabe anlegen: Zeit-Trap, Tasten-Trap mit Eingabestrom-Nachweis, `SetUEvent`, die drei Maskierungszustände, `EVENT`-Hauptschalter, Rückkehr in eine unterbrochene Schleife; verifiziert durch `cargo test -p tb-cli`, byte-genau gegen die `.out` +- [x] 6.3 Denselben Korpuslauf zweimal ausführen und auf identische Ausgabe prüfen; verifiziert durch einen Test, der die Reproduzierbarkeit sicherstellt + +## 7. Dokumentation und Inventar + +- [x] 7.1 `docs/sprachreferenz.md` §8 ausformulieren: Zustellpunkte, Maskierung, Wiedereintritt, Wertebereiche; verifiziert durch den Abschnitt, der jede Spec-Zusicherung wiederfindet +- [x] 7.2 `docs/bibliothek.md` um die zehn Elemente erweitern; verifiziert durch je einen Eintrag mit Syntax und Wirkung +- [x] 7.3 `docs/inventar.md`: die zehn Einträge auf `implementiert` mit Fundstelle setzen; verifiziert durch `inventar_stimmt_mit_code_ueberein` +- [x] 7.4 PLAN.md: Eingangsaufgabe und Punkt „Klassische Ereignis-Traps" der Phase 4 abhaken, Befunde festhalten; verifiziert durch die aktualisierte Phase-4-Liste + +## 8. Abnahme + +- [x] 8.1 `benches/vm.rs` vor und nach der Änderung messen und das Ergebnis in der Änderungsnotiz festhalten; verifiziert durch die Messwerte zur Zusicherung „ohne aktiven Trap kein messbarer Aufwand" +- [x] 8.2 `cargo test` über alle Crates grün und `openspec validate phase-4-ereignisschleife --strict` ohne Befund; verifiziert durch beide Kommandos diff --git a/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/umfang-und-form.md b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/umfang-und-form.md new file mode 100644 index 0000000..ad280ca --- /dev/null +++ b/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/umfang-und-form.md @@ -0,0 +1,261 @@ +# Umfang und Form der Ereignis-Elemente (Aufgaben 1.1, 1.2) + +**Quelle.** `bas7advr.hlp` (BASIC 7 Advisor), Einzelseiten unter +https://dos-help.soulsphere.org/bas7advr.hlp/ (abgerufen 2026-09-04); +Gegenprobe `qb45advr.hlp` unter derselben Domain. Zitate wörtlich aus den +Einzelseiten, Übersetzung nur in den Kommentarzeilen. + +## 1.1 Einzelseiten + +| Element | Seite | +|---|---| +| `ON TIMER` | `x_dot_ontimerrr.html` | +| `ON KEY` | `x_dot_onkeyr.html` | +| `ON UEVENT` | `x_dot_onur.html` | +| `ON SIGNAL` | `x_dot_onsigbar.html` | +| `TIMER` (Anweisung) | `x_dot_timersr.html` | +| `KEY(n)` (Ereignis) | `x_dot_ketvr.html` | +| `UEVENT` | `x_dot_uevvvr.html` | +| `SIGNAL` | `x_dot_signalr.html` | +| `EVENT` | `x_dot_evntonr.html` | +| `SetUEvent` | `x_dot_SETUEVr.html` | + +### ON TIMER + +> `ON TIMER(n&) GOSUB {linenumber | linelabel}` +> +> „The maximum number of seconds that can be specified for the time +> interval, n&, is the number of seconds in a 24-hour day (86,400)." +> +> „A linenumber value of 0 disables event trapping and does not specify +> line 0 as the start of the routine." + +Der Zeitwert ist **LONG** (`n&`), Bereich 1–86 400 Sekunden. +`GOSUB 0` schaltet den Trap ab. + +### TIMER ON/OFF/STOP + +> „TIMER ON enables timer-event trapping." +> +> „TIMER OFF disables timer-event trapping. … Events occurring while +> trapping is off are ignored." +> +> „TIMER STOP suspends timer-event trapping. … Events occurring while +> trapping is off are remembered and processed when the next TIMER ON +> statement is executed." + +**Wiedereintritt — wörtlich belegt:** + +> „an automatic TIMER STOP is executed so that recursive traps cannot +> take place." +> +> „The RETURN from the trapping routine automatically performs a TIMER ON +> statement unless an explicit TIMER OFF was performed inside the +> routine." + +Das ist genauer als „die Quelle gilt während des Handlers als gestoppt": +`RETURN` schaltet die Quelle **auf ON**, außer im Handler stand ein +ausdrückliches `OFF`. + +### KEY(n) — Wertebereich + +> ``` +> KEY(n%) ON +> KEY(n%) OFF +> KEY(n%) STOP +> ``` + +| n% | Taste | +|---|---| +| 0 | alle Tasten dieser Tabelle | +| 1–10 | F1–F10 | +| 11 | Pfeil hoch | +| 12 | Pfeil links | +| 13 | Pfeil rechts | +| 14 | Pfeil runter | +| 15–25 | benutzerdefinierte Tasten | +| 30–31 | F11–F12 (101-Tasten-Tastatur) | + +`ON KEY` selbst: `ON KEY(n%) GOSUB {linenumber | linelabel}`. Zur Frage, +ob eine getrappte Taste im Eingabestrom bleibt, sagt die Einzelseite nur: + +> „After a key is trapped, the information on which key was trapped is no +> longer available." + +Die Gegenprobe in `qb45advr.hlp` (`x_dot_onkeyba.html`, `x_dot_ketv.html`) +enthält dazu **nichts**. → Festlegung nötig, siehe 1.2. + +### Benutzerdefinierte Tasten 15–25 + +Die Einzelseite zu `KEY n, zeichenkette$` (`x_dot_setfkr.html`) behandelt +**nur** die Makrozuweisung für 1–10 und 30–31; die Erklärungsform für +15–25 steht in keiner der beiden Hilfen, ebenso wenig die Bedeutung der +Bits des Tastaturflagbytes. Belegt ist dagegen die **Scancode-Tabelle** +(`qb45advr.hlp`, `x_dot_kbsct.html`), aus der die Zuordnung Zeichen → +Scancode stammt: + +> ESC 01 · „! or 1" 02 … „) or 0" 0B · „_ or -" 0C · „+ or =" 0D · +> TAB 0F · Q 10 … P 19 · ENTER 1C · A 1E … L 26 · Z 2C … M 32 · +> SPACEBAR 39 · F1 3B … F10 44 · HOME 47 · UP 48 · LEFT 4B · RIGHT 4D · +> END 4F · DOWN 50 · INS 52 · DEL 53 + +→ Festlegung nötig für das Flagbyte, siehe 1.2. + +### UEVENT und SetUEvent + +> `ON UEVENT GOSUB {linenumber | linelabel}` +> +> „This event typically originates from a hardware interrupt but can also +> be triggered by software." +> +> „SetUEvent … sets a flag that BASIC checks before executing the next +> statement." + +Bestätigt die Zustellung **an der Anweisungsgrenze**. + +> ``` +> UEVENT ON +> UEVENT OFF +> UEVENT STOP +> ``` +> +> UEVENT STOP „preserves events in memory for processing once UEVENT ON +> resumes, though executing UEVENT OFF afterward will discard these queued +> events." + +`OFF` verwirft also auch ein unter `STOP` gemerktes Ereignis. + +`SetUEvent` ist eine **Routine** ohne dokumentierte Argumentliste und ohne +dokumentierten Rückgabewert: + +> „It sets the BASIC entry point that causes a user-defined event." + +### SIGNAL + +> ``` +> SIGNAL(n%) ON +> SIGNAL(n%) OFF +> SIGNAL(n%) STOP +> ``` +> +> `ON SIGNAL(n%) GOSUB {linenumber | linelabel}` +> +> „A linenumber value of 0 disables event trapping." + +Quelle der Signale — wörtlich: + +> „OS/2 protected-mode signal" +> +> „Process flags A, B, and C are used for communicating between +> processes." +> +> aktiviert durch die OS/2-Funktion `DOSFLAGPROCESS` aus einem anderen +> Prozess. + +Ein **Wertebereich für `n%` ist nicht dokumentiert**; die Einzelseite +verweist auf „Chapter 9, ‚Event Handling'" des Programmer's Guide, der +nicht vorliegt. + +### EVENT + +> ``` +> EVENT ON +> EVENT OFF +> ``` +> +> „EVENT OFF … equivalent to compiling a program from the BC command line +> without using /V or /W." +> +> „EVENT OFF and EVENT ON can be used to bracket sections of code where +> events do not need to be detected and trapped, and fast performance is +> required." + +**`EVENT STOP` ist nicht dokumentiert.** `EVENT` ist im Vorbild kein +Laufzeit-Hauptschalter mit drei Zuständen, sondern eine Klammer, die die +Erzeugung der Ereignisprüfungen im Kompilat ein- und ausschaltet. + +### Reihenfolge und Trapping in Prozeduren + +Zu beidem geben die Einzelseiten **nichts** her; sie verweisen auf +Kapitel 9 des Programmer's Guide. → Festlegung nötig, siehe 1.2. + +## 1.2 Abweichungen der Delta-Spec vom Beleg + +Nach der Leitplanke „Referenzverhalten schlägt Eleganz … das gilt auch +gegenüber den eigenen Spezifikationen" ist bei jeder Abweichung die +**Spec** anzugleichen. + +| Nr. | Delta-Spec sagt | Beleg sagt | Folge | +|---|---|---|---| +| A | `EVENT` hat `ON`/`OFF`/`STOP` und überlagert die Einzelzustände | nur `ON`/`OFF`; Klammer um die Erzeugung der Prüfungen | Spec angleichen; `EVENT STOP` wird namentlich abgewiesen | +| B | „während des Handlers gilt die Quelle als gestoppt; mit `RETURN` gilt wieder ihr Zustand" | Eintritt = automatisches `STOP`; `RETURN` = automatisches `ON`, außer im Handler stand ein ausdrückliches `OFF` | Spec präzisieren | +| C | `OFF` verwirft künftige Ereignisse | `OFF` verwirft **auch** ein unter `STOP` gemerktes | Spec ergänzen | +| D | `ON KEY(n)` mit Wertebereich | 0, 1–25, 30–31 mit fester Tastenzuordnung | Spec unverändert, Bereich jetzt belegt | +| E | `ON TIMER(n)` | `n&` ist LONG, 1–86 400; `GOSUB 0` schaltet ab | Spec unverändert, Form jetzt belegt | + +**Ohne Beleg — unsere Festlegungen (gehören nach `sprachreferenz.md` +unter „Abweichungen"):** + +1. **Getrappte Tasten verlassen den Eingabestrom.** Von keiner der beiden + Hilfen belegt. Wir legen fest: eine Taste mit aktivem Trap erscheint + nicht zusätzlich bei `INKEY$`/`INPUT`. Begründung: andernfalls sähe ein + Programm dieselbe Taste zweimal, und `KEY(n) OFF` wäre die einzige Art, + eine Taste überhaupt normal zu lesen. +2. **Reihenfolge gleichzeitig fälliger Ereignisse.** Unbelegt. Wir legen + fest: `KEY` (aufsteigend nach `n`), dann `TIMER`, dann `UEVENT`, dann + `SIGNAL` — Tasten zuerst, weil sie an einer Benutzereingabe hängen. +3. **Trapping in Prozeduren.** Unbelegt. Wir legen fest: die Zustellung + erfolgt an jeder Anweisungsgrenze, auch innerhalb von `SUB`/`FUNCTION`; + das Ziel liegt im Modulrumpf. Das entspricht dem bereits umgesetzten + Verhalten des modulweiten `ON ERROR`. +4. **Bits des Tastaturflagbytes** in `KEY n, CHR$(flag)+CHR$(scan)`. + Unbelegt. Wir legen die Belegung des BIOS-Flagbytes zugrunde: + 1 rechte Umschalttaste, 2 linke Umschalttaste, 4 Strg, 8 Alt. Im + Terminal sind linke und rechte Umschalttaste nicht unterscheidbar, + beide Bits gelten deshalb gleich; NumLock und CapsLock spielen für + einen Trap keine Rolle und werden ignoriert. +5. **`SIGNAL`-Quelle.** Die belegte Quelle ist OS/2-gebunden + (`DOSFLAGPROCESS`, Prozessflags A/B/C) und existiert auf keiner + Zielplattform; ein Wertebereich ist nicht dokumentiert. → **Entscheidung + des Projektinhabers ausstehend**, siehe unten. + +## Entscheidung zu `SIGNAL` (Projektinhaber, 2026-09-04) + +`SIGNAL` wird auf **Betriebssystemsignale** abgebildet, und zwar nur auf +die minimale Menge, die auf allen Zielplattformen existiert. + +Der C-Standard garantiert überall — Linux, macOS **und** Windows — +`SIGABRT`, `SIGFPE`, `SIGILL`, `SIGINT`, `SIGSEGV`, `SIGTERM`. Von diesen +sind nur zwei asynchron, von außen ausgelöst und fortsetzbar; die vier +Fault-Signale entstehen aus dem eigenen Programmzustand, und ein +`RETURN` aus ihrem Handler führte in denselben Fehler zurück. Abgebildet +werden deshalb: + +| `n%` | Signal | Bedeutung | +|---|---|---| +| 1 | `SIGINT` | Unterbrechungswunsch von außen | +| 2 | `SIGTERM` | Beendigungswunsch von außen | + +Ein anderer Wert als 1 oder 2 wird namentlich abgewiesen — zur +Übersetzungszeit, wenn er konstant ist, sonst mit Laufzeitfehler 5. + +**Zustellung.** Signale erreichen die Laufzeit wie die Zeit über den +Host; nur er berührt das Betriebssystem. Ein Signal wird nicht im +Signalkontext verarbeitet, sondern setzt eine Marke, die der Host beim +nächsten Abholen in ein Ereignis übersetzt. + +**Verhältnis zum Abbruch.** Im Rohmodus liefert das Terminal Strg+C als +Taste, nicht als Signal; daran ändert sich nichts. Trifft `SIGINT` +dagegen von außen ein (`kill -INT`), gilt: mit aktivem Trap läuft der +Handler, ohne aktiven Trap bleibt es beim bisherigen Abbruch. Dieselbe +Regel wie bei getrappten Tasten. + +**Abweichungen, die zu dokumentieren sind:** + +1. Die belegte Quelle des Vorbilds (OS/2-Prozessflags A/B/C über + `DOSFLAGPROCESS`) existiert nicht; `n%` bedeutet bei uns etwas + anderes als im Vorbild. Ein Wertebereich war im Vorbild ohnehin nicht + dokumentiert. +2. `SIGTERM` ist unter Windows von außen nicht zustellbar (es gibt kein + `kill`); `SIGNAL(2)` bleibt dort wirkungslos. `SIGNAL(1)` wird dort + über den Konsolen-Steuerhandler (`CTRL_C_EVENT`) bedient. diff --git a/openspec/specs/ereignissteuerung/spec.md b/openspec/specs/ereignissteuerung/spec.md new file mode 100644 index 0000000..13f412d --- /dev/null +++ b/openspec/specs/ereignissteuerung/spec.md @@ -0,0 +1,210 @@ +# ereignissteuerung Specification + +## Purpose + +Die Ereignissteuerung bestimmt, woher Ereignisse kommen, wann ein +laufendes Programm sie zu sehen bekommt und wie es darauf reagiert: +Ereignisquellen (Tastatur, Maus, Zeit, benutzerdefiniert) und ihre +Warteschlange, die abschließende Liste der +Zustellpunkte, die Maskierung je Quelle und global, sowie die Zustellung +als `GOSUB` in das laufende Programm. Sie ist die gemeinsame Grundlage +der klassischen Ereignis-Traps und späterer Forms-Ereignisse. + +## Requirements + +### Requirement: Ereignisquellen mit Zeit vom Host +Die Laufzeit SHALL Ereignisse aus vier Quellen führen: Tastatur, Maus, +Zeit und benutzerdefinierte Ereignisse. Die Zeit SHALL vom Host bezogen +werden; +die Laufzeit MUST NOT für die Ereigniszustellung selbst auf die +Systemuhr zugreifen. Ein Host ohne Terminal MUST die Zeit frei stellen +können, sodass zeitgesteuerte Programme ohne Wartezeit und mit +reproduzierbarem Ergebnis prüfbar sind. + +#### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit +- **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat +- **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet + +#### Scenario: Wiederholter Lauf liefert dasselbe Ergebnis +- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird +- **THEN** ist die Ausgabe beide Male identisch + +### Requirement: Mausereignisse in Zellenkoordinaten +Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an +die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit +gedrückter Taste, Umschaltzustand und Position. Die Position SHALL in +Zellen des Textbildschirms angegeben werden, gezählt wie bei `LOCATE`, +also 1-basiert. Ereignisse außerhalb der aktuellen Bildschirmfläche MUST +verworfen werden. Die Reihenfolge zwischen Maus- und Tastenereignissen +SHALL der Reihenfolge ihres Eintreffens entsprechen. Solange kein +Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen +werden, damit die Warteschlange nicht unbegrenzt wächst. + +#### Scenario: Position als Zelle +- **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird +- **THEN** trägt das Ereignis Zeile 1 und Spalte 1 + +#### Scenario: Reihenfolge bleibt erhalten +- **WHEN** eine Taste gedrückt und danach die Maus geklickt wird +- **THEN** liefert die Warteschlange erst das Tasten-, dann das Mausereignis + +#### Scenario: Ereignis außerhalb der Fläche +- **WHEN** ein Mausereignis eine Position außerhalb der aktuellen Bildschirmgröße nennt +- **THEN** wird es verworfen und erreicht die Warteschlange nicht + +#### Scenario: Kein Verbraucher, keine Anhäufung +- **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird +- **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter + +### Requirement: Abschließende Liste der Zustellpunkte +Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden +— das Vorbild prüft die Ereignismarke „before executing the next +statement": +an einer Anweisungsgrenze, bei `DOEVENTS`, während `SLEEP` und vor einer +blockierenden Eingabe. Zwischen zwei Zustellpunkten MUST NOT ein +Trap-Handler beginnen; insbesondere MUST NOT eine Anweisung in ihrer +Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes +`SLEEP` beenden. + +#### Scenario: Keine Zustellung innerhalb einer Anweisung +- **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird +- **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze + +#### Scenario: SLEEP endet durch Ereignis +- **WHEN** ein Programm `SLEEP 60` ausführt und nach 2 Sekunden ein aktiver Trap fällig wird +- **THEN** wird der Handler ausgeführt und `SLEEP` kehrt danach zurück, ohne die vollen 60 Sekunden abzuwarten + +#### Scenario: DOEVENTS stellt zu +- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird +- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert + +### Requirement: Maskierung je Quelle mit drei Zuständen +Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse +zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST +NOT nachträglich zugestellt werden, und ein zuvor unter `STOP` gemerktes +Ereignis MUST von `OFF` verworfen werden. `STOP` merkt höchstens ein +anstehendes Ereignis und stellt es beim nächsten `ON` zu. Ohne +vorangehende `ON …`-Trap-Definition MUST eine Steueranweisung wirkungslos +bleiben, aber keinen Fehler auslösen. + +#### Scenario: OFF verwirft +- **WHEN** `TIMER OFF` gilt, die Frist verstreicht und danach `TIMER ON` ausgeführt wird +- **THEN** wird kein Handler ausgeführt + +#### Scenario: STOP merkt genau eines +- **WHEN** `KEY(1) STOP` gilt, F1 dreimal gedrückt wird und danach `KEY(1) ON` ausgeführt wird +- **THEN** läuft der Handler genau einmal + +#### Scenario: OFF verwirft das unter STOP Gemerkte +- **WHEN** `TIMER STOP` gilt, die Frist verstreicht, danach `TIMER OFF` und dann `TIMER ON` ausgeführt werden +- **THEN** läuft kein Handler + +### Requirement: EVENT klammert Abschnitte ohne Ereignisprüfung +`EVENT OFF` SHALL die Ereignisprüfung für den folgenden Abschnitt +abschalten, `EVENT ON` sie wieder einschalten; die beiden klammern einen +Abschnitt, in dem keine Ereignisse erkannt werden müssen. Ein dritter +Zustand MUST NOT bestehen: `EVENT STOP` ist im Vorbild nicht +dokumentiert und MUST namentlich abgewiesen werden. Der Schalter SHALL +die Zustände der einzelnen Quellen überlagern, ohne sie zu verändern: +nach `EVENT ON` gilt für jede Quelle wieder ihr eigener Zustand. +Ereignisse, die während `EVENT OFF` eintreten, MUST NOT nachträglich +zugestellt werden. + +#### Scenario: Klammer überlagert Einzelzustand +- **WHEN** `TIMER ON` gilt und danach `EVENT OFF` ausgeführt wird +- **THEN** wird kein Zeit-Trap zugestellt, und nach `EVENT ON` wird wieder zugestellt, ohne dass `TIMER ON` erneut nötig ist + +#### Scenario: EVENT STOP gibt es nicht +- **WHEN** ein Modul `EVENT STOP` enthält +- **THEN** wird es namentlich abgewiesen + +### Requirement: Zustellung als GOSUB ohne Wiedereintritt +Ein zugestelltes Ereignis SHALL das `GOSUB`-Ziel seines Traps ausführen +und danach an die unterbrochene Stelle zurückkehren; der unterbrochene +Zustand (aufrufende Prozedur, Schleifen, Variablen) MUST erhalten +bleiben. Beim Eintritt in den Handler SHALL für seine eigene Quelle +selbsttätig `STOP` wirken, sodass keine rekursiven Traps entstehen; das +`RETURN` SHALL für sie selbsttätig `ON` ausführen — es sei denn, im +Handler wurde ausdrücklich `OFF` für diese Quelle ausgeführt, dann bleibt +sie aus. Ein `RETURN label` aus einem Handler MUST erlaubt sein. + +#### Scenario: Rückkehr in die unterbrochene Schleife +- **WHEN** ein Trap während einer `FOR`-Schleife zugestellt wird +- **THEN** läuft die Schleife nach `RETURN` mit unverändertem Zählerstand weiter + +#### Scenario: Kein Wiedereintritt +- **WHEN** ein Zeit-Trap läuft und die nächste Frist während des Handlers verstreicht +- **THEN** wird der Handler nicht erneut betreten, sondern das Ereignis nach `RETURN` einmal zugestellt + +#### Scenario: OFF im Handler überlebt das RETURN +- **WHEN** ein Handler `TIMER OFF` ausführt und danach `RETURN` erreicht +- **THEN** bleibt die Quelle aus und kein weiterer Zeit-Trap wird zugestellt + +### Requirement: Getrappte Tasten verlassen den Eingabestrom +Eine Taste, für die ein aktiver `ON KEY`-Trap besteht, SHALL dem Trap +zugestellt werden und MUST NOT zusätzlich über `INKEY$`, `INPUT`, +`LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der +Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben. + +#### Scenario: Getrappte Taste erscheint nicht bei INKEY$ +- **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird +- **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String + +#### Scenario: Nicht getrappte Taste bleibt im Strom +- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird +- **THEN** liefert `INKEY$` die Sondertastenfolge für F1 + +### Requirement: Benutzerdefiniertes Ereignis +`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über +`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für +Maskierung und Zustellung unterliegt wie die übrigen Quellen. + +#### Scenario: SetUEvent löst den Handler aus +- **WHEN** `ON UEVENT GOSUB Marke` mit `UEVENT ON` gilt und `SetUEvent` aufgerufen wird +- **THEN** läuft der Handler am nächsten Zustellpunkt genau einmal + +### Requirement: Reihenfolge bei mehreren fälligen Ereignissen +Sind an einem Zustellpunkt mehrere Ereignisse fällig, SHALL genau eines +zugestellt werden; die übrigen bleiben anstehend und werden an +folgenden Zustellpunkten zugestellt. Die Reihenfolge SHALL festgelegt +und dokumentiert sein, sodass sie über Läufe hinweg gleich bleibt. + +#### Scenario: Zwei gleichzeitig fällige Traps +- **WHEN** an einem Zustellpunkt ein Zeit- und ein Tasten-Trap zugleich fällig sind +- **THEN** läuft zuerst genau ein Handler und der andere am nächsten Zustellpunkt + +### Requirement: Fehler in einem Handler +Ein Laufzeitfehler in einem Trap-Handler SHALL der geltenden +Fehlerbehandlung unterliegen. `RESUME` und `RESUME NEXT` SHALL sich auf +die Anweisung im Handler beziehen, nicht auf die unterbrochene +Anweisung. + +#### Scenario: Fehler im Handler erreicht den Handler des Moduls +- **WHEN** ein Trap-Handler eine Anweisung mit Laufzeitfehler ausführt und ein modulweiter `ON ERROR GOTO`-Handler gesetzt ist +- **THEN** wird dieser Handler betreten und `ERL` nennt die Zeile im Trap-Handler + +### Requirement: Signal-Trap auf Betriebssystemsignalen +`ON SIGNAL(n%) GOSUB` und die Steueranweisung `SIGNAL(n%)` SHALL +denselben Regeln für Maskierung, Zustellung und Wiedereintritt +unterliegen wie die übrigen Quellen. `n%` SHALL auf +Betriebssystemsignale abgebildet werden, und zwar ausschließlich auf die +Menge, die auf allen Zielplattformen besteht: 1 auf den +Unterbrechungswunsch (`SIGINT`), 2 auf den Beendigungswunsch +(`SIGTERM`). Ein anderer Wert MUST namentlich abgewiesen werden — zur +Übersetzungszeit, wenn er konstant ist, sonst mit Laufzeitfehler 5. Ein +Signal MUST NOT im Signalkontext verarbeitet werden; es SHALL über den +Host als Ereignis in die Warteschlange gelangen. Die Abweichung zur +OS/2-gebundenen Quelle des Vorbilds SHALL in der Sprachreferenz +ausgewiesen sein. + +#### Scenario: Signal-Trap folgt der Maskierung +- **WHEN** `ON SIGNAL(1) GOSUB Marke` mit `SIGNAL(1) STOP` gilt und ein `SIGINT` eintrifft +- **THEN** läuft der Handler erst nach `SIGNAL(1) ON`, und zwar genau einmal + +#### Scenario: Unzulässige Signalnummer +- **WHEN** ein Modul `ON SIGNAL(7) GOSUB Marke` enthält +- **THEN** wird es namentlich abgewiesen und die Meldung nennt den zulässigen Bereich + +#### Scenario: Signal ohne Trap bleibt Abbruch +- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist +- **THEN** bleibt es beim bisherigen Abbruchverhalten diff --git a/openspec/specs/kompat-testkorpus/spec.md b/openspec/specs/kompat-testkorpus/spec.md index d243e7a..a54e6b6 100644 --- a/openspec/specs/kompat-testkorpus/spec.md +++ b/openspec/specs/kompat-testkorpus/spec.md @@ -110,3 +110,21 @@ durchlaufen und danach die neuen Grenzen ausnutzen. #### Scenario: Größenänderung mitten im Programm - **WHEN** der Test-Host während der Ausführung von 80×25 auf 120×40 wechselt - **THEN** bleibt der bisherige Inhalt oben links erhalten und die anschließende Ausgabe nutzt die neuen Grenzen + +### Requirement: Korpusabdeckung der Ereignis-Traps +Der Testkorpus SHALL die Ereignissteuerung mit Programmen abdecken, die +ohne echte Wartezeit und mit reproduzierbarer Ausgabe laufen: Zeit-Trap, +Tasten-Trap einschließlich der Frage, ob die Taste im Eingabestrom +verbleibt, benutzerdefiniertes Ereignis, die drei Maskierungszustände je +Quelle, der Hauptschalter `EVENT` sowie Rückkehr und Nicht-Wiedereintritt +eines Handlers. Ein Korpusprogramm mit Zeitbezug SHALL seinen +Zeitverlauf im Programmkopf deklarieren; ohne Deklaration MUST der +Harness den Lauf abweisen statt auf die Systemuhr auszuweichen. + +#### Scenario: Zeit-Trap im Korpus +- **WHEN** ein Korpusprogramm mit deklariertem Zeitverlauf und `ON TIMER` ausgeführt wird +- **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe und der Lauf benötigt keine messbare Wartezeit + +#### Scenario: Fehlende Zeitdeklaration +- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert +- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt diff --git a/openspec/specs/sprach-frontend/spec.md b/openspec/specs/sprach-frontend/spec.md index 5df0c90..7f64230 100644 --- a/openspec/specs/sprach-frontend/spec.md +++ b/openspec/specs/sprach-frontend/spec.md @@ -199,3 +199,51 @@ Bezeichner oder mit einem generischen Syntaxfehler abgewiesen werden #### Scenario: Typprüfung des Satzarguments - **WHEN** `RETRIEVE #1, x$` mit einem String statt einer Variablen des Tabellentyps geprüft wird - **THEN** wird „Type mismatch" mit Quelltextposition gemeldet + +### Requirement: Ereignis-Traps in Grammatik und Signaturprüfung +Der Parser SHALL die Trap-Anweisungen des Vorbilds als eigene Konstrukte +erkennen: `ON TIMER(n) GOSUB ziel`, `ON KEY(n) GOSUB ziel`, +`ON UEVENT GOSUB ziel` und `ON SIGNAL(n) GOSUB ziel`. Diese Formen MUST +NOT als berechnetes `ON ausdruck GOSUB liste` behandelt werden. Die +Steueranweisungen `TIMER`, `KEY(n)`, `UEVENT` und `SIGNAL(n)` mit `ON`, +`OFF` oder `STOP` SHALL erkannt und nur für diese Quellen zugelassen +werden; `EVENT` SHALL nur `ON` und `OFF` zulassen, weil das Vorbild +keinen dritten Zustand kennt. `COM`, `PEN`, `PLAY` und `STRIG` MUST als +Non-Feature namentlich abgewiesen werden. `SetUEvent` SHALL eine +Signatur besitzen. Die Wertebereiche SHALL geprüft werden: `ON KEY(n%)` +mit `n%` aus 0, 1–25 oder 30–31, `ON TIMER(n&)` mit `n&` aus 1–86 400 +und `ON SIGNAL(n%)` mit `n%` aus 1–2. Eine fehlerhafte Verwendung MUST +eine Diagnose erzeugen, die das Element namentlich nennt. + +#### Scenario: ON TIMER ist kein berechnetes GOSUB +- **WHEN** ein Modul `ON TIMER(5) GOSUB Marke` enthält +- **THEN** wird die Anweisung als Zeit-Trap erfasst und nicht als `ON ausdruck GOSUB` mit einer einelementigen Zielliste + +#### Scenario: Ungültige Kennung wird namentlich gemeldet +- **WHEN** ein Modul `ON KEY(99) GOSUB Marke` enthält +- **THEN** nennt die Diagnose `ON KEY` und den zulässigen Wertebereich + +#### Scenario: EVENT kennt kein STOP +- **WHEN** ein Modul `EVENT STOP` enthält +- **THEN** nennt die Diagnose `EVENT` und die zulässigen Formen `ON` und `OFF` + +#### Scenario: Zeitintervall außerhalb des Bereichs +- **WHEN** ein Modul `ON TIMER(90000) GOSUB Marke` enthält +- **THEN** nennt die Diagnose `ON TIMER` und den Bereich 1 bis 86400 + +#### Scenario: Non-Feature-Quelle wird abgewiesen +- **WHEN** ein Modul `ON PEN GOSUB Marke` enthält +- **THEN** meldet die Übersetzung „Feature unavailable" an der Fundstelle der Anweisung — wie bei jedem anderen Non-Feature — und nicht einen generischen Syntaxfehler + +#### Scenario: SetUEvent besitzt eine Signatur +- **WHEN** ein Modul `CALL SetUEvent` enthält +- **THEN** entsteht keine Diagnose „Subprogram not defined" + +### Requirement: Trap-Ziele sind Sprungziele des Moduls +Das Ziel eines Ereignis-Traps SHALL wie ein `GOSUB`-Ziel aufgelöst +werden. Ein unbekanntes Ziel MUST zur Übersetzungszeit gemeldet werden +und nennt dabei das Label. + +#### Scenario: Unbekanntes Trap-Ziel +- **WHEN** ein Modul `ON TIMER(1) GOSUB Fehlt` ohne Label `Fehlt` enthält +- **THEN** meldet die Übersetzung ein undefiniertes Sprungziel und nennt `Fehlt` diff --git a/openspec/specs/vm-ausfuehrung/spec.md b/openspec/specs/vm-ausfuehrung/spec.md index 8ecf799..70d3efe 100644 --- a/openspec/specs/vm-ausfuehrung/spec.md +++ b/openspec/specs/vm-ausfuehrung/spec.md @@ -114,3 +114,44 @@ Programmbestandteil. #### Scenario: STOP im Runner - **WHEN** ein per `tbc run` gestartetes Programm `STOP` in Zeile 7 ausführt - **THEN** endet der Prozess mit Exit-Code ungleich 0 und die Meldung nennt Zeile 7 + +### Requirement: Ereigniszustellung an Anweisungsgrenzen +Die VM SHALL an Anweisungsgrenzen prüfen, ob ein Ereignis zuzustellen +ist, und in diesem Fall das `GOSUB`-Ziel des zugehörigen Traps +ausführen. Der unterbrochene Zustand — Aufrufkette, Schleifen, Locals, +`GOSUB`-Stapel und Fehlerzustand — MUST dabei erhalten bleiben; mit +`RETURN` SHALL die Ausführung an der unterbrochenen Stelle fortfahren. +Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist, +MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen. + +#### Scenario: Trap aus einer Prozedur heraus +- **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt +- **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort + +#### Scenario: Ohne aktiven Trap wird die Uhr nicht gelesen +- **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird +- **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert + +### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis +Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`, +`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine +Anweisungsgrenze führen, auch wenn der Schleifenrumpf leer ist. Andernfalls +gäbe es in einer Warteschleife keinen Zustellpunkt: kein Ereignis, kein +Breakpoint und kein Abbruch würden dort wirken. + +#### Scenario: Leere Warteschleife +- **WHEN** `FOR i% = 1 TO 10000` unmittelbar von `NEXT` gefolgt wird +- **THEN** führt der Rücksprung über eine Anweisungsgrenze und ein aktiver Zeit-Trap wird darin zugestellt + +#### Scenario: Leere Schleife in jeder Form +- **WHEN** eine `WHILE`/`WEND`-, `DO WHILE`/`LOOP`- oder `DO`/`LOOP UNTIL`-Schleife ohne Rumpf übersetzt wird +- **THEN** liegt zwischen Rücksprungziel und Rücksprung eine Anweisungsgrenze + +### Requirement: DOEVENTS gibt die Kontrolle ab +`DOEVENTS` SHALL anstehende Ereignisse zustellen und danach einen Wert +liefern. Solange keine Formulare geladen werden können, SHALL der Wert 0 +sein. + +#### Scenario: DOEVENTS in einer Warteschleife +- **WHEN** ein Programm in einer Schleife `DOEVENTS` aufruft und ein Trap fällig wird +- **THEN** läuft der Handler innerhalb dieser Schleife diff --git a/tests/compat/traps.bas b/tests/compat/traps.bas new file mode 100644 index 0000000..c98e56b --- /dev/null +++ b/tests/compat/traps.bas @@ -0,0 +1,76 @@ +' tb-screen: 80x25 +' tb-clock: 250ms/Zustellpunkt +' tb-keys: a +' Ereignis-Traps: Zeitquelle, Tasten, UEVENT, Maskierung und Hauptschalter. +' Geprueft werden Verhaeltnisse, keine Zaehlwerte -- wie oft die Frist im +' Lauf faellt, haengt an der Zahl der Zustellpunkte und ist kein Vertrag. +ON TIMER(1) GOSUB Tick +n% = 0 + +TIMER ON +FOR i% = 1 TO 400 +NEXT +PRINT "an: "; n% > 0 + +TIMER OFF +m% = n% +FOR i% = 1 TO 400 +NEXT +PRINT "aus: "; n% = m% + +TIMER ON +EVENT OFF +m% = n% +FOR i% = 1 TO 400 +NEXT +PRINT "klammer: "; n% = m% + +EVENT ON +m% = n% +FOR i% = 1 TO 400 +NEXT +PRINT "wieder: "; n% > m% + +TIMER STOP +m% = n% +FOR i% = 1 TO 400 +NEXT +PRINT "stop: "; n% = m% +TIMER ON +FOR i% = 1 TO 10 +NEXT +PRINT "gemerkt: "; n% = m% + 1 +TIMER OFF +PRINT "rueckkehr:"; i% = 11 + +ON KEY(1) GOSUB Taste +KEY(1) ON +FOR i% = 1 TO 20 +NEXT +PRINT "taste: "; taste% = 1 +PRINT "strom: "; INKEY$ = "a" + +ON UEVENT GOSUB Nutzer +UEVENT ON +CALL SetUEvent +FOR i% = 1 TO 20 +NEXT +PRINT "uevent: "; uevent% = 2 +PRINT "kein re: "; wiedereintritt% = 0 +END + +Tick: + n% = n% + 1 +RETURN + +Taste: + taste% = taste% + 1 +RETURN + +Nutzer: + IF im_handler% THEN wiedereintritt% = -1 + im_handler% = -1 + uevent% = uevent% + 1 + IF uevent% = 1 THEN CALL SetUEvent + im_handler% = 0 +RETURN diff --git a/tests/compat/traps.out b/tests/compat/traps.out new file mode 100644 index 0000000..db4964b --- /dev/null +++ b/tests/compat/traps.out @@ -0,0 +1,11 @@ +an: -1 +aus: -1 +klammer: -1 +wieder: -1 +stop: -1 +gemerkt: -1 +rueckkehr:-1 +taste: -1 +strom: -1 +uevent: -1 +kein re: -1