229 lines
11 KiB
Markdown
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.
|