# 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), // 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>). } ``` - 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 Interpreterschleife läuft in **Ticks**: nach jeder Anweisung (Grenze aus der Zeilentabelle) prüft sie ein Flag-Set: Breakpoint? Einzelschritt? Strg+Untbr? Ereignis-Queue nicht leer und Zustellung erlaubt? - Ereigniszustellung (Forms, Timer) erfolgt kooperativ: nur an Zustellpunkten (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer Ereignisprozedur) — wie im Vorbild. - Die VM ist eine gewöhnliche zustandsbehaftete Struktur, `step()`-basiert; die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt. ## `.tbc`-Container (Stand Phase 2, Formatversion 1) ``` Magic "TBC\0" · Formatversion u16 · Flags u16 · Abschnittsanzahl u32 Abschnittstabelle: [ (Kennung 4 Byte, Offset u32, Länge u32) ] Abschnitte: MODN Modulname + OPTION BASE CONS deduplizierter Stringpool TYPS TYPE-Layouts (Feld-Initialisierungstypen) GLOB globale Slots (Initialisierungstyp + Name für Debugger) PROC Prozedurtabelle: Name, Parameteranzahl, Frame-Slots, Code (Zeileninfo liegt inline im Code: Stmt-/SetErl-Instruktionen — ersetzt den früher geplanten separaten LINES-Abschnitt) DATA DATA-Konstanten (Rohtext + Quellzeile) JMPT Sprungtabellen für ON n GOTO/GOSUB FORMS (ab Phase 4: serialisierte .FRM-Beschreibungen) ``` Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig, Format bleibt stabil und dokumentiert); unbekannte Formatversionen werden mit Meldung abgewiesen. Instruktions-Encoding: 1 Opcode-Byte + Operanden little-endian, Opcode-Bytes gruppenweise mit Lücken vergeben (`crates/tb-vm/src/bytecode.rs` ist die normative Liste). ## Eigenständige Executables `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. - Modulweise inkrementell: `.BAS`/`.FRM` werden unabhängig zu Bytecode- Einheiten übersetzt und beim Build nur zusammengebunden; die IDE recompiliert nur geänderte Module (Hash über Quelltext). - 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` (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 (Feindesign, 2026-09-02) In-Memory führt die VM dekodierte Instruktionen (`Vec`, 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. | Gruppe | Instruktionen | Bemerkung | |---|---|---| | Anweisungsgrenze | `Stmt(line:u32)` · `SetErl(n:u32)` | `Stmt` prüft das Tick-Flag-Wort (Breakpoint/Einzelschritt/Abbruch), aktualisiert Zeile und Resume-Punkt; `SetErl` bei numerischen Zeilennummern | | Konstanten/Stack | `PushInt(i16)` `PushLng(i32)` `PushSng(f32)` `PushDbl(f64)` `PushCur(i64)` `PushStr(pool:u16)` · `Dup` `Pop` | Stringpool dedupliziert | | Variablen | `LoadGlobal/StoreGlobal(u16)` · `LoadLocal/StoreLocal(u16)` · `MakeRefGlobal/MakeRefLocal(u16)` · `LoadRef/StoreRef(u16)` | Slots statisch aufgelöst; `*Ref` bedienen BYREF-Parameter (Referenz-Werte) | | Arrays | `DimGlobal/DimLocal{slot,dims,elem}` `RedimGlobal/RedimLocal{…}` `EraseGlobal/EraseLocal(u16)` · `LoadElem/StoreElem{dims:u8}` · `MakeRefElem{dims:u8}` | Grenzen auf dem Stack (lo/hi je Dimension als LONG); Indexprüfung → Fehler 9 | | UDT | `LoadField/StoreField(u16)` · `MakeRefField(u16)` | Feldindex aus Typtabelle; verschachtelt durch Verkettung | | Arithmetik | `Add/Sub/Mul{I2,I4,R4,R8,CY}` · `Neg{…}` · `Div{R4,R8}` · `IDiv/Mod{I2,I4}` · `PowR8` (`^` rechnet in DOUBLE, SINGLE-Ergebnis per Conv) | monomorph; Ganzzahl-/CY-Überlauf → Fehler 6, Division durch 0 → Fehler 11 | | Konvertierung | `Conv__` für alle 20 geordneten Paare aus {I2,I4,R4,R8,CY} | Semantik exakt nach Matrix (Rundung/Überlauf) | | Logik | `Not/And/Or/Xor/Eqv/Imp{I2,I4}` | bitweise | | Vergleich | `Cmp{Eq,Ne,Lt,Le,Gt,Ge}{I2,I4,R4,R8,CY,STR}` | Ergebnis INTEGER −1/0 | | Strings | `Concat` | `Rc`, Kopien sind Pointer-Kopien | | Kontrollfluss | `Jump(u32)` `JumpIfFalse(u32)` `JumpIfTrue(u32)` | Ziele absolut (Instruktionsindex) innerhalb der Code-Einheit; Fixups beim Emit | | GOSUB | `Gosub(u32)` `RetGosub` `RetGosubTo(u32)` | GOSUB-Stack pro Frame; leer → Fehler 3 | | Berechnete Sprünge | `OnJump{table:u16,gosub:bool}` | Sprungtabellen im Modul; 0/zu groß: kein Sprung; negativ/>255 → Fehler 5 | | Prozeduren | `Call(proc:u16)` `RetProc` `RetFn` `CallBuiltin{id:u16,argc:u8}` | Argumente links→rechts auf dem Stack (Werte oder Referenzen); `RetFn` transportiert den Funktionswert über den Frame-Abbau | | Fehler | `OnErrorGoto(u32)` `OnErrorLocal(u32)` `OnErrorDisable` `OnErrorLocalDisable` · `Resume0` `ResumeNext` `ResumeLabel(u32)` · `RaiseError` | Scoping siehe unten | | DATA | `ReadData(art)` `Restore(u32)` | art 0 = String, 1 = Zahl (DOUBLE, dann Conv nach Matrix); Ende → Fehler 4, unkonvertierbar → Fehler 13 | | E/A | `Input{…}` sowie Builtins (`PRINT`-Familie über `CallBuiltin`) | Konsolenwirkung ausschließlich über das `Host`-Trait | | Ende | `End` `StopInstr` `SystemInstr` | Verhalten siehe Runner | 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. `… 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.