Files
TerminalBasic/docs/tbvm-design.md

654 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 13 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 07; 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 018 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.