Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -636,6 +636,7 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"log",
|
"log",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"signal-hook",
|
||||||
"tb-runtime",
|
"tb-runtime",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
8
PLAN.md
8
PLAN.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
// 15–25 weisen kein Makro zu, sondern erklären eine Trap-Taste über
|
||||||
|
// Tastaturflagbyte und Scancode (Sprachreferenz §8).
|
||||||
|
if (15..=25).contains(&n) {
|
||||||
|
let mut it = text.chars();
|
||||||
|
let (Some(flag), Some(scan)) = (it.next(), it.next()) else {
|
||||||
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||||
|
};
|
||||||
|
if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 {
|
||||||
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||||
|
}
|
||||||
|
st.traps.benutzertaste(n as u8, flag as u8, scan as u8);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
520
crates/tb-runtime/src/traps.rs
Normal file
520
crates/tb-runtime/src/traps.rs
Normal 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, 1–25, 30–31.
|
||||||
|
Key(u8),
|
||||||
|
/// `ON TIMER(n)`.
|
||||||
|
Timer,
|
||||||
|
/// `ON UEVENT`.
|
||||||
|
UEvent,
|
||||||
|
/// `ON SIGNAL(n)` — n aus 1–2.
|
||||||
|
Signal(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Zustand {
|
||||||
|
/// Zustellen.
|
||||||
|
An,
|
||||||
|
/// Verwerfen — auch das bereits Gemerkte.
|
||||||
|
#[default]
|
||||||
|
Aus,
|
||||||
|
/// Höchstens eines merken und beim nächsten `ON` zustellen.
|
||||||
|
Gestoppt,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein definierter Trap: Ziel, Zustand, ob etwas ansteht.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Trap {
|
||||||
|
quelle: Quelle,
|
||||||
|
/// Sprungziel im Modulrumpf. `None` = `GOSUB 0`, also abgeschaltet.
|
||||||
|
ziel: Option<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 15–25: Umschaltzustand und Scancode aus
|
||||||
|
/// `KEY n, CHR$(flag) + CHR$(scancode)`.
|
||||||
|
benutzertasten: [Option<(u8, u8)>; 11],
|
||||||
|
/// Gibt es überhaupt einen nicht abgeschalteten Trap? Gepflegt statt
|
||||||
|
/// gerechnet: die VM fragt das an jeder Anweisungsgrenze (design.md, D4).
|
||||||
|
aktiv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Traps {
|
||||||
|
pub fn neu() -> Self {
|
||||||
|
Traps {
|
||||||
|
event_an: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finde(&mut self, q: Quelle) -> Option<&mut Trap> {
|
||||||
|
self.traps.iter_mut().find(|t| t.quelle == q)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ON <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, 1–86 400) und die erste
|
||||||
|
/// Fälligkeit von `jetzt_ms` aus rechnen.
|
||||||
|
pub fn timer_intervall(&mut self, sekunden: u64, jetzt_ms: u64) {
|
||||||
|
self.timer_intervall_ms = sekunden.saturating_mul(1000);
|
||||||
|
self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Steueranweisung `<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 = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
||||||
|
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
|
||||||
|
if (15..=25).contains(&n) {
|
||||||
|
self.benutzertasten[n as usize - 15] = Some((flag_zu_umschalt(flag), scancode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Welcher `KEY`-Trap fängt diesen Tastendruck? Geprüft wird gegen den
|
||||||
|
/// Scancode und — bei den benutzerdefinierten Tasten — zusätzlich gegen
|
||||||
|
/// den Umschaltzustand. `KEY(0)` fängt alle Tasten der Tabelle.
|
||||||
|
///
|
||||||
|
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
|
||||||
|
/// wissen, welche Quelle er melden soll.
|
||||||
|
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<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, // F1–F10 → 1–10
|
||||||
|
0x48 => 11, // Pfeil hoch
|
||||||
|
0x4B => 12, // Pfeil links
|
||||||
|
0x4D => 13, // Pfeil rechts
|
||||||
|
0x50 => 14, // Pfeil runter
|
||||||
|
0x85 => 30, // F11
|
||||||
|
0x86 => 31, // F12
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn mit_timer() -> Traps {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Timer, Some(100));
|
||||||
|
t.timer_intervall(1, 0);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ohne_trap_kostet_nichts() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
assert!(!t.aktiv());
|
||||||
|
assert_eq!(t.naechstes(), None);
|
||||||
|
t.definieren(Quelle::Timer, Some(7));
|
||||||
|
assert!(t.aktiv());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aus_verwirft_auch_das_gemerkte() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::Gestoppt, 0);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
t.setzen(Quelle::Timer, Zustand::Aus, 1000);
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 1000);
|
||||||
|
assert_eq!(t.naechstes(), None, "OFF verwirft das unter STOP Gemerkte");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gestoppt_merkt_genau_eines() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Key(1), Some(42));
|
||||||
|
t.setzen(Quelle::Key(1), Zustand::Gestoppt, 0);
|
||||||
|
t.melden(Quelle::Key(1));
|
||||||
|
t.melden(Quelle::Key(1));
|
||||||
|
t.melden(Quelle::Key(1));
|
||||||
|
t.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Key(1), 42)));
|
||||||
|
t.handler_beendet(Quelle::Key(1));
|
||||||
|
assert_eq!(t.naechstes(), None, "gemerkt wird hoechstens eines");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aus_verwirft_kuenftige() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::Aus, 0);
|
||||||
|
t.zeit_pruefen(5000);
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 5000);
|
||||||
|
assert_eq!(t.naechstes(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zeit_wird_erst_nach_der_frist_faellig() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.zeit_pruefen(999);
|
||||||
|
assert_eq!(t.naechstes(), None);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uebersprungene_fristen_zaehlen_als_eine() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.zeit_pruefen(10_000); // zehn Fristen auf einmal
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||||
|
t.handler_beendet(Quelle::Timer);
|
||||||
|
t.zeit_pruefen(10_000);
|
||||||
|
assert_eq!(t.naechstes(), None, "nicht nachgeholt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kein_wiedereintritt_waehrend_der_handler_laeuft() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||||
|
t.zeit_pruefen(2000);
|
||||||
|
assert_eq!(t.naechstes(), None, "kein Wiedereintritt");
|
||||||
|
t.handler_beendet(Quelle::Timer);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)), "danach einmal");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn off_im_handler_ueberlebt_das_return() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||||
|
t.setzen(Quelle::Timer, Zustand::Aus, 1000); // OFF im Handler
|
||||||
|
t.handler_beendet(Quelle::Timer);
|
||||||
|
t.zeit_pruefen(9000);
|
||||||
|
assert_eq!(t.naechstes(), None, "RETURN schaltet nicht wieder ein");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_schaltet_sonst_selbsttaetig_ein() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
t.naechstes().unwrap();
|
||||||
|
t.handler_beendet(Quelle::Timer);
|
||||||
|
t.zeit_pruefen(2000);
|
||||||
|
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_off_verwirft_ereignisse_und_laesst_die_zustaende() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.event(false);
|
||||||
|
t.zeit_pruefen(1000);
|
||||||
|
assert_eq!(t.naechstes(), None);
|
||||||
|
t.event(true);
|
||||||
|
assert_eq!(
|
||||||
|
t.naechstes(),
|
||||||
|
None,
|
||||||
|
"EVENT OFF muss Eingetretenes verwerfen"
|
||||||
|
);
|
||||||
|
t.zeit_pruefen(2000);
|
||||||
|
assert_eq!(
|
||||||
|
t.naechstes(),
|
||||||
|
Some((Quelle::Timer, 100)),
|
||||||
|
"TIMER ON blieb erhalten"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reihenfolge_key_vor_timer_vor_uevent_vor_signal() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
for (q, ziel) in [
|
||||||
|
(Quelle::Signal(1), 4),
|
||||||
|
(Quelle::UEvent, 3),
|
||||||
|
(Quelle::Timer, 2),
|
||||||
|
(Quelle::Key(5), 1),
|
||||||
|
] {
|
||||||
|
t.definieren(q, Some(ziel));
|
||||||
|
t.setzen(q, Zustand::An, 0);
|
||||||
|
t.melden(q);
|
||||||
|
}
|
||||||
|
let mut reihe = Vec::new();
|
||||||
|
while let Some((q, _)) = t.naechstes() {
|
||||||
|
reihe.push(q);
|
||||||
|
t.handler_beendet(q);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
reihe,
|
||||||
|
vec![
|
||||||
|
Quelle::Key(5),
|
||||||
|
Quelle::Timer,
|
||||||
|
Quelle::UEvent,
|
||||||
|
Quelle::Signal(1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_null_faengt_alle_tasten_der_tabelle() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Key(0), Some(9));
|
||||||
|
t.setzen(Quelle::Key(0), Zustand::An, 0);
|
||||||
|
assert_eq!(t.taste_faengt(0x3B, 0), Some(0), "F1");
|
||||||
|
assert_eq!(t.taste_faengt(0x48, 0), Some(0), "Pfeil hoch");
|
||||||
|
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ist keine Trap-Taste");
|
||||||
|
t.setzen(Quelle::Key(0), Zustand::Aus, 0);
|
||||||
|
assert_eq!(t.taste_faengt(0x3B, 0), None, "OFF gibt die Taste frei");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vordefinierte_taste_wird_ueber_den_scancode_erkannt() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Key(1), Some(1));
|
||||||
|
t.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||||
|
assert_eq!(t.taste_faengt(0x3B, 0), Some(1), "F1");
|
||||||
|
assert_eq!(t.taste_faengt(0x3C, 0), None, "F2 hat keinen Trap");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn benutzertaste_braucht_umschaltzustand_und_scancode() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Key(15), Some(5));
|
||||||
|
t.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||||
|
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
|
||||||
|
t.benutzertaste(15, 4, 0x1E);
|
||||||
|
assert_eq!(t.taste_faengt(0x1E, 2), Some(15), "Strg+A");
|
||||||
|
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ohne Strg nicht");
|
||||||
|
assert_eq!(t.taste_faengt(0x1F, 2), None, "Strg+S nicht");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beide_umschaltbits_gelten_gleich() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
t.definieren(Quelle::Key(16), Some(6));
|
||||||
|
t.setzen(Quelle::Key(16), Zustand::An, 0);
|
||||||
|
t.benutzertaste(16, 1, 0x1E); // rechte Umschalttaste
|
||||||
|
assert_eq!(t.taste_faengt(0x1E, 1), Some(16));
|
||||||
|
let mut t2 = Traps::neu();
|
||||||
|
t2.definieren(Quelle::Key(16), Some(6));
|
||||||
|
t2.setzen(Quelle::Key(16), Zustand::An, 0);
|
||||||
|
t2.benutzertaste(16, 2, 0x1E); // linke Umschalttaste
|
||||||
|
assert_eq!(t2.taste_faengt(0x1E, 1), Some(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn genauer_trap_geht_dem_sammeltrap_vor() {
|
||||||
|
let mut t = Traps::neu();
|
||||||
|
for (q, z) in [(Quelle::Key(0), 9), (Quelle::Key(1), 1)] {
|
||||||
|
t.definieren(q, Some(z));
|
||||||
|
t.setzen(q, Zustand::An, 0);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
t.taste_faengt(0x3B, 0),
|
||||||
|
Some(1),
|
||||||
|
"F1 hat einen eigenen Trap"
|
||||||
|
);
|
||||||
|
assert_eq!(t.taste_faengt(0x3C, 0), Some(0), "F2 faellt an KEY(0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gosub_null_schaltet_den_trap_ab() {
|
||||||
|
let mut t = mit_timer();
|
||||||
|
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||||
|
t.definieren(Quelle::Timer, None);
|
||||||
|
t.zeit_pruefen(5000);
|
||||||
|
assert_eq!(t.naechstes(), None);
|
||||||
|
assert!(!t.aktiv());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
112
crates/tb-ui/src/signale.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 1–86 400 Sekunden. |
|
||||||
|
| `ON UEVENT` | Anweisung | `ON UEVENT GOSUB ziel` bindet das benutzerdefinierte Ereignis. |
|
||||||
|
| `SetUEvent` | Routine | `CALL SetUEvent` löst ein benutzerdefiniertes Ereignis aus. |
|
||||||
|
| `SIGNAL` | Anweisung | `SIGNAL(n) ON\|OFF\|STOP` steuert einen Signal-Trap. |
|
||||||
|
| `TIMER` | Anweisung | `TIMER ON\|OFF\|STOP` steuert den Zeit-Trap. |
|
||||||
|
| `UEVENT` | Anweisung | `UEVENT ON\|OFF\|STOP` steuert den benutzerdefinierten Trap. |
|
||||||
|
|
||||||
## Fehlerbehandlung
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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&` = 1–86 400 Sekunden | verstrichene Zeit |
|
||||||
|
| `ON KEY(n%) GOSUB ziel` | `n%` = 0 (alle), 1–10 (F1–F10), 11 ↑, 12 ←, 13 →, 14 ↓, 15–25 (benutzerdefiniert), 30–31 (F11–F12) | Tastendruck |
|
||||||
|
| `ON UEVENT GOSUB ziel` | — | `SetUEvent` |
|
||||||
|
| `ON SIGNAL(n%) GOSUB ziel` | `n%` = 1 (`SIGINT`), 2 (`SIGTERM`) | Betriebssystem |
|
||||||
|
|
||||||
|
`GOSUB 0` schaltet den jeweiligen Trap ab. Ein Wert außerhalb des
|
||||||
|
Bereichs wird namentlich abgewiesen — zur Übersetzungszeit, wenn er
|
||||||
|
konstant ist, sonst mit Fehler 5.
|
||||||
|
|
||||||
|
**Maskierung.** Je Quelle `ON` (zustellen), `OFF` (verwerfen — auch ein
|
||||||
|
unter `STOP` bereits gemerktes Ereignis) und `STOP` (höchstens eines
|
||||||
|
merken und beim nächsten `ON` zustellen). `EVENT ON`/`EVENT OFF`
|
||||||
|
klammern einen Abschnitt, in dem keine Ereignisse erkannt werden müssen;
|
||||||
|
ein `EVENT STOP` gibt es nicht.
|
||||||
|
|
||||||
|
**Zustellpunkte.** Anweisungsgrenze, `DOEVENTS`, während `SLEEP` und vor
|
||||||
|
blockierender Eingabe — sonst nirgends. Ein zugestelltes Ereignis
|
||||||
|
beendet ein laufendes `SLEEP`.
|
||||||
|
|
||||||
|
**Wiedereintritt.** Der Eintritt in einen Handler führt selbsttätig
|
||||||
|
`STOP` seiner Quelle aus, das `RETURN` selbsttätig `ON` — außer im
|
||||||
|
Handler stand ein ausdrückliches `OFF`, dann bleibt die Quelle aus.
|
||||||
|
|
||||||
|
**Benutzerdefinierte Trap-Tasten.** `KEY n, CHR$(flag) + CHR$(scancode)`
|
||||||
|
erklärt für `n` = 15–25, welche Taste der zugehörige `ON KEY(n)`-Trap
|
||||||
|
fängt — anders als für 1–10 und 30–31, wo dieselbe Anweisung ein Makro
|
||||||
|
zuweist. `flag` ist das Tastaturflagbyte (1 rechte Umschalttaste, 2 linke,
|
||||||
|
4 Strg, 8 Alt), `scancode` der Scancode der Taste. Beispiel: `KEY 15,
|
||||||
|
CHR$(4) + CHR$(30)` fängt Strg+A.
|
||||||
|
|
||||||
|
**Festlegungen ohne Beleg im Vorbild** (2026-09-04; die Original-Hilfe
|
||||||
|
verweist dazu auf ein nicht vorliegendes Kapitel des Programmer's Guide):
|
||||||
|
|
||||||
|
- Eine Taste mit aktivem `ON KEY`-Trap erscheint **nicht** zusätzlich bei
|
||||||
|
`INKEY$`/`INPUT`; sonst sähe ein Programm dieselbe Taste zweimal.
|
||||||
|
- Sind mehrere Ereignisse zugleich fällig, gilt die Reihenfolge `KEY`
|
||||||
|
(aufsteigend nach `n`), `TIMER`, `UEVENT`, `SIGNAL`; zugestellt wird
|
||||||
|
eines je Zustellpunkt.
|
||||||
|
- Zugestellt wird an jeder Anweisungsgrenze, auch innerhalb von
|
||||||
|
`SUB`/`FUNCTION`; das Sprungziel liegt im Modulrumpf — dieselbe Regel
|
||||||
|
wie beim modulweiten `ON ERROR`.
|
||||||
|
- Die Bits des Tastaturflagbytes folgen der BIOS-Belegung. Linke und
|
||||||
|
rechte Umschalttaste sind im Terminal nicht unterscheidbar; beide Bits
|
||||||
|
gelten gleich. NumLock und CapsLock werden ignoriert.
|
||||||
|
- Zeichen ohne Eintrag in der Scancode-Tabelle des Vorbilds können keinen
|
||||||
|
benutzerdefinierten Trap auslösen.
|
||||||
|
|
||||||
## 9. Konsolen-E/A
|
## 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.
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-04
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Phase 4 (Eingang) — Ereignisschleife und klassische Ereignis-Traps
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Phase 4 beginnt laut PLAN mit der Ereignisschleifen-Architektur. Diese
|
||||||
|
Architektur lässt sich an den **klassischen Ereignis-Traps** vollständig
|
||||||
|
bauen und beweisen, ohne eine Zeile Forms-Objektmodell: Sie brauchen
|
||||||
|
dieselbe Warteschlange, dieselben Zustellpunkte, dieselbe Maskierung und
|
||||||
|
denselben Rückruf in die VM, aber keine Formulare, keine Steuerelemente
|
||||||
|
und kein `.FRM`. Umgekehrt müsste der Entwurf der Schleife mitten im
|
||||||
|
Objektmodell blind bezahlt werden.
|
||||||
|
|
||||||
|
Phase 3 hat die halbe Maschinerie bereits nebenbei gebaut: die
|
||||||
|
`Ereignis`-Warteschlange, das Einsortieren in `RtState::pump`, die
|
||||||
|
Zustellpunkte an Anweisungsgrenzen und vor blockierender Eingabe sowie
|
||||||
|
den `CaptureHost` für Tests ohne Terminal. Es fehlen drei Dinge: eine
|
||||||
|
**Zeitquelle**, die **Maskierung** einzelner Ereignisquellen und vor
|
||||||
|
allem der **Rückruf in die VM** — heute gibt es keinen Weg, aus der
|
||||||
|
Laufzeit heraus eine Prozedur oder ein `GOSUB`-Ziel des laufenden
|
||||||
|
Programms auszuführen. `DOEVENTS()` liefert deshalb konstant 0.
|
||||||
|
|
||||||
|
Zugleich sind die zehn Ereignis-Elemente die letzte Lücke des Inventars
|
||||||
|
außerhalb von Forms: von den 15 offenen Einträgen entfallen zehn auf
|
||||||
|
diese Gruppe, drei auf die Forms-Dialoge und zwei auf Phase 5.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Ereignisquellen und Warteschlange**: Das Ereignismodell der Laufzeit
|
||||||
|
wird um eine **Zeitquelle** erweitert. Die Zeit kommt vom Host, nicht
|
||||||
|
aus einem direkten Uhrzugriff der Laufzeit — nur so kann der
|
||||||
|
`CaptureHost` virtuelle Zeit stellen und ein Korpusprogramm mit
|
||||||
|
`ON TIMER` deterministisch prüfen. **BREAKING** für den `Host`-Trait.
|
||||||
|
- **Mausereignisse als vierte Quelle**: Der `TerminalHost` verwirft die
|
||||||
|
Mausereignisse von `crossterm` heute. Sie werden aufgenommen und in
|
||||||
|
Zellenkoordinaten geführt — Tastendruck, Loslassen und Bewegung mit
|
||||||
|
Taste und Umschaltzustand. Die Eingangsaufgabe der Phase lautet
|
||||||
|
„Terminal-Events → Event-Queue → VM-Ticks"; die Maus ist ein
|
||||||
|
Terminal-Event und damit Teil der Schleife, auch wenn sie erst die
|
||||||
|
Steuerelemente auswerten.
|
||||||
|
- **Rückruf in die VM**: Ereigniszustellung wird ein Vorgang der VM, kein
|
||||||
|
Builtin. Ein zugestelltes Ereignis führt das `GOSUB`-Ziel seines Traps
|
||||||
|
aus und kehrt danach an die unterbrochene Stelle zurück. Damit
|
||||||
|
bekommt auch `DOEVENTS()` seine Wirkung.
|
||||||
|
- **Zustellpunkte**: Die vorhandenen Punkte (Anweisungsgrenze,
|
||||||
|
blockierende Eingabe) werden zu einer geschlossenen, dokumentierten
|
||||||
|
Liste ausgebaut: Anweisungsgrenze, `DOEVENTS`, `SLEEP` (wird durch ein
|
||||||
|
zugestelltes Ereignis beendet) und blockierende Eingabe. Zwischen
|
||||||
|
Zustellpunkten unterbricht nichts das Programm.
|
||||||
|
- **Klassische Ereignis-Traps** (Sprachreferenz §8, **10
|
||||||
|
Inventareinträge**): `ON TIMER(n&)`, `ON KEY(n%)`, `ON UEVENT` und
|
||||||
|
`ON SIGNAL(n%)` je mit `GOSUB label`; die Steueranweisungen `TIMER`,
|
||||||
|
`KEY(n)`, `UEVENT` und `SIGNAL(n)` je mit `ON`/`OFF`/`STOP` sowie die
|
||||||
|
Klammer `EVENT` mit `ON`/`OFF`; dazu `SetUEvent`. Wertebereiche nach
|
||||||
|
Beleg: `KEY` 0, 1–25, 30–31; `TIMER` 1–86 400 Sekunden; `SIGNAL` 1–2.
|
||||||
|
Ihr Status im Inventar wechselt
|
||||||
|
auf `implementiert`.
|
||||||
|
- **Maskierung mit drei Zuständen**: `ON` stellt zu, `OFF` verwirft —
|
||||||
|
auch ein unter `STOP` bereits gemerktes Ereignis —, `STOP` merkt eines
|
||||||
|
und stellt es beim nächsten `ON` zu. Der Eintritt in einen Handler
|
||||||
|
führt selbsttätig `STOP` seiner Quelle aus, das `RETURN` selbsttätig
|
||||||
|
`ON`, außer im Handler stand ein ausdrückliches `OFF`.
|
||||||
|
- **`SIGNAL` auf Betriebssystemsignale** (Entscheidung 2026-09-04): Die
|
||||||
|
Quelle des Vorbilds war OS/2-gebunden (Prozessflags über
|
||||||
|
`DOSFLAGPROCESS`) und existiert nicht. Abgebildet wird die minimale
|
||||||
|
Menge, die auf allen Zielplattformen besteht: `SIGNAL(1)` auf `SIGINT`,
|
||||||
|
`SIGNAL(2)` auf `SIGTERM`. Signale erreichen die Laufzeit über den
|
||||||
|
Host, nicht im Signalkontext.
|
||||||
|
- **Getrappte Tasten verlassen den Eingabestrom**: Eine Taste, für die
|
||||||
|
ein aktiver `ON KEY`-Trap besteht, wird zugestellt und MUST NOT
|
||||||
|
zusätzlich bei `INKEY$`, `INPUT` oder `INPUT$` erscheinen.
|
||||||
|
- **Namentliche Abweisung im Frontend**: Die zehn Elemente erhalten
|
||||||
|
Grammatik und Signaturen. Heute rutscht `ON TIMER(5) GOSUB marke`
|
||||||
|
stillschweigend in das berechnete `ON ausdruck GOSUB` — genau der
|
||||||
|
Fall, den der Guiding Principle als Bug definiert.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Die **Auswertung** von Mausereignissen durch Steuerelemente
|
||||||
|
(Trefferprüfung, Fokuswechsel, Ziehen) — sie setzt das Objektmodell
|
||||||
|
voraus und gehört zum Change der Steuerelemente. Quelle,
|
||||||
|
Warteschlange und Koordinatenmodell entstehen hier.
|
||||||
|
- Das Forms-Objektmodell, Steuerelemente, `.FRM` und die Dialoge
|
||||||
|
(`MSGBOX`, `INPUTBOX$`) — eigene Changes derselben Phase.
|
||||||
|
- `COM`, `PEN`, `PLAY`, `STRIG` bleiben Non-Feature (Fehler 73).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `ereignissteuerung`: Ereignisquellen (Tastatur, Zeit,
|
||||||
|
benutzerdefiniert), Warteschlange und Reihenfolge, Zustellpunkte,
|
||||||
|
Maskierung mit `ON`/`OFF`/`STOP` je Quelle und global, Zustellung als
|
||||||
|
`GOSUB` in das laufende Programm, Zusammenspiel mit Fehlerbehandlung
|
||||||
|
und blockierender Eingabe.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `sprach-frontend`: Grammatik und Signaturprüfung SHALL die
|
||||||
|
Trap-Anweisungen und `SetUEvent` umfassen; `ON <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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Ereignis-Traps in Grammatik und Signaturprüfung
|
||||||
|
Der Parser SHALL die Trap-Anweisungen des Vorbilds als eigene Konstrukte
|
||||||
|
erkennen: `ON TIMER(n) GOSUB ziel`, `ON KEY(n) GOSUB ziel`,
|
||||||
|
`ON UEVENT GOSUB ziel` und `ON SIGNAL(n) GOSUB ziel`. Diese Formen MUST
|
||||||
|
NOT als berechnetes `ON ausdruck GOSUB liste` behandelt werden. Die
|
||||||
|
Steueranweisungen `TIMER`, `KEY(n)`, `UEVENT` und `SIGNAL(n)` mit `ON`,
|
||||||
|
`OFF` oder `STOP` SHALL erkannt und nur für diese Quellen zugelassen
|
||||||
|
werden; `EVENT` SHALL nur `ON` und `OFF` zulassen, weil das Vorbild
|
||||||
|
keinen dritten Zustand kennt. `COM`, `PEN`, `PLAY` und `STRIG` MUST als
|
||||||
|
Non-Feature namentlich abgewiesen werden. `SetUEvent` SHALL eine
|
||||||
|
Signatur besitzen. Die Wertebereiche SHALL geprüft werden: `ON KEY(n%)`
|
||||||
|
mit `n%` aus 0, 1–25 oder 30–31, `ON TIMER(n&)` mit `n&` aus 1–86 400
|
||||||
|
und `ON SIGNAL(n%)` mit `n%` aus 1–2. Eine fehlerhafte Verwendung MUST
|
||||||
|
eine Diagnose erzeugen, die das Element namentlich nennt.
|
||||||
|
|
||||||
|
#### Scenario: ON TIMER ist kein berechnetes GOSUB
|
||||||
|
- **WHEN** ein Modul `ON TIMER(5) GOSUB Marke` enthält
|
||||||
|
- **THEN** wird die Anweisung als Zeit-Trap erfasst und nicht als `ON ausdruck GOSUB` mit einer einelementigen Zielliste
|
||||||
|
|
||||||
|
#### Scenario: Ungültige Kennung wird namentlich gemeldet
|
||||||
|
- **WHEN** ein Modul `ON KEY(99) GOSUB Marke` enthält
|
||||||
|
- **THEN** nennt die Diagnose `ON KEY` und den zulässigen Wertebereich
|
||||||
|
|
||||||
|
#### Scenario: EVENT kennt kein STOP
|
||||||
|
- **WHEN** ein Modul `EVENT STOP` enthält
|
||||||
|
- **THEN** nennt die Diagnose `EVENT` und die zulässigen Formen `ON` und `OFF`
|
||||||
|
|
||||||
|
#### Scenario: Zeitintervall außerhalb des Bereichs
|
||||||
|
- **WHEN** ein Modul `ON TIMER(90000) GOSUB Marke` enthält
|
||||||
|
- **THEN** nennt die Diagnose `ON TIMER` und den Bereich 1 bis 86400
|
||||||
|
|
||||||
|
#### Scenario: Non-Feature-Quelle wird abgewiesen
|
||||||
|
- **WHEN** ein Modul `ON PEN GOSUB Marke` enthält
|
||||||
|
- **THEN** meldet die Übersetzung „Feature unavailable" an der Fundstelle der Anweisung — wie bei jedem anderen Non-Feature — und nicht einen generischen Syntaxfehler
|
||||||
|
|
||||||
|
#### Scenario: SetUEvent besitzt eine Signatur
|
||||||
|
- **WHEN** ein Modul `CALL SetUEvent` enthält
|
||||||
|
- **THEN** entsteht keine Diagnose „Subprogram not defined"
|
||||||
|
|
||||||
|
### Requirement: Trap-Ziele sind Sprungziele des Moduls
|
||||||
|
Das Ziel eines Ereignis-Traps SHALL wie ein `GOSUB`-Ziel aufgelöst
|
||||||
|
werden. Ein unbekanntes Ziel MUST zur Übersetzungszeit gemeldet werden
|
||||||
|
und nennt dabei das Label.
|
||||||
|
|
||||||
|
#### Scenario: Unbekanntes Trap-Ziel
|
||||||
|
- **WHEN** ein Modul `ON TIMER(1) GOSUB Fehlt` ohne Label `Fehlt` enthält
|
||||||
|
- **THEN** meldet die Übersetzung ein undefiniertes Sprungziel und nennt `Fehlt`
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
## 1. Belege aus der Original-Hilfe
|
||||||
|
|
||||||
|
- [x] 1.1 Einzelseiten zu `ON TIMER`, `ON KEY`, `ON UEVENT`, `ON SIGNAL`, `TIMER`, `KEY(n)`, `UEVENT`, `SIGNAL(n)`, `EVENT` und `SetUEvent` erfassen und in `umfang-und-form.md` dieses Changes wortgetreu festhalten (Syntaxzeilen, Wertebereiche von `n`, Reihenfolge gleichzeitig fälliger Ereignisse, Verhalten in Prozeduren); verifiziert durch die vorhandene Datei mit Quellenangabe je Element
|
||||||
|
- [x] 1.2 Aus 1.1 die Festlegungen ableiten, die die Hilfe **nicht** hergibt, und sie als Abweichung in `docs/sprachreferenz.md` §8 eintragen — insbesondere die Quellenzuordnung von `SIGNAL(n)`; verifiziert durch den Abschnitt „Abweichungen" mit je einem Satz Begründung
|
||||||
|
- [x] 1.3 Delta-Specs an die Belege aus 1.1 angleichen (Leitplanke „Referenzverhalten schlägt Eleganz"): `EVENT` ohne `STOP`, automatischer `STOP`/`ON` um den Handler, `OFF` verwirft Gemerktes, Wertebereiche von `KEY`/`TIMER`/`SIGNAL`; verifiziert durch `openspec validate --strict` und die geänderten Requirements
|
||||||
|
|
||||||
|
## 2. Zeit- und Mausquelle im Host (BREAKING)
|
||||||
|
|
||||||
|
- [x] 2.1 `Host`-Trait um eine monotone Zeitabfrage erweitern und `TerminalHost` daran anschließen; verifiziert durch `cargo build`, das alle drei Host-Implementierungen ohne Warnung übersetzt
|
||||||
|
- [x] 2.2 `CaptureHost` um eine stellbare virtuelle Uhr erweitern (setzen und vorrücken); verifiziert durch einen Unit-Test, der die Uhr vorrückt und den gelesenen Wert prüft
|
||||||
|
- [x] 2.3 Signalquelle: Marke im Signalhandler setzen, Übersetzung in ein Ereignis beim Abholen (D8), `SIGINT` und `SIGTERM` plattformübergreifend über `signal-hook`; verifiziert durch einen Unit-Test, der eine gesetzte Marke als Ereignis wiederfindet
|
||||||
|
- [x] 2.4 Mausereignis in das Ereignismodell aufnehmen (Drücken, Loslassen, Bewegung, Taste, Umschaltzustand, Position) und `TerminalHost` die `crossterm`-Mausereignisse durchreichen lassen, statt sie zu verwerfen; verifiziert durch Unit-Tests der Abbildung je Ereignisart
|
||||||
|
- [x] 2.5 Umrechnung auf 1-basierte Zellenkoordinaten in der Darstellungsschicht, Ereignisse außerhalb der Fläche verwerfen (D7); verifiziert durch Unit-Tests an den Rändern — Zelle 1,1, letzte Zelle, Position außerhalb, und dieselben Fälle nach einer Größenänderung
|
||||||
|
- [x] 2.6 `CaptureHost` kann Mausereignisse einspeisen und die Reihenfolge gegenüber Tastenereignissen bleibt erhalten; verifiziert durch einen Unit-Test mit gemischter Folge
|
||||||
|
|
||||||
|
## 3. Trap-Zustände in der Laufzeit
|
||||||
|
|
||||||
|
- [x] 3.1 Trap-Tabelle in `RtState`: je Quelle Zustand `ON`/`OFF`/`STOP`, höchstens ein anstehendes Ereignis, Zielkennung; verifiziert durch Unit-Tests für die drei Zustandsübergänge inklusive „STOP merkt genau eines"
|
||||||
|
- [x] 3.2 Hauptschalter `EVENT` als Überlagerung der Einzelzustände; verifiziert durch einen Unit-Test, der nach `EVENT ON` den vorherigen Einzelzustand wiederfindet
|
||||||
|
- [x] 3.3 Fälligkeit der Zeit-Traps aus der Host-Zeit berechnen; verifiziert durch einen Unit-Test mit virtueller Uhr über mehrere Fristen inklusive übersprungener Frist
|
||||||
|
- [x] 3.4 `SetUEvent` als Auslöser des benutzerdefinierten Ereignisses; verifiziert durch einen Unit-Test, der genau eine Zustellung sieht
|
||||||
|
- [x] 3.5 Getrappte Tasten am Zustellpunkt aus der Tastenschlange nehmen (D5); verifiziert durch Unit-Tests für beide Richtungen — getrappte Taste erscheint nicht bei `INKEY$`, nicht getrappte bleibt erhalten
|
||||||
|
- [x] 3.7 Umschaltzustand im Tastenereignis mitführen und `KEY n, CHR$(flag)+CHR$(scan)` für n = 15–25 als Erklärung einer Trap-Taste (statt Makrozuweisung); verifiziert durch Unit-Tests für Strg+A mit und ohne Strg sowie durch die Scancode-Tabelle aus der Original-Hilfe
|
||||||
|
- [x] 3.6 Mausereignisse ohne Verbraucher am Zustellpunkt verwerfen und die Warteschlange begrenzen (D7); verifiziert durch einen Unit-Test, der viele Bewegungsereignisse einspeist und die Warteschlangenlänge prüft
|
||||||
|
|
||||||
|
## 4. Zustellung in der VM
|
||||||
|
|
||||||
|
- [x] 4.1 Zustellbit im Flagwort und Fälligkeitsprüfung an `Instr::Stmt`; verifiziert durch einen VM-Test, der ohne aktiven Trap keine Zustellung auslöst
|
||||||
|
- [x] 4.2 Trap-Frame auf den Modulrumpf legen und mit `RETURN` abräumen (D2), inklusive eigenem `GOSUB`-Stapel und `RETURN label`; verifiziert durch VM-Tests für Zustellung aus dem Modulrumpf und aus einer `SUB` heraus, jeweils mit unverändertem Zustand danach
|
||||||
|
- [x] 4.3 Eigene Quelle während des Handlers stoppen und mit `RETURN` freigeben; verifiziert durch einen VM-Test, der den Handler nicht erneut betritt
|
||||||
|
- [x] 4.7 Anweisungsgrenze in den Rücksprung jeder Schleife legen — ohne sie ist `FOR i = 1 TO n: NEXT` für Ereignisse, Breakpoints und Abbruch taub (Befund bei 4.2); verifiziert durch einen Test, der für alle vier Schleifenformen eine Grenze im Kreis nachweist, und dadurch, dass die Trap-Tests mit leerem Schleifenrumpf laufen
|
||||||
|
- [x] 4.4 Reihenfolge bei mehreren fälligen Ereignissen nach Beleg aus 1.1 festlegen; verifiziert durch einen VM-Test mit zwei gleichzeitig fälligen Quellen
|
||||||
|
- [x] 4.5 `DOEVENTS` und `SLEEP` von Builtins auf Anweisungen mit Zustellung umstellen; verifiziert durch VM-Tests — Handler läuft innerhalb einer `DOEVENTS`-Schleife, und `SLEEP` endet vorzeitig durch eine Zustellung
|
||||||
|
- [x] 4.8 Befund bei 4.6: `ERL` meldete die Zeile des Fehlerhandlers statt der Fehlerzeile, sobald der Handler selbst nummeriert war — laufende Zeilennummer und `ERL` getrennt; verifiziert durch einen Regressionstest mit nummeriertem Handler
|
||||||
|
- [x] 4.6 Fehler in einem Handler an die geltende Fehlerbehandlung geben, `RESUME` bezieht sich auf den Handler; verifiziert durch einen VM-Test, dessen `ERL` die Zeile im Handler nennt
|
||||||
|
|
||||||
|
## 5. Frontend
|
||||||
|
|
||||||
|
- [x] 5.1 Grammatik für `ON TIMER(n)/KEY(n)/UEVENT/SIGNAL(n) GOSUB ziel` als eigene Konstrukte; verifiziert durch einen Parser-Test, der zeigt, dass daraus kein berechnetes `ON ausdruck GOSUB` entsteht
|
||||||
|
- [x] 5.2 Signaturen und Wertebereichsprüfung der Steueranweisungen und von `SetUEvent`; verifiziert durch Sema-Tests, deren Diagnosen das Element namentlich nennen (`ON KEY(99)`, `ON PEN`, `CALL SetUEvent`)
|
||||||
|
- [x] 5.3 Trap-Ziele wie `GOSUB`-Ziele auflösen; verifiziert durch einen Sema-Test, der ein fehlendes Label namentlich meldet
|
||||||
|
- [x] 5.4 Absenkung ins HIR/Bytecode, `Unsupported`-Absenkung von `SETUEVENT` entfernen; verifiziert durch `cargo test -p tb-frontend`, inklusive des Inventartests
|
||||||
|
|
||||||
|
## 6. Korpus und Harness
|
||||||
|
|
||||||
|
- [x] 6.1 Kopfzeilen-Direktive `' tb-clock: <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
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# Umfang und Form der Ereignis-Elemente (Aufgaben 1.1, 1.2)
|
||||||
|
|
||||||
|
**Quelle.** `bas7advr.hlp` (BASIC 7 Advisor), Einzelseiten unter
|
||||||
|
https://dos-help.soulsphere.org/bas7advr.hlp/ (abgerufen 2026-09-04);
|
||||||
|
Gegenprobe `qb45advr.hlp` unter derselben Domain. Zitate wörtlich aus den
|
||||||
|
Einzelseiten, Übersetzung nur in den Kommentarzeilen.
|
||||||
|
|
||||||
|
## 1.1 Einzelseiten
|
||||||
|
|
||||||
|
| Element | Seite |
|
||||||
|
|---|---|
|
||||||
|
| `ON TIMER` | `x_dot_ontimerrr.html` |
|
||||||
|
| `ON KEY` | `x_dot_onkeyr.html` |
|
||||||
|
| `ON UEVENT` | `x_dot_onur.html` |
|
||||||
|
| `ON SIGNAL` | `x_dot_onsigbar.html` |
|
||||||
|
| `TIMER` (Anweisung) | `x_dot_timersr.html` |
|
||||||
|
| `KEY(n)` (Ereignis) | `x_dot_ketvr.html` |
|
||||||
|
| `UEVENT` | `x_dot_uevvvr.html` |
|
||||||
|
| `SIGNAL` | `x_dot_signalr.html` |
|
||||||
|
| `EVENT` | `x_dot_evntonr.html` |
|
||||||
|
| `SetUEvent` | `x_dot_SETUEVr.html` |
|
||||||
|
|
||||||
|
### ON TIMER
|
||||||
|
|
||||||
|
> `ON TIMER(n&) GOSUB {linenumber | linelabel}`
|
||||||
|
>
|
||||||
|
> „The maximum number of seconds that can be specified for the time
|
||||||
|
> interval, n&, is the number of seconds in a 24-hour day (86,400)."
|
||||||
|
>
|
||||||
|
> „A linenumber value of 0 disables event trapping and does not specify
|
||||||
|
> line 0 as the start of the routine."
|
||||||
|
|
||||||
|
Der Zeitwert ist **LONG** (`n&`), Bereich 1–86 400 Sekunden.
|
||||||
|
`GOSUB 0` schaltet den Trap ab.
|
||||||
|
|
||||||
|
### TIMER ON/OFF/STOP
|
||||||
|
|
||||||
|
> „TIMER ON enables timer-event trapping."
|
||||||
|
>
|
||||||
|
> „TIMER OFF disables timer-event trapping. … Events occurring while
|
||||||
|
> trapping is off are ignored."
|
||||||
|
>
|
||||||
|
> „TIMER STOP suspends timer-event trapping. … Events occurring while
|
||||||
|
> trapping is off are remembered and processed when the next TIMER ON
|
||||||
|
> statement is executed."
|
||||||
|
|
||||||
|
**Wiedereintritt — wörtlich belegt:**
|
||||||
|
|
||||||
|
> „an automatic TIMER STOP is executed so that recursive traps cannot
|
||||||
|
> take place."
|
||||||
|
>
|
||||||
|
> „The RETURN from the trapping routine automatically performs a TIMER ON
|
||||||
|
> statement unless an explicit TIMER OFF was performed inside the
|
||||||
|
> routine."
|
||||||
|
|
||||||
|
Das ist genauer als „die Quelle gilt während des Handlers als gestoppt":
|
||||||
|
`RETURN` schaltet die Quelle **auf ON**, außer im Handler stand ein
|
||||||
|
ausdrückliches `OFF`.
|
||||||
|
|
||||||
|
### KEY(n) — Wertebereich
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> KEY(n%) ON
|
||||||
|
> KEY(n%) OFF
|
||||||
|
> KEY(n%) STOP
|
||||||
|
> ```
|
||||||
|
|
||||||
|
| n% | Taste |
|
||||||
|
|---|---|
|
||||||
|
| 0 | alle Tasten dieser Tabelle |
|
||||||
|
| 1–10 | F1–F10 |
|
||||||
|
| 11 | Pfeil hoch |
|
||||||
|
| 12 | Pfeil links |
|
||||||
|
| 13 | Pfeil rechts |
|
||||||
|
| 14 | Pfeil runter |
|
||||||
|
| 15–25 | benutzerdefinierte Tasten |
|
||||||
|
| 30–31 | F11–F12 (101-Tasten-Tastatur) |
|
||||||
|
|
||||||
|
`ON KEY` selbst: `ON KEY(n%) GOSUB {linenumber | linelabel}`. Zur Frage,
|
||||||
|
ob eine getrappte Taste im Eingabestrom bleibt, sagt die Einzelseite nur:
|
||||||
|
|
||||||
|
> „After a key is trapped, the information on which key was trapped is no
|
||||||
|
> longer available."
|
||||||
|
|
||||||
|
Die Gegenprobe in `qb45advr.hlp` (`x_dot_onkeyba.html`, `x_dot_ketv.html`)
|
||||||
|
enthält dazu **nichts**. → Festlegung nötig, siehe 1.2.
|
||||||
|
|
||||||
|
### Benutzerdefinierte Tasten 15–25
|
||||||
|
|
||||||
|
Die Einzelseite zu `KEY n, zeichenkette$` (`x_dot_setfkr.html`) behandelt
|
||||||
|
**nur** die Makrozuweisung für 1–10 und 30–31; die Erklärungsform für
|
||||||
|
15–25 steht in keiner der beiden Hilfen, ebenso wenig die Bedeutung der
|
||||||
|
Bits des Tastaturflagbytes. Belegt ist dagegen die **Scancode-Tabelle**
|
||||||
|
(`qb45advr.hlp`, `x_dot_kbsct.html`), aus der die Zuordnung Zeichen →
|
||||||
|
Scancode stammt:
|
||||||
|
|
||||||
|
> ESC 01 · „! or 1" 02 … „) or 0" 0B · „_ or -" 0C · „+ or =" 0D ·
|
||||||
|
> TAB 0F · Q 10 … P 19 · ENTER 1C · A 1E … L 26 · Z 2C … M 32 ·
|
||||||
|
> SPACEBAR 39 · F1 3B … F10 44 · HOME 47 · UP 48 · LEFT 4B · RIGHT 4D ·
|
||||||
|
> END 4F · DOWN 50 · INS 52 · DEL 53
|
||||||
|
|
||||||
|
→ Festlegung nötig für das Flagbyte, siehe 1.2.
|
||||||
|
|
||||||
|
### UEVENT und SetUEvent
|
||||||
|
|
||||||
|
> `ON UEVENT GOSUB {linenumber | linelabel}`
|
||||||
|
>
|
||||||
|
> „This event typically originates from a hardware interrupt but can also
|
||||||
|
> be triggered by software."
|
||||||
|
>
|
||||||
|
> „SetUEvent … sets a flag that BASIC checks before executing the next
|
||||||
|
> statement."
|
||||||
|
|
||||||
|
Bestätigt die Zustellung **an der Anweisungsgrenze**.
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> UEVENT ON
|
||||||
|
> UEVENT OFF
|
||||||
|
> UEVENT STOP
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> UEVENT STOP „preserves events in memory for processing once UEVENT ON
|
||||||
|
> resumes, though executing UEVENT OFF afterward will discard these queued
|
||||||
|
> events."
|
||||||
|
|
||||||
|
`OFF` verwirft also auch ein unter `STOP` gemerktes Ereignis.
|
||||||
|
|
||||||
|
`SetUEvent` ist eine **Routine** ohne dokumentierte Argumentliste und ohne
|
||||||
|
dokumentierten Rückgabewert:
|
||||||
|
|
||||||
|
> „It sets the BASIC entry point that causes a user-defined event."
|
||||||
|
|
||||||
|
### SIGNAL
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> SIGNAL(n%) ON
|
||||||
|
> SIGNAL(n%) OFF
|
||||||
|
> SIGNAL(n%) STOP
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> `ON SIGNAL(n%) GOSUB {linenumber | linelabel}`
|
||||||
|
>
|
||||||
|
> „A linenumber value of 0 disables event trapping."
|
||||||
|
|
||||||
|
Quelle der Signale — wörtlich:
|
||||||
|
|
||||||
|
> „OS/2 protected-mode signal"
|
||||||
|
>
|
||||||
|
> „Process flags A, B, and C are used for communicating between
|
||||||
|
> processes."
|
||||||
|
>
|
||||||
|
> aktiviert durch die OS/2-Funktion `DOSFLAGPROCESS` aus einem anderen
|
||||||
|
> Prozess.
|
||||||
|
|
||||||
|
Ein **Wertebereich für `n%` ist nicht dokumentiert**; die Einzelseite
|
||||||
|
verweist auf „Chapter 9, ‚Event Handling'" des Programmer's Guide, der
|
||||||
|
nicht vorliegt.
|
||||||
|
|
||||||
|
### EVENT
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> EVENT ON
|
||||||
|
> EVENT OFF
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> „EVENT OFF … equivalent to compiling a program from the BC command line
|
||||||
|
> without using /V or /W."
|
||||||
|
>
|
||||||
|
> „EVENT OFF and EVENT ON can be used to bracket sections of code where
|
||||||
|
> events do not need to be detected and trapped, and fast performance is
|
||||||
|
> required."
|
||||||
|
|
||||||
|
**`EVENT STOP` ist nicht dokumentiert.** `EVENT` ist im Vorbild kein
|
||||||
|
Laufzeit-Hauptschalter mit drei Zuständen, sondern eine Klammer, die die
|
||||||
|
Erzeugung der Ereignisprüfungen im Kompilat ein- und ausschaltet.
|
||||||
|
|
||||||
|
### Reihenfolge und Trapping in Prozeduren
|
||||||
|
|
||||||
|
Zu beidem geben die Einzelseiten **nichts** her; sie verweisen auf
|
||||||
|
Kapitel 9 des Programmer's Guide. → Festlegung nötig, siehe 1.2.
|
||||||
|
|
||||||
|
## 1.2 Abweichungen der Delta-Spec vom Beleg
|
||||||
|
|
||||||
|
Nach der Leitplanke „Referenzverhalten schlägt Eleganz … das gilt auch
|
||||||
|
gegenüber den eigenen Spezifikationen" ist bei jeder Abweichung die
|
||||||
|
**Spec** anzugleichen.
|
||||||
|
|
||||||
|
| Nr. | Delta-Spec sagt | Beleg sagt | Folge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A | `EVENT` hat `ON`/`OFF`/`STOP` und überlagert die Einzelzustände | nur `ON`/`OFF`; Klammer um die Erzeugung der Prüfungen | Spec angleichen; `EVENT STOP` wird namentlich abgewiesen |
|
||||||
|
| B | „während des Handlers gilt die Quelle als gestoppt; mit `RETURN` gilt wieder ihr Zustand" | Eintritt = automatisches `STOP`; `RETURN` = automatisches `ON`, außer im Handler stand ein ausdrückliches `OFF` | Spec präzisieren |
|
||||||
|
| C | `OFF` verwirft künftige Ereignisse | `OFF` verwirft **auch** ein unter `STOP` gemerktes | Spec ergänzen |
|
||||||
|
| D | `ON KEY(n)` mit Wertebereich | 0, 1–25, 30–31 mit fester Tastenzuordnung | Spec unverändert, Bereich jetzt belegt |
|
||||||
|
| E | `ON TIMER(n)` | `n&` ist LONG, 1–86 400; `GOSUB 0` schaltet ab | Spec unverändert, Form jetzt belegt |
|
||||||
|
|
||||||
|
**Ohne Beleg — unsere Festlegungen (gehören nach `sprachreferenz.md`
|
||||||
|
unter „Abweichungen"):**
|
||||||
|
|
||||||
|
1. **Getrappte Tasten verlassen den Eingabestrom.** Von keiner der beiden
|
||||||
|
Hilfen belegt. Wir legen fest: eine Taste mit aktivem Trap erscheint
|
||||||
|
nicht zusätzlich bei `INKEY$`/`INPUT`. Begründung: andernfalls sähe ein
|
||||||
|
Programm dieselbe Taste zweimal, und `KEY(n) OFF` wäre die einzige Art,
|
||||||
|
eine Taste überhaupt normal zu lesen.
|
||||||
|
2. **Reihenfolge gleichzeitig fälliger Ereignisse.** Unbelegt. Wir legen
|
||||||
|
fest: `KEY` (aufsteigend nach `n`), dann `TIMER`, dann `UEVENT`, dann
|
||||||
|
`SIGNAL` — Tasten zuerst, weil sie an einer Benutzereingabe hängen.
|
||||||
|
3. **Trapping in Prozeduren.** Unbelegt. Wir legen fest: die Zustellung
|
||||||
|
erfolgt an jeder Anweisungsgrenze, auch innerhalb von `SUB`/`FUNCTION`;
|
||||||
|
das Ziel liegt im Modulrumpf. Das entspricht dem bereits umgesetzten
|
||||||
|
Verhalten des modulweiten `ON ERROR`.
|
||||||
|
4. **Bits des Tastaturflagbytes** in `KEY n, CHR$(flag)+CHR$(scan)`.
|
||||||
|
Unbelegt. Wir legen die Belegung des BIOS-Flagbytes zugrunde:
|
||||||
|
1 rechte Umschalttaste, 2 linke Umschalttaste, 4 Strg, 8 Alt. Im
|
||||||
|
Terminal sind linke und rechte Umschalttaste nicht unterscheidbar,
|
||||||
|
beide Bits gelten deshalb gleich; NumLock und CapsLock spielen für
|
||||||
|
einen Trap keine Rolle und werden ignoriert.
|
||||||
|
5. **`SIGNAL`-Quelle.** Die belegte Quelle ist OS/2-gebunden
|
||||||
|
(`DOSFLAGPROCESS`, Prozessflags A/B/C) und existiert auf keiner
|
||||||
|
Zielplattform; ein Wertebereich ist nicht dokumentiert. → **Entscheidung
|
||||||
|
des Projektinhabers ausstehend**, siehe unten.
|
||||||
|
|
||||||
|
## Entscheidung zu `SIGNAL` (Projektinhaber, 2026-09-04)
|
||||||
|
|
||||||
|
`SIGNAL` wird auf **Betriebssystemsignale** abgebildet, und zwar nur auf
|
||||||
|
die minimale Menge, die auf allen Zielplattformen existiert.
|
||||||
|
|
||||||
|
Der C-Standard garantiert überall — Linux, macOS **und** Windows —
|
||||||
|
`SIGABRT`, `SIGFPE`, `SIGILL`, `SIGINT`, `SIGSEGV`, `SIGTERM`. Von diesen
|
||||||
|
sind nur zwei asynchron, von außen ausgelöst und fortsetzbar; die vier
|
||||||
|
Fault-Signale entstehen aus dem eigenen Programmzustand, und ein
|
||||||
|
`RETURN` aus ihrem Handler führte in denselben Fehler zurück. Abgebildet
|
||||||
|
werden deshalb:
|
||||||
|
|
||||||
|
| `n%` | Signal | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `SIGINT` | Unterbrechungswunsch von außen |
|
||||||
|
| 2 | `SIGTERM` | Beendigungswunsch von außen |
|
||||||
|
|
||||||
|
Ein anderer Wert als 1 oder 2 wird namentlich abgewiesen — zur
|
||||||
|
Übersetzungszeit, wenn er konstant ist, sonst mit Laufzeitfehler 5.
|
||||||
|
|
||||||
|
**Zustellung.** Signale erreichen die Laufzeit wie die Zeit über den
|
||||||
|
Host; nur er berührt das Betriebssystem. Ein Signal wird nicht im
|
||||||
|
Signalkontext verarbeitet, sondern setzt eine Marke, die der Host beim
|
||||||
|
nächsten Abholen in ein Ereignis übersetzt.
|
||||||
|
|
||||||
|
**Verhältnis zum Abbruch.** Im Rohmodus liefert das Terminal Strg+C als
|
||||||
|
Taste, nicht als Signal; daran ändert sich nichts. Trifft `SIGINT`
|
||||||
|
dagegen von außen ein (`kill -INT`), gilt: mit aktivem Trap läuft der
|
||||||
|
Handler, ohne aktiven Trap bleibt es beim bisherigen Abbruch. Dieselbe
|
||||||
|
Regel wie bei getrappten Tasten.
|
||||||
|
|
||||||
|
**Abweichungen, die zu dokumentieren sind:**
|
||||||
|
|
||||||
|
1. Die belegte Quelle des Vorbilds (OS/2-Prozessflags A/B/C über
|
||||||
|
`DOSFLAGPROCESS`) existiert nicht; `n%` bedeutet bei uns etwas
|
||||||
|
anderes als im Vorbild. Ein Wertebereich war im Vorbild ohnehin nicht
|
||||||
|
dokumentiert.
|
||||||
|
2. `SIGTERM` ist unter Windows von außen nicht zustellbar (es gibt kein
|
||||||
|
`kill`); `SIGNAL(2)` bleibt dort wirkungslos. `SIGNAL(1)` wird dort
|
||||||
|
über den Konsolen-Steuerhandler (`CTRL_C_EVENT`) bedient.
|
||||||
210
openspec/specs/ereignissteuerung/spec.md
Normal file
210
openspec/specs/ereignissteuerung/spec.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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, 1–25 oder 30–31, `ON TIMER(n&)` mit `n&` aus 1–86 400
|
||||||
|
und `ON SIGNAL(n%)` mit `n%` aus 1–2. Eine fehlerhafte Verwendung MUST
|
||||||
|
eine Diagnose erzeugen, die das Element namentlich nennt.
|
||||||
|
|
||||||
|
#### Scenario: ON TIMER ist kein berechnetes GOSUB
|
||||||
|
- **WHEN** ein Modul `ON TIMER(5) GOSUB Marke` enthält
|
||||||
|
- **THEN** wird die Anweisung als Zeit-Trap erfasst und nicht als `ON ausdruck GOSUB` mit einer einelementigen Zielliste
|
||||||
|
|
||||||
|
#### Scenario: Ungültige Kennung wird namentlich gemeldet
|
||||||
|
- **WHEN** ein Modul `ON KEY(99) GOSUB Marke` enthält
|
||||||
|
- **THEN** nennt die Diagnose `ON KEY` und den zulässigen Wertebereich
|
||||||
|
|
||||||
|
#### Scenario: EVENT kennt kein STOP
|
||||||
|
- **WHEN** ein Modul `EVENT STOP` enthält
|
||||||
|
- **THEN** nennt die Diagnose `EVENT` und die zulässigen Formen `ON` und `OFF`
|
||||||
|
|
||||||
|
#### Scenario: Zeitintervall außerhalb des Bereichs
|
||||||
|
- **WHEN** ein Modul `ON TIMER(90000) GOSUB Marke` enthält
|
||||||
|
- **THEN** nennt die Diagnose `ON TIMER` und den Bereich 1 bis 86400
|
||||||
|
|
||||||
|
#### Scenario: Non-Feature-Quelle wird abgewiesen
|
||||||
|
- **WHEN** ein Modul `ON PEN GOSUB Marke` enthält
|
||||||
|
- **THEN** meldet die Übersetzung „Feature unavailable" an der Fundstelle der Anweisung — wie bei jedem anderen Non-Feature — und nicht einen generischen Syntaxfehler
|
||||||
|
|
||||||
|
#### Scenario: SetUEvent besitzt eine Signatur
|
||||||
|
- **WHEN** ein Modul `CALL SetUEvent` enthält
|
||||||
|
- **THEN** entsteht keine Diagnose „Subprogram not defined"
|
||||||
|
|
||||||
|
### Requirement: Trap-Ziele sind Sprungziele des Moduls
|
||||||
|
Das Ziel eines Ereignis-Traps SHALL wie ein `GOSUB`-Ziel aufgelöst
|
||||||
|
werden. Ein unbekanntes Ziel MUST zur Übersetzungszeit gemeldet werden
|
||||||
|
und nennt dabei das Label.
|
||||||
|
|
||||||
|
#### Scenario: Unbekanntes Trap-Ziel
|
||||||
|
- **WHEN** ein Modul `ON TIMER(1) GOSUB Fehlt` ohne Label `Fehlt` enthält
|
||||||
|
- **THEN** meldet die Übersetzung ein undefiniertes Sprungziel und nennt `Fehlt`
|
||||||
|
|||||||
@@ -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
76
tests/compat/traps.bas
Normal 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
11
tests/compat/traps.out
Normal 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
|
||||||
Reference in New Issue
Block a user