Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user