Phase 4: Ereignisschleife und klassische Traps

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

1
Cargo.lock generated
View File

@@ -636,6 +636,7 @@ dependencies = [
"crossterm", "crossterm",
"log", "log",
"ratatui", "ratatui",
"signal-hook",
"tb-runtime", "tb-runtime",
"thiserror", "thiserror",
] ]

View File

@@ -21,6 +21,10 @@ authors = ["Georg Bauer"]
ratatui = "0.29" ratatui = "0.29"
crossterm = "0.28" 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 # Text und Zeit
unicode-width = "0.2" unicode-width = "0.2"
# Nur fuer den UTC-Versatz zu einem Zeitpunkt (siehe design.md, D1/D2). # Nur fuer den UTC-Versatz zu einem Zeitpunkt (siehe design.md, D1/D2).

View File

@@ -393,10 +393,12 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
Transaktion verfällt. Transaktion verfällt.
## Phase 4 — Forms-Engine (`tb-ui::forms`) ## 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 Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative
Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
Ereignisprozedur) 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` - [ ] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE`
(modal/nicht-modal) (modal/nicht-modal)
- [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox, - [ ] 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 https://github.com/cout/vbdos ohne Non-Features kompilieren und
sind nutzbar (Konsolenprogramme bereits ab Phase 3) sind nutzbar (Konsolenprogramme bereits ab Phase 3)
- [ ] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`) - [ ] 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 `ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit
`GOSUB label`, dazu die Steueranweisungen `TIMER`, `KEY(n)`, `GOSUB label`, dazu die Steueranweisungen `TIMER`, `KEY(n)`,
`UEVENT`, `SIGNAL(n)` mit `ON`/`OFF`/`STOP`, das `EVENT`-Statement `UEVENT`, `SIGNAL(n)` mit `ON`/`OFF`/`STOP`, das `EVENT`-Statement
und `SetUEvent`. Setzt die Ereignisschleife (erste Aufgabe dieser und `SetUEvent`. Setzt die Ereignisschleife (erste Aufgabe dieser
Phase) voraus; `COM`/`PEN`/`PLAY`/`STRIG` bleiben Non-Feature Phase) voraus; `COM`/`PEN`/`PLAY`/`STRIG` bleiben Non-Feature
(Fehler 73). **10 Inventareinträge** (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 - [ ] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als
Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`. Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`.
Sie hängen am Formularmodell, nicht an der Steuerelementliste, und Sie hängen am Formularmodell, nicht an der Steuerelementliste, und

View File

@@ -153,11 +153,16 @@ fn cmd_run(args: &[String]) -> ExitCode {
struct PipeHost { struct PipeHost {
puffer: std::collections::VecDeque<Ereignis>, puffer: std::collections::VecDeque<Ereignis>,
eof: bool, eof: bool,
start: std::time::Instant,
} }
impl PipeHost { impl PipeHost {
fn new() -> Self { 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. /// Eine Zeile von stdin in Tastendrücke zerlegen.
@@ -177,10 +182,12 @@ impl PipeHost {
zeile.pop(); zeile.pop();
} }
for c in zeile.chars() { 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 self.puffer.push_back(Ereignis::Taste(
.push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string())); tb_runtime::host::taste::ENTER.to_string(),
0,
));
} }
} }
} }
@@ -195,4 +202,8 @@ impl Host for PipeHost {
} }
self.puffer.pop_front() self.puffer.pop_front()
} }
fn jetzt_ms(&mut self) -> u64 {
self.start.elapsed().as_millis() as u64
}
} }

View File

