Phase 2 abgeschlossen: Bytecode, TBVM, Runtime-Scheibe, tbc run

- 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>
This commit is contained in:
2026-09-02 11:28:07 +02:00
parent da23d52036
commit f7e57b0bd8
42 changed files with 9225 additions and 484 deletions

View File

@@ -48,6 +48,9 @@ pub enum Expr {
},
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
/// Geklammerter Ausdruck. Semantisch transparent, aber als Argument
/// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF.
Paren(Box<Expr>),
/// Ausgelassenes Argument (`LOCATE , 5`).
Missing,
}
@@ -58,6 +61,7 @@ impl Expr {
Expr::Name { pos, .. }
| Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } => *pos,
Expr::Paren(e) => e.pos(),
_ => SourcePos::default(),
}
}
@@ -242,9 +246,9 @@ pub enum Stmt {
Gosub { target: LabelRef, pos: SourcePos },
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
Return { target: Option<LabelRef>, pos: SourcePos },
End,
StopStmt,
System,
End(SourcePos),
StopStmt(SourcePos),
System(SourcePos),
Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).

View File

@@ -0,0 +1,459 @@
//! Typisiertes, abgesenktes HIR — die Ausgabe der semantischen Analyse
//! und Eingabe des Codegenerators (`tb-vm`).
//!
//! Eigenschaften (siehe Design der Phase-2-Änderung):
//! - Namen sind aufgelöst: Variablen sind Slot-Indizes (global/lokal),
//! Prozeduren und UDTs Tabellenindizes, Sprungziele `LabelId`s je Rumpf.
//! - Jeder Ausdrucksknoten trägt seinen Ergebnistyp; implizite
//! Konvertierungen sind als explizite `Conv`-Knoten materialisiert
//! (Semantik: Konvertierungsmatrix in docs/tbvm-design.md).
//! - Kontrollzucker ist abgesenkt: `SELECT CASE` zu Vergleichsketten,
//! `ELSEIF` zu verschachteltem `If`, `EXIT FOR/DO` zu `Goto` auf
//! synthetisierte Labels, `SWAP` zu Zuweisungen über einen Temp-Slot.
//! - `STATIC`-Locals und versteckte Temps liegen im globalen Slot-Bereich.
//!
//! Ein vollständiges, korrektes HIR ist nur bei diagnose-freier Analyse
//! garantiert.
/// Numerischer Skalartyp (Kürzel wie im Opcode-Satz: I2/I4/CY/R4/R8).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumTy {
Int,
Lng,
Cur,
Sng,
Dbl,
}
/// Ganzzahlbreite der Logik-Operatoren.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntKind {
I2,
I4,
}
/// Aufgelöster HIR-Typ.
#[derive(Debug, Clone, PartialEq)]
pub enum HTy {
Num(NumTy),
Str,
/// Fester String mit Zeichenlänge (Zuweisung padded/kürzt).
FixedStr(u32),
/// Benutzerdefinierter Typ (Index in `HirModule::udts`).
Udt(u16),
}
impl HTy {
pub fn num(&self) -> Option<NumTy> {
match self {
HTy::Num(n) => Some(*n),
_ => None,
}
}
pub fn is_str(&self) -> bool {
matches!(self, HTy::Str | HTy::FixedStr(_))
}
}
/// Slot-Referenz: globaler Bereich (Modulvariablen, STATICs, versteckte
/// Temps des Hauptprogramms) oder Frame-lokal (Parameter zuerst).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarSlot {
Global(u16),
Local(u16),
}
/// Sprungziel innerhalb eines Prozedurrumpfs.
pub type LabelId = u16;
/// Variablen-/Slotbeschreibung (auch für die Debugger-Inspektion).
#[derive(Debug, Clone)]
pub struct HVar {
pub name: String,
pub ty: HTy,
pub array: bool,
}
#[derive(Debug, Clone)]
pub struct HUdt {
pub name: String,
pub fields: Vec<(String, HTy)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HProcKind {
/// Hauptprogramm (Modulrumpf) — immer Prozedur 0.
Main,
Sub,
Function,
/// `DEF FN` — Parameter BYVAL, freie Namen binden an Modulvariablen.
DefFn,
}
#[derive(Debug, Clone)]
pub struct HParam {
pub name: String,
pub ty: HTy,
pub array: bool,
/// Skalar-Parameter, der als Referenz übergeben wird (Arrays und
/// UDTs sind implizit immer Referenzen auf ihr Handle).
pub by_ref: bool,
}
#[derive(Debug)]
pub struct HProc {
pub name: String,
pub kind: HProcKind,
pub params: Vec<HParam>,
/// Alle Frame-Slots; `params.len()` erste Einträge sind die Parameter.
pub locals: Vec<HVar>,
/// Slot der Rückgabevariablen (Function/DefFn).
pub ret_slot: Option<VarSlot>,
pub ret_ty: Option<HTy>,
pub body: Vec<HStmt>,
/// Anzahl vergebener LabelIds in diesem Rumpf.
pub label_count: u16,
}
/// Eine DATA-Konstante (unkonvertiert; `READ` konvertiert zur Laufzeit).
#[derive(Debug, Clone)]
pub struct DataItem {
pub text: String,
pub line: u32,
}
#[derive(Debug)]
pub struct HirModule {
pub name: String,
pub globals: Vec<HVar>,
pub udts: Vec<HUdt>,
/// Prozeduren; Index 0 ist das Hauptprogramm.
pub procs: Vec<HProc>,
pub data: Vec<DataItem>,
pub option_base: u8,
}
// ---- Ausdrücke -------------------------------------------------------------
/// L-Wert: Basis-Slot, optional Array-Indizes, optional UDT-Feldpfad.
#[derive(Debug, Clone)]
pub struct HPlace {
pub base: VarSlot,
/// Skalar-BYREF-Parameter: der Slot enthält eine Referenz.
pub base_is_ref: bool,
/// Array-Elementzugriff (leer = Skalar bzw. ganzes Array).
pub indices: Vec<HExpr>,
/// UDT-Feldpfad (Feldindizes je Ebene).
pub fields: Vec<u16>,
/// Typ des adressierten Werts.
pub ty: HTy,
/// Element-/Basistyp und Dimension für Auto-DIM impliziter Arrays.
pub array_elem: Option<(HTy, u8)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HArith {
Add,
Sub,
Mul,
Div,
IDiv,
Mod,
Pow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HCmp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HLogic {
And,
Or,
Xor,
Eqv,
Imp,
}
/// Vergleichs-Operandentyp.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmpKind {
Num(NumTy),
Str,
}
/// Bibliotheksfunktionen/-anweisungen der Phase-2-Scheibe. Der
/// Diskriminant ist zugleich der stabile Index der Dispatch-Tabelle
/// (`CALL_BUILTIN`); Phase 3 erweitert ausschließlich am Ende.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum Builtin {
// Strings
Len,
LeftS,
RightS,
MidS,
InstrF,
UcaseS,
LcaseS,
LtrimS,
RtrimS,
SpaceS,
StringS,
ChrS,
Asc,
StrS,
Val,
HexS,
OctS,
/// MID$-Anweisung als reine Funktion: (ziel, start, länge, ersatz) → neuer String.
MidAssign,
// Mathematik
Abs,
Sgn,
IntF,
Fix,
Sqr,
Exp,
Log,
Sin,
Cos,
Tan,
Atn,
Rnd,
Randomize,
// Konsole (PRINT-Familie; Wirkung über Host + Druckspalten-Zustand)
PrintVal,
PrintStrLit,
PrintComma,
PrintTab,
PrintSpc,
PrintNewline,
// Sonstiges
Timer,
DateS,
TimeS,
CommandS,
Doevents,
Sleep,
Beep,
}
#[derive(Debug, Clone)]
pub enum HExpr {
Int(i16),
Lng(i32),
Sng(f32),
Dbl(f64),
Cur(i64),
Str(String),
Load(Box<HPlace>),
/// Numerische Konvertierung nach Matrix (Rundung/Überlauf).
Conv {
from: NumTy,
to: NumTy,
arg: Box<HExpr>,
},
/// Kürzen/Padden auf feste Stringlänge.
FixStr {
len: u32,
arg: Box<HExpr>,
},
Neg {
ty: NumTy,
arg: Box<HExpr>,
},
/// Monomorphe Arithmetik: beide Operanden und das Ergebnis haben `ty`
/// (bei `Div`/`Pow` nur R4/R8, bei `IDiv`/`Mod` nur I2/I4).
Bin {
op: HArith,
ty: NumTy,
l: Box<HExpr>,
r: Box<HExpr>,
},
Not {
ty: IntKind,
arg: Box<HExpr>,
},
Logic {
op: HLogic,
ty: IntKind,
l: Box<HExpr>,
r: Box<HExpr>,
},
/// Vergleich; Ergebnis ist INTEGER (1/0).
Cmp {
op: HCmp,
ty: CmpKind,
l: Box<HExpr>,
r: Box<HExpr>,
},
Concat(Box<HExpr>, Box<HExpr>),
/// FUNCTION-/DEF FN-Aufruf.
FnCall {
proc: u16,
args: Vec<HArg>,
ret: HTy,
},
Builtin {
b: Builtin,
args: Vec<HExpr>,
ret: HTy,
},
/// LBOUND/UBOUND eines Arrays.
ArrayBound {
lower: bool,
place: Box<HPlace>,
dim: Box<HExpr>,
},
Err,
Erl,
/// Dokumentiertes, aber erst in einer späteren Phase implementiertes
/// Feature: löst zur Laufzeit Fehler 73 „Advanced feature" aus.
Unsupported(&'static str),
}
/// Prozedurargument.
#[derive(Debug, Clone)]
pub enum HArg {
ByRef(HPlace),
/// Ganzes Array als Referenz (`prozedur a()`).
ArrayRef(HPlace),
ByVal(HExpr),
}
// ---- Anweisungen -----------------------------------------------------------
#[derive(Debug, Clone)]
pub enum HPrintItem {
Val(HExpr),
Tab(HExpr),
Spc(HExpr),
Comma,
}
#[derive(Debug, Clone)]
pub enum HResume {
Retry,
Next,
Label(LabelId),
}
#[derive(Debug)]
pub struct HStmt {
pub line: u32,
pub kind: HStmtKind,
}
#[derive(Debug)]
pub enum HStmtKind {
/// Numerische Zeilennummer durchlaufen (setzt `ERL`).
SetErl(u32),
Label(LabelId),
Assign {
place: HPlace,
value: HExpr,
},
Print {
items: Vec<HPrintItem>,
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?
trailing: bool,
},
Input {
line_mode: bool,
prompt: Option<String>,
/// Fragezeichen nach dem Prompt (`;`-Form).
question: bool,
targets: Vec<HPlace>,
},
If {
cond: HExpr,
then: Vec<HStmt>,
els: Vec<HStmt>,
},
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
Loop {
pre: Option<(bool, HExpr)>,
post: Option<(bool, HExpr)>,
body: Vec<HStmt>,
exit_label: LabelId,
},
For {
var: HPlace,
ty: NumTy,
from: HExpr,
to: HExpr,
step: Option<HExpr>,
/// Versteckte Slots für Grenze/Schritt (Schritt nur wenn dynamisch).
limit_slot: VarSlot,
step_slot: Option<VarSlot>,
body: Vec<HStmt>,
exit_label: LabelId,
},
Goto(LabelId),
Gosub(LabelId),
OnGoto {
sel: HExpr,
gosub: bool,
targets: Vec<LabelId>,
},
ReturnGosub(Option<LabelId>),
/// EXIT SUB/FUNCTION/DEF bzw. Rumpfende.
ExitProc,
CallSub {
proc: u16,
args: Vec<HArg>,
},
BuiltinStmt {
b: Builtin,
args: Vec<HExpr>,
},
OnError {
local: bool,
/// `None` = `GOTO 0` (deaktivieren).
target: Option<LabelId>,
},
OnErrorResumeNext {
local: bool,
},
Resume(HResume),
/// `ERROR n`.
RaiseError(HExpr),
Read(Vec<HPlace>),
/// Ziel als Index in `HirModule::data` (0 = Anfang).
Restore(u32),
/// DIM/REDIM eines Arrays: Grenzen (lo, hi) je Dimension.
Dim {
slot: VarSlot,
elem: HTy,
dims: Vec<(HExpr, HExpr)>,
redim: bool,
},
Erase(Vec<VarSlot>),
End,
Stop,
System,
/// Dokumentiertes Feature einer späteren Phase → Laufzeitfehler 73.
Unsupported(&'static str),
}
/// Konstanter Literalwert eines Ausdrucks (z. B. FOR-STEP-Erkennung im
/// Codegen: konstanter Schritt braucht keinen versteckten Slot).
pub fn literal_value(e: &HExpr) -> Option<f64> {
match e {
HExpr::Int(v) => Some(*v as f64),
HExpr::Lng(v) => Some(*v as f64),
HExpr::Sng(v) => Some(*v as f64),
HExpr::Dbl(v) => Some(*v),
HExpr::Cur(v) => Some(*v as f64 / 10_000.0),
HExpr::Conv { arg, .. } => literal_value(arg),
HExpr::Neg { arg, .. } => literal_value(arg).map(|v| -v),
_ => None,
}
}

View File

@@ -6,6 +6,7 @@
//! übersetzt.
pub mod ast;
pub mod hir;
pub mod lexer;
pub mod parser;
pub mod sema;
@@ -35,15 +36,18 @@ impl std::fmt::Display for Diagnostic {
pub struct Analysis {
pub module: ast::Module,
pub diagnostics: Vec<Diagnostic>,
/// Typisiertes HIR (vollständig nur bei leeren Diagnosen).
pub hir: Option<hir::HirModule>,
}
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung.
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung + Lowering.
pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
let lexed = lexer::lex(source);
let mut diagnostics = lexed.diagnostics;
let parsed = parser::parse(module_name, &lexed.tokens);
diagnostics.extend(parsed.diagnostics);
diagnostics.extend(sema::check(&parsed.module));
let (hir, sema_diags) = sema::lower(&parsed.module);
diagnostics.extend(sema_diags);
diagnostics.sort_by_key(|d| (d.pos.line, d.pos.column));
Analysis { module: parsed.module, diagnostics }
Analysis { module: parsed.module, diagnostics, hir }
}

View File

@@ -285,15 +285,15 @@ impl<'a> P<'a> {
return None;
}
self.advance();
Some(Stmt::End)
Some(Stmt::End(pos))
}
TokenKind::Kw(Kw::Stop) => {
self.advance();
Some(Stmt::StopStmt)
Some(Stmt::StopStmt(pos))
}
TokenKind::Kw(Kw::System) => {
self.advance();
Some(Stmt::System)
Some(Stmt::System(pos))
}
TokenKind::Kw(Kw::Exit) => {
self.advance();
@@ -1498,7 +1498,16 @@ impl<'a> P<'a> {
}
// Impliziter Aufruf: `name [arg [, arg …]]`
if let Expr::Name { name, suffix, args, .. } = target {
let mut call_args = args.unwrap_or_default();
// Ohne CALL-Keyword sind Klammern Wert-Klammern (BYVAL), keine
// Argumentlisten-Klammern: `Foo (n%)` übergibt `(n%)`.
let mut call_args: Vec<Expr> = args
.unwrap_or_default()
.into_iter()
.map(|a| match a {
p @ Expr::Paren(_) => p,
other => Expr::Paren(Box::new(other)),
})
.collect();
if !self.at_stmt_end() && call_args.is_empty() {
call_args = self.parse_arg_list_to_stmt_end();
}
@@ -1722,7 +1731,7 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) {
self.err("Expected: )");
}
Some(e)
Some(Expr::Paren(Box::new(e)))
}
TokenKind::Ident { .. } => self.parse_name_ref(),
_ => {

File diff suppressed because it is too large Load Diff