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:
- Builtins sehen die VM nicht. Ihre Signatur ist
(&mut RtState, &mut dyn Host, &mut [Value]).DOEVENTSist heute ein Builtin und kann deshalb prinzipiell keine Prozedur des laufenden Programms ausführen. 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.
crosstermzä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.rsmisst vorher und nachher; die Zusicherung aus der Spec ist ein Messwert, keine Behauptung. Die zusätzliche Grenze jeFOR-Durchlauf (D10) geht in dieselbe Messung ein. SIGNAL(2)ist unter Windows wirkungslos — es gibt dort keinkill→ als Abweichung dokumentiert, statt ein Ersatzereignis zu erfinden, das es im Vorbild nicht gab.SIGINTkollidiert mit dem Abbruch → im Rohmodus liefert das Terminal Strg+C als Taste, nicht als Signal; ein von außen gesendetesSIGINTgeht an den Trap, wenn einer aktiv ist, sonst bleibt es beim Abbruch. Dieselbe Regel wie bei getrappten Tasten.Host-Trait ist BREAKING → betrifftTerminalHost,CaptureHostund den Test-Host intb-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
(
MouseMovekommt sehr häufig), zeigt sich erst mit dem ersten Verbraucher; das Ereignismodell bleibt davon unberührt.