654 lines
35 KiB
Markdown
654 lines
35 KiB
Markdown
# TBVM — Designentwurf
|
||
|
||
Eigene Stack-basierte Bytecode-VM (Entscheidung siehe PLAN.md, 0.2).
|
||
Die VM ist in `tb` (IDE) und in von `tbc` erzeugte Executables
|
||
eingebettet — identischer Code, identisches Verhalten, schneller Turnaround.
|
||
|
||
## Werte-Modell
|
||
|
||
Tagged Enum, kein NaN-Boxing (Einfachheit und Debugbarkeit vor Mikro-
|
||
performance):
|
||
|
||
```rust
|
||
enum Value {
|
||
Int(i16), // INTEGER
|
||
Long(i32), // LONG
|
||
Single(f32), // SINGLE
|
||
Double(f64), // DOUBLE
|
||
Currency(i64), // CURRENCY, Festkomma ×10 000
|
||
Str(Rc<str>), // STRING (immutabel geteilt; Copy-on-Write bei MID$-Anweisung)
|
||
// Arrays und TYPE-Instanzen leben im Heap-Bereich der VM,
|
||
// Variablen-Slots referenzieren sie per Handle (Rc<RefCell<…>>).
|
||
}
|
||
```
|
||
|
||
- Kein GC nötig: der Dialekt kennt keine Zyklen (keine Objektreferenzen in
|
||
TYPEs, keine Closures) → `Rc` genügt.
|
||
- Feste Strings (`STRING * n`) und TYPE-Records werden als eigene
|
||
Speicherobjekte mit fester Zeichen-/Elementstruktur geführt.
|
||
|
||
## Bytecode
|
||
|
||
- Stack-Maschine, Opcodes 1 Byte + Operanden variabler Länge (u8/u16/u32,
|
||
little-endian).
|
||
- **Modul-Struktur:** Konstantenpool (Strings, Zahlen), Typtabelle
|
||
(TYPE-Layouts), Prozedurtabelle (Signatur, Locals-Anzahl, Code-Offset),
|
||
globale Slots, DATA-Segment (für READ/RESTORE), Zeilentabelle.
|
||
- **Zeilentabelle:** Abbildung Code-Offset → (Moduldatei, Zeile). Grundlage
|
||
für `ERL`, Fehlermeldungen, Breakpoints und Einzelschritt.
|
||
- **Aufrufkonventionen:**
|
||
- SUB/FUNCTION: eigener Frame (Locals-Slots, Operandenstack-Basis);
|
||
BYREF-Parameter als Referenz-Slots (Handle auf Variablen-Slot).
|
||
- GOSUB: **kein** Frame — Rücksprungadresse auf separatem GOSUB-Stack im
|
||
aktuellen Frame (RETURN prüft diesen zuerst).
|
||
- **Fehlerbehandlung:** Zwei Ebenen (Scoping geklärt 2026-09-04).
|
||
`ON LOCAL ERROR GOTO x` setzt einen Handler im aktuellen Frame; sein
|
||
Sprungziel muss im selben Rumpf liegen. `ON ERROR GOTO x` **ohne**
|
||
`LOCAL` setzt den modulweiten Handler — auch aus einer Prozedur heraus,
|
||
und sein Sprungziel liegt dann im Modulrumpf (der Codegenerator löst es
|
||
über Prozedur 0 auf, die zuerst übersetzt wird).
|
||
Ein Laufzeitfehler sucht zuerst lokale Handler die Aufrufkette hinauf,
|
||
dann den modulweiten; beim modulweiten wird bis Frame 0 abgewickelt,
|
||
weshalb `RESUME NEXT` dort hinter dem Prozeduraufruf fortsetzt. Der
|
||
Handler setzt `ERR`/`ERL`; `RESUME` nutzt den gemerkten
|
||
Anweisungs-Offset. Ein Fehler im Handler ist fatal (keine Kaskaden).
|
||
|
||
## Ausführungsmodell / Unterbrechbarkeit
|
||
|
||
Die VM läuft auf dem aufrufenden Thread. `Vm::run` liefert bei STOP,
|
||
Breakpoint, Einzelschritt, Programmende oder Fehler ein `RunEvent` an den
|
||
Runner zurück. `Stmt` markiert Anweisungsgrenzen; es gibt keine Prüfung
|
||
nach jedem Opcode.
|
||
|
||
Die gemeinsame Zustellentscheidung gilt an Anweisungsgrenzen, bei
|
||
`DOEVENTS`, während `SLEEP` und während blockierender Konsoleneingabe. Eine
|
||
zugestellte Ereignisprozedur beginnt ihre erste Anweisung, bevor ein
|
||
weiterer Handler sie unterbrechen darf. Forms-Handler laufen normalerweise
|
||
bis zur Rückkehr; explizites DOEVENTS und blockierendes Warten können andere
|
||
Handler verschachteln, aber nie dieselbe laufende Forms-Ereignisprozedur.
|
||
Die Rückkehr aus dem äußersten normalen Forms-Handler arbeitet wartende
|
||
Forms-Ereignisse geordnet ab. Objekt-Opcodes stellen nur erforderliche
|
||
synchrone LOAD-Handler zu; deren Rückkehr zieht keine unabhängigen Ereignisse
|
||
mitten in eine Anweisung vor. Ein offenes
|
||
Menü sperrt klassische Traps und Forms-Timer, ohne ON/OFF/STOP zu verändern.
|
||
Beim Menüschließen wird die Zustellbarkeit vor erneutem Warten geprüft.
|
||
|
||
INPUT, LINE INPUT, INPUT$ und RANDOMIZE ohne Argument teilen einen
|
||
fortsetzbaren Konsolenleser. Teiltext und Restlänge liegen im aufrufenden
|
||
Frame; Referenzen und Ausdrucksoperanden bleiben bis zum Abschluss auf dem
|
||
Stack. Nach jedem Hostereignis kann die VM einen Handler ausführen und
|
||
danach am Eingabe-Opcode fortsetzen, auch über STOP/CONT und verschachtelte
|
||
Eingaben hinweg. Fehler-Unwinding und RETURN label verwerfen die abgebrochene
|
||
Eingabe. Während dieses Lesens gehören ungetrappte Tasten der Konsole.
|
||
|
||
`RtState::eingaben` hält Tasten und Maus in Hostreihenfolge, auch beim
|
||
Übergeben an Dialoge. Die VM nimmt eine Forms-Eingabe samt ihren Handlern
|
||
vor der nächsten Eingabe an. Konsolentasten bleiben lesbar; Maus ohne
|
||
Formular oder außerhalb der Bildschirmfläche wird verworfen. KEY ON
|
||
entnimmt getrappte Tasten. KEY STOP merkt höchstens einen Trap, markiert die
|
||
betroffenen Eingaben als bereits gemerkt und lässt sie lesbar, auch nach ON.
|
||
Vor der klassischen Quellenauswahl wird der getrappte Tastenanfang der Queue
|
||
gemeldet: KEY behält seine Priorität vor TIMER/UEVENT/SIGNAL, ohne eine davor
|
||
wartende Mauseingabe zu überholen. EOF beendet die Quelle; modale und
|
||
modellose Formularschleifen arbeiten schon angenommene Ereignisse noch ab.
|
||
|
||
`Host::jetzt_ms` ist die einzige Uhr für Ereignisfristen. Die VM fragt sie
|
||
nur für aktive Zeitquellen, SLEEP-Fristen und zeitabhängige Mausauswertung
|
||
ab. Der klassische TIMER wird weiterhin bei Anzeigen, expliziter
|
||
Kooperation und ansonsten alle 64 Anweisungsgrenzen geprüft.
|
||
`Host::warten(Option<u64>)` wartet auf ein Ereignis oder die absolute
|
||
Deadline derselben Uhr; `None` als Ergebnis meldet eine erreichte Deadline.
|
||
Ohne Deadline liefert der Host erst ein Ereignis oder explizites
|
||
`Ereignis::Ende`. Die VM verwendet weder Instant noch Sleep/Yield als
|
||
Rückfallebene. Auch modale und nach Programmende sichtbare Forms verwenden
|
||
diesen Wartepfad. CaptureHost liefert vorbereitete Ereignisse zuerst und
|
||
rückt beim Warten andernfalls bis zur Deadline vor; ohne Deadline meldet
|
||
er Eingabeende. Seine Uhr bleibt außerhalb dieser ausdrücklichen
|
||
Warteoperation oder manueller Zeitänderungen stehen.
|
||
|
||
`tb-ui` hat standardmäßig keine Terminalabhängigkeiten. Das Feature
|
||
`terminal` schaltet die Module `host`, `screen`, `signale` und die optionalen
|
||
Dependencies crossterm, ratatui und signal-hook ein. `tb-cli` aktiviert es
|
||
explizit; VM, Forms-Modell, FRM-Reader und Dialoge brauchen es nicht. Einbetter
|
||
müssen die neue Host-Wartemethode implementieren. Dialog-Eingabequeues tragen
|
||
`Eingabe` statt nackter `Ereignis`-Werte, damit KEY-STOP-Markierungen erhalten
|
||
bleiben; Ereignisse lassen sich mit `.into()` einreihen.
|
||
|
||
Der separate Workspace `tests/terminalfrei` prüft Einbettung ohne
|
||
Feature-Unifikation mit CLI oder IDE:
|
||
|
||
```sh
|
||
cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline
|
||
cargo tree --manifest-path tests/terminalfrei/Cargo.toml --edges normal
|
||
```
|
||
|
||
Der erste Befehl lädt ein FRM-Korpusprogramm und prüft dessen `KM`-Ausgabe
|
||
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
|
||
nicht enthalten.
|
||
|
||
## `.tbc`-Container (Formatversion 4, 2026-09-05)
|
||
|
||
`tbc build app.mak` erzeugt `app.tbc`; `tbc run app.tbc` benötigt die
|
||
BAS-/FRM-/MAK-/Include-Quellen nicht mehr. Dieselbe VM führt Quellprogramme
|
||
und geladene Kompilate aus. Versionen 1–3 und unbekannte Versionen werden
|
||
mit Angabe der gefundenen und unterstützten Version abgewiesen.
|
||
|
||
Alle Zahlen sind little-endian, Ganzzahlen mit Vorzeichen im Zweierkomplement,
|
||
`f32`/`f64` nach IEEE 754. `bool` ist genau ein Byte (0/1). `string` bedeutet
|
||
`u32` UTF-8-Bytelänge, gefolgt von genau diesen Bytes ohne Abschlussnull.
|
||
Listen beginnen mit `u32` Elementanzahl; `[...]` bezeichnet die folgenden
|
||
wiederholten Elemente. Tabellen-IDs sind nullbasiert.
|
||
|
||
Der Header besteht aus `TBC\0` (4 Byte), Version `u16 = 4`, Flags `u16 = 0`,
|
||
Abschnittsanzahl `u32 = 9`. Darauf folgen neun Einträge mit Kennung (4 ASCII-Bytes),
|
||
absolutem Datei-Offset `u32` und Bytelänge `u32`. Der Writer schreibt die
|
||
folgenden Abschnitte in dieser Reihenfolge; der Reader findet sie über die Tabelle.
|
||
|
||
| Kennung | Payload in Reihenfolge |
|
||
|---|---|
|
||
| `MODN` | Projektname `string`, Vorgabe-OPTION-BASE `u8` (wie Modul 0) |
|
||
| `SRCS` | Modulanzahl `u32`, `[Name string, OPTION BASE u8 (0/1)]`; Quellenanzahl `u32`, `[Modul-ID u16, Dateipfad string]` |
|
||
| `CONS` | Anzahl `u32`, `[string]`; Stringkonstantenpool |
|
||
| `TYPS` | Anzahl `u32`, `[TYPE-Name string, Feldanzahl u32, [TypeInit]]` |
|
||
| `GLOB` | Slotanzahl `u32`, `[TypeInit, Debuggername string]` |
|
||
| `PROC` | Prozeduranzahl `u32`, `[Prozedurbeschreibung gemäß unten]` |
|
||
| `DATA` | Anzahl `u32`, `[Rohtext string, physische Zeile u32]` |
|
||
| `JMPT` | Tabellenanzahl `u32`, `[Zielanzahl u32, [Instruktionsindex u32]]` |
|
||
| `OBJS` | Objekt-, Ereignis- und Anfangsdaten gemäß unten |
|
||
|
||
Eine `PROC`-Beschreibung enthält: Name `string`, Modul-ID `u16`, Art `u8`
|
||
(0 Hauptprogramm, 1 SUB, 2 FUNCTION, 3 DEF FN), Parameteranzahl `u32`,
|
||
`[Parametername string, HTy, Array bool, BYREF bool]`, Rückgabetyp-vorhanden
|
||
`bool`, gegebenenfalls `HTy`, Aufrufparameteranzahl `u16`, Local-Anzahl `u32`,
|
||
`[TypeInit, Local-Name string]`, Instruktionsanzahl `u32`, Code-Bytelänge `u32`,
|
||
Code-Bytes. Die beiden Parameteranzahlen müssen übereinstimmen. Prozedur 0
|
||
enthält die zusammengeführten Modulrümpfe; weitere Prozeduren behalten ihre
|
||
Modul-ID. In Mehrmodulprojekten sind Prozedur-/globale Debuggernamen
|
||
`Modul!Name` mit dem aufgelösten Typ-Suffix bei implizit/suffixdeklarierten
|
||
Variablen. AS-deklarierte Namen bleiben suffixlos. Die Inspektion unterscheidet
|
||
explizite Suffixe; ohne Suffix muss der Basisname eindeutig sein. Aufrufe und
|
||
Variablenzugriffe verwenden ausschließlich IDs.
|
||
|
||
`TypeInit` belegt immer 5 Byte: Tag `u8` und Zusatz `u32`. Tags: 0 INTEGER,
|
||
1 LONG, 2 SINGLE, 3 DOUBLE, 4 CURRENCY, 5 STRING, 6 fester STRING (Zusatz:
|
||
Länge), 7 UDT (Zusatz: TYPE-ID, maximal 65535), 8 leerer Slot. Sonst ist der
|
||
Zusatz 0. UDT-Felder dürfen nur frühere TYPE-IDs referenzieren.
|
||
`HTy` ist ein `u8`-Tag mit denselben skalaren Tags 0–7; nur bei Tag 6 folgen
|
||
`u32` Länge und bei Tag 7 `u16` TYPE-ID. Tag 8 bezeichnet FORM, Tag 9 CONTROL.
|
||
Arrays werden separat im Parametersatz markiert; ihr Local-Slot beginnt leer.
|
||
|
||
`OBJS` enthält Objektanzahl `u32`, `[Name string, Klasse u8, Elternname string
|
||
(leer bei Wurzel), Array bool, Eltern-ID u16 (FFFF = keine)]`, dann
|
||
Ereignisanzahl `u32`, `[Objekt-ID u16, Ereignisname string, Prozedur-ID u16]`,
|
||
Startformular-ID `u16` (FFFF = keines), Anzahl Anfangsinstanzen `u32`,
|
||
`[Objekt-ID u16, Designindex i32, Eigenschaftsanzahl u32,
|
||
[Eigenschafts-ID u16, PropertyValue]]`. Eltern-IDs zeigen auf frühere Objekte;
|
||
gleichnamige Controls anderer Formulare bleiben dadurch getrennt. Auch
|
||
Designinstanzen mit Index ungleich 0 und sämtliche FRM-Anfangswerte sind enthalten.
|
||
Eigenschaften stehen in aufsteigender ID-Reihenfolge.
|
||
|
||
`PropertyValue` beginnt mit Tag `u8`: 0 Integer (`i32`), 1 Single (`f32`),
|
||
2 String (`string`), 3 Boolean (`bool`), 4 Objekt (`bool` vorhanden, falls ja
|
||
`u16` Objekt-ID und `bool` Index-vorhanden, gegebenenfalls `i32` Index),
|
||
5 Integer-Array (`u32` Anzahl, `[i32]`). Klassen-IDs 0–18 entsprechen in
|
||
Reihenfolge Form, CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox,
|
||
FileListBox, Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox,
|
||
TextBox, Timer, VScrollBar, Screen, Spin. Eigenschafts-/Methoden-IDs sind
|
||
Indizes der jeweiligen Klassentabellen in `tb-frontend/src/forms.rs`.
|
||
|
||
Code und Quellorte liegen inline in `PROC`; es gibt keine `CODE`-/`LINES`-
|
||
oder `FORMS`-Abschnitte. `Source(Datei-ID, Spalte)` steht unmittelbar vor
|
||
`Stmt(physische Zeile)`. Die VM liest diese Zuordnung auch bei einem Sprung
|
||
direkt auf `Stmt`. Globale DIM-Anweisungen tragen `InitStmt(physische Zeile)`: Fehlerorte und
|
||
Debugger bleiben aktiv, Ereignisse werden bis nach den Initialisierungen
|
||
zurückgestellt. Zeile 0 ist eine synthetische Initialisierungsgrenze.
|
||
`SetErl(BASIC-Label)` aktualisiert getrennt davon die numerische Zeilennummer
|
||
für ERL. Include-Dateien behalten ihren Pfad und physischen Quellort sowie
|
||
die ID ihres einbindenden Moduls. `add_module_breakpoint(module, line)` und
|
||
`set_step(true)` halten an diesen Grenzen; `current_module`, `current_file`
|
||
und `current_source_pos` liefern den Halt-/Fehlerort. `inspect` akzeptiert
|
||
lokale, im aktuellen Modul sichtbare und explizit qualifizierte Variablennamen.
|
||
|
||
Der Reader prüft Version, Flags, eindeutige bekannte Abschnitte,
|
||
Längen/Überlappungen, vollständige Payloads, Typ-/Objekt-/Prozedur-/Quell-IDs,
|
||
Sprungziele und Anfangseigenschaften vor der Ausführung. Beschädigte Daten
|
||
liefern einen Ladefehler. Das Format ist kein Sandbox-Format für fremden
|
||
Programmcode; ein vollständiger Stack-/Kontrollflussverifizierer ist nicht Teil
|
||
von TBC. Ein gültiges vom Writer erzeugtes Kompilat lässt sich bytegleich
|
||
laden und erneut serialisieren.
|
||
|
||
## Eigenständige Executables
|
||
|
||
Geplant, noch nicht implementiert: `tbc build --exe` kopiert den vorkompilierten Runner (dieselbe
|
||
tb-vm/tb-runtime/tb-ui-Bibliothek wie die IDE) und hängt das `.tbc` als
|
||
Ressource an (Anhängen ans Binary + Fußzeile mit Offset/Magic; portabel für
|
||
alle drei Plattformen). Alternative — `include_bytes!` + Cargo-Build beim
|
||
Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender.
|
||
|
||
## Performance
|
||
|
||
**Compiler (Instant-Compile, Anforderung siehe PLAN.md):**
|
||
- Single-Pass pro Modul: Lexen, Parsen und Codegen in einem Durchlauf;
|
||
Vorwärtsreferenzen (Prozeduren, Labels) über Fixup-Listen statt zweitem
|
||
Pass. Die Sprache ist dafür gemacht — das Vorbild kompilierte auf
|
||
1992er-Hardware gefühlt sofort.
|
||
- `.BAS`/`.FRM` werden getrennt geparst, semantisch aufgelöst und zu Bytecode-
|
||
Einheiten übersetzt. Der Projektlink versetzt Slots, Prozeduren, Typen,
|
||
Strings, DATA und Sprungtabellen. Modulrümpfe laufen in Projektreihenfolge;
|
||
ihre globalen Initialisierungen liegen vor dem ersten Ereignis. Eindeutige
|
||
externe Prozeduren werden mit dem DEFtype-Kontext ihres Ursprungs importiert;
|
||
prozedurlokale DEFtype-Angaben beeinflussen keine anderen Prozeduren.
|
||
Importierte Konstanten werden im Ursprung gefaltet, auch bei transitiven
|
||
Modulabhängigkeiten. TYPE-Abhängigkeiten behalten ihr ursprüngliches Layout;
|
||
interne qualifizierte Typnamen verhindern Konflikte mit abweichenden lokalen
|
||
Definitionen. Echte lokale Duplikate werden diagnostiziert. SHARED
|
||
bleibt modullokal. COMMON-Variablen desselben Blocks und derselben vollständigen
|
||
Variablenidentität teilen einen Slot; Elementtyp, Rang und bekannte Grenzen
|
||
müssen kompatibel sein. Der IDE-Cache speichert Parsing und unverknüpfte
|
||
Modulprodukte im vorhandenen Projektcompiler; der Vollcompile nutzt dieselben
|
||
Schritte mit einem leeren Cache. Details stehen in [ide-bedienung.md](ide-bedienung.md#übersetzung-und-revisionsbindung).
|
||
- Keine Optimierungspasses. Erlaubt sind nur Gratis-Optimierungen im
|
||
Codegen (Konstantenfaltung im Ausdruck, Peephole beim Emit).
|
||
- Budget als Test verankern (Phase 2): Benchmark-Projekt (~50k Zeilen)
|
||
muss unter 1 s kompilieren (Release-Build der Toolchain, Referenzrechner);
|
||
einzelnes Modul < 50 ms.
|
||
|
||
**VM-Ausführung:**
|
||
- Alle Namen werden zur Compilezeit aufgelöst: Variablen/Parameter sind
|
||
Slot-Indizes, Prozeduren Tabellenindizes — zur Laufzeit keine
|
||
Hash-Lookups.
|
||
- Statische Typen des Dialekts ausnutzen: typisierte Opcodes
|
||
(`ADD_I16`, `ADD_F64`, `CONCAT` …) statt generischem Dispatch über
|
||
`Value`-Tags in heißen Pfaden.
|
||
- Dichte Opcodes, `match`-Dispatch in einer engen Schleife; Tick-Prüfung
|
||
(Events/Breakpoints) nur an Anweisungsgrenzen über ein einzelnes
|
||
Flag-Wort, nicht pro Opcode.
|
||
- Strings immutabel via `Rc<str>` (Kopien sind Pointer-Kopien);
|
||
Konstantenpool dedupliziert.
|
||
- Messlatte (Phase 2, Benchmarks in `benches/`): typische
|
||
Schleifen-/String-Lasten mindestens ~100× schneller als das Vorbild auf
|
||
Originalhardware; Richtwert grob Lua-Interpreter-Klasse, gemessen und
|
||
dokumentiert statt geraten.
|
||
|
||
**Benchmark-Ergebnisse (2026-09-02, Release-Build, Entwicklerrechner
|
||
Windows 11, `cargo bench -p tb-vm`):**
|
||
|
||
- Compile-Budget (`benches/compile.rs`):
|
||
- Einzelmodul, 508 Zeilen: **1,2 ms** (Budget 50 ms — eingehalten)
|
||
- Projekt, 49 760 Zeilen über 20 Module: **124 ms** (Budget 1 000 ms —
|
||
eingehalten); Durchsatz ≈ 400 000 Zeilen/s
|
||
- VM-Durchsatz (`benches/vm.rs`):
|
||
- INTEGER-Schleife (Arithmetik + MOD): ≈ 4,8 Mio Iterationen/s
|
||
- DOUBLE-Schleife: ≈ 5,2 Mio Iterationen/s
|
||
- SUB-Aufrufe mit BYREF: ≈ 5,2 Mio Aufrufe/s
|
||
- String-Lasten (MID$/INSTR/Verkettung): ≈ 0,8 Mio Runden/s
|
||
**Nachmessung nach Phase 3 (2026-09-04, macOS, Apple Silicon,
|
||
`cargo bench -p tb-vm`)** — die Zustellpunkte des neuen `Host` (Anzeige
|
||
nur bei geändertem Bildschirm, Ereignisabholung alle 1024 Anweisungen)
|
||
kosten nichts Messbares:
|
||
|
||
- Compile-Budget: Einzelmodul 508 Zeilen **0,77 ms**; Projekt 49 760
|
||
Zeilen **53 ms** (≈ 950 000 Zeilen/s)
|
||
- VM-Durchsatz: INTEGER-Schleife **11,0 Mio/s**, DOUBLE **12,2 Mio/s**,
|
||
SUB-Aufrufe **10,7 Mio/s**, String-Lasten **2,4 Mio Runden/s**
|
||
|
||
(Die höheren Zahlen gegenüber 2026-09-02 gehen auf die andere Maschine
|
||
zurück, nicht auf eine Optimierung — gemessen wurde beide Male derselbe
|
||
Benchmark.)
|
||
|
||
- Einordnung: bei ≈ 13 Instruktionen je Schleifeniteration entspricht
|
||
das ≈ 60 Mio Instruktionen/s — Vorbild-Programme auf 1992er-Hardware
|
||
werden um weit mehr als den Faktor 100 übertroffen; für 80×25-
|
||
Terminalprogramme mehr als ausreichend. Ein Performance-Pass ist erst
|
||
bei Bedarf vorgesehen (Phase 6).
|
||
|
||
## Zahlenkonvertierungs-Matrix (Feindesign, 2026-09-02)
|
||
|
||
Grundlage: Original-Hilfe (Topics „Arithmetic Operators", „CINT/CLNG",
|
||
„MOD", „Numeric Types"). Vier Punkte sind dort **nicht** geregelt; sie
|
||
wurden am 2026-09-04 entschieden und sind unten als *Projektentscheidung*
|
||
gekennzeichnet — sie gelten damit als bewusste Festlegung, nicht als
|
||
rekonstruiertes Vorbildverhalten. Der Korpustest `konvertierung.bas`
|
||
verankert die Matrix ausführbar.
|
||
|
||
**Rangfolge für Promotion:** INTEGER < LONG < CURRENCY < SINGLE < DOUBLE.
|
||
Ausnahme: CURRENCY gemischt mit SINGLE oder DOUBLE ergibt **DOUBLE**
|
||
(nicht SINGLE). *Projektentscheidung 2026-09-04:* Die Original-Hilfe
|
||
regelt diesen Fall nicht. CURRENCY trägt 19 signifikante Stellen, SINGLE
|
||
nur 7 — eine Promotion nach SINGLE zerstörte genau die Genauigkeit, für
|
||
die es den Typ gibt. DOUBLE (16 Stellen) ist die nächstbeste Wahl.
|
||
|
||
**Ergebnistyp binärer Operatoren** (l op r; T = promoteter Typ):
|
||
|
||
| Operator | Ergebnistyp | Besonderheiten |
|
||
|---|---|---|
|
||
| `+ - *` | T; gleiche Typen bleiben erhalten | Ganzzahl-/CURRENCY-Überlauf → Fehler 6 (auch INTEGER+INTEGER promotet **nicht**) |
|
||
| `/` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE | Division durch 0 → Fehler 11 |
|
||
| `\` `MOD` | INTEGER, wenn beide Operanden INTEGER, sonst LONG | Operanden werden **vorher** kaufmännisch (banker's) auf Ganzzahl gerundet; außerhalb LONG → Fehler 6; Divisor 0 → Fehler 11; `MOD`-Vorzeichen wie Dividend |
|
||
| `^` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE (*Projektentscheidung 2026-09-04*) | Rechnung intern in DOUBLE; 0 ^ negativ → Fehler 11; negative Basis mit nicht-ganzem Exponent → Fehler 5 |
|
||
| `= <> < <= > >=` | INTEGER (−1/0) | Vergleich im promoteten Typ; Strings codepoint-weise |
|
||
| `NOT AND OR XOR EQV IMP` | INTEGER, wenn alle Operanden INTEGER, sonst LONG | bitweise; Operanden vorher banker's-gerundet auf Ganzzahl, außerhalb LONG → Fehler 6 |
|
||
| unäres `-` | Typ des Operanden | −(−32768) INTEGER → Fehler 6 |
|
||
|
||
**Konvertierung Quelle → Ziel** (implizit bei Zuweisung/Argument, explizit
|
||
via `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR` — identische Semantik):
|
||
|
||
| von \ nach | INTEGER | LONG | SINGLE | DOUBLE | CURRENCY |
|
||
|---|---|---|---|---|---|
|
||
| INTEGER | — | exakt | exakt | exakt | ×10 000 |
|
||
| LONG | Bereich prüfen → 6 | — | nächstgelegener f32 | exakt | ×10 000 |
|
||
| SINGLE | banker's + Bereich → 6 | banker's + Bereich → 6 | — | exakt (Widening) | banker's auf 4 Nachkommastellen + Bereich → 6 |
|
||
| DOUBLE | banker's + Bereich → 6 | banker's + Bereich → 6 | Bereich prüfen → 6, sonst nächstgelegener f32 | — | banker's auf 4 Nachkommastellen + Bereich → 6 |
|
||
| CURRENCY | ÷10 000, banker's + Bereich → 6 | ÷10 000, banker's + Bereich → 6 | ÷10 000.0 | ÷10 000.0 | — |
|
||
|
||
- Banker's Rounding: zur nächsten Ganzzahl, bei .5 zur **geraden**
|
||
(Original-Hilfe: CINT). Gilt für alle Gleitkomma→Ganzzahl-Übergänge
|
||
inklusive `\`/`MOD`/Logik-Operanden und CURRENCY-Skalierung.
|
||
- Zahl ↔ String nie implizit (Compile-Zeit „Type mismatch"); explizit nur
|
||
über `STR$`/`VAL`.
|
||
|
||
**Textdarstellung (PRINT und STR$):**
|
||
|
||
- INTEGER/LONG: dezimal ohne Dezimalpunkt.
|
||
- SINGLE: bis 7 signifikante Stellen, kürzeste Darstellung, keine
|
||
führende Null vor dem Dezimalpunkt (`.5`), keine nachgestellten
|
||
Nullen; außerhalb des Festformat-Bereichs Exponentialform `mE±xx`.
|
||
*Projektentscheidung 2026-09-04:* Die Original-Hilfe nennt keine
|
||
Schwelle. Gewählt ist der Dezimalexponent < −7 oder ≥ 7 — damit bleibt
|
||
jeder Wert im Festformat, der sich mit den 7 signifikanten Stellen des
|
||
Typs verlustfrei schreiben lässt (`9999999` fest, `1E+07`
|
||
exponentiell; `.0000001` fest, `1E-08` exponentiell).
|
||
- DOUBLE: wie SINGLE mit bis 16 signifikanten Stellen und `D±xx` als
|
||
Exponentmarke.
|
||
- CURRENCY: Festformat mit bis zu 4 Nachkommastellen, nachgestellte
|
||
Nullen entfallen (`2.5000@` → `2.5`, `1.10@` → `1.1`).
|
||
*Projektentscheidung 2026-09-04:* Die Original-Hilfe nennt kein
|
||
Ausgabeformat. Gewählt ist dieselbe Regel wie bei den übrigen
|
||
Zahlentypen — nachgestellte Nullen entfallen —, damit `PRINT` über alle
|
||
Typen einheitlich wirkt. Wer feste Nachkommastellen braucht, nutzt
|
||
`PRINT USING`.
|
||
- `PRINT` stellt nicht-negativen Zahlen ein Leerzeichen, negativen ein
|
||
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
|
||
Leerzeichen/`-`, kein nachgestelltes.
|
||
|
||
## Opcode-Satz (Formatversion 4, 2026-09-05)
|
||
|
||
In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust-
|
||
Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung
|
||
im heißen Pfad); die `.tbc`-Serialisierung bildet jede Instruktion auf
|
||
1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER,
|
||
`I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING.
|
||
|
||
Die folgende Tabelle führt jeden serialisierbaren Opcode einzeln auf;
|
||
Gesamtbytes schließen das Opcode-Byte ein. `bool`, `TypeInit` und primitive
|
||
Breiten sind im Containerabschnitt definiert. `CmpOp` belegt ein Byte:
|
||
0 gleich, 1 ungleich, 2 kleiner, 3 kleiner/gleich, 4 größer, 5 größer/gleich.
|
||
Nicht aufgeführte Opcode-Bytes sind ungültig.
|
||
|
||
| Byte | Instruktion | Operanden in Reihenfolge | Gesamtbytes |
|
||
|---|---|---|---|
|
||
| `0x00` | `Stmt` | line: u32 | 5 |
|
||
| `0x01` | `SetErl` | label: u32 | 5 |
|
||
| `0x02` | `End` | — | 1 |
|
||
| `0x03` | `StopInstr` | — | 1 |
|
||
| `0x04` | `SystemInstr` | — | 1 |
|
||
| `0x05` | `Unsupported` | name: u16 | 3 |
|
||
| `0x06` | `Source` | source: u32, column: u32 | 9 |
|
||
| `0x07` | `InitStmt` | line: u32 | 5 |
|
||
| `0x10` | `PushInt` | value: i16 | 3 |
|
||
| `0x11` | `PushLng` | value: i32 | 5 |
|
||
| `0x12` | `PushSng` | value: f32 | 5 |
|
||
| `0x13` | `PushDbl` | value: f64 | 9 |
|
||
| `0x14` | `PushCur` | value: i64 | 9 |
|
||
| `0x15` | `PushStr` | pool: u16 | 3 |
|
||
| `0x16` | `Dup` | — | 1 |
|
||
| `0x17` | `Pop` | — | 1 |
|
||
| `0x18` | `PushUdtId` | type: u16 | 3 |
|
||
| `0x20` | `LoadGlobal` | slot: u16 | 3 |
|
||
| `0x21` | `StoreGlobal` | slot: u16 | 3 |
|
||
| `0x22` | `LoadLocal` | slot: u16 | 3 |
|
||
| `0x23` | `StoreLocal` | slot: u16 | 3 |
|
||
| `0x24` | `LoadRef` | slot: u16 | 3 |
|
||
| `0x25` | `StoreRef` | slot: u16 | 3 |
|
||
| `0x26` | `MakeRefGlobal` | slot: u16 | 3 |
|
||
| `0x27` | `MakeRefLocal` | slot: u16 | 3 |
|
||
| `0x28` | `MakeRefElem` | dimensions: u8 | 2 |
|
||
| `0x29` | `MakeRefField` | field: u16 | 3 |
|
||
| `0x30` | `LoadArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||
| `0x31` | `LoadElem` | dimensions: u8 | 2 |
|
||
| `0x32` | `StoreElem` | dimensions: u8 | 2 |
|
||
| `0x33` | `DimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||
| `0x34` | `RedimArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||
| `0x35` | `EraseSlot` | global: bool, slot: u16 | 4 |
|
||
| `0x36` | `LoadField` | field: u16 | 3 |
|
||
| `0x37` | `StoreField` | field: u16 | 3 |
|
||
| `0x38` | `CopyRec` | — | 1 |
|
||
| `0x39` | `ArrBound` | lower: bool | 2 |
|
||
| `0x3A` | `FixStr` | length: u32 | 5 |
|
||
| `0x3B` | `CommonArr` | global: bool, slot: u16, dimensions: u8, element: TypeInit | 10 |
|
||
| `0x40` | `AddI2` | — | 1 |
|
||
| `0x41` | `AddI4` | — | 1 |
|
||
| `0x42` | `AddR4` | — | 1 |
|
||
| `0x43` | `AddR8` | — | 1 |
|
||
| `0x44` | `AddCy` | — | 1 |
|
||
| `0x45` | `SubI2` | — | 1 |
|
||
| `0x46` | `SubI4` | — | 1 |
|
||
| `0x47` | `SubR4` | — | 1 |
|
||
| `0x48` | `SubR8` | — | 1 |
|
||
| `0x49` | `SubCy` | — | 1 |
|
||
| `0x4A` | `MulI2` | — | 1 |
|
||
| `0x4B` | `MulI4` | — | 1 |
|
||
| `0x4C` | `MulR4` | — | 1 |
|
||
| `0x4D` | `MulR8` | — | 1 |
|
||
| `0x4E` | `MulCy` | — | 1 |
|
||
| `0x4F` | `NegI2` | — | 1 |
|
||
| `0x50` | `NegI4` | — | 1 |
|
||
| `0x51` | `NegR4` | — | 1 |
|
||
| `0x52` | `NegR8` | — | 1 |
|
||
| `0x53` | `NegCy` | — | 1 |
|
||
| `0x54` | `DivR4` | — | 1 |
|
||
| `0x55` | `DivR8` | — | 1 |
|
||
| `0x56` | `IDivI2` | — | 1 |
|
||
| `0x57` | `IDivI4` | — | 1 |
|
||
| `0x58` | `ModI2` | — | 1 |
|
||
| `0x59` | `ModI4` | — | 1 |
|
||
| `0x5A` | `PowR8` | — | 1 |
|
||
| `0x5B` | `Concat` | — | 1 |
|
||
| `0x60` | `ConvI2I4` | — | 1 |
|
||
| `0x61` | `ConvI2R4` | — | 1 |
|
||
| `0x62` | `ConvI2R8` | — | 1 |
|
||
| `0x63` | `ConvI2Cy` | — | 1 |
|
||
| `0x64` | `ConvI4I2` | — | 1 |
|
||
| `0x65` | `ConvI4R4` | — | 1 |
|
||
| `0x66` | `ConvI4R8` | — | 1 |
|
||
| `0x67` | `ConvI4Cy` | — | 1 |
|
||
| `0x68` | `ConvR4I2` | — | 1 |
|
||
| `0x69` | `ConvR4I4` | — | 1 |
|
||
| `0x6A` | `ConvR4R8` | — | 1 |
|
||
| `0x6B` | `ConvR4Cy` | — | 1 |
|
||
| `0x6C` | `ConvR8I2` | — | 1 |
|
||
| `0x6D` | `ConvR8I4` | — | 1 |
|
||
| `0x6E` | `ConvR8R4` | — | 1 |
|
||
| `0x6F` | `ConvR8Cy` | — | 1 |
|
||
| `0x70` | `ConvCyI2` | — | 1 |
|
||
| `0x71` | `ConvCyI4` | — | 1 |
|
||
| `0x72` | `ConvCyR4` | — | 1 |
|
||
| `0x73` | `ConvCyR8` | — | 1 |
|
||
| `0x80` | `NotI2` | — | 1 |
|
||
| `0x81` | `NotI4` | — | 1 |
|
||
| `0x82` | `AndI2` | — | 1 |
|
||
| `0x83` | `AndI4` | — | 1 |
|
||
| `0x84` | `OrI2` | — | 1 |
|
||
| `0x85` | `OrI4` | — | 1 |
|
||
| `0x86` | `XorI2` | — | 1 |
|
||
| `0x87` | `XorI4` | — | 1 |
|
||
| `0x88` | `EqvI2` | — | 1 |
|
||
| `0x89` | `EqvI4` | — | 1 |
|
||
| `0x8A` | `ImpI2` | — | 1 |
|
||
| `0x8B` | `ImpI4` | — | 1 |
|
||
| `0x90` | `CmpI2` | compare: CmpOp | 2 |
|
||
| `0x91` | `CmpI4` | compare: CmpOp | 2 |
|
||
| `0x92` | `CmpR4` | compare: CmpOp | 2 |
|
||
| `0x93` | `CmpR8` | compare: CmpOp | 2 |
|
||
| `0x94` | `CmpCy` | compare: CmpOp | 2 |
|
||
| `0x95` | `CmpStr` | compare: CmpOp | 2 |
|
||
| `0xA0` | `Jump` | target: u32 | 5 |
|
||
| `0xA1` | `JumpIfFalse` | target: u32 | 5 |
|
||
| `0xA2` | `JumpIfTrue` | target: u32 | 5 |
|
||
| `0xA3` | `Gosub` | target: u32 | 5 |
|
||
| `0xA4` | `RetGosub` | — | 1 |
|
||
| `0xA5` | `RetGosubTo` | target: u32 | 5 |
|
||
| `0xA6` | `OnJump` | table: u16, gosub: bool | 4 |
|
||
| `0xA7` | `Run` | kind: u8 | 2 |
|
||
| `0xB0` | `Call` | proc: u16, argc: u8 | 4 |
|
||
| `0xB1` | `RetProc` | — | 1 |
|
||
| `0xB2` | `RetFn` | — | 1 |
|
||
| `0xB3` | `CallBuiltin` | builtin: u16, argc: u8 | 4 |
|
||
| `0xB4` | `LoadObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
|
||
| `0xB5` | `StoreObjectProperty` | object: u16, property: u16, indexed: bool | 6 |
|
||
| `0xB6` | `PushObject` | object: u16, indexed: bool | 4 |
|
||
| `0xB7` | `TypeOf` | class: u8 | 2 |
|
||
| `0xB8` | `ObjectMethod` | object: u16, method: u16, argc_flags: u8 | 6 |
|
||
| `0xB9` | `ObjectLoad` | object: u16, unload: bool, indexed: bool | 5 |
|
||
| `0xBA` | `LoadDynamicObjectProperty` | name: u16 | 3 |
|
||
| `0xBB` | `StoreDynamicObjectProperty` | name: u16 | 3 |
|
||
| `0xBC` | `LoadObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
|
||
| `0xBD` | `ObjectMethodFn` | object: u16, method: u16, argc_flags: u8 | 6 |
|
||
| `0xBE` | `StoreObjectIndexedProperty` | object: u16, property_flags: u16 | 5 |
|
||
| `0xC0` | `OnErrorGoto` | target: u32 | 5 |
|
||
| `0xC1` | `OnErrorLocal` | target: u32 | 5 |
|
||
| `0xC2` | `OnErrorDisable` | — | 1 |
|
||
| `0xC3` | `OnErrorLocalDisable` | — | 1 |
|
||
| `0xC4` | `OnErrorResumeNext` | local: bool | 2 |
|
||
| `0xC5` | `Resume0` | — | 1 |
|
||
| `0xC6` | `ResumeNext` | — | 1 |
|
||
| `0xC7` | `ResumeLabel` | target: u32 | 5 |
|
||
| `0xC8` | `RaiseError` | — | 1 |
|
||
| `0xC9` | `LoadErr` | — | 1 |
|
||
| `0xCA` | `LoadErl` | — | 1 |
|
||
| `0xCB` | `SetErr` | — | 1 |
|
||
| `0xD0` | `ReadData` | kind: u8 | 2 |
|
||
| `0xD1` | `Restore` | data: u32 | 5 |
|
||
| `0xD2` | `Input` | argc: u8, line_mode: bool, prompt: u16, question: bool | 6 |
|
||
| `0xD3` | `InputFile` | argc: u8, line_mode: bool | 3 |
|
||
| `0xD4` | `GetPut` | put: bool, has_record: bool, kind: u8, extra: u16 | 6 |
|
||
| `0xD5` | `Field` | count: u8 | 2 |
|
||
| `0xD6` | `LsetRset` | right: bool | 2 |
|
||
| `0xE0` | `TrapDefine` | kind: u8, target: u32 | 6 |
|
||
| `0xE1` | `TrapDisable` | kind: u8 | 2 |
|
||
| `0xE2` | `TrapSet` | kind: u8, state: u8 | 3 |
|
||
| `0xE3` | `EventSwitch` | enabled: bool | 2 |
|
||
| `0xE4` | `Doevents` | — | 1 |
|
||
| `0xE5` | `Sleep` | has_seconds: bool | 2 |
|
||
|
||
`target` bezeichnet einen Instruktionsindex innerhalb der aktuellen Prozedur;
|
||
`OnErrorGoto` zeigt in Prozedur 0. `table` referenziert `JMPT`, `proc` die
|
||
Prozedurtabelle, `pool`/`name`/`prompt` den Stringpool. `global=true` wählt
|
||
`GLOB`, sonst einen Local-Slot. Arrayindizes und Dimensionsgrenzen (je lo/hi)
|
||
liegen auf dem Stack. `ArrBound(lower)` unterscheidet LBOUND/UBOUND;
|
||
`MakeRef*` erzeugt Referenzen, `CopyRec` eine UDT-Wertkopie. `PushUdtId` legt
|
||
eine beim Linken versetzte TYPE-ID als LONG für ISAM auf den Stack.
|
||
|
||
`Call`/`CallBuiltin` konsumieren `argc` Argumente in Quellreihenfolge.
|
||
`builtin` ist die feste ID aus `tb-runtime/src/builtins.rs::ids`; `RetFn`
|
||
transportiert den Funktionswert. `ObjectMethod`/`ObjectMethodFn` verwenden
|
||
klassenlokale Methodenindizes, keine Stringpool-IDs. Das höchste Bit von
|
||
`argc_flags` kennzeichnet einen zusätzlichen Control-Arrayindex vor den
|
||
Argumenten; die unteren 7 Bit enthalten die Argumentanzahl. Bei indizierten
|
||
Eigenschaften markiert Bit 15 von `property_flags` den zusätzlichen
|
||
Control-Arrayindex; die unteren 15 Bit sind die Eigenschafts-ID. Der
|
||
Eigenschaftsindex liegt anschließend auf dem Stack. `indexed` bedeutet
|
||
ansonsten ebenfalls Control-Arrayindex auf dem Stack. Dynamische
|
||
Eigenschaftszugriffe verwenden den Namen im Stringpool und einen Objektwert
|
||
vom Stack. `TypeOf(class)` verwendet die oben genannten Klassen-IDs.
|
||
|
||
`TrapDefine`/`TrapDisable`/`TrapSet`: `kind` ist 0 KEY, 1 TIMER, 2 UEVENT,
|
||
3 SIGNAL; die Kennung bzw. TIMER-Dauer kommt vom Stack. `state` ist 0 ON,
|
||
1 OFF, 2 STOP. `Doevents` legt 0 ab; `Sleep(true)` nimmt Sekunden vom Stack.
|
||
`OnErrorResumeNext(local)` wählt Frame- oder Modulhandler. `RaiseError` und
|
||
`SetErr` lesen einen Fehlercode vom Stack; `LoadErr`/`LoadErl` legen ihren Wert ab.
|
||
`Run(kind)` ist 0 Neustart, 1 numerische Startzeile, 2 Dateiname; bei 1/2
|
||
liegt das Ziel auf dem Stack. `ReadData(kind)` ist 0 STRING, 1 DOUBLE.
|
||
|
||
`Input`/`InputFile` lesen `argc` Referenzen; bei Dateiinput liegt die
|
||
Dateinummer darunter. `line_mode` wählt LINE INPUT, `prompt=FFFF` bedeutet
|
||
keinen Prompt, `question` ergänzt das Fragezeichen. `GetPut` unterscheidet
|
||
GET/PUT und eine optionale Recordnummer auf dem Stack. `kind` ist 0 ohne
|
||
Variable, 1 INTEGER, 2 LONG, 3 SINGLE, 4 DOUBLE, 5 CURRENCY, 6 fester STRING,
|
||
7 UDT, 8 variabler STRING; `extra` enthält bei 6 die Länge, bei 7 die TYPE-ID,
|
||
sonst 0. `Field` konsumiert Dateinummer und `count` Paare (Länge, Referenz).
|
||
`LsetRset(right)` konsumiert Referenz und Wert.
|
||
|
||
FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal
|
||
in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen
|
||
die Vergleichsrichtung zur Compilezeit, sonst verzweigt ein einmalig
|
||
berechnetes Vorzeichen-Flag auf zwei Vergleichssequenzen. Inkrement mit
|
||
regulärer typisierter Addition (Überlauf → Fehler 6, wie Vorbild).
|
||
`SELECT CASE` wertet den Selektor in einen versteckten Slot aus und wird
|
||
als Vergleichs-/Sprungkette abgesenkt.
|
||
|
||
## Fehlerbehandlung: Scoping (Feindesign, 2026-09-02)
|
||
|
||
- `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer
|
||
Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen**
|
||
Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.
|
||
Jedes Quellmodul besitzt einen eigenen Modulhandler; `GOTO 0` in einer
|
||
Bibliothek deaktiviert daher keinen Handler des aufrufenden Moduls.
|
||
`… GOTO 0` deaktiviert den jeweiligen Handler.
|
||
- Fehlerfall: erst die Frame-Kette von innen nach außen nach
|
||
LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum
|
||
Handler-Frame abgewickelt, Anweisungs-Offset und Frame für `RESUME`
|
||
gemerkt, `ERR`/`ERL` gesetzt. Ohne Handler: Abbruch mit Meldung und
|
||
Quellzeile.
|
||
- Fehler **während aktiver Behandlung** (zwischen Handler-Eintritt und
|
||
`RESUME`): sofortiger Abbruch, keine Kaskade (Vorbild).
|
||
- `ERL` liefert die zuletzt durchlaufene **numerische** Zeilennummer
|
||
(Labels zählen nicht; ohne numerische Zeilen: 0).
|
||
|
||
## Runner-Verhalten `STOP`/`CONT` (Entscheidung 2026-09-02)
|
||
|
||
- `END`, `SYSTEM` und das Erreichen des Programmendes beenden den
|
||
Prozess mit Exit-Code 0.
|
||
- `STOP` außerhalb der IDE terminiert mit der Meldung `STOP in line n`
|
||
(bzw. Modul/Zeile) und Exit-Code ≠ 0. Offene Dateien werden wie bei
|
||
`END` geschlossen.
|
||
- `CONT` ist ein Direktmodus-Konzept der IDE (Phase 5): dort suspendiert
|
||
`STOP` die VM (`step()`-Architektur), `CONT` setzt fort. Im Kompilat
|
||
existiert kein `CONT`.
|
||
|
||
## Offene Punkte
|
||
|
||
Keine — die Punkte aus Phase 0 (Opcode-Satz, Konvertierungsmatrix,
|
||
`STOP`/`CONT` im Runner) sind oben ausgearbeitet. Die vier Stellen, zu
|
||
denen die Original-Hilfe schweigt, sind am 2026-09-04 als
|
||
*Projektentscheidung* festgelegt, an Ort und Stelle begründet und über
|
||
`tests/compat/konvertierung.bas` ausführbar verankert. Sollte je eine
|
||
belastbare Fundstelle auftauchen, die ihnen widerspricht, gilt sie —
|
||
die Entscheidungen sind Lückenfüller, keine Setzungen gegen das Vorbild.
|
||
|
||
`CommonArr` dimensioniert einen leeren COMMON-Array-Slot einmal. Bei bereits
|
||
vorhandenem Array prüft es Elementtyp und die ausgewerteten Grenzen, ohne
|
||
Werte oder Handle zu ersetzen (Konflikt: Fehler 13). Statisch erkennbare
|
||
COMMON-Typ-/Grenzenkonflikte werden bereits beim Linken diagnostiziert.
|
||
|
||
### Inkrementeller Compile (Phase 5, Change 03)
|
||
|
||
Messung vom 2026-09-06 auf macOS/aarch64, Apple M5 Max. Aufruf:
|
||
`cargo bench -p tb-vm --bench compile`; Release mit opt-level=3, LTO und
|
||
codegen-units=1. Derselbe Generator erzeugt das Referenz-Einzelmodul mit
|
||
508 Zeilen und das Projekt mit 20 Modulen / 49.760 Zeilen. Vollcompile und
|
||
Cacheänderung schließen die Projektauflösung und den Link ein. Cacheänderungen
|
||
ändern einen privaten Prozedurrumpf; sieben Läufe liefern den Median und prüfen
|
||
die Zahl tatsächlich neu übersetzter Module. Die Grenzwerte werden nur im
|
||
expliziten Release-Benchmark geprüft, nicht in normalen Debug-Tests.
|
||
|
||
| Referenz | Vollcompile | Cacheänderung (Median) | Neu / wiederverwendet | Budget |
|
||
| --- | ---: | ---: | ---: | ---: |
|
||
| Einzelmodul, 508 Zeilen | 1,21 ms | 0,74 ms | 1 / 0 | < 50 ms: erfüllt |
|
||
| Projekt, 49.760 Zeilen | 103 ms | 22,10 ms | 1 / 19 | < 1.000 ms: erfüllt |
|
||
|
||
Der Vollcompile des Einzelmoduls ist wie im bestehenden Harness der beste
|
||
von zehn Läufen; das Projekt wird einmal vollständig gemessen. Der Benchmark
|
||
zählt 335.421 verknüpfte Instruktionen. Diese Werte sind eine lokale Messung,
|
||
keine hardwareunabhängige Zeitgarantie.
|