Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren

This commit is contained in:
2026-09-05 19:15:31 +02:00
parent 57d6386a5a
commit 644a86212f
39 changed files with 2013 additions and 271 deletions

View File

@@ -1,6 +1,6 @@
## Why
Ereignisse gehen verloren, laufen in falscher Reihenfolge oder umgehen Menüsperren. F04F08, F18 und F20 zeigen außerdem Zeitabfragen ohne Bedarf und eine unerwünschte Terminalabhängigkeit der VM. Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md).
Ereignisse gehen verloren, laufen in falscher Reihenfolge oder umgehen Menüsperren. F04F08, F18 und F20 zeigen außerdem Zeitabfragen ohne Bedarf und eine unerwünschte Terminalabhängigkeit der VM. Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
## What Changes

View File

@@ -0,0 +1,167 @@
//! Ursprüngliche Reproduktionen: fünf behobene Befunde und eine Kontrollprobe.
//! Separater Cargo-Treiber; siehe review.md. Alle sechs Prüfungen müssen bestehen.
use tb_frontend::forms::{FormCatalog, ObjectClass};
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};
fn vm(src: &str) -> Vm {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false);
catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false);
Vm::new(
tb_vm::compile_source_with_forms("FORM1", src, &catalog)
.unwrap_or_else(|d| panic!("{d:?}")),
)
}
fn stop(vm: &mut Vm, host: &mut dyn Host) {
assert!(matches!(vm.run(host), RunEvent::Stopped { .. }));
}
fn string(vm: &Vm, name: &str) -> String {
match vm.inspect(name) {
Some(Value::Str(s)) => s.to_string(),
other => panic!("{other:?}"),
}
}
fn key(s: &str) -> Ereignis {
Ereignis::Taste(s.into(), 0)
}
fn mouse() -> Ereignis {
Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
})
}
#[test]
fn review_signal_waehrend_blockierender_eingabe() {
struct Input {
reads: usize,
}
impl Host for Input {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
if !block {
return None;
}
self.reads += 1;
Some(if self.reads == 1 {
Ereignis::Signal(2)
} else {
Ereignis::Ende
})
}
fn jetzt_ms(&mut self) -> u64 {
0
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
self.next_event(true)
}
}
let mut results = vec![];
for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
let src =
format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND");
let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap());
let result = vm.run(&mut Input { reads: 0 });
println!(
"{statement}: {result:?}; Ausgabe={:?}",
tb_runtime::snapshot::text(&vm.rt.screen)
);
results.push(result);
}
assert!(results.iter().all(|result| *result == RunEvent::Ended));
}
#[test]
fn review_objektargument_darf_gotfocus_nicht_vorziehen() {
let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\nCALL Mark(Text1)\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("\t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "LlGg");
}
#[test]
fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() {
let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("t"));
vm.rt.pump(&mut host, false);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "KU");
}
#[test]
fn review_modale_eingabe_vor_eof_wird_zugestellt() {
let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB");
let mut host = CaptureHost::default();
host.ereignis_nach(2, mouse());
host.ereignis_nach(2, Ereignis::Ende);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "M");
}
#[test]
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
struct MenuHost {
time: u64,
waits: usize,
}
impl Host for MenuHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.time
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
self.waits += 1;
if self.waits == 1 {
return Some(key("\u{1b}"));
}
self.time = deadline.unwrap();
None
}
}
let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
vm.forms.handle_key("d", 4);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
let mut host = MenuHost { time: 0, waits: 0 };
assert_eq!(vm.run(&mut host), RunEvent::Ended);
println!(
"Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}",
host.time,
host.waits,
string(&vm, "s")
);
assert_eq!(host.waits, 1);
}
#[test]
fn review_maus_vor_getrappter_taste() {
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(mouse());
host.ereignis(key("t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "MK");
}

View File

@@ -0,0 +1,191 @@
# Verifikation: ereigniszustellung-und-hostgrenzen
Stand nach Behebung: 05.09.2026, aktueller Arbeitsbaum auf `57d6386`.
**Alle fünf Verifikationsbefunde sind behoben.** Die ursprünglichen sechs
Review-Proben bestehen unverändert in ihren Erwartungen; zusätzlich wurden
sie in die Workspace-Regressionen übernommen und um verwandte Pfade ergänzt.
| Dimension | Ergebnis |
| --- | --- |
| Vollständigkeit | 12/12 Tasks erledigt; ursprüngliche Zuordnung der 9 Delta-Requirements und 33 Szenarien um Korrektur-Nachweise ergänzt. |
| Korrektheit | 419 Workspace-Tests und alle sechs separaten Review-Proben bestanden; keine offenen V1V5-Befunde. |
| Kohärenz | Gemeinsame Zustellung auch während Eingabe und nach Menüschluss; synchrone LOAD-Wirkungen getrennt, klassische Quellpriorität erhalten. |
## Korrekturen und Regressionen
| Befund | Behebung | Nachweis in `crates/tb-vm/tests/events.rs` |
| --- | --- | --- |
| V1 | Fortsetzbarer Konsolenleser für INPUT, LINE INPUT, INPUT$ und RANDOMIZE ohne Argument. Frame hält Teiltext/Restlänge, Stack hält Operanden und Referenzen. Trap-Unwinding erhält gemeinsam genutzte Locals; Fehler und RETURN label verwerfen abgebrochene Eingaben. | Ursprüngliche Signalprobe; Teiltext/BYREF/Ausdruck, verschachtelte Eingaben mit STOP/CONT, Timer, Redo, lokaler Fehlerhandler, RANDOMIZE und RETURN label. |
| V2 | Implizite Objektaufrufe stellen ausschließlich notwendige LOAD-Handler zu. Deren Rückkehr zieht keine unabhängigen Forms-Ereignisse vor. | LlGg mit Control-Argument, Property-Lesen/-Schreiben, Refresh und LOAD; bestehende Load-vor-Show- und Unload-Tests. |
| V3 | Nach dem Schließen des Menüs wird die gemeinsame Auswahl vor jedem erneuten Warten wiederholt. | Esc bei Zeit 0, UEVENT zugestellt nach genau einem Warteaufruf; bestehende Menüsperrtests einschließlich Forms-Timer. |
| V4 | Modale und modellose Schleifen beenden bei EOF erst, wenn kein Handler mehr zugestellt wurde. | Ursprüngliche MouseDown/Unload-Probe; jeweils zwei Mausereignisse vor EOF ergeben MM in beiden Schleifen. |
| V5 | Getrappte Tasten am Queue-Anfang werden vor der klassischen Quellenauswahl gemeldet. | Gleichzeitiges KEY/UEVENT ergibt KU; Maus vor getrappter Taste bleibt MK. |
## Ursprüngliche Befunde — historischer Stand vor der Behebung
Die folgenden Ist-Beobachtungen und Ursachen dokumentieren den fehlgeschlagenen
Review vor diesen Korrekturen. Alle fünf sind durch die Nachweise oben geschlossen.
### V1 — Während blockierender Eingabe werden eintreffende Traps nicht zugestellt (hoch)
**Reproduktion:** `ON SIGNAL(2) GOSUB H`, `SIGNAL(2) ON`, danach INPUT.
Der Host liefert erst im blockierenden `next_event(true)` Signal(2), beim
nächsten Aufruf Eingabeende. Der Handler H würde `PRINT "H"` und END ausführen.
**Ist:** INPUT, LINE INPUT und INPUT$(1) enden jeweils mit Fehler 62, H läuft
nicht. Ohne nachfolgende Eingabe kann die Laufzeit stattdessen erneut
blockieren, obwohl der Signal-Trap schon ansteht.
**Soll:** Vor dem erneuten Blockieren muss der erlaubte Zustellpunkt genutzt
werden; H beendet das Programm, Ausgabe H, kein Fehler 62.
**Ursache:** Der einmalige VM-Check vor der Eingabeinstruktion genügt nicht.
[RtState::naechste_taste](../../../../crates/tb-runtime/src/builtins.rs#L209)
pumpt innerhalb seiner Schleife weitere Ereignisse und blockiert erneut,
ohne zur VM-Zustellung zurückzukehren. Dasselbe betrifft die drei genannten
Eingabefunktionen über ihren gemeinsamen Leser.
**Vertrag:** `ereignissteuerung` / Abschließende Liste der Zustellpunkte,
Signal-Trap auf Betriebssystemsignalen; Design D2; Task 2.1.
**Korrektur:** Blockierende Eingabe so fortsetzbar machen, dass nach einem
Nicht-Tasten-Ereignis und vor dem nächsten Host-Warten die gemeinsame
VM-Zustellung laufen kann. Eingabepuffer, Referenzen und Operanden erhalten.
### V2 — Control-Argumente stellen wartende Forms-Handler mitten in einer Anweisung zu (hoch)
**Reproduktion:** Fokuswechsel Text1 → Text2. Text1_LostFocus ruft als erste
wirksame Anweisung `CALL Mark(Text1)` auf; Mark hängt L an eine gemeinsame
Variable, LostFocus danach l. Text2_GotFocus hängt G und g an.
**Ist:** `GgLl`. **Soll:** `LlGg`.
**Ursache:** [Instr::PushObject](../../../../crates/tb-vm/src/interp.rs#L1344)
ruft nach dem Ablegen eines bereits geladenen Control-Objekts bedingungslos
`dispatch_next_form_event` auf. Dadurch läuft GotFocus noch vor dem eigentlichen
CALL, obwohl weder DOEVENTS noch modales Warten vorliegt. Der Schutz am ersten
Stmt-Eintritt und `forms_frei` werden damit umgangen.
**Vertrag:** Fokus, Tabreihenfolge und Access-Keys / Handlerwirkungen in
Fokusreihenfolge; Abschließende Liste der Zustellpunkte; Design D1/D2;
Tasks 1.2 und 2.1.
**Korrektur:** Unabhängig wartende Ereignisse aus Objekt-Opcodes heraus nicht
zustellen. Notwendige synchrone Load-/Unload-Wirkungen gezielt behandeln;
alle Aufrufer von `dispatch_next_form_event` gegen dieselben Zustellregeln prüfen.
### V3 — Menüschließen während SLEEP führt zu erneutem Warten trotz bereitem Trap (hoch)
**Reproduktion:** Menü offen, UEVENT anstehend, `SLEEP 60`. Der erste
Host-Warteaufruf liefert Esc bei unveränderter virtueller Zeit 0.
**Ist:** Esc schließt das Menü; die VM wartet ein zweites Mal bis Hostzeit
60.000 ms. Erst dann läuft der UEVENT-Handler.
**Soll:** Nach Esc läuft der freigegebene Handler ohne zweiten Warteaufruf
und beendet SLEEP sofort. Bei einer Wartephase ohne eigene Deadline droht
entsprechend Warten auf ein weiteres externes Ereignis.
**Ursache:** Die klassische Trap-Auswahl in
[Vm::zustellen](../../../../crates/tb-vm/src/interp.rs#L625) erfolgt vor der
Eingabeverarbeitung. Nachdem Esc die Menüsperre aufgehoben hat, kehrt die
Methode ohne erneute Auswahl zurück;
[SLEEP](../../../../crates/tb-vm/src/interp.rs#L707) ruft unmittelbar wieder
`warten` auf.
**Vertrag:** Menüsystem / Traps ruhen im geöffneten Menü und werden danach
fortgesetzt; Abschließende Liste der Zustellpunkte / SLEEP endet durch
Ereignis; Design D2/D3; Tasks 2.1/2.2.
**Korrektur:** Nach einer Eingabe, die die Zustellbarkeit verändert, bereite
Ereignisse vor jedem weiteren blockierenden Warten erneut auswählen.
### V4 — Modale Schleife verliert bereits angenommenes Ereignis unmittelbar vor EOF (hoch)
**Reproduktion:** `Form1.Show 1`; nach der zweiten Anzeige liefert der Host
MouseDown und unmittelbar danach Eingabeende. Der MouseDown-Handler hängt M
an und entlädt das Formular.
**Ist:** VM meldet Ended, die gemeinsame Variable bleibt leer; der Handler
wurde nicht ausgeführt. **Soll:** Das vor EOF eingetroffene Mausereignis wird
noch wirksam, Ergebnis M, Formular entladen.
**Ursache:** In der [modalen Run-Schleife](../../../../crates/tb-vm/src/interp.rs#L890)
setzt `zustellen` einen Handlerframe auf. Direkt danach wird ungeachtet von
`dispatched` wegen `rt.ende` zurückgekehrt. Das Ereignis ist bereits aus der
Queue entfernt und wird stillschweigend verloren.
**Vertrag:** Mausereignisse in Zellenkoordinaten / Reihenfolge bis zur
beobachtbaren Handlerausführung; Design D1/D2; Tasks 1.2/2.1.
**Korrektur:** EOF als Ende der Eingabequelle behandeln und bereits angenommene
Ereignisse/Handler geordnet abarbeiten, bevor die modale Ausführung endet.
## Ursprüngliche weitere Designabweichung
### V5 — Gleichzeitig wartende klassische Quellen erhalten eine andere Priorität (mittel)
**Reproduktion:** KEY(15) ON und UEVENT ON. Vor demselben Zustellpunkt liegen
eine passende Taste in der Eingabequeue und ein gemeldeter UEVENT vor.
Die Handler hängen K bzw. U an.
**Ist:** `UK`. **Soll:** `KU` gemäß der bestehenden klassischen Quellpriorität.
**Ursache:** [Vm::zustellen](../../../../crates/tb-vm/src/interp.rs#L631) wählt
bereits gemeldete Traps aus, bevor die wartende Taste durch `taste_trappen`
gemeldet wird. Die Prioritätsauswahl in Traps sieht die Taste deshalb noch
nicht. Früher erfolgte die Tastenprüfung vor der gemeinsamen Trap-Auswahl.
**Vertrag:** Design / Non-Goals: keine Änderung der festgelegten Priorität
gleichzeitig fälliger klassischer Quellen; D1/D2; Task 2.1.
**Korrektur:** Fälligkeitserfassung und Trap-Auswahl so koordinieren, dass die
bestehende Quellpriorität erhalten bleibt, ohne die Reihenfolge zwischen
Maus- und Tasteneingaben wieder nach Quellarten umzusortieren.
## Abdeckung und positive Nachweise
| Delta-Requirement | Nachweis / Einschränkung |
| --- | --- |
| Ereignisquellen mit Zeit vom Host | Host-Warteschnittstelle, eingefrorene virtuelle Zeit, klassischer Timer und Forms-Timer-SLEEP getestet. Keine reale Rückfalluhr in der VM; Zustellung nach Menüschluss durch V3-Regression abgesichert. |
| Mausereignisse in Zellenkoordinaten | Mapper-/Bounds-Tests, leere Verbraucher, KM/MK-Korpus und gemischte Eingaben. V4-Regressionen sichern EOF in modalen und modellosen Schleifen ab. |
| Abschließende Liste der Zustellpunkte | DOEVENTS-Wert, SLEEP, vorbereitete und erst beim Warten eintreffende INPUT-Ereignisse getestet; V1V3 geschlossen. |
| Getrappte Tasten verlassen den Eingabestrom | ON/OFF/STOP × INKEY$/INPUT/LINE INPUT/INPUT$, mehrfaches STOP-Merken und Dialogrestpuffer getestet. |
| Signal-Trap auf Betriebssystemsignalen | Beide Kennungen, Maskierung und ungültige Werte getestet. V1-Regression sichert Signalzustellung während blockierender Eingabe ab. |
| Fokus, Tabreihenfolge und Access-Keys | Tab/Shift-Tab, BackTab-Mapper, Access-Keys und einfacher LostFocus→GotFocus-Lauf belegt. V2-Regressionen sichern Control-Argumente und verwandte Objektaufrufe ab. |
| Menüsystem | Menüeigenschaften/-auswahl und Sperre an normalen Grenzen getestet. V3-Regression sichert Freigabe innerhalb von SLEEP ab. |
| Zellenpuffer ohne Terminalabhängigkeit | Separater Workspace kompiliert FRM/VM und liefert KM; 16 Pakete im normalen Dependency-Graph, keines davon crossterm/ratatui/signal-hook. CLI schaltet das Feature ausdrücklich ein. |
| Ereigniszustellung an Anweisungsgrenzen | Bestehende SUB-/GOSUB-/BYREF-/RESUME-Tests und Host ohne erlaubte Uhrabfragen grün. V2/V5-Regressionen sichern zulässige Handlerbeginn-Reihenfolge und klassische Priorität ab. |
Erneut ausgeführt:
- `cargo test --workspace`: **419 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
- `openspec validate ereigniszustellung-und-hostgrenzen --strict`: gültig.
- Isolierter Einbetter mit CaptureHost: **KM**, Dependency-Prüfung erfolgreich.
- Separate [Review-Proben](review-probes.rs): **6 bestanden, 0 fehlgeschlagen**.
Die Kontrollprobe prüft Maus unmittelbar vor getrappter Taste.
- Workspace-Ereignistests: **25 bestanden**, einschließlich Frist-Watchdog.
- Keine bestehenden Sollausgaben neu aufgezeichnet oder geändert.
Warnings-denied Clippy, Formatprüfungen und Compile-/VM-Benchmarks wurden
nach der Behebung erneut erfolgreich ausgeführt; Zahlen siehe
[verification.md](verification.md). Kein interaktiver Terminaltest und keine
Windows-/Linux-Matrix. Die Proben speisen Ereignisse
gezielt über Host ein und benötigen weder echte Signale noch reale Wartefristen.
## Review-Proben reproduzieren
Im Repository-Wurzelverzeichnis ausführen. Das erzeugt ausschließlich einen
separaten temporären Cargo-Treiber und referenziert den aktuellen Produktcode:
```sh
python3 - <<'PY'
from pathlib import Path
import json, subprocess, tempfile
root = Path.cwd()
p = Path(tempfile.mkdtemp(prefix='tb-review-'))
manifest = '[package]\nname="tb-review"\nversion="0.0.0"\nedition="2021"\n[workspace]\n'
manifest += '[[test]]\nname="befunde"\npath=' + json.dumps(str(root / 'openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/review-probes.rs')) + '\n[dependencies]\n'
for name in ['tb-vm', 'tb-runtime', 'tb-ui', 'tb-frontend']:
manifest += name + ' = { path = ' + json.dumps(str(root / 'crates' / name)) + ' }\n'
(p / 'Cargo.toml').write_text(manifest)
subprocess.run(['cargo', 'test', '--manifest-path', str(p / 'Cargo.toml'), '--test', 'befunde', '--', '--nocapture'], check=True, timeout=120)
PY
```
Alle sechs Proben müssen auf dem korrigierten Stand bestehen. Ihre Erwartungen
wurden auch in den regulären Workspace-Testlauf übernommen.

View File

@@ -0,0 +1,25 @@
## 1. Hosteingabe und Maskierung
- [x] 1.1 SIGNAL(2), KEY STOP und BackTab am gemeinsamen Eingang berichtigen; verifizieren durch Tests für beide Signale, alle drei KEY-Zustände mit INKEY$/INPUT und den Terminal-Key-Mapper.
- [x] 1.2 Geordnete Tasten-/Mausannahme implementieren; verifizieren mit K→M, M→K und gemischten Folgen einschließlich ungetrappter und getrappter Tasten.
## 2. Dispatch und Warten
- [x] 2.1 Handlerbeginn/Rückkehr und Menüsperre an allen Zustellpunkten vereinheitlichen; verifizieren mit LostFocus→GotFocus, gleichzeitig wartenden Handlern, UEVENT bei offenem Menü, DOEVENTS und den bestehenden Fehler-/Modalitätstests.
- [x] 2.2 Hostgesteuertes Warten und bedarfsgerechte Zeitabfrage implementieren; verifizieren mit einem Host, der unerwartete Uhrabfragen zählt/abweist, eingefrorener und vorrückender Zeit sowie einem Forms-Timer, der SLEEP beendet.
- [x] 2.3 Terminal-Feature in tb-ui abgrenzen und CLI ausdrücklich anschließen; verifizieren durch isolierten terminalfreien VM-Einbetter, Dependency-Graph ohne crossterm/ratatui/signal-hook und normalen CLI-Build.
## 3. Abnahme
- [x] 3.1 Gezielte geordnete Ereignisprogramme zum Korpus ergänzen; verifizieren mit cargo test --workspace und Vergleich der bestehenden Snapshots ohne automatisches Neuaufzeichnen.
- [x] 3.2 Zeit-/Zustellverträge und Backend-Feature dokumentieren; verifizieren mit Compile-/VM-Benchmarks und openspec validate ereigniszustellung-und-hostgrenzen --strict.
Abnahmenachweise: [verification.md](verification.md).
## 4. Verifikationsbefunde
- [x] 4.1 V1: Blockierende Konsoleneingabe fortsetzbar machen; Signale, Timer, Teiltexte und Handler-Rückkehr absichern.
- [x] 4.2 V2: Synchrone Load-Wirkungen von wartenden Forms-Ereignissen trennen; Control-Argumente und Geschwisterpfade prüfen.
- [x] 4.3 V3: Nach Menüschluss vor erneutem Warten bereit gewordene Ereignisse zustellen.
- [x] 4.4 V4: Vor EOF angenommene Ereignisse im modalen Lauf vollständig abarbeiten.
- [x] 4.5 V5: Klassische Quellpriorität bei gleichzeitig wartenden Tasten und UEVENT erhalten.

View File

@@ -0,0 +1,82 @@
# Implementierungsnachweis — 05.09.2026
Change: `ereigniszustellung-und-hostgrenzen`, Ausgangscommit `57d6386`.
Alle zwölf Implementierungs-/Abnahmetasks einschließlich der fünf
[Verifikationsbefunde](review.md) sind abgeschlossen. Die vier betroffenen
Hauptspezifikationen sind synchronisiert; der Change wurde am 05.09.2026 archiviert.
## Befunde und Umsetzung
| Befund / Tasks | Umsetzung | Ausführbarer Nachweis |
| --- | --- | --- |
| F04 / 1.1 | SIGNAL(1) und SIGNAL(2) werden im gemeinsamen RtState-Eingang gemeldet; nur ungetrapptes SIGINT wird Abbruch. Zusätzlich erkennt der Parser SIGNAL(n) ON/OFF/STOP. | VM-Test `beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge` einschließlich ungültiger konstanter/dynamischer Kennungen; Runtime-Tests für STOP/OFF beider Quellen und SIGINT ohne Trap. |
| F05 / 1.1 | ON verbraucht die Taste; STOP merkt den Trap einmal und belässt die Eingabe auch nach erneutem ON lesbar. Die Markierung bleibt beim Dialogaufruf erhalten. | VM-Test `key_on_off_stop_gelten_fuer_alle_konsoleneingaben`: drei Zustände × INKEY$, INPUT, LINE INPUT, INPUT$. Runtime-Test mit drei STOP-Tasten; VM-Regression für verbleibende Dialogeingaben. |
| F06 / 1.2, 2.1 | Eine Eingabequeue für Tasten/Maus bis zur Annahme. Forms-Eingaben und ihre Handler werden geordnet verarbeitet; erster Handler-Statement-Eintritt geschützt, gleiche Forms-Prozedur nicht rekursiv zugestellt. | VM-Tests für aM, Ma, aMbTM einschließlich KEY-Trap; Fokusfolge LlGg; explizites DOEVENTS ergibt arbr; UNLOAD im eigenen Handler ergibt ur. |
| F07 / 2.1 | Gemeinsame Zustellung; separate Menüsperre in Traps ohne Änderung der ON/OFF/STOP-Zustände. Bereits wartende Forms-Timer bleiben gesperrt, Menübedienung kann fortfahren. | VM-Menütest über gewöhnliche Zuweisung, DOEVENTS, SLEEP und LINE INPUT: Zähler bleibt 1, nach Esc werden UEVENT und Forms-Timer nachgeholt, Ergebnis 111. |
| F08 / 2.2 | Host besitzt Zeit und Deadline-Warten; keine Instant-/Sleep-/Yield-Rückfallebene in der VM. Auch Forms-SLEEP, modales Warten und sichtbare Forms nach Programmende nutzen den Host. Zeitabfragen nur bei tatsächlichem Zeitbedarf. | Host, der jede unerwartete Uhrabfrage/Warteoperation ablehnt; eingefrorene Uhr über reale 10 ms mit zwei identischen Hostdeadlines; Forms-Timer beendet SLEEP 60 bei Hostzeit 100 ms. Bestehender klassischer SLEEP-/TIMER-Test. |
| F18 / 1.1 | BackTab wird als TAB mit SHIFT normalisiert, auch wenn crossterm kein separates SHIFT-Flag liefert. | Terminal-Mapper-Test `backtab_ist_tab_mit_shift_auch_ohne_modifier`; bestehende Forms-Tests für Rückwärtstab, übersprungene Controls und Access-Keys. |
| F20 / 2.3 | tb-ui ist standardmäßig terminalfrei; `terminal` aktiviert Backendmodule und optionale Dependencies. CLI aktiviert dieses Feature ausdrücklich. | Separater Workspace `tests/terminalfrei`: FRM laden/kompilieren, Forms ausführen, CaptureHost-Ausgabe KM prüfen. Normaler Dependency-Graph: 16 Pakete, keines davon crossterm, ratatui oder signal-hook. CLI-Kompilation und CLI-Prozesstests erfolgreich. |
| 3.1 | Zwei kleine geordnete Ereignisprogramme mit ausdrücklich festgelegten Ausgaben ergänzt. | `eingabe-taste-maus.frm` → KM; `eingabe-maus-taste.frm` → MK. Beide laufen zweimal im bestehenden Korpusharness. Keine bestehende .out-Datei verändert. |
| 3.2 | Zeit-, Zustell-, Eingabequeue- und Backend-Verträge einschließlich Einbetter-Migration dokumentiert. | [VM-Design](../../../../docs/tbvm-design.md#ausführungsmodell--unterbrechbarkeit), Checks und Benchmarks unten. |
Die neuen VM-Regressionen liegen in
[crates/tb-vm/tests/events.rs](../../../../crates/tb-vm/tests/events.rs).
Bestehende Trap-Rückkehr-, GOSUB-, Fehler-/RESUME-, BYREF- und Modalitätstests
wurden im vollständigen Workspace-Testlauf mit ausgeführt.
## Behebung der Verifikationsbefunde
V1V5 sind geschlossen. Der [aktualisierte Report](review.md) ordnet jede
Korrektur den ursprünglichen Beobachtungen und ihren Regressionen zu.
Die 25 Tests in `events.rs` decken nun auch fortsetzbare Eingabe samt Teiltext,
Ausdrucksoperanden, BYREF, verschachtelten Handlern, STOP/CONT, Timer,
Fehler-Unwinding und RETURN label ab. Objektaufrufe bewahren die Fokusfolge,
Menüschluss gibt Traps sofort frei, beide Formularschleifen arbeiten Eingaben
vor EOF ab und die klassische KEY-/UEVENT-Priorität bleibt erhalten.
## Ausgeführte Prüfungen
- `cargo test --workspace`: **419 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
Ignoriert bleibt ausschließlich `erzeuge_sollausgaben`; keine automatische
Neuaufzeichnung von Golden Files.
- Separater Treiber der ursprünglichen `review-probes.rs`: **6 bestanden, 0 fehlgeschlagen**.
- `cargo clippy -p tb-runtime -p tb-ui -p tb-vm -p tb-cli --all-targets --all-features -- -D warnings`: erfolgreich, ohne Warnungen.
- `cargo fmt --all -- --check` und Formatprüfung des separaten Einbetters: erfolgreich.
- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: erfolgreich,
`Terminalfreier FRM-/VM-Lauf: KM`.
- `cargo tree --manifest-path tests/terminalfrei/Cargo.toml --offline --edges normal --prefix none --format '{p}'`:
mit expliziter Prüfung auf ausgeschlossene Terminalpakete, erfolgreich.
- `openspec validate ereigniszustellung-und-hostgrenzen --strict`: erfolgreich.
- `git diff --check`: erfolgreich.
## Release-Benchmarks
`cargo bench -p tb-vm --bench compile --bench vm`, auf diesem Rechner:
| Last | Ergebnis |
| --- | --- |
| Compile: 508 Zeilen | 0,83 ms, Budget 50 ms eingehalten |
| Compile: 49.760 Zeilen in 20 Modulen | 58 ms, Budget 1.000 ms eingehalten |
| INTEGER: 10 Mio. Iterationen | 1.048 ms, 9,5 Mio. Iterationen/s |
| DOUBLE: 5 Mio. Iterationen | 504 ms, 9,9 Mio. Iterationen/s |
| SUB/BYREF: 1 Mio. Aufrufe | 123 ms, 8,2 Mio. Aufrufe/s |
| String-Funktionen: 200.000 Runden | 99 ms, 2,0 Mio. Runden/s |
Der Compile-Projektbenchmark übersetzt 20 Module einzeln; er prüft keinen
Mehrmodul-Linker. Die VM-Zahlen sind lokale Durchsatzmessungen, keine
zugesicherte Performance auf anderer Hardware.
## Synchronisation und Archivierung
Neun bestehende Requirements in vier Hauptspezifikationen wurden um elf
Szenarien ergänzt; alle bisherigen Szenarien bleiben erhalten. Vollständiger
Abgleich aller Delta-Requirements erfolgreich; `openspec validate --specs
--strict`: 15 bestanden, 0 fehlgeschlagen.
## Grenzen
Terminaleingaben wurden über den Mapper und vorbereitete Hostereignisse
geprüft. Kein interaktiver Terminal-/Fremdprogrammvergleich und keine
Windows-/Linux-Ausführung. Der neue Vertrag verlangt von Einbettern eine
Implementierung von Host::warten; CaptureHost modelliert eine endliche
Ereignisquelle und explizites virtuelles Fortschreiten bis zur Deadline.

View File

@@ -1,15 +0,0 @@
## 1. Hosteingabe und Maskierung
- [ ] 1.1 SIGNAL(2), KEY STOP und BackTab am gemeinsamen Eingang berichtigen; verifizieren durch Tests für beide Signale, alle drei KEY-Zustände mit INKEY$/INPUT und den Terminal-Key-Mapper.
- [ ] 1.2 Geordnete Tasten-/Mausannahme implementieren; verifizieren mit K→M, M→K und gemischten Folgen einschließlich ungetrappter und getrappter Tasten.
## 2. Dispatch und Warten
- [ ] 2.1 Handlerbeginn/Rückkehr und Menüsperre an allen Zustellpunkten vereinheitlichen; verifizieren mit LostFocus→GotFocus, gleichzeitig wartenden Handlern, UEVENT bei offenem Menü, DOEVENTS und den bestehenden Fehler-/Modalitätstests.
- [ ] 2.2 Hostgesteuertes Warten und bedarfsgerechte Zeitabfrage implementieren; verifizieren mit einem Host, der unerwartete Uhrabfragen zählt/abweist, eingefrorener und vorrückender Zeit sowie einem Forms-Timer, der SLEEP beendet.
- [ ] 2.3 Terminal-Feature in tb-ui abgrenzen und CLI ausdrücklich anschließen; verifizieren durch isolierten terminalfreien VM-Einbetter, Dependency-Graph ohne crossterm/ratatui/signal-hook und normalen CLI-Build.
## 3. Abnahme
- [ ] 3.1 Gezielte geordnete Ereignisprogramme zum Korpus ergänzen; verifizieren mit cargo test --workspace und Vergleich der bestehenden Snapshots ohne automatisches Neuaufzeichnen.
- [ ] 3.2 Zeit-/Zustellverträge und Backend-Feature dokumentieren; verifizieren mit Compile-/VM-Benchmarks und openspec validate ereigniszustellung-und-hostgrenzen --strict.

View File

@@ -21,6 +21,8 @@ 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.
Auch Wartefristen der VM SHALL ausschließlich von der Hostzeit abhängen. Ein Host mit stehender virtueller Zeit MUST NOT durch reale verstrichene Zeit eine Frist scheinbar erreichen.
#### 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
@@ -29,6 +31,10 @@ reproduzierbarem Ergebnis prüfbar sind.
- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird
- **THEN** ist die Ausgabe beide Male identisch
#### Scenario: Stehende virtuelle Zeit
- **WHEN** ein Host während SLEEP seine Zeit unverändert hält
- **THEN** endet SLEEP nicht wegen realer Zeit; eine vom Host gemeldete Frist oder ein zugestelltes Ereignis beendet das Warten
### Requirement: Mausereignisse in Zellenkoordinaten
Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an
die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit
@@ -40,6 +46,8 @@ 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.
Die Reihenfolge SHALL bis zur beobachtbaren Ausführung der zugehörigen Handler erhalten bleiben; getrennte Quellarten MUST NOT nachträglich nach ihrer Art umgeordnet werden.
#### 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
@@ -56,6 +64,14 @@ werden, damit die Warteschlange nicht unbegrenzt wächst.
- **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
#### Scenario: Taste vor Maus bis zum Handler
- **WHEN** eine Taste vor einem Mausklick eintrifft und beide Handler eine Kennung anhängen
- **THEN** steht die Tastenkennung vor der Mauskennung
#### Scenario: Maus vor Taste bis zum Handler
- **WHEN** ein Mausklick vor einer Taste eintrifft
- **THEN** wird zuerst der Maushandler wirksam
### 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
@@ -66,6 +82,8 @@ Trap-Handler beginnen; insbesondere MUST NOT eine Anweisung in ihrer
Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
`SLEEP` beenden.
Forms-Ereignisse SHALL auch während SLEEP an dessen bestehenden Zustellpunkten verarbeitet werden. Eine für das Menü geltende Zustellsperre SHALL an jedem Zustellpunkt gleichermaßen gelten.
#### 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
@@ -78,6 +96,10 @@ Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird
- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert
#### Scenario: Forms-Timer beendet SLEEP
- **WHEN** während SLEEP ein aktiver Forms-Timer fällig wird
- **THEN** läuft dessen Handler und SLEEP kehrt danach zurück
### 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
@@ -146,6 +168,8 @@ 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.
Unter STOP SHALL die Taste normal lesbar bleiben und zugleich höchstens ein Ereignis für das spätere ON gemerkt werden.
#### 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
@@ -154,6 +178,10 @@ Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird
- **THEN** liefert `INKEY$` die Sondertastenfolge für F1
#### Scenario: STOP merkt und erhält Eingabe
- **WHEN** unter KEY(1) STOP F1 gedrückt, per INKEY$ gelesen und danach KEY(1) ON ausgeführt wird
- **THEN** liefert INKEY$ die Sondertastenfolge und der Handler läuft nach ON genau einmal
### Requirement: Benutzerdefiniertes Ereignis
`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über
`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für
@@ -208,3 +236,7 @@ ausgewiesen sein.
#### Scenario: Signal ohne Trap bleibt Abbruch
- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist
- **THEN** bleibt es beim bisherigen Abbruchverhalten
#### Scenario: SIGTERM wird zugestellt
- **WHEN** SIGNAL(2) ON gilt und ein SIGTERM über den Host eintrifft
- **THEN** wird das Ziel von ON SIGNAL(2) GOSUB am nächsten erlaubten Zustellpunkt ausgeführt

View File

@@ -36,6 +36,8 @@ auslöst oder ihm den Fokus gibt. Enter SHALL die `Default`-Schaltfläche
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
Die über das Terminal gelieferte Rückwärtstab-Taste SHALL denselben Fokuswechsel wie Tab mit Umschalt auslösen. Die Reihenfolge LostFocus vor GotFocus SHALL auch an den Wirkungen der BASIC-Handler sichtbar bleiben.
#### Scenario: Tab überspringt
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
- **THEN** erhält das dritte Element den Fokus
@@ -48,6 +50,14 @@ am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
#### Scenario: Handlerwirkungen in Fokusreihenfolge
- **WHEN** LostFocus den Text L und GotFocus den Text G an dieselbe Variable anhängen
- **THEN** lautet das Ergebnis LG
#### Scenario: Rückwärtstab vom Terminal
- **WHEN** das Terminal eine Rückwärtstab-Taste liefert
- **THEN** wechselt der Fokus zum vorherigen zulässigen TabIndex
### Requirement: Maussteuerung mit Trefferprüfung
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
@@ -116,6 +126,10 @@ fortgesetzt werden.
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
#### Scenario: UEVENT an gewöhnlicher Anweisungsgrenze
- **WHEN** ein Menü offen ist und UEVENT vor einer Zuweisung ansteht
- **THEN** läuft dessen Handler erst nach dem Schließen des Menüs
### Requirement: Vordefinierte Dialoge
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als

View File

@@ -98,10 +98,16 @@ Darstellungsschicht liegen. Damit MUST jedes Bildschirmverhalten in
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
MUST NOT von einer Terminal-Bibliothek abhängen.
Ein separater Einbetter SHALL die VM einschließlich Forms ohne Terminalbackend bauen und ausführen können. Der CLI-Runner SHALL das Backend ausdrücklich zuschalten.
#### Scenario: Bildschirmverhalten im Test ohne Terminal
- **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt
- **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal
#### Scenario: Terminalfreier Einbetter
- **WHEN** ein separates Programm ausschließlich die VM mit einem Capture-Host einbindet
- **THEN** enthält sein aufgelöster Abhängigkeitsbaum keine Terminalbibliothek und die Forms-Tests können darin laufen
### Requirement: Bildschirmanweisungen des Dialekts
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich

View File

@@ -124,6 +124,8 @@ ausführen. Der unterbrochene Zustand — Aufrufkette, Schleifen, Locals,
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.
Ohne klassische Traps, aktive Forms-Timer oder zeitabhängige Mausauswertung SHALL auch die Forms-Integration keine Hostzeit abfragen.
#### 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
@@ -132,6 +134,10 @@ MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
- **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
#### Scenario: Einfaches Programm liest keine Ereigniszeit
- **WHEN** ein Programm nur Zuweisungen und END ausführt und ein Testhost Zeitabfragen zählt
- **THEN** ist der Zähler am Ende 0
### 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