- Sema zum Lowering-Pass umgebaut: typisiertes HIR (Slots, explizite Konvertierungsknoten) als Codegen-Eingabe; BYREF verlangt exakten Typ - Bytecode-Feindesign umgesetzt: monomorpher Opcode-Satz, .tbc-Container (Formatversion 1) mit eigenem Writer/Reader - Codegenerator HIR -> Bytecode (Fixup-Listen, keine globalen Passes) - TBVM-Interpreter: Kontrollfluss, GOSUB-Stack je Frame, BYREF/BYVAL, STATIC, DEF FN, DATA/READ/RESTORE, ON [LOCAL] ERROR/RESUME/ERR/ERL, Breakpoints/Einzelschritt/Inspektion, STOP fortsetzbar - Runtime-Scheibe: Host-Trait (Konsole/Capture), Builtin-Tabelle, Konvertierungsmatrix, PRINT-Formatierung/Druckzonen, Stringfunktionen - tbc run/build/check mit Exit-Codes nach Entscheidung D6 - Korpus-Harness (byte-genauer Vergleich) + 3 neue Korpusdateien (konvertierung, fehlerbehandlung, byref); 137 Tests gruen - Benchmarks: Einzelmodul 1,2 ms / Projekt 49.760 Zeilen 124 ms (Budgets eingehalten), VM ~5 Mio Schleifeniterationen/s - Doku fortgeschrieben (tbvm-design, sprachreferenz, PLAN); verlagerte Punkte als explizite Aufgaben in Phase 3 - OpenSpec-Change phase-2-bytecode-vm (27/27 Tasks) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
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):
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) →
Rcgenü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: Pro Frame ein Handler-Zustand (
ON ERROR GOTO x). Laufzeitfehler → VM sucht aktiven Handler im aktuellen Modulkontext (Vorbild: Handler sind modul-/prozedurlokal, TODO: exakte Scoping-Regel testen), setztERR/ERL, springt.RESUMEnutzt gemerkten Anweisungs-Offset.
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/.FRMwerden 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 überValue-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
- 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"). Zellen ohne belastbare Fundstelle sind mit
TODO verify markiert; 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), damit keine Festkomma-Präzision in f32 verloren geht
(TODO verify gegen PDS-Hilfe; plausibelste Lesart).
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 (TODO verify) |
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 ExponentialformmE±xx(exakte SchwelleTODO verify; umgesetzt: Exponentialform, wenn der Dezimalexponent < −7 oder ≥ 7 ist). - DOUBLE: wie SINGLE mit bis 16 signifikanten Stellen und
D±xxals Exponentmarke. - CURRENCY: Festformat mit bis zu 4 Nachkommastellen, nachgestellte
Nullen entfallen (
TODO verify). PRINTstellt 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<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.
| 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_<src>_<dst> 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<str>, 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 xsetzt den modulweiten Handler — auch aus einer Prozedur heraus.ON LOCAL ERROR GOTO xsetzt einen Frame-lokalen Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.… GOTO 0deaktiviert 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
RESUMEgemerkt,ERR/ERLgesetzt. Ohne Handler: Abbruch mit Meldung und Quellzeile. - Fehler während aktiver Behandlung (zwischen Handler-Eintritt und
RESUME): sofortiger Abbruch, keine Kaskade (Vorbild). ERLliefert die zuletzt durchlaufene numerische Zeilennummer (Labels zählen nicht; ohne numerische Zeilen: 0).
Runner-Verhalten STOP/CONT (Entscheidung 2026-09-02)
END,SYSTEMund das Erreichen des Programmendes beenden den Prozess mit Exit-Code 0.STOPaußerhalb der IDE terminiert mit der MeldungSTOP in line n(bzw. Modul/Zeile) und Exit-Code ≠ 0. Offene Dateien werden wie beiENDgeschlossen.CONTist ein Direktmodus-Konzept der IDE (Phase 5): dort suspendiertSTOPdie VM (step()-Architektur),CONTsetzt fort. Im Kompilat existiert keinCONT.
Offene Punkte
Keine — die Punkte aus Phase 0 (Opcode-Satz, Konvertierungsmatrix,
STOP/CONT im Runner) sind oben ausgearbeitet; verbleibende
Detailunsicherheiten sind als TODO verify direkt an den betroffenen
Zellen markiert und über den Testkorpus abgesichert.