@@ -39,7 +39,9 @@ fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) {
/// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen. /// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen.
fn direktive(src: &str, marke: &str) -> Option<String> { fn direktive(src: &str, marke: &str) -> Option<String> {
src.lines() src.lines()
.take(4) // Kopfzeilenfenster: Groesse, Zeitzone, Uhr, Tasten, Tempdir,
// Resize passen zusammen in die ersten Zeilen.
.take(8)
.find(|l| l.contains(marke)) .find(|l| l.contains(marke))
.map(|l| l.split(marke).nth(1).unwrap().trim().to_string()) .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: <folge>`. Sondertasten als
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
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<char> = 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: <n>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<u64> {
let wert = direktive(src, "tb-clock:")?;
let ms = wert
.trim()
.strip_suffix("ms/Zustellpunkt")
.unwrap_or_else(|| panic!("{name}: tb-clock erwartet `<n>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<u64> {
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: <n>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<tb_runtime::host::Ereignis> {
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 /// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit
/// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen /// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen
/// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt. /// 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 }, 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) { match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen), RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!( other => panic!(
@@ -207,12 +341,8 @@ fn assert_output_matches(file: &str, want: &str, got: &str) {
.position(|(a, b)| a != b) .position(|(a, b)| a != b)
.map(|i| i / 2 + 1) .map(|i| i / 2 + 1)
.unwrap_or(0); .unwrap_or(0);
let paar = |s: &str| { let paar =
s.chars() |s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::<String>();
.skip((spalte - 1) * 2)
.take(2)
.collect::<String>()
};
panic!( panic!(
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \ "{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
Soll: {}\n Ist: {}", Soll: {}\n Ist: {}",
@@ -257,12 +387,31 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
let got = run_corpus_file(&path, Some((*cols, *rows))); let got = run_corpus_file(&path, Some((*cols, *rows)));
let etikett = format!("{name} @ {cols}x{rows}"); let etikett = format!("{name} @ {cols}x{rows}");
assert_output_matches(&etikett, &want, &got); 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; checked += 1;
} }
} }
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}"); 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) ---------------------------------------- // ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
#[test] #[test]

View File

@@ -331,10 +331,28 @@ pub enum Stmt {
to: Option<Expr>, to: Option<Expr>,
pos: SourcePos, pos: SourcePos,
}, },
NameStmt { old: Expr, new: Expr, pos: SourcePos }, NameStmt {
old: Expr,
new: Expr,
pos: SourcePos,
},
// ---- Bildschirm/Ereignisse ---- // ---- Bildschirm/Ereignisse ----
/// `VIEW PRINT [oben TO unten]`. /// `VIEW PRINT [oben TO unten]`.
ViewPrint { top: Option<Expr>, bottom: Option<Expr>, pos: SourcePos }, ViewPrint {
top: Option<Expr>,
bottom: Option<Expr>,
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<Expr>,
target: LabelRef,
pos: SourcePos,
},
/// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … /// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
EventControl { EventControl {
device: String, device: String,

View File

@@ -329,6 +329,8 @@ pub enum Builtin {
CommandS, CommandS,
Doevents, Doevents,
Sleep, Sleep,
/// `SetUEvent` — löst das benutzerdefinierte Ereignis aus (§8).
SetUEvent,
Beep, Beep,
// ISAM (Change `phase-3-isam`) // ISAM (Change `phase-3-isam`)
IsamOpen, IsamOpen,
@@ -537,6 +539,22 @@ pub enum HStmtKind {
targets: Vec<LabelId>, targets: Vec<LabelId>,
}, },
ReturnGosub(Option<LabelId>), ReturnGosub(Option<LabelId>),
/// 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<LabelId>,
},
/// `<quelle>(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. /// EXIT SUB/FUNCTION/DEF bzw. Rumpfende.
ExitProc, ExitProc,
CallSub { CallSub {

View File

@@ -666,12 +666,11 @@ impl<'a> P<'a> {
let decls = self.parse_var_decls(); let decls = self.parse_var_decls();
Some(Stmt::StaticDecl { decls, pos }) Some(Stmt::StaticDecl { decls, pos })
} }
TokenKind::Ident { ref name, .. } if name == "LPRINT" => { TokenKind::Ident { ref name, .. } if name == "LPRINT" => self.parse_print(pos, true),
self.parse_print(pos, true) TokenKind::Ident {
} ref name,
TokenKind::Ident { ref name, suffix: None } suffix: None,
if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) => } if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) => {
{
self.advance(); // VIEW self.advance(); // VIEW
self.advance(); // PRINT self.advance(); // PRINT
let (mut top, mut bottom) = (None, None); let (mut top, mut bottom) = (None, None);
@@ -972,9 +971,7 @@ impl<'a> P<'a> {
} }
if matches!(self.k(), TokenKind::Eol) { if matches!(self.k(), TokenKind::Eol) {
// Blockform // Blockform
let stop = |p: &P| { let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
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 then_body = self.parse_stmt_list(stop);
let mut elseifs = Vec::new(); let mut elseifs = Vec::new();
while self.eat_kw(Kw::ElseIf) { while self.eat_kw(Kw::ElseIf) {
@@ -1071,10 +1068,12 @@ impl<'a> P<'a> {
} }
} }
} }
let body = self.parse_stmt_list(|p: &P| { let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select));
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) { if self.at_end_pair(Kw::Select) {
self.advance(); self.advance();
@@ -1149,6 +1148,46 @@ impl<'a> P<'a> {
Some(Stmt::While { cond, body, pos }) Some(Stmt::While { cond, body, pos })
} }
/// `ON <quelle>[(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<Stmt> {
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<Stmt> { fn parse_on(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // ON self.advance(); // ON
let local = self.eat_kw(Kw::Local); let local = self.eat_kw(Kw::Local);
@@ -1186,7 +1225,13 @@ impl<'a> P<'a> {
self.sync(); self.sync();
return None; return None;
} }
// ON ausdruck GOTO/GOSUB liste (Event-Traps: Phase 3/4) // ON <quelle>[(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 expr = self.parse_expr()?;
let gosub = if self.eat_kw(Kw::Goto) { let gosub = if self.eat_kw(Kw::Goto) {
false false
@@ -1528,26 +1573,34 @@ impl<'a> P<'a> {
return None; return None;
} }
// Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … // 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!( if matches!(
name.as_str(), name.as_str(),
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" "TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT"
| "EVENT"
) { ) {
let action = match self.k() { let action = match self.k() {
TokenKind::Kw(Kw::On) => Some(EventAction::On), TokenKind::Kw(Kw::On) => Some(EventAction::On),
TokenKind::Kw(Kw::Stop) => Some(EventAction::Stop), TokenKind::Kw(Kw::Stop) => Some(EventAction::Stop),
TokenKind::Ident { ref name, suffix: None } TokenKind::Ident {
if name == "OFF" => ref name,
{ suffix: None,
Some(EventAction::Off) } if name == "OFF" => Some(EventAction::Off),
}
_ => None, _ => None,
}; };
if let Some(action) = action { if let Some(action) = action {
self.advance(); self.advance();
let index = args.clone().and_then(|mut a| { 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 { return Some(Stmt::EventControl {
device: name.clone(), device: name.clone(),

View File

@@ -233,9 +233,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"NOW" => (0, 0, &[], Db), "NOW" => (0, 0, &[], Db),
"DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db), "DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
"DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db), "DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => { "DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
(1, 1, &[N], I)
}
"FORMAT$" => (1, 2, &[A, S], St), "FORMAT$" => (1, 2, &[A, S], St),
"ERR" | "ERL" => (0, 0, &[], L), "ERR" | "ERL" => (0, 0, &[], L),
"FRE" => (1, 1, &[A], 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#" => { "FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
(5, 5, &[N, N, N, N, N], Db) (5, 5, &[N, N, N, N, N], Db)
} }
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => { "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db),
(6, 6, &[N, N, N, N, N, N], Db)
}
"NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db), "NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db),
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db), "MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db), "SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
@@ -636,9 +632,9 @@ impl Sema {
self.prescan(&a.body, scope, module_data); self.prescan(&a.body, scope, module_data);
} }
} }
Stmt::For { body, .. } Stmt::For { body, .. } | Stmt::DoLoop { body, .. } | Stmt::While { body, .. } => {
| Stmt::DoLoop { body, .. } self.prescan(body, scope, module_data)
| 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::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())), Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
Expr::Paren(e) => self.fold_const(e), Expr::Paren(e) => self.fold_const(e),
Expr::Name { name, args: None, .. } => { Expr::Name {
self.consts.get(name).and_then(|(_, v)| v.clone()) name, args: None, ..
} } => self.consts.get(name).and_then(|(_, v)| v.clone()),
Expr::Unary { op: UnOp::Neg, operand, .. } => { Expr::Unary {
match self.fold_const(operand)? { op: UnOp::Neg,
ConstVal::Num(n) => Some(ConstVal::Num(-n)), operand,
ConstVal::Str(_) => None, ..
} } => match self.fold_const(operand)? {
} ConstVal::Num(n) => Some(ConstVal::Num(-n)),
Expr::Unary { op: UnOp::Not, operand, .. } => { ConstVal::Str(_) => None,
match self.fold_const(operand)? { },
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)), Expr::Unary {
ConstVal::Str(_) => None, 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, .. } => { Expr::Binary { op, lhs, rhs, .. } => {
let l = self.fold_const(lhs)?; let l = self.fold_const(lhs)?;
let r = self.fold_const(rhs)?; let r = self.fold_const(rhs)?;
@@ -1513,25 +1513,41 @@ impl Sema {
Some(t) => { Some(t) => {
self.check_label_exists(t, scope, *pos); self.check_label_exists(t, scope, *pos);
match t { match t {
LabelRef::Name(n) => { LabelRef::Name(n) => self.data_marks_name.get(n).copied().unwrap_or(0),
self.data_marks_name.get(n).copied().unwrap_or(0) LabelRef::Line(n) => self.data_marks_line.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)); 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); 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); self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
} }
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ---- // ---- 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); let (fe, _) = self.want_str(file, scope);
self.reject_com_device(file, *pos); self.reject_com_device(file, *pos);
let (ne, nt) = self.want_num(number, scope); let (ne, nt) = self.want_num(number, scope);
@@ -1779,12 +1795,67 @@ impl Sema {
self.push( self.push(
out, out,
scope, 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() { match device.as_str() {
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {} "TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
_ => self.err(*pos, "Feature unavailable"), _ => self.err(*pos, "Feature unavailable"),
} }
if let Some(i) = index { if let Some(i) = index {
@@ -1805,7 +1876,45 @@ impl Sema {
); );
return; 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); let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos);
match name { match name {
"BEEP" => self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Beep, args: vec![] }), "BEEP" => self.push(
"DOEVENTS" => { out,
self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Doevents, args: vec![] }) scope,
} HStmtKind::BuiltinStmt {
b: Builtin::Beep,
args: vec![],
},
),
"DOEVENTS" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Doevents,
args: vec![],
},
),
"RANDOMIZE" => { "RANDOMIZE" => {
let a = lowered let a = lowered
.into_iter() .into_iter()
@@ -2494,19 +2615,29 @@ impl Sema {
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a }); self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
} }
// ---- ISAM ------------------------------------------------------ // ---- ISAM ------------------------------------------------------
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" "CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE"
| "UPDATE" | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS"
| "MOVENEXT" | "MOVEPREVIOUS" | "SEEKEQ" | "SEEKGT" | "SEEKGE" | "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
| "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
self.lower_isam_stmt(name, args, lowered, scope, out) 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. // Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
_ => self.push(out, scope, HStmtKind::Unsupported(match name { _ => self.push(
"RUN" => "RUN", out,
"MSGBOX" => "MSGBOX", scope,
"SETUEVENT" => "SETUEVENT", HStmtKind::Unsupported(match name {
_ => "Anweisung", "RUN" => "RUN",
})), "MSGBOX" => "MSGBOX",
_ => "Anweisung",
}),
),
} }
} }
@@ -2897,9 +3028,12 @@ impl Sema {
let (re, rt) = self.lower_expr(rhs, scope); let (re, rt) = self.lower_expr(rhs, scope);
self.lower_binary(*op, le, lt, re, rt, *pos) self.lower_binary(*op, le, lt, re, rt, *pos)
} }
Expr::Name { name, suffix, args, pos } => { Expr::Name {
self.lower_name_expr(name, suffix, args, *pos, scope) 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::LockStmt { pos, .. }
| Stmt::NameStmt { pos, .. } | Stmt::NameStmt { pos, .. }
| Stmt::ViewPrint { pos, .. } | Stmt::ViewPrint { pos, .. }
| Stmt::TrapDef { pos, .. }
| Stmt::EventControl { pos, .. } | Stmt::EventControl { pos, .. }
| Stmt::Include { pos, .. } | Stmt::Include { pos, .. }
| Stmt::MetaArrays { 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<u8> {
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<i64> {
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",
}
}

View File

@@ -7,13 +7,13 @@
use crate::console::PrintState; use crate::console::PrintState;
use crate::datetime as dt; use crate::datetime as dt;
use crate::finanz as fin;
use crate::fileio as fio;
use crate::value;
use crate::errors::RuntimeError; use crate::errors::RuntimeError;
use crate::fileio as fio;
use crate::finanz as fin;
use crate::format; use crate::format;
use crate::host::{Ereignis, Host}; use crate::host::{Ereignis, Host};
use crate::screen::TextScreen; use crate::screen::TextScreen;
use crate::value;
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value}; use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::rc::Rc; use std::rc::Rc;
@@ -28,8 +28,11 @@ pub struct RtState {
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion. /// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
pub isam: crate::isam::Isam, pub isam: crate::isam::Isam,
pub print: PrintState, pub print: PrintState,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form. /// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
pub tasten: VecDeque<String>, /// 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. /// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool, pub abbruch: bool,
/// Eingabeende wurde gemeldet. /// Eingabeende wurde gemeldet.
@@ -42,6 +45,8 @@ pub struct RtState {
pub waehrung: crate::using::Waehrung, pub waehrung: crate::using::Waehrung,
/// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet? /// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet?
pub trace: bool, pub trace: bool,
/// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
pub traps: crate::traps::Traps,
/// Trefferliste und Zeiger für `DIR$`. /// Trefferliste und Zeiger für `DIR$`.
pub dir_treffer: Vec<String>, pub dir_treffer: Vec<String>,
pub dir_index: usize, pub dir_index: usize,
@@ -73,6 +78,7 @@ impl Default for RtState {
key_zeile: false, key_zeile: false,
waehrung: Default::default(), waehrung: Default::default(),
trace: false, trace: false,
traps: crate::traps::Traps::neu(),
dir_treffer: Vec::new(), dir_treffer: Vec::new(),
dir_index: 0, dir_index: 0,
uhr_offset: 0, uhr_offset: 0,
@@ -100,10 +106,19 @@ impl RtState {
return; return;
}; };
match e { 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::Groesse { cols, rows } => self.screen.resize(cols, rows),
Ereignis::Abbruch => self.abbruch = true, Ereignis::Abbruch => self.abbruch = true,
Ereignis::Ende => self.ende = 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 { if blockierend {
return; return;
@@ -116,12 +131,46 @@ impl RtState {
pub fn tick(&mut self, host: &mut dyn Host) { pub fn tick(&mut self, host: &mut dyn Host) {
host.present(&self.screen); host.present(&self.screen);
self.pump(host, false); 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`. /// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`.
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> { pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
loop { loop {
if let Some(t) = self.tasten.pop_front() { if let Some((t, _)) = self.tasten.pop_front() {
return Some(t); return Some(t);
} }
if self.ende { if self.ende {
@@ -386,7 +435,9 @@ pub mod ids {
pub const ISAM_SAVEPOINT: u16 = 145; pub const ISAM_SAVEPOINT: u16 = 145;
pub const ISAM_SETMEM: u16 = 146; pub const ISAM_SETMEM: u16 = 146;
pub const ISAM_BOF: u16 = 147; 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. /// Dispatch-Tabelle in Index-Reihenfolge.
@@ -541,6 +592,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_isam_savepoint, bi_isam_savepoint,
bi_isam_setmem, bi_isam_setmem,
bi_isam_bof, bi_isam_bof,
bi_setuevent,
]; ];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize); debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE TABLE
@@ -1439,9 +1491,26 @@ fn key_index(n: i32) -> Option<usize> {
} }
} }
fn bi_key_assign(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_key_assign(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?; let n = arg_i32(a, 0)?;
let text = arg_str(a, 1)?; let text = arg_str(a, 1)?;
// 1525 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)?; let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild). // Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild).
st.key_makros[i] = text.chars().take(15).collect(); 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<Op
Ok(None) Ok(None)
} }
fn bi_beep(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> { fn bi_setuevent(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, 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<Option<Value>, RuntimeError> {
// ponytail: Der Signalton geht direkt an das Terminal, nicht durch den // ponytail: Der Signalton geht direkt an das Terminal, nicht durch den
// Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host // Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host
// mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf. // mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf.
@@ -1885,4 +1968,155 @@ mod tests {
}; };
assert_eq!(n, 3); 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()));
}
} }

View File

@@ -16,17 +16,56 @@ use crate::screen::TextScreen;
/// Was vom Terminal hereinkommt. /// Was vom Terminal hereinkommt.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ereignis { pub enum Ereignis {
/// Tastendruck in `INKEY$`-Form: ein Zeichen für Zeichentasten, /// Tastendruck in `INKEY$`-Form (ein Zeichen für Zeichentasten,
/// Nullzeichen + Kennung für Sondertasten. /// Nullzeichen + Kennung für Sondertasten) samt Umschaltzustand.
Taste(String), ///
/// 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. /// Die Darstellungsfläche hat ihre Größe geändert.
Groesse { cols: usize, rows: usize }, 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). /// Abbruchwunsch (Strg+Untbr).
Abbruch, Abbruch,
/// Eingabeende — es kommt nichts mehr. /// Eingabeende — es kommt nichts mehr.
Ende, 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). /// Steuerzeichen in `INKEY$`-Form (Kodierung des Vorbilds).
pub mod taste { pub mod taste {
pub const ENTER: &str = "\r"; pub const ENTER: &str = "\r";
@@ -38,6 +77,84 @@ pub mod taste {
pub fn sonder(code: u8) -> String { pub fn sonder(code: u8) -> String {
format!("\0{}", code as char) 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<u8> {
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<u8> {
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 { pub trait Host {
@@ -47,6 +164,16 @@ pub trait Host {
/// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`, /// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`,
/// wenn nichts anliegt (Grundlage von `INKEY$`). /// wenn nichts anliegt (Grundlage von `INKEY$`).
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>; fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
/// 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 /// 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 — /// Ereignisse, die erst nach der n-ten Anzeige eingereiht werden —
/// so lässt sich eine Größenänderung mitten im Lauf nachstellen. /// so lässt sich eine Größenänderung mitten im Lauf nachstellen.
verzoegert: Vec<(usize, Ereignis)>, 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 { impl CaptureHost {
@@ -77,10 +207,16 @@ impl CaptureHost {
/// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter. /// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter.
pub fn tippe(&mut self, text: &str) { pub fn tippe(&mut self, text: &str) {
for c in text.chars() { 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 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). /// Beliebiges Ereignis einreihen (Größenänderung, Abbruch, Sondertaste).
@@ -92,6 +228,16 @@ impl CaptureHost {
pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) { pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) {
self.verzoegert.push((nach, e)); 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 { impl Host for CaptureHost {
@@ -113,6 +259,10 @@ impl Host for CaptureHost {
fn next_event(&mut self, _blockierend: bool) -> Option<Ereignis> { fn next_event(&mut self, _blockierend: bool) -> Option<Ereignis> {
self.ereignisse.pop_front() self.ereignisse.pop_front()
} }
fn jetzt_ms(&mut self) -> u64 {
self.uhr_ms
}
} }
#[cfg(test)] #[cfg(test)]
@@ -137,37 +287,84 @@ mod tests {
#[test] #[test]
fn verzoegertes_ereignis_kommt_nach_der_anzeige() { fn verzoegertes_ereignis_kommt_nach_der_anzeige() {
let mut h = CaptureHost::default(); 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(); let s = TextScreen::new();
h.present(&s); h.present(&s);
assert_eq!(h.next_event(false), None); assert_eq!(h.next_event(false), None);
h.present(&s); h.present(&s);
assert_eq!( assert_eq!(
h.next_event(false), h.next_event(false),
Some(Ereignis::Groesse { cols: 120, rows: 40 }) Some(Ereignis::Groesse {
cols: 120,
rows: 40
})
); );
} }
#[test] #[test]
fn groessenaenderung_und_abbruch_als_ereignis() { fn groessenaenderung_und_abbruch_als_ereignis() {
let mut h = CaptureHost::default(); 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); h.ereignis(Ereignis::Abbruch);
assert_eq!( assert_eq!(
h.next_event(true), 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)); 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] #[test]
fn vorbereitete_zeile_wird_zu_tasten() { fn vorbereitete_zeile_wird_zu_tasten() {
let mut h = CaptureHost::with_input(&["ab"]); 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("a".into(), 0)));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into()))); assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0)));
assert_eq!( assert_eq!(
h.next_event(true), h.next_event(true),
Some(Ereignis::Taste(taste::ENTER.into())) Some(Ereignis::Taste(taste::ENTER.into(), 0))
); );
assert_eq!(h.next_event(false), None); assert_eq!(h.next_event(false), None);
} }

View File

@@ -4,18 +4,19 @@
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive //! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3). //! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe) pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds pub mod datetime; // DATE$, TIME$, TIMER
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb) pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod host; 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 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 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 strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE … pub mod traps; // Zustaende der klassischen Ereignis-Traps (Sprachreferenz §8)
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING … pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix

View File

@@ -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, 125, 3031.
Key(u8),
/// `ON TIMER(n)`.
Timer,
/// `ON UEVENT`.
UEvent,
/// `ON SIGNAL(n)` — n aus 12.
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<u32>,
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<Trap>,
/// `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 1525: 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 <quelle> 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<u32>) {
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, 186 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 `<quelle> 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 = 1525). `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<u8> {
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<usize> = (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<u8> {
Some(match scancode {
0x3B..=0x44 => scancode - 0x3A, // F1F10 → 110
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());
}
}

View File

@@ -10,6 +10,7 @@ authors.workspace = true
tb-runtime.workspace = true tb-runtime.workspace = true
ratatui.workspace = true ratatui.workspace = true
crossterm.workspace = true crossterm.workspace = true
signal-hook.workspace = true
thiserror.workspace = true thiserror.workspace = true
log.workspace = true log.workspace = true

View File

@@ -5,11 +5,11 @@
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2). //! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
use std::io::{self, Stdout}; use std::io::{self, Stdout};
use std::time::Duration; use std::time::{Duration, Instant};
use crossterm::event::{ use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
KeyModifiers, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
}; };
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
@@ -18,10 +18,11 @@ use crossterm::terminal::{
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::Terminal; 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 tb_runtime::screen::TextScreen;
use crate::screen::ScreenWidget; use crate::screen::ScreenWidget;
use crate::signale::Signalquelle;
/// Host für laufende Programme im Terminal. /// 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. /// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
pub struct TerminalHost { pub struct TerminalHost {
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
/// 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 { impl TerminalHost {
@@ -37,7 +44,16 @@ impl TerminalHost {
let mut out = io::stdout(); let mut out = io::stdout();
execute!(out, EnterAlternateScreen, EnableMouseCapture)?; execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
let terminal = Terminal::new(CrosstermBackend::new(out))?; 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. /// 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())); .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<Ereignis> { fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
loop { 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) { if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
return None; return None;
} }
@@ -78,10 +103,16 @@ impl Host for TerminalHost {
} }
} }
Ok(Event::Resize(cols, rows)) => { Ok(Event::Resize(cols, rows)) => {
self.flaeche = (cols as usize, rows as usize);
return Some(Ereignis::Groesse { return Some(Ereignis::Groesse {
cols: cols as usize, cols: cols as usize,
rows: rows as usize, rows: rows as usize,
}) });
}
Ok(Event::Mouse(m)) => {
if let Some(e) = maus_zu_ereignis(m, self.flaeche) {
return Some(e);
}
} }
Ok(_) => {} Ok(_) => {}
Err(_) => return Some(Ereignis::Ende), 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<Ereignis> {
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. /// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> { fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
// Strg+Untbr bzw. Strg+C: Abbruchwunsch. // Strg+Untbr bzw. Strg+C: Abbruchwunsch.
@@ -118,7 +196,17 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
KeyCode::Delete => taste::sonder(83), KeyCode::Delete => taste::sonder(83),
_ => return None, _ => 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)] #[cfg(test)]
@@ -128,13 +216,13 @@ mod tests {
#[test] #[test]
fn zeichentaste_wird_zu_inkey_form() { fn zeichentaste_wird_zu_inkey_form() {
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); 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] #[test]
fn sondertaste_traegt_fuehrendes_nullzeichen() { fn sondertaste_traegt_fuehrendes_nullzeichen() {
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE); 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"); panic!("keine Taste");
}; };
assert_eq!(s.chars().count(), 2); assert_eq!(s.chars().count(), 2);
@@ -142,6 +230,123 @@ mod tests {
assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59 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] #[test]
fn strg_c_ist_abbruch() { fn strg_c_ist_abbruch() {
let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);

View File

@@ -8,7 +8,8 @@
//! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge, //! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge,
//! Tastatur- und Mausereignisse. //! Tastatur- und Mausereignisse.
pub mod screen;
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
pub mod forms;
pub mod events; 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)

112
crates/tb-ui/src/signale.rs Normal file
View File

@@ -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<AtomicBool>,
term: Arc<AtomicBool>,
}
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<u8> {
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");
}
}

View File

@@ -304,6 +304,14 @@ instrs! {
0xB2 RetFn; 0xB2 RetFn;
0xB3 CallBuiltin(a: u16, b: u8); 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 — Fehlerbehandlung
0xC0 OnErrorGoto(a: u32); 0xC0 OnErrorGoto(a: u32);
0xC1 OnErrorLocal(a: u32); 0xC1 OnErrorLocal(a: u32);
@@ -614,6 +622,12 @@ mod tests {
Instr::Call(2, 3), Instr::Call(2, 3),
Instr::Input(2, false, 0xFFFF, true), Instr::Input(2, false, 0xFFFF, true),
Instr::ConvCyR8, Instr::ConvCyR8,
Instr::TrapDefine(1, 4242),
Instr::TrapDisable(3),
Instr::TrapSet(0, 2),
Instr::EventSwitch(false),
Instr::Doevents,
Instr::Sleep(true),
Instr::RetFn, Instr::RetFn,
]; ];
let mut buf = Vec::new(); let mut buf = Vec::new();

View File

@@ -95,6 +95,24 @@ impl ProcCtx {
fn here(&self) -> u32 { fn here(&self) -> u32 {
self.code.len() as 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) { fn bind(&mut self, label: u16) {
let pc = self.here(); let pc = self.here();
if (label as usize) >= self.label_pc.len() { if (label as usize) >= self.label_pc.len() {
@@ -170,7 +188,8 @@ impl Codegen {
| Instr::RetGosubTo(t) | Instr::RetGosubTo(t)
| Instr::ResumeLabel(t) | Instr::ResumeLabel(t)
| Instr::OnErrorGoto(t) | Instr::OnErrorGoto(t)
| Instr::OnErrorLocal(t) => *t = pc, | Instr::OnErrorLocal(t)
| Instr::TrapDefine(_, t) => *t = pc,
other => unreachable!("Fixup auf {other:?}"), other => unreachable!("Fixup auf {other:?}"),
} }
} }
@@ -330,9 +349,14 @@ impl Codegen {
ctx.bind(l_end); ctx.bind(l_end);
} }
} }
HStmtKind::Loop { pre, post, body, exit_label } => { HStmtKind::Loop {
pre,
post,
body,
exit_label,
} => {
let l_start = ctx.new_label(); let l_start = ctx.new_label();
ctx.bind(l_start); ctx.bind_auf_stmt(l_start);
if let Some((is_until, cond)) = pre { if let Some((is_until, cond)) = pre {
self.expr(ctx, cond); self.expr(ctx, cond);
if *is_until { if *is_until {
@@ -370,19 +394,49 @@ impl Codegen {
exit_label, exit_label,
} => { } => {
self.gen_for( 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, *exit_label,
stmt.line,
); );
} }
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l), HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(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); self.expr(ctx, sel);
let table = self.jump_tables.len(); let table = self.jump_tables.len();
self.jump_tables.push(Vec::new()); self.jump_tables.push(Vec::new());
ctx.table_fixups.push((table, targets.clone())); ctx.table_fixups.push((table, targets.clone()));
ctx.emit(Instr::OnJump(table as u16, *gosub)); 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 { HStmtKind::ReturnGosub(target) => match target {
None => ctx.emit(Instr::RetGosub), None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l), Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
@@ -398,6 +452,17 @@ impl Codegen {
for a in args { for a in args {
self.expr(ctx, a); 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); let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8)); ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if builtin_returns_value(*b) { if builtin_returns_value(*b) {
@@ -484,6 +549,7 @@ impl Codegen {
step_slot: Option<VarSlot>, step_slot: Option<VarSlot>,
body: &[HStmt], body: &[HStmt],
exit_label: u16, exit_label: u16,
line: u32,
) { ) {
// Startwert, Grenze, ggf. Schritt einmal auswerten. // Startwert, Grenze, ggf. Schritt einmal auswerten.
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from)); 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_test = ctx.new_label();
let l_body = ctx.new_label(); let l_body = ctx.new_label();
ctx.bind(l_test); 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 { match const_step {
Some(s) => { Some(s) => {
// Vergleichsrichtung zur Compilezeit. // Vergleichsrichtung zur Compilezeit.
@@ -625,6 +700,10 @@ impl Codegen {
for a in args { for a in args {
self.expr(ctx, a); 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)); ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8));
} }
HExpr::ArrayBound { lower, place, dim } => { 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. /// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit.
/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege. /// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege.
/// `DOEVENTS`/`SLEEP` → eigene Instruktion statt Builtin-Aufruf.
fn zustellpunkt_instr(b: Builtin, argc: usize) -> Option<Instr> {
match b {
Builtin::Doevents => Some(Instr::Doevents),
Builtin::Sleep => Some(Instr::Sleep(argc > 0)),
_ => None,
}
}
fn builtin_id(b: Builtin) -> u16 { fn builtin_id(b: Builtin) -> u16 {
match b { match b {
Builtin::Len => ids::LEN, Builtin::Len => ids::LEN,
@@ -1060,6 +1148,7 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::CommandS => ids::COMMAND_S, Builtin::CommandS => ids::COMMAND_S,
Builtin::Doevents => ids::DOEVENTS, Builtin::Doevents => ids::DOEVENTS,
Builtin::Sleep => ids::SLEEP, Builtin::Sleep => ids::SLEEP,
Builtin::SetUEvent => ids::SETUEVENT,
Builtin::Beep => ids::BEEP, Builtin::Beep => ids::BEEP,
// ISAM // ISAM
Builtin::IsamOpen => ids::ISAM_OPEN, Builtin::IsamOpen => ids::ISAM_OPEN,

View File

@@ -15,9 +15,8 @@ use std::rc::Rc;
use tb_runtime::builtins::{builtin_table, RtState}; use tb_runtime::builtins::{builtin_table, RtState};
use tb_runtime::errors::RuntimeError; use tb_runtime::errors::RuntimeError;
use tb_runtime::host::Host; use tb_runtime::host::Host;
use tb_runtime::value::{ use tb_runtime::traps::Quelle;
self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef, use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
};
/// Warum die VM die Kontrolle abgibt. /// Warum die VM die Kontrolle abgibt.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -54,6 +53,11 @@ struct Frame {
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`). /// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
last_stmt_pc: usize, last_stmt_pc: usize,
line: u32, line: u32,
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
/// beendet den Handler (design.md, D2).
trap: Option<Quelle>,
} }
pub struct Vm { pub struct Vm {
@@ -65,7 +69,12 @@ pub struct Vm {
pub rt: RtState, pub rt: RtState,
// Fehlerzustand // Fehlerzustand
err: u16, 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, erl: u32,
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
zeile_nr: u32,
module_handler: Handler, module_handler: Handler,
in_handler: bool, in_handler: bool,
resume_pc: usize, resume_pc: usize,
@@ -77,6 +86,34 @@ pub struct Vm {
data_ptr: usize, data_ptr: usize,
} }
/// Quellenarten, wie der Codegenerator sie kodiert.
const ART_KEY: u8 = 0;
const ART_TIMER: u8 = 1;
const ART_UEVENT: u8 = 2;
const ART_SIGNAL: u8 = 3;
/// Art + Kennung → Quelle. Die Kennung kann ein Ausdruck sein, deshalb
/// wird der Wertebereich hier noch einmal geprüft (Fehler 5).
///
/// Das Zeitintervall gehört **nicht** hierher: `TIMER ON` trägt keine
/// Kennung, geprüft wird es nur an `ON TIMER(n&)`.
fn trap_quelle(art: u8, n: i32) -> Result<Quelle, RuntimeError> {
let ok = match art {
ART_KEY => matches!(n, 0..=25 | 30 | 31),
ART_SIGNAL => (1..=2).contains(&n),
_ => true,
};
if !ok {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(match art {
ART_KEY => Quelle::Key(n as u8),
ART_TIMER => Quelle::Timer,
ART_UEVENT => Quelle::UEvent,
_ => Quelle::Signal(n as u8),
})
}
enum Flow { enum Flow {
Normal, Normal,
Event(RunEvent), Event(RunEvent),
@@ -97,6 +134,7 @@ impl Vm {
rt: RtState::default(), rt: RtState::default(),
err: 0, err: 0,
erl: 0, erl: 0,
zeile_nr: 0,
module_handler: Handler::None, module_handler: Handler::None,
in_handler: false, in_handler: false,
resume_pc: 0, resume_pc: 0,
@@ -134,9 +172,88 @@ impl Vm {
local_handler: Handler::None, local_handler: Handler::None,
last_stmt_pc: 0, last_stmt_pc: 0,
line: 0, line: 0,
trap: None,
}); });
} }
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
if !self.rt.traps.aktiv() {
return false;
}
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
self.rt.tasten_traps_pruefen();
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
false
}
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
/// dem `SLEEP` weiter.
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
let start = host.jetzt_ms();
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
// virtuelle nie vorrückt.
let real_start = std::time::Instant::now();
loop {
self.rt.tick(host);
if self.zustellen(host) {
return;
}
if self.rt.ende || self.rt.abbruch {
return;
}
if sekunden <= 0.0 {
// Ohne Argument: bis zum Tastendruck.
if self.rt.naechste_taste(host, false).is_some() {
return;
}
} else {
let vergangen = host.jetzt_ms().saturating_sub(start);
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
/// Trap-Handler aufsetzen: ein Frame auf den Modulrumpf, der dessen
/// Locals mitbenutzt, mit eigenem `GOSUB`-Stapel und einer Rückmarke
/// auf die unterbrochene Stelle (design.md, D2).
fn trap_frame_aufsetzen(&mut self, q: Quelle, ziel: u32) {
let locals_base = self.frames[0].locals_base;
let stack_base = self.stack.len();
self.frames.push(Frame {
proc: 0,
pc: ziel as usize,
locals_base,
stack_base,
gosub: Vec::new(),
local_handler: Handler::None,
last_stmt_pc: 0,
line: 0,
trap: Some(q),
});
}
/// Trap-Frame abräumen. Anders als `pop_frame` bleiben die Locals
/// stehen — sie gehören dem Modulrumpf, nicht dem Handler.
fn trap_frame_abraeumen(&mut self) -> Option<Quelle> {
let f = self.frames.pop()?;
self.stack.truncate(f.stack_base);
f.trap
}
fn pop_frame(&mut self) { fn pop_frame(&mut self) {
let f = self.frames.pop().expect("Frame-Unterlauf"); let f = self.frames.pop().expect("Frame-Unterlauf");
self.locals.truncate(f.locals_base); self.locals.truncate(f.locals_base);
@@ -273,6 +390,8 @@ impl Vm {
// Fehler im Handler: fatal, keine Kaskade. // Fehler im Handler: fatal, keine Kaskade.
return Some(self.error_event(code, line)); 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. // Frame-Kette von innen nach außen nach LOCAL-Handlern absuchen.
let mut target: Option<(usize, Handler)> = None; let mut target: Option<(usize, Handler)> = None;
for (depth, f) in self.frames.iter().enumerate().rev() { for (depth, f) in self.frames.iter().enumerate().rev() {
@@ -523,6 +642,24 @@ impl Vm {
self.rt.tick(host); self.rt.tick(host);
self.rt.screen.veraenderung_quittieren(); 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 != 0 {
if self.flags & F_STEP != 0 { if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line })); return Ok(Flow::Event(RunEvent::Stepped { line }));
@@ -536,12 +673,64 @@ impl Vm {
} }
Ok(Flow::Normal) 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) => { I::SetErl(n) => {
self.erl = n; self.zeile_nr = n;
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::End | I::SystemInstr => Ok(Flow::Event(RunEvent::Ended)), 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::Unsupported(_) => Err(RuntimeError(73)), // Advanced feature
I::PushInt(v) => { I::PushInt(v) => {
@@ -1086,6 +1275,14 @@ impl Vm {
f.pc = ret; f.pc = ret;
Ok(Flow::Normal) 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), None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
} }
} }
@@ -1096,6 +1293,20 @@ impl Vm {
f.pc = t as usize; f.pc = t as usize;
Ok(Flow::Normal) 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), None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
} }
} }

View File

@@ -476,9 +476,10 @@ fn print_zonen_und_tab() {
#[test] #[test]
fn unsupported_feature_fehler_73() { 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). // → 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 { match ev {
RunEvent::Error { code, message, .. } => { RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73); assert_eq!(code, 73);
@@ -510,9 +511,8 @@ fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
/// die Eingabeaufforderung unsichtbar da. /// die Eingabeaufforderung unsichtbar da.
#[test] #[test]
fn ausgabe_erscheint_vor_der_eingabe() { fn ausgabe_erscheint_vor_der_eingabe() {
use tb_runtime::host::{CaptureHost, Host}; use tb_runtime::host::CaptureHost;
let module = let module = tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
let mut vm = Vm::new(module); let mut vm = Vm::new(module);
let mut host = CaptureHost::with_input(&["7"]); let mut host = CaptureHost::with_input(&["7"]);
assert_eq!(vm.run(&mut host), RunEvent::Ended); 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 vm = Vm::new(module);
let mut host = CaptureHost::default(); let mut host = CaptureHost::default();
for c in ['a', 'b', 'x'] { 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::BACKSPACE.to_string(), 0));
host.ereignis(Ereignis::Taste(taste::ENTER.to_string())); host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(vm.run(&mut host), RunEvent::Ended);
let text = tb_runtime::snapshot::text(&vm.rt.screen); let text = tb_runtime::snapshot::text(&vm.rt.screen);
// Erste Zeile: das Echo ohne das gelöschte Zeichen. // Erste Zeile: das Echo ohne das gelöschte Zeichen.
@@ -609,8 +609,8 @@ fn inkey_liefert_zeichen_und_sondertaste() {
.unwrap(); .unwrap();
let mut vm = Vm::new(module); let mut vm = Vm::new(module);
let mut host = CaptureHost::default(); let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste("q".into())); host.ereignis(Ereignis::Taste("q".into(), 0));
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1 host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // F1
assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(vm.run(&mut host), RunEvent::Ended);
let t = tb_runtime::snapshot::text(&vm.rt.screen); let t = tb_runtime::snapshot::text(&vm.rt.screen);
// "q", dann Länge 2 und führendes Nullzeichen. // "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 vm = Vm::new(module);
let mut host = CaptureHost::default(); let mut host = CaptureHost::default();
for c in ['x', 'y', 'z', 'w'] { 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); assert_eq!(vm.run(&mut host), RunEvent::Ended);
// Nur das PRINT erscheint — INPUT$ echot nicht. // 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:?}"); assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
} }
// ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) ---------------------
/// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem
/// Zustellpunkt um `schritt_ms` vor (design.md, D6).
fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
struct UhrHost {
inner: CaptureHost,
schritt: u64,
}
impl tb_runtime::host::Host for UhrHost {
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
self.inner.present(s);
}
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(b)
}
fn jetzt_ms(&mut self) -> u64 {
let j = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt);
j
}
}
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
panic!("Compile-Fehler: {d:?}");
});
let mut vm = Vm::new(module);
let mut host = UhrHost {
inner: CaptureHost::default(),
schritt: schritt_ms,
};
let ev = vm.run(&mut host);
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
}
#[test]
fn zeit_trap_laeuft_und_kehrt_zurueck() {
// Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die
// Schleife unveraendert weiter — der unterbrochene Zustand bleibt.
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"i=\"; i%\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}");
assert!(
out.contains("i= 201"),
"Schleife lief nicht sauber weiter: {out:?}"
);
}
#[test]
fn ohne_timer_on_kein_trap() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"fertig\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}");
assert!(out.contains("fertig"));
}
#[test]
fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
CALL Rechne\n\
PRINT \"ende\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n\
SUB Rechne\n\
FOR j% = 1 TO 200\n\
NEXT\n\
PRINT \"sub\"; j%\n\
END SUB\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
assert!(
out.contains("sub 201"),
"SUB lief nicht sauber weiter: {out:?}"
);
assert!(out.contains("ende"));
}
#[test]
fn event_off_klammert_die_zustellung() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
EVENT OFF\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"|\";\n\
EVENT ON\n\
FOR i% = 1 TO 200\n\
NEXT\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
let text = out.trim();
let strich = text.find('|').expect("Trennzeichen fehlt");
assert!(
!text[..strich].contains('T'),
"waehrend EVENT OFF wurde zugestellt: {text:?}"
);
assert!(
text[strich..].contains('T'),
"nach EVENT ON wurde nicht zugestellt: {text:?}"
);
}
#[test]
fn timer_off_im_handler_ueberlebt_das_return() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 400\n\
NEXT\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
TIMER OFF\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert_eq!(
out.trim().matches('T').count(),
1,
"RETURN hat den Trap wieder eingeschaltet: {out:?}"
);
}
#[test]
fn gosub_null_schaltet_den_trap_ab() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
ON TIMER(1) GOSUB 0\n\
FOR i% = 1 TO 300\n\
NEXT\n\
PRINT \"fertig\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}");
}
#[test]
fn setuevent_loest_den_uevent_trap_aus() {
let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\
UEVENT ON\n\
CALL SetUEvent\n\
FOR i% = 1 TO 20\n\
NEXT\n\
PRINT \"|\";\n\
END\n\
Nutzer:\n\
PRINT \"U\";\n\
RETURN\n");
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}");
}
#[test]
fn kennung_ausserhalb_des_bereichs_ist_fehler_5() {
// Nicht konstant, also erst zur Laufzeit pruefbar.
let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n";
assert_eq!(err_code(src), 5);
}
#[test]
fn leere_warteschleife_hat_eine_anweisungsgrenze() {
// `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne
// Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt,
// kein Breakpoint erreicht und kein Abbruch bemerkt.
use tb_vm::bytecode::Instr;
for src in [
"FOR i% = 1 TO 3\nNEXT\n",
"WHILE i% < 3\nWEND\n",
"DO WHILE i% < 3\nLOOP\n",
"DO\nLOOP UNTIL i% > 3\n",
] {
let m = tb_vm::compile_source("T", src).unwrap();
let code = &m.procs[0].code;
let mut kreise = 0;
for (j, i) in code.iter().enumerate() {
let ziel = match i {
Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize,
_ => continue,
};
if ziel >= j {
continue;
}
kreise += 1;
assert!(
code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))),
"Schleife ohne Anweisungsgrenze im Kreis: {src:?}"
);
}
assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}");
}
}
#[test]
fn doevents_stellt_zu() {
// Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen.
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 5\n\
x% = DOEVENTS\n\
NEXT\n\
PRINT \"|\";\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
let t = out.trim();
let strich = t.find('|').expect("Trennzeichen fehlt");
assert!(
t[..strich].contains('T'),
"DOEVENTS stellte nicht zu: {t:?}"
);
}
#[test]
fn doevents_liefert_null() {
assert_eq!(out("PRINT DOEVENTS").trim(), "0");
}
#[test]
fn sleep_endet_durch_ein_ereignis() {
// SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige
// Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter.
let t = std::time::Instant::now();
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
SLEEP 60\n\
PRINT \"danach\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
assert!(
out.contains("danach"),
"SLEEP kehrte nicht zurueck: {out:?}"
);
assert!(
t.elapsed().as_secs() < 5,
"SLEEP wartete die vollen 60 Sekunden ab"
);
}
#[test]
fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() {
// `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist
// das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im
// Handler), nicht an der unterbrochenen Stelle.
let (ev, out) = run_mit_uhr(
"10 ON ERROR GOTO 200\n 20 ON TIMER(1) GOSUB 90\n 30 TIMER ON\n 40 FOR i% = 1 TO 300\n 50 NEXT\n 60 PRINT \"|\";\n 70 END\n 90 ERROR 7\n 100 RETURN\n 200 PRINT \"E\"; ERR; ERL;\n 210 RESUME NEXT\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}");
assert!(
out.contains(" 90"),
"ERL nennt nicht die Zeile im Handler: {out:?}"
);
}
#[test]
fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
// Regression: der Handler steht selbst auf einer nummerierten Zeile.
// `ERL` muss trotzdem die Zeile des Fehlers melden (Spec
// vm-fehlerbehandlung, „ERR und ERL").
let out = out("10 ON ERROR GOTO 100\n\
20 ERROR 7\n\
30 END\n\
100 PRINT ERL;\n\
110 RESUME NEXT\n");
assert_eq!(out.trim(), "20");
}

View File

@@ -103,9 +103,19 @@ nicht um.
## Ereignisse ## Ereignisse
| Element | Art | | Element | Art | Syntax und Wirkung |
|---|---| |---|---|---|
| `DOEVENTS` | Funktion | | `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 186 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 ## Fehlerbehandlung

View File

@@ -21,7 +21,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der **Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
Abschnitt der Sprachreferenz, bei `offen` ein Strich. 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 | | 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 | | `TYPE` | Anweisung | Deklaration | implementiert | tb-frontend::sema | bas7advr |
| `UBOUND` | Funktion | Deklaration | implementiert | tb-frontend::sema | bas7advr | | `UBOUND` | Funktion | Deklaration | implementiert | tb-frontend::sema | bas7advr |
| `DOEVENTS` | Funktion | Ereignisse | implementiert | tb-runtime::builtins | sprachreferenz | | `DOEVENTS` | Funktion | Ereignisse | implementiert | tb-runtime::builtins | sprachreferenz |
| `EVENT` | Anweisung | Ereignisse | offen | - | bas7advr | | `EVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `KEY (Ereignis)` | Anweisung | Ereignisse | offen | - | bas7advr | | `KEY (Ereignis)` | Anweisung | Ereignisse | implementiert | tb-runtime::traps | bas7advr |
| `ON KEY` | Anweisung | Ereignisse | offen | - | bas7advr | | `ON KEY` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `ON PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `ON PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `ON PLAY` | 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 STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `ON TIMER` | Anweisung | Ereignisse | offen | - | bas7advr | | `ON TIMER` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `ON UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr | | `ON UEVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `PEN` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `PLAY (Ereignis)` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `PLAY (Ereignis)` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `SetUEvent` | Routine | Ereignisse | offen | - | bas7advr | | `SetUEvent` | Routine | Ereignisse | implementiert | tb-runtime::builtins | bas7advr |
| `SIGNAL` | Anweisung | Ereignisse | offen | - | bas7advr | | `SIGNAL` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr | | `STRIG` | Anweisung | Ereignisse | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
| `TIMER` | Anweisung | Ereignisse | offen | - | bas7advr | | `TIMER` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `UEVENT` | Anweisung | Ereignisse | offen | - | bas7advr | | `UEVENT` | Anweisung | Ereignisse | implementiert | tb-vm::interp | bas7advr |
| `ERL` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | | `ERL` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
| `ERR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | | `ERR` | Anweisung | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |
| `ERR` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr | | `ERR` | Funktion | Fehlerbehandlung | implementiert | tb-vm::interp | bas7advr |

View File

@@ -184,11 +184,63 @@ INTEGER 1 (wahr) / 0 (falsch).
## 8. Ereignis-Traps (klassisch, ohne Forms) ## 8. Ereignis-Traps (klassisch, ohne Forms)
Vorhanden im Vorbild: `ON COM/KEY/PEN/PLAY/STRIG/TIMER/UEVENT(…) GOSUB label` Vorhanden im Vorbild: `ON COM/KEY/PEN/PLAY/SIGNAL/STRIG/TIMER/UEVENT(…)
mit Steueranweisungen `… ON/OFF/STOP`, dazu das `EVENT`-Statement (auch für GOSUB label` mit Steueranweisungen `… ON/OFF/STOP`, dazu das
Forms-Ereignisse) und `SetUEvent`. Timer-Events und Trapping sind ausgesetzt, `EVENT`-Statement (auch für Forms-Ereignisse) und `SetUEvent`.
solange ein Menü-Control den Fokus hat. Für Terminal Basic relevant: Timer-Events und Trapping sind ausgesetzt, solange ein Menü-Control den
`TIMER`, `KEY`, `UEVENT`; `COM/PEN/PLAY/STRIG`: Nicht-Ziel (Fehler 73). 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&` = 186 400 Sekunden | verstrichene Zeit |
| `ON KEY(n%) GOSUB ziel` | `n%` = 0 (alle), 110 (F1F10), 11 ↑, 12 ←, 13 →, 14 ↓, 1525 (benutzerdefiniert), 3031 (F11F12) | 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` = 1525, welche Taste der zugehörige `ON KEY(n)`-Trap
fängt — anders als für 110 und 3031, 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 ## 9. Konsolen-E/A
@@ -482,3 +534,16 @@ Dialoge:
links erhalten, der Cursor wird in die neuen Grenzen geklemmt, und ein links erhalten, der Cursor wird in die neuen Grenzen geklemmt, und ein
`VIEW PRINT`-Bereich wird am unteren Rand geklemmt bzw. — wenn er ganz `VIEW PRINT`-Bereich wird am unteren Rand geklemmt bzw. — wenn er ganz
vom Schirm fällt — auf den vollen Bildschirm zurückgesetzt. 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.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-04

View File

@@ -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: <n>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.

View File

@@ -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.

View File

@@ -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, 125, 3031; `TIMER` 186 400 Sekunden; `SIGNAL` 12.
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 <quelle>(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.

View File

@@ -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

View File

@@ -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

View File

@@ -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, 125 oder 3031, `ON TIMER(n&)` mit `n&` aus 186 400
und `ON SIGNAL(n%)` mit `n%` aus 12. 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`

View File

@@ -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

View File

@@ -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 = 1525 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: <n>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

View File

@@ -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 186 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 |
| 110 | F1F10 |
| 11 | Pfeil hoch |
| 12 | Pfeil links |
| 13 | Pfeil rechts |
| 14 | Pfeil runter |
| 1525 | benutzerdefinierte Tasten |
| 3031 | F11F12 (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 1525
Die Einzelseite zu `KEY n, zeichenkette$` (`x_dot_setfkr.html`) behandelt
**nur** die Makrozuweisung für 110 und 3031; die Erklärungsform für
1525 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, 125, 3031 mit fester Tastenzuordnung | Spec unverändert, Bereich jetzt belegt |
| E | `ON TIMER(n)` | `n&` ist LONG, 186 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.

View File

@@ -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

View File

@@ -110,3 +110,21 @@ durchlaufen und danach die neuen Grenzen ausnutzen.
#### Scenario: Größenänderung mitten im Programm #### Scenario: Größenänderung mitten im Programm
- **WHEN** der Test-Host während der Ausführung von 80×25 auf 120×40 wechselt - **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 - **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

View File

@@ -199,3 +199,51 @@ Bezeichner oder mit einem generischen Syntaxfehler abgewiesen werden
#### Scenario: Typprüfung des Satzarguments #### Scenario: Typprüfung des Satzarguments
- **WHEN** `RETRIEVE #1, x$` mit einem String statt einer Variablen des Tabellentyps geprüft wird - **WHEN** `RETRIEVE #1, x$` mit einem String statt einer Variablen des Tabellentyps geprüft wird
- **THEN** wird „Type mismatch" mit Quelltextposition gemeldet - **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, 125 oder 3031, `ON TIMER(n&)` mit `n&` aus 186 400
und `ON SIGNAL(n%)` mit `n%` aus 12. 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`

View File

@@ -114,3 +114,44 @@ Programmbestandteil.
#### Scenario: STOP im Runner #### Scenario: STOP im Runner
- **WHEN** ein per `tbc run` gestartetes Programm `STOP` in Zeile 7 ausführt - **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 - **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

76
tests/compat/traps.bas Normal file
View File

@@ -0,0 +1,76 @@
' tb-screen: 80x25
' tb-clock: 250ms/Zustellpunkt
' tb-keys: <F1>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

11
tests/compat/traps.out Normal file
View File

@@ -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