Files
TerminalBasic/openspec/changes/archive/2026-09-04-phase-4-ereignisschleife/design.md

229 lines
11 KiB
Markdown

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