# Design — Ereignisschleife und klassische Ereignis-Traps ## Context Siehe proposal.md — Why. Technisch maßgeblich ist der Bestand aus Phase 3: ``` crossterm TerminalHost RtState Vm --------- ------------ ------- -- Key --> next_event() --> Ereignis::Taste --> tasten[] <-- INKEY$ Resize --> next_event() --> Ereignis::Groesse -> screen.resize() Ctrl+C --> next_event() --> Ereignis::Abbruch -> abbruch Maus --> x verworfen (`Ok(_) => {}` im TerminalHost) <-- fehlt Zeit --> x keine Quelle <-- fehlt Zustellpunkte heute: Instr::Stmt (screen dirty oder alle 1024 Ticks), vor blockierender Eingabe. ``` Zwei Randbedingungen schneiden den Entwurf zu: 1. **Builtins sehen die VM nicht.** Ihre Signatur ist `(&mut RtState, &mut dyn Host, &mut [Value])`. `DOEVENTS` ist heute ein Builtin und kann deshalb prinzipiell keine Prozedur des laufenden Programms ausführen. 2. **`Vm::run()` ist nicht wiedereintrittsfähig.** Es gibt keinen Einstiegspunkt „führe Ziel X aus und komm zurück". ## Goals / Non-Goals **Goals:** - Eine Zustellung, die den unterbrochenen Zustand nicht verliert und ohne aktiven Trap nichts kostet. - Zeitgesteuerte Programme im Korpus ohne echte Wartezeit und ohne Flackern zwischen zwei Läufen. - Ein Ereignismodell, das die Forms-Ereignisse des nächsten Changes trägt, ohne dann umgebaut zu werden. **Non-Goals:** - Nebenläufigkeit. Es gibt weiterhin genau einen Ausführungsstrang; die Zeitquelle ist kein Thread, sondern eine Abfrage am Zustellpunkt. - Priorisierbare oder verschachtelte Handler. ## Decisions ### D1 — Zustellung ist ein Vorgang der VM, kein Builtin Die Prüfung „ist etwas fällig?" sitzt in der bestehenden Tick-Prüfung an `Instr::Stmt`. Ist etwas fällig, springt die VM das Trap-Ziel an, als hätte das Programm dort ein `GOSUB` ausgeführt. `DOEVENTS` und `SLEEP` werden zu Anweisungen, die denselben Weg auslösen, statt zu Builtins, die ihn nicht auslösen können. | Alternative | Warum nicht | |---|---| | `&mut Vm` an alle Builtins durchreichen | Signaturänderung an ~150 Builtins; echte Rekursion in `run()` mit unbegrenzter Stapeltiefe | | Trampolin: Builtin liefert „bitte Ziel X rufen" zurück | Trägt die klassischen Traps, aber modales `SHOW` des nächsten Changes wird zur Zustandsmaschine — der Umbau käme dann doch | ### D2 — Der Handler läuft in einem eigenen Frame, es wird nicht abgewickelt `ON ERROR` wickelt die Frame-Kette bis zum Handler-Frame ab. Für einen Trap ist das falsch: das Programm muss danach weiterlaufen. Die Zustellung **legt** deshalb einen Frame auf den Modulrumpf **auf**, mit einer Rückmarke auf die unterbrochene Stelle; `RETURN` nimmt ihn wieder herunter. ``` vor der Zustellung waehrend des Handlers nach RETURN ------------------ --------------------- ----------- [ SUB Rechne ] <- top [ Trap-Frame @Marke ] [ SUB Rechne ] [ Modulrumpf ] [ SUB Rechne ] [ Modulrumpf ] [ Modulrumpf ] ``` Der Trap-Frame teilt die Modulvariablen (sie sind Globals), hat aber einen eigenen `GOSUB`-Stapel — damit kann ein Handler selbst `GOSUB` verwenden, ohne den unterbrochenen Stapel zu berühren. ### D3 — Die Zeit kommt vom Host Der `Host`-Trait bekommt eine monotone Zeitabfrage (**BREAKING**). Die Laufzeit rechnet daraus die Fälligkeit der Zeit-Traps; der Host weiß nichts von Traps. Grund ist die Prüfbarkeit: der `CaptureHost` stellt die Zeit selbst und ein Korpusprogramm mit `ON TIMER(5)` läuft in Mikrosekunden und immer gleich. Ein direkter Uhrzugriff in der Laufzeit hätte beides verhindert und wäre zudem die zweite Uhr neben der von `TIMER`/`TIME$` — die bleibt, wo sie ist, denn sie liefert Ortszeit, keine monotone Dauer. ### D4 — Maskierung als Tabelle, Prüfung über das vorhandene Flagwort Je Quelle ein Zustand (`ON`/`OFF`/`STOP`) und höchstens ein anstehendes Ereignis. Belegt aus der Original-Hilfe (`umfang-und-form.md`): der Eintritt in den Handler führt selbsttätig `STOP` aus, das `RETURN` selbsttätig `ON` — außer im Handler stand ein ausdrückliches `OFF`; und `OFF` verwirft auch ein unter `STOP` gemerktes Ereignis. `EVENT` ist keine dritte Zustandsebene, sondern eine Klammer mit nur `ON` und `OFF`. Die Tick-Prüfung bekommt ein weiteres Bit im vorhandenen Flagwort neben `F_STEP`/`F_BREAK`/`F_POLL`. Ohne definierten Trap ist das Bit aus, und die heiße Schleife prüft weiterhin genau ein Wort — das trägt die Zusicherung „ohne aktiven Trap kein messbarer Aufwand" aus der Spec. ### D5 — Getrappte Tasten werden erst am Zustellpunkt aussortiert Die Tastenschlange bleibt, wie sie ist. Am Zustellpunkt vergleicht die Laufzeit den Kopf der Schlange mit den aktiven `KEY`-Traps und nimmt die Taste nur dann heraus. Damit wirkt ein `KEY(1) OFF` sofort und auch auf Tasten, die schon in der Schlange liegen — Filtern beim Einsortieren hätte den Zustand zum Zeitpunkt des Tastendrucks eingefroren. ### D6 — Virtuelle Zeit im Korpus über eine Kopfzeilen-Direktive Der Harness kennt bereits `' tb-screen:` und `' tb-tz:`. Dazu kommt `' tb-clock: ms/Zustellpunkt`: die virtuelle Uhr des `CaptureHost` rückt an jedem Zustellpunkt um `n` Millisekunden vor. Ein Programm mit `ON TIMER(1)` und `' tb-clock: 100ms/Zustellpunkt` feuert damit reproduzierbar nach zehn Zustellpunkten. Fehlt die Direktive und nutzt das Programm Zeit-Traps, bricht der Harness ab, statt auf die Systemuhr auszuweichen. ### D7 — Maus: Quelle jetzt, Auswertung mit den Steuerelementen Die Maus kommt als Quelle in diesem Change herein, weil die Eingangsaufgabe der Phase die Terminal-Events in die Warteschlange führt. Zwei Festlegungen fallen dabei an: - **Koordinaten in Zellen, 1-basiert.** `crossterm` zählt ab 0, der Bildschirm des Dialekts ab 1 (`LOCATE`, `CSRLIN`, `SCREEN(z, s)`). Umgerechnet wird an der Stelle, an der die Ereignisse entstehen — in der Darstellungsschicht —, damit es in der Laufzeit nur eine Zählweise gibt. Die Forms-Referenz nennt Mauskoordinaten SINGLE, aber in Zellen; die Umrechnung in Formularkoordinaten macht später das Objektmodell. - **Ohne Verbraucher verworfen.** Bis die Steuerelemente da sind, nimmt niemand ein Mausereignis an. Es am Zustellpunkt zu verwerfen ist die einzige Variante, die keine unbegrenzt wachsende Warteschlange hinterlässt; Bewegungsereignisse kommen bei gedrückter Taste in hoher Frequenz. ### D8 — `SIGNAL` liegt auf Betriebssystemsignalen Entscheidung des Projektinhabers vom 2026-09-04, Beleg und Begründung in `umfang-und-form.md`. Abgebildet wird nur, was auf **allen** Zielplattformen besteht: | `n%` | Signal | Bemerkung | |---|---|---| | 1 | `SIGINT` | überall zustellbar | | 2 | `SIGTERM` | unter Windows von außen nicht zustellbar (Abweichung) | Der Signalhandler tut nichts weiter, als eine Marke zu setzen; der Host liest sie beim nächsten Abholen und macht ein Ereignis daraus. Damit läuft kein Programmcode im Signalkontext, und die Zustellung folgt denselben Punkten wie jede andere Quelle. Liegen beide Marken an, hat `SIGINT` Vorrang. Ein Codepfad für alle drei Plattformen: `signal-hook` gilt sein `flag`-Modul nicht nach Plattform ab und bildet unter Windows auf das `signal` der C-Laufzeit ab. Die Abhängigkeit liegt ohnehin im Baum — `crossterm` nutzt sie unter Unix für `SIGWINCH` —, sie wird für `tb-ui` nur ausdrücklich deklariert, nicht neu hereingeholt. Damit entfällt der sonst nötige zweite Pfad über den Konsolen-Steuerhandler von Windows. ### D9 — Umfang und Form aus den Einzelseiten der Original-Hilfe Befund aus `phase-3-isam`: die Themenliste klärt den *Umfang*, die Einzelseiten klären die *Form*. Für diesen Change heißt das: die Wertebereiche von `ON KEY(n)`, die Reihenfolge gleichzeitig fälliger Ereignisse und die Frage, ob Trapping in Prozeduren ruht, werden vor der Implementierung aus den Einzelseiten belegt (erste Aufgabe in tasks.md), nicht aus der Erinnerung nachgebaut. Ergibt die Hilfe nichts, gilt eine dokumentierte Festlegung, die in der Sprachreferenz als Abweichung steht. ### D10 — Schleifen bekommen eine Anweisungsgrenze im Rücksprung Befund bei der Umsetzung von D1: der Codegenerator übersetzte eine Schleife **ohne Rumpf** in reine Sprünge — in `FOR i = 1 TO 10000: NEXT` gab es keine Anweisungsgrenze und damit keinen Zustellpunkt. Das trifft alle vier Schleifenformen und betraf auch Breakpoints und den Abbruch, war also schon vor diesem Change eine Lücke. Der Rücksprung führt jetzt in jedem Fall über eine Grenze. Bei `DO`/`WHILE` zeigt er auf die ohnehin vorhandene Grenze des Schleifenkopfs — das kostet keine Instruktion. Nur `FOR` braucht eine eigene, weil vor dem Schleifentest die Initialisierung steht. ## Risks / Trade-offs - **Der Trap-Frame sieht einen halbfertigen Zustand** → Zustellung nur an Anweisungsgrenzen; die Liste der Zustellpunkte ist in der Spec abschließend und wird nicht „für den Komfort" erweitert. - **Ereignisse feuern schneller, als der Handler zurückkehrt** → höchstens ein anstehendes Ereignis je Quelle, und die eigene Quelle ist während des Handlers gestoppt. Ein Zeit-Trap kann damit Fristen überspringen; das entspricht dem Vorbild. - **Tick-Kosten** → `benches/vm.rs` misst vorher und nachher; die Zusicherung aus der Spec ist ein Messwert, keine Behauptung. Die zusätzliche Grenze je `FOR`-Durchlauf (D10) geht in dieselbe Messung ein. - **`SIGNAL(2)` ist unter Windows wirkungslos** — es gibt dort kein `kill` → als Abweichung dokumentiert, statt ein Ersatzereignis zu erfinden, das es im Vorbild nicht gab. - **`SIGINT` kollidiert mit dem Abbruch** → im Rohmodus liefert das Terminal Strg+C als Taste, nicht als Signal; ein von außen gesendetes `SIGINT` geht an den Trap, wenn einer aktiv ist, sonst bleibt es beim Abbruch. Dieselbe Regel wie bei getrappten Tasten. - **`Host`-Trait ist BREAKING** → betrifft `TerminalHost`, `CaptureHost` und den Test-Host in `tb-cli`; alle drei liegen im Repo, kein externer Verbraucher. - **Mausereignisse ohne Verbraucher könnten stumm falsch sein** — ein Fehler im Koordinatenmodell fiele erst im nächsten Change auf → Unit-Tests der Umrechnung an den Rändern (Zelle 1,1 und die letzte Zelle) und nach einer Größenänderung, nicht erst über Steuerelemente. ## Open Questions - Ruht das Trapping in Prozeduren eines *anderen* Moduls? Mehrere Module gibt es erst mit der Projektverwaltung in Phase 5; die Antwort ändert hier weder Spec noch Aufgaben. - Ob die Zeitquelle später auch Forms-`Timer`-Controls speist oder diese eine eigene Fälligkeitsliste bekommen, entscheidet der Change zum Objektmodell. - Ob Mausbewegungen ohne gedrückte Taste zusammengefasst werden müssen (`MouseMove` kommt sehr häufig), zeigt sich erst mit dem ersten Verbraucher; das Ereignismodell bleibt davon unberührt.