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

11 KiB

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-Kostenbenches/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.