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

1
Cargo.lock generated
View File

@@ -508,6 +508,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"log", "log",
"tb-frontend", "tb-frontend",
"tb-runtime",
"thiserror", "thiserror",
] ]

57
PLAN.md
View File

@@ -212,21 +212,38 @@ gefundene Frontend-Lücken sind Bugs im Sinne des Guiding Principle und
werden sofort geschlossen (kein Wiedereröffnen der Phase nötig). werden sofort geschlossen (kein Wiedereröffnen der Phase nötig).
## Phase 2 — Bytecode und VM (`tb-vm`) ## Phase 2 — Bytecode und VM (`tb-vm`)
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
**Status: abgeschlossen (2026-09-02).** OpenSpec-Änderung
`phase-2-bytecode-vm` (Proposal/Design/Specs/Tasks). Zusätzliche
Entscheidung: Die Sema liefert ein **typisiertes HIR** (Namen → Slots,
Konvertierungen explizit) als Codegen-Eingabe; der Codegenerator ist ein
reiner Tree-Walk.
- [x] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
Opcode-Satz, Zahlenkonvertierungs-Matrix (implizite Casts, Rundung, Opcode-Satz, Zahlenkonvertierungs-Matrix (implizite Casts, Rundung,
Überlauf), `STOP`/`CONT`-Verhalten im Runner ohne IDE Überlauf), `STOP`/`CONT`-Verhalten im Runner ohne IDE
(offene Punkte in docs/tbvm-design.md abarbeiten) (offene Punkte in docs/tbvm-design.md abgearbeitet)
- [ ] Bytecode-Format und Serialisierung (`.tbc`) - [x] Bytecode-Format und Serialisierung (`.tbc`, Formatversion 1;
- [ ] Codegenerator AST → Bytecode Roundtrip- und Versionsablehnungs-Tests)
- [ ] Interpreter: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`, Schleifen, - [x] Codegenerator HIR → Bytecode (monomorphe Opcodes, Fixup-Listen)
`GOTO`/`GOSUB`), Prozeduraufrufe, `BYREF`-Semantik - [x] Interpreter: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`, Schleifen,
- [ ] Fehlerbehandlung: `ON ERROR GOTO/RESUME`, `ERR`/`ERL`, Fehlerkaskaden `GOTO`/`GOSUB`), Prozeduraufrufe, `BYREF`-Semantik (inkl.
- [ ] Unterbrechbarkeit: Tick-Grenzen, Breakpoints, Einzelschritt, Klammer-BYVAL, STATIC, Rekursion, DEF FN, DATA/READ/RESTORE)
Variableninspektion (Grundlage für IDE-Debugger) - [x] Fehlerbehandlung: `ON [LOCAL] ERROR GOTO/RESUME`, `ERR`/`ERL`,
- [ ] Benchmarks in `benches/`: Compile-Budget messen (Projekt ≈50k Zeilen Fehler im Handler fatal (keine Kaskaden), Propagation über Frames
< 1 s, einzelnes Modul < 50 ms) und VM-Durchsatz (Schleifen/Strings); - [x] Unterbrechbarkeit: Tick-Grenzen, Breakpoints, Einzelschritt,
Ergebnisse in docs/tbvm-design.md festhalten Variableninspektion (Grundlage für IDE-Debugger); `STOP` ist in
- [ ] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe (`tbc run`) der Einbettung fortsetzbar (CONT-Grundlage)
- [x] Benchmarks in `benches/`: Compile-Budget gemessen (49 760 Zeilen in
124 ms; Einzelmodul 1,2 ms — Budgets eingehalten) und VM-Durchsatz
(≈5 Mio Schleifeniterationen/s); Ergebnisse in docs/tbvm-design.md
- [x] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe
(`tbc run` + Harness `crates/tb-cli/tests/compat.rs`; Korpus um
konvertierung/fehlerbehandlung/byref erweitert)
Vorgezogen aus Phase 3 (Meilenstein-Voraussetzung): `Host`-Trait
(Konsole/Capture), Builtin-Dispatch-Tabelle, PRINT-Formatierung mit
Druckzonen sowie die Korpus-Stringfunktionen (`tb-runtime`).
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`) ## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`)
@@ -261,6 +278,20 @@ Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
`CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer `CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer
- [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros - [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros
(`KEY LIST`/`ON`/`OFF`) und in docs/sprachreferenz.md festhalten (`KEY LIST`/`ON`/`OFF`) und in docs/sprachreferenz.md festhalten
- [ ] Aus Phase 2 verlagert — `ON ERROR GOTO`-Scoping des Vorbilds im
Frontend abbilden: Handler-Label auf Modulebene ist auch aus
Prozeduren heraus ansprechbar (derzeit wird nicht-lokales
`ON ERROR` in einer Prozedur wie `ON LOCAL ERROR` behandelt,
siehe sema.rs, lower_stmt/OnError)
- [ ] Aus Phase 2 verlagert — DATA-Rohtext erhalten: unquotierte
DATA-Elemente verlieren derzeit die Groß-/Kleinschreibung, weil
der Lexer Bezeichner normalisiert; Rohtext-Erfassung im Lexer,
Korpustest ergänzen
- [ ] Aus Phase 2 verlagert — `TODO verify`-Zellen der
Konvertierungsmatrix (docs/tbvm-design.md) gegen die
Original-Hilfe auflösen: CURRENCY-Promotion mit Gleitkomma,
`^`-Ergebnistyp, Exponentialform-Schwellen der Textdarstellung,
CURRENCY-Ausgabeformat; zugehörige Korpustests nachschärfen
- [ ] Meilenstein: klassische Konsolenprogramme laufen unverändert - [ ] Meilenstein: klassische Konsolenprogramme laufen unverändert
## Phase 4 — Forms-Engine (`tb-ui::forms`) ## Phase 4 — Forms-Engine (`tb-ui::forms`)

View File

@@ -1,13 +1,110 @@
//! `tbc` — Standalone-Compiler von Terminal Basic. //! `tbc` — Standalone-Compiler von Terminal Basic.
//! //!
//! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode //! Unterbefehle (Phase 2):
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle //! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
//! (siehe PLAN.md): //! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
//! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis //! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen //!
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung //! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
//! 1 = Compile-Fehler/Bedienfehler.
fn main() -> anyhow::Result<()> { use std::path::{Path, PathBuf};
println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)"); use std::process::ExitCode;
Ok(()) use tb_runtime::host::ConsoleHost;
use tb_vm::interp::{RunEvent, Vm};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("run") => cmd_run(&args[1..]),
Some("build") => cmd_build(&args[1..]),
Some("check") => cmd_check(&args[1..]),
_ => {
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
ExitCode::from(1)
}
}
}
fn module_name(path: &Path) -> String {
path.file_stem()
.map(|s| s.to_string_lossy().to_uppercase())
.unwrap_or_else(|| "MODUL".into())
}
fn compile(path_arg: Option<&String>) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
let Some(path) = path_arg else {
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
return Err(ExitCode::from(1));
};
let path = PathBuf::from(path);
let source = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("{}: {e}", path.display());
return Err(ExitCode::from(1));
}
};
match tb_vm::compile_source(&module_name(&path), &source) {
Ok(m) => Ok((path, m)),
Err(diags) => {
for d in &diags {
eprintln!("{}:{d}", path.display());
}
eprintln!("{} Fehler.", diags.len());
Err(ExitCode::from(1))
}
}
}
fn cmd_check(args: &[String]) -> ExitCode {
match compile(args.first()) {
Ok(_) => ExitCode::SUCCESS,
Err(code) => code,
}
}
fn cmd_build(args: &[String]) -> ExitCode {
let (path, module) = match compile(args.first()) {
Ok(x) => x,
Err(code) => return code,
};
let out = path.with_extension("tbc");
match std::fs::write(&out, module.to_tbc()) {
Ok(()) => {
println!("{}", out.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{}: {e}", out.display());
ExitCode::from(1)
}
}
}
fn cmd_run(args: &[String]) -> ExitCode {
let (_path, module) = match compile(args.first()) {
Ok(x) => x,
Err(code) => return code,
};
let mut vm = Vm::new(module);
vm.rt.command = args[1..].join(" ");
let mut host = ConsoleHost;
match vm.run(&mut host) {
RunEvent::Ended => ExitCode::SUCCESS,
RunEvent::Stopped { line } => {
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
eprintln!("STOP in line {line}");
ExitCode::from(3)
}
RunEvent::Error { code, line, message } => {
eprintln!("Runtime error {code}: {message} in line {line}");
ExitCode::from(2)
}
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } | RunEvent::Interrupted { .. } => {
ExitCode::from(2)
}
}
} }

View File

@@ -0,0 +1,132 @@
//! Kompatibilitäts-Harness (Phase-2-Meilenstein): jede Korpusdatei
//! `tests/compat/*.bas` wird kompiliert, im Capture-Host ausgeführt und
//! byte-genau gegen ihre `.out` verglichen. Bei Abweichung nennt der
//! Test Datei, erste abweichende Zeile sowie Soll und Ist.
use std::path::{Path, PathBuf};
use std::process::Command;
use tb_runtime::host::CaptureHost;
use tb_vm::interp::{RunEvent, Vm};
fn compat_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat")
}
fn run_corpus_file(path: &Path) -> String {
let src = std::fs::read_to_string(path).unwrap();
let name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
let module = tb_vm::compile_source(&name, &src)
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
match vm.run(&mut host) {
RunEvent::Ended => host.output,
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nAusgabe bisher:\n{}",
path.display(),
host.output
),
}
}
/// Erste abweichende Zeile melden (byte-genau, inkl. Leerzeichen am Ende).
fn assert_output_matches(file: &str, want: &str, got: &str) {
if want == got {
return;
}
let want_lines: Vec<&str> = want.split('\n').collect();
let got_lines: Vec<&str> = got.split('\n').collect();
for (i, (w, g)) in want_lines.iter().zip(got_lines.iter()).enumerate() {
if w != g {
panic!(
"{file}: Abweichung in Zeile {}:\n Soll: {w:?}\n Ist: {g:?}",
i + 1
);
}
}
panic!(
"{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want}\nIst:\n{got}",
want_lines.len(),
got_lines.len()
);
}
#[test]
fn korpus_laeuft_mit_korrekter_ausgabe() {
let dir = compat_dir();
let mut checked = 0;
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
.collect();
entries.sort();
for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let out_path = path.with_extension("out");
let want = std::fs::read_to_string(&out_path)
.unwrap_or_else(|_| panic!("{name}: Sollausgabe {} fehlt", out_path.display()));
// .out-Dateien sind LF-normiert (.gitattributes); zur Sicherheit
// CRLF des Checkouts entfernen.
let want = want.replace("\r\n", "\n");
let got = run_corpus_file(&path);
assert_output_matches(&name, &want, &got);
checked += 1;
}
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
}
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
#[test]
fn tbc_run_hello() {
let exe = env!("CARGO_BIN_EXE_tbc");
let out = Command::new(exe)
.args(["run"])
.arg(compat_dir().join("hello.bas"))
.output()
.expect("tbc startet");
assert!(out.status.success(), "{out:?}");
assert_eq!(String::from_utf8_lossy(&out.stdout), "Hallo, Welt!\n");
}
#[test]
fn tbc_run_stop_exitcode() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_stop_test.bas");
std::fs::write(&f, "PRINT \"x\"\nSTOP\n").unwrap();
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
assert_eq!(out.status.code(), Some(3), "{out:?}");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("STOP in line 2"), "{err}");
let _ = std::fs::remove_file(&f);
}
#[test]
fn tbc_run_laufzeitfehler_exitcode() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_err_test.bas");
std::fs::write(&f, "i% = 40000\n").unwrap();
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
assert_eq!(out.status.code(), Some(2), "{out:?}");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Overflow"), "{err}");
let _ = std::fs::remove_file(&f);
}
#[test]
fn tbc_build_erzeugt_tbc() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_build_test.bas");
std::fs::write(&f, "PRINT 1\n").unwrap();
let out = Command::new(exe).args(["build"]).arg(&f).output().unwrap();
assert!(out.status.success(), "{out:?}");
let tbc = f.with_extension("tbc");
let bytes = std::fs::read(&tbc).unwrap();
assert_eq!(&bytes[..4], b"TBC\0");
let _ = std::fs::remove_file(&f);
let _ = std::fs::remove_file(&tbc);
}

View File

@@ -48,6 +48,9 @@ pub enum Expr {
}, },
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos }, Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
Binary { op: BinOp, lhs: Box<Expr>, rhs: 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`). /// Ausgelassenes Argument (`LOCATE , 5`).
Missing, Missing,
} }
@@ -58,6 +61,7 @@ impl Expr {
Expr::Name { pos, .. } Expr::Name { pos, .. }
| Expr::Unary { pos, .. } | Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } => *pos, | Expr::Binary { pos, .. } => *pos,
Expr::Paren(e) => e.pos(),
_ => SourcePos::default(), _ => SourcePos::default(),
} }
} }
@@ -242,9 +246,9 @@ pub enum Stmt {
Gosub { target: LabelRef, pos: SourcePos }, Gosub { target: LabelRef, pos: SourcePos },
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos }, OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
Return { target: Option<LabelRef>, pos: SourcePos }, Return { target: Option<LabelRef>, pos: SourcePos },
End, End(SourcePos),
StopStmt, StopStmt(SourcePos),
System, System(SourcePos),
Exit { kind: ExitKind, pos: SourcePos }, Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos }, Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen). /// `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. //! übersetzt.
pub mod ast; pub mod ast;
pub mod hir;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod sema; pub mod sema;
@@ -35,15 +36,18 @@ impl std::fmt::Display for Diagnostic {
pub struct Analysis { pub struct Analysis {
pub module: ast::Module, pub module: ast::Module,
pub diagnostics: Vec<Diagnostic>, 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 { pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
let lexed = lexer::lex(source); let lexed = lexer::lex(source);
let mut diagnostics = lexed.diagnostics; let mut diagnostics = lexed.diagnostics;
let parsed = parser::parse(module_name, &lexed.tokens); let parsed = parser::parse(module_name, &lexed.tokens);
diagnostics.extend(parsed.diagnostics); 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)); 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; return None;
} }
self.advance(); self.advance();
Some(Stmt::End) Some(Stmt::End(pos))
} }
TokenKind::Kw(Kw::Stop) => { TokenKind::Kw(Kw::Stop) => {
self.advance(); self.advance();
Some(Stmt::StopStmt) Some(Stmt::StopStmt(pos))
} }
TokenKind::Kw(Kw::System) => { TokenKind::Kw(Kw::System) => {
self.advance(); self.advance();
Some(Stmt::System) Some(Stmt::System(pos))
} }
TokenKind::Kw(Kw::Exit) => { TokenKind::Kw(Kw::Exit) => {
self.advance(); self.advance();
@@ -1498,7 +1498,16 @@ impl<'a> P<'a> {
} }
// Impliziter Aufruf: `name [arg [, arg …]]` // Impliziter Aufruf: `name [arg [, arg …]]`
if let Expr::Name { name, suffix, args, .. } = target { 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() { if !self.at_stmt_end() && call_args.is_empty() {
call_args = self.parse_arg_list_to_stmt_end(); call_args = self.parse_arg_list_to_stmt_end();
} }
@@ -1722,7 +1731,7 @@ impl<'a> P<'a> {
if !self.eat(&TokenKind::RParen) { if !self.eat(&TokenKind::RParen) {
self.err("Expected: )"); self.err("Expected: )");
} }
Some(e) Some(Expr::Paren(Box::new(e)))
} }
TokenKind::Ident { .. } => self.parse_name_ref(), TokenKind::Ident { .. } => self.parse_name_ref(),
_ => { _ => {

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,14 @@ fn korpus_parst_und_wird_typgeprueft() {
"{name}: {:?}", "{name}: {:?}",
analysis.diagnostics analysis.diagnostics
); );
// Phase 2: diagnose-freie Module liefern ein vollständiges HIR
// (Hauptprogramm vorhanden, Codegen-fähig ohne erneute Auflösung).
let hir = analysis.hir.unwrap_or_else(|| panic!("{name}: HIR fehlt"));
assert!(!hir.procs.is_empty(), "{name}: HIR ohne Hauptprogramm");
assert!(
!hir.procs[0].body.is_empty(),
"{name}: leerer HIR-Hauptrumpf"
);
checked += 1; checked += 1;
} }
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}"); assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");

View File

@@ -0,0 +1,623 @@
//! Builtin-Dispatch-Tabelle (`CALL_BUILTIN`-ABI): Argumente kommen vom
//! Operandenstack der VM, der Index steht im Opcode. Die Tabelle ist ohne
//! Änderung am Opcode-Satz erweiterbar — Phase 3 füllt sie auf.
//!
//! Die Indizes (`ids::*`) sind stabil; der Codegenerator (`tb-vm`)
//! bildet `hir::Builtin` über ein erschöpfendes `match` darauf ab.
use crate::console::PrintState;
use crate::errors::RuntimeError;
use crate::format;
use crate::host::Host;
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
use std::rc::Rc;
/// Laufzeitzustand der Bibliothek (PRNG, Druckspalte, Kommandozeile).
pub struct RtState {
pub print: PrintState,
rng: u32,
rnd_last: f32,
pub command: String,
}
impl Default for RtState {
fn default() -> Self {
RtState {
print: PrintState::default(),
// Startzustand des Vorbild-PRNG; die exakte
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
// („RND/RANDOMIZE — kompatibler PRNG").
rng: 0x50000,
rnd_last: 0.0,
command: String::new(),
}
}
}
impl RtState {
fn rng_next(&mut self) -> f32 {
self.rng = self.rng.wrapping_mul(0xFD43FD).wrapping_add(0xC39EC3) & 0xFF_FFFF;
self.rnd_last = self.rng as f32 / 16_777_216.0;
self.rnd_last
}
}
pub type BuiltinFn =
fn(&mut RtState, &mut dyn Host, &mut [Value]) -> Result<Option<Value>, RuntimeError>;
/// Stabile Tabellenindizes (Ordnung = `hir::Builtin` des Frontends).
pub mod ids {
pub const LEN: u16 = 0;
pub const LEFT_S: u16 = 1;
pub const RIGHT_S: u16 = 2;
pub const MID_S: u16 = 3;
pub const INSTR: u16 = 4;
pub const UCASE_S: u16 = 5;
pub const LCASE_S: u16 = 6;
pub const LTRIM_S: u16 = 7;
pub const RTRIM_S: u16 = 8;
pub const SPACE_S: u16 = 9;
pub const STRING_S: u16 = 10;
pub const CHR_S: u16 = 11;
pub const ASC: u16 = 12;
pub const STR_S: u16 = 13;
pub const VAL: u16 = 14;
pub const HEX_S: u16 = 15;
pub const OCT_S: u16 = 16;
pub const MID_ASSIGN: u16 = 17;
pub const ABS: u16 = 18;
pub const SGN: u16 = 19;
pub const INT_F: u16 = 20;
pub const FIX: u16 = 21;
pub const SQR: u16 = 22;
pub const EXP: u16 = 23;
pub const LOG: u16 = 24;
pub const SIN: u16 = 25;
pub const COS: u16 = 26;
pub const TAN: u16 = 27;
pub const ATN: u16 = 28;
pub const RND: u16 = 29;
pub const RANDOMIZE: u16 = 30;
pub const PRINT_VAL: u16 = 31;
pub const PRINT_STR_LIT: u16 = 32;
pub const PRINT_COMMA: u16 = 33;
pub const PRINT_TAB: u16 = 34;
pub const PRINT_SPC: u16 = 35;
pub const PRINT_NEWLINE: u16 = 36;
pub const TIMER: u16 = 37;
pub const DATE_S: u16 = 38;
pub const TIME_S: u16 = 39;
pub const COMMAND_S: u16 = 40;
pub const DOEVENTS: u16 = 41;
pub const SLEEP: u16 = 42;
pub const BEEP: u16 = 43;
pub const COUNT: u16 = 44;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
pub fn builtin_table() -> &'static [BuiltinFn] {
const TABLE: &[BuiltinFn] = &[
bi_len,
bi_left,
bi_right,
bi_mid,
bi_instr,
bi_ucase,
bi_lcase,
bi_ltrim,
bi_rtrim,
bi_space,
bi_string,
bi_chr,
bi_asc,
bi_str,
bi_val,
bi_hex,
bi_oct,
bi_mid_assign,
bi_abs,
bi_sgn,
bi_int,
bi_fix,
bi_sqr,
bi_exp,
bi_log,
bi_sin,
bi_cos,
bi_tan,
bi_atn,
bi_rnd,
bi_randomize,
bi_print_val,
bi_print_val, // PRINT_STR_LIT: identisch (Strings per Tag)
bi_print_comma,
bi_print_tab,
bi_print_spc,
bi_print_newline,
bi_timer,
bi_date,
bi_time,
bi_command,
bi_doevents,
bi_sleep,
bi_beep,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
}
// ---- Argument-Hilfen --------------------------------------------------------
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
match args.get(i) {
Some(Value::Str(s)) => Ok(s.clone()),
_ => Err(RuntimeError::TYPE_MISMATCH),
}
}
fn arg_i32(args: &[Value], i: usize) -> Result<i32, RuntimeError> {
match args.get(i) {
Some(Value::Lng(v)) => Ok(*v),
Some(Value::Int(v)) => Ok(*v as i32),
Some(v) => Ok(as_f64(v) as i32),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn arg_f64(args: &[Value], i: usize) -> Result<f64, RuntimeError> {
match args.get(i) {
Some(v) => Ok(as_f64(v)),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn s_ok(s: String) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Str(Rc::from(s.as_str()))))
}
// ---- Strings ----------------------------------------------------------------
fn bi_len(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = s.chars().count();
Ok(Some(Value::Int(i16::try_from(n).unwrap_or(i16::MAX))))
}
fn bi_left(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = arg_i32(a, 1)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
s_ok(s.chars().take(n as usize).collect())
}
fn bi_right(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = arg_i32(a, 1)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let len = s.chars().count();
let skip = len.saturating_sub(n as usize);
s_ok(s.chars().skip(skip).collect())
}
fn bi_mid(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let start = arg_i32(a, 1)?;
let len = arg_i32(a, 2)?; // -1 = Rest
if start < 1 || len < -1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let iter = s.chars().skip((start - 1) as usize);
if len < 0 {
s_ok(iter.collect())
} else {
s_ok(iter.take(len as usize).collect())
}
}
fn bi_instr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let start = arg_i32(a, 0)?;
let s = arg_str(a, 1)?;
let t = arg_str(a, 2)?;
if start < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let chars: Vec<char> = s.chars().collect();
let slen = chars.len();
if start as usize > slen {
// Vorbild: Start hinter Stringende → 0
return Ok(Some(Value::Int(0)));
}
if t.is_empty() {
return Ok(Some(Value::Int(start as i16)));
}
let hay: String = chars[(start - 1) as usize..].iter().collect();
match hay.find(&*t) {
Some(byte_pos) => {
let char_pos = hay[..byte_pos].chars().count();
Ok(Some(Value::Int((start as usize + char_pos) as i16)))
}
None => Ok(Some(Value::Int(0))),
}
}
fn bi_ucase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.to_uppercase())
}
fn bi_lcase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.to_lowercase())
}
fn bi_ltrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.trim_start_matches(' ').to_string())
}
fn bi_rtrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.trim_end_matches(' ').to_string())
}
fn bi_space(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
s_ok(" ".repeat(n as usize))
}
fn bi_string(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let ch = match a.get(1) {
Some(Value::Str(s)) => s.chars().next().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?,
Some(v) => {
let code = as_f64(v) as i64;
char::from_u32(u32::try_from(code).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?)
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?
}
None => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
};
s_ok(ch.to_string().repeat(n as usize))
}
fn bi_chr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
let ch = u32::try_from(n)
.ok()
.and_then(char::from_u32)
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
s_ok(ch.to_string())
}
fn bi_asc(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
match s.chars().next() {
// Codepoints > 32767 passen nicht in INTEGER → Overflow wie beim
// Vorbild bei Bereichsüberschreitung.
Some(c) => i16::try_from(c as u32)
.map(|v| Some(Value::Int(v)))
.map_err(|_| RuntimeError::OVERFLOW),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn bi_str(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
s_ok(format::format_str_fn(v))
}
fn bi_val(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
Ok(Some(Value::Dbl(format::val(&s))))
}
fn bi_hex(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
s_ok(format!("{:X}", n as u32))
}
fn bi_oct(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
s_ok(format!("{:o}", n as u32))
}
/// MID$-Anweisung als Funktion: (ziel, start, länge, ersatz) → neuer
/// String; die Länge des Ziels bleibt unverändert.
fn bi_mid_assign(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let target = arg_str(a, 0)?;
let start = arg_i32(a, 1)?;
let len = arg_i32(a, 2)?; // -1 = Länge des Ersatzes
let repl = arg_str(a, 3)?;
if start < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let tchars: Vec<char> = target.chars().collect();
let rchars: Vec<char> = repl.chars().collect();
let start0 = (start - 1) as usize;
if start0 >= tchars.len() {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let max_repl = if len < 0 { rchars.len() } else { (len as usize).min(rchars.len()) };
let n = max_repl.min(tchars.len() - start0);
let mut out = tchars.clone();
out[start0..start0 + n].copy_from_slice(&rchars[..n]);
s_ok(out.into_iter().collect())
}
// ---- Mathematik --------------------------------------------------------------
fn bi_abs(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
Value::Int(v) => Value::Int(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
Value::Lng(v) => Value::Lng(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
Value::Sng(v) => Value::Sng(v.abs()),
Value::Dbl(v) => Value::Dbl(v.abs()),
Value::Cur(v) => Value::Cur(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_sgn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
Ok(Some(Value::Int(if x > 0.0 {
1
} else if x < 0.0 {
-1
} else {
0
})))
}
fn bi_int(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
Value::Sng(v) => Value::Sng(v.floor()),
Value::Dbl(v) => Value::Dbl(v.floor()),
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).floor())?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_fix(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
Value::Sng(v) => Value::Sng(v.trunc()),
Value::Dbl(v) => Value::Dbl(v.trunc()),
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).trunc())?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_sqr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
if x < 0.0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(x.sqrt())))
}
fn bi_exp(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
let r = x.exp();
if !r.is_finite() {
return Err(RuntimeError::OVERFLOW);
}
Ok(Some(Value::Dbl(r)))
}
fn bi_log(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
if x <= 0.0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(x.ln())))
}
fn bi_sin(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.sin())))
}
fn bi_cos(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.cos())))
}
fn bi_tan(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.tan())))
}
fn bi_atn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.atan())))
}
fn bi_rnd(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = if a.is_empty() {
st.rng_next()
} else {
let x = arg_f64(a, 0)?;
if x == 0.0 {
st.rnd_last
} else {
if x < 0.0 {
// Neu aussäen aus dem Argument (deterministisch).
st.rng = ((-x).to_bits() >> 20) as u32 & 0xFF_FFFF;
}
st.rng_next()
}
};
Ok(Some(Value::Sng(v)))
}
fn bi_randomize(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
// 16 Bit aus dem Argument in Bits 823 des Zustands (Vorbild-Schema;
// die exakte Mischung klärt die Phase-3-Aufgabe „RND/RANDOMIZE —
// kompatibler PRNG" in PLAN.md).
let b = x.to_bits();
let m = ((b >> 32) ^ (b >> 48)) as u16;
st.rng = ((m as u32) << 8) | (st.rng & 0xFF);
Ok(None)
}
// ---- Konsole ------------------------------------------------------------------
fn bi_print_val(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
st.print.print_value(host, v);
Ok(None)
}
fn bi_print_comma(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.print.print_comma(host);
Ok(None)
}
fn bi_print_tab(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_tab(host, n);
Ok(None)
}
fn bi_print_spc(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_spc(host, n);
Ok(None)
}
fn bi_print_newline(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.print.print_newline(host);
Ok(None)
}
// ---- Sonstiges ------------------------------------------------------------------
fn bi_timer(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Sekunden seit Mitternacht (UTC-basiert; lokale Zeitzone: Phase 3).
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64() % 86_400.0)
.unwrap_or(0.0);
Ok(Some(Value::Sng(secs as f32)))
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
// Howard Hinnant, days→(y,m,d)
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
(y + if m <= 2 { 1 } else { 0 }, m, d)
}
fn bi_date(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (y, m, d) = civil_from_days((secs / 86_400) as i64);
s_ok(format!("{m:02}-{d:02}-{y:04}"))
}
fn bi_time(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let s = secs % 86_400;
s_ok(format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60))
}
fn bi_command(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
s_ok(st.command.clone())
}
fn bi_doevents(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Ereigniszustellung kommt mit Phase 4.
Ok(Some(Value::Int(0)))
}
fn bi_sleep(_: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
host.sleep(secs);
Ok(None)
}
fn bi_beep(_: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
host.write("\u{0007}");
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::CaptureHost;
fn call(id: u16, args: Vec<Value>) -> Result<Option<Value>, RuntimeError> {
let mut st = RtState::default();
let mut host = CaptureHost::default();
let mut a = args;
builtin_table()[id as usize](&mut st, &mut host, &mut a)
}
fn s(v: &str) -> Value {
Value::Str(Rc::from(v))
}
#[test]
fn stringfunktionen_randfaelle() {
// VAL liest Präfix
let Some(Value::Dbl(v)) = call(ids::VAL, vec![s(" 12.5abc")]).unwrap() else {
panic!()
};
assert_eq!(v, 12.5);
// STR$ mit führendem Leerzeichen
let Some(Value::Str(r)) = call(ids::STR_S, vec![Value::Int(42)]).unwrap() else {
panic!()
};
assert_eq!(&*r, " 42");
// ASC("") → Fehler 5
assert_eq!(
call(ids::ASC, vec![s("")]).unwrap_err(),
RuntimeError::ILLEGAL_FUNCTION_CALL
);
// MID$-Anweisung
let Some(Value::Str(r)) =
call(ids::MID_ASSIGN, vec![s("hallo"), Value::Lng(2), Value::Lng(2), s("EY")])
.unwrap()
else {
panic!()
};
assert_eq!(&*r, "hEYlo");
// LEN zählt Zeichen (Unicode)
let Some(Value::Int(n)) = call(ids::LEN, vec![s("äöü")]).unwrap() else {
panic!()
};
assert_eq!(n, 3);
// INSTR
let Some(Value::Int(p)) =
call(ids::INSTR, vec![Value::Lng(1), s("Terminal Basic"), s("Basic")]).unwrap()
else {
panic!()
};
assert_eq!(p, 10);
}
#[test]
fn builtin_ueber_tabelle() {
// Spec-Szenario: LEN über Tabellenindex liefert 3.
let Some(Value::Int(n)) = call(ids::LEN, vec![s("abc")]).unwrap() else {
panic!()
};
assert_eq!(n, 3);
}
}

View File

@@ -0,0 +1,112 @@
//! Konsolen-Druckzustand: Spaltenverfolgung, PRINT-Formatierung,
//! 14-Zeichen-Druckzonen, TAB/SPC. Wirkung ausschließlich über `Host`.
use crate::format::format_print;
use crate::host::Host;
use crate::value::Value;
/// Breite einer Druckzone (Vorbild: 14 Zeichen).
pub const ZONE_WIDTH: usize = 14;
/// Druckzustand (Spalte 0-basiert, in Zeichen).
#[derive(Default)]
pub struct PrintState {
pub col: usize,
}
impl PrintState {
/// Text ausgeben und Spalte nachführen.
pub fn write(&mut self, host: &mut dyn Host, s: &str) {
host.write(s);
match s.rfind('\n') {
Some(i) => self.col = s[i + 1..].chars().count(),
None => self.col += s.chars().count(),
}
}
/// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
/// Leerzeichen, Strings unverändert.
pub fn print_value(&mut self, host: &mut dyn Host, v: &Value) {
match v {
Value::Str(s) => {
let s = s.clone();
self.write(host, &s);
}
_ => {
let s = format_print(v);
self.write(host, &s);
}
}
}
/// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
pub fn print_comma(&mut self, host: &mut dyn Host) {
let next = (self.col / ZONE_WIDTH + 1) * ZONE_WIDTH;
let pad = next - self.col;
self.write(host, &" ".repeat(pad));
}
/// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
/// dahinter, zuerst Zeilenumbruch (Vorbild).
pub fn print_tab(&mut self, host: &mut dyn Host, n: i32) {
let target = (n.max(1) as usize) - 1;
if self.col > target {
self.write(host, "\n");
}
if target > self.col {
let pad = target - self.col;
self.write(host, &" ".repeat(pad));
}
}
/// `SPC(n)` — n Leerzeichen.
pub fn print_spc(&mut self, host: &mut dyn Host, n: i32) {
if n > 0 {
self.write(host, &" ".repeat(n as usize));
}
}
pub fn print_newline(&mut self, host: &mut dyn Host) {
self.write(host, "\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::CaptureHost;
#[test]
fn druckzonen() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
// "a" (1 Zeichen) , → Spalte 14
ps.write(&mut h, "a");
ps.print_comma(&mut h);
ps.write(&mut h, "b");
assert_eq!(h.output, "a b");
assert_eq!(ps.col, 15);
}
#[test]
fn volle_zone_springt_zur_uebernaechsten() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
ps.write(&mut h, "12345678901234"); // 14 Zeichen, Zone voll
ps.print_comma(&mut h);
ps.write(&mut h, "x");
// x beginnt in Spalte 29 (1-basiert) = Index 28
assert_eq!(h.output.chars().count(), 29);
assert!(h.output.ends_with("x"));
}
#[test]
fn zahlen_mit_vorzeichenspalte() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
ps.print_value(&mut h, &Value::Int(1));
ps.print_value(&mut h, &Value::Int(-2));
ps.print_newline(&mut h);
assert_eq!(h.output, " 1 -2 \n");
}
}

View File

@@ -0,0 +1,240 @@
//! Textdarstellung von Zahlen (PRINT/STR$) und `VAL`-Parsen nach der
//! Matrix in docs/tbvm-design.md: SINGLE bis 7, DOUBLE bis 16
//! signifikante Stellen, keine führende Null vor dem Dezimalpunkt,
//! Exponentialform `E±xx` bzw. `D±xx` außerhalb des Festformat-Bereichs.
use crate::value::Value;
/// Nackte Ziffernfolge ohne Vorzeichenspalte (`1.5`, `-2.5`, `.5`, `1E+08`).
pub fn format_number(v: &Value) -> String {
match v {
Value::Int(x) => x.to_string(),
Value::Lng(x) => x.to_string(),
Value::Sng(x) => fmt_float(*x as f64, 7, 'E'),
Value::Dbl(x) => fmt_float(*x, 16, 'D'),
Value::Cur(c) => fmt_currency(*c),
_ => String::new(),
}
}
/// STR$-Stil: führendes Leerzeichen für nicht-negative Werte, `-` sonst;
/// kein nachgestelltes Leerzeichen.
pub fn format_str_fn(v: &Value) -> String {
let s = format_number(v);
if s.starts_with('-') {
s
} else {
format!(" {s}")
}
}
/// PRINT-Stil: wie STR$ plus nachgestelltes Leerzeichen.
pub fn format_print(v: &Value) -> String {
let mut s = format_str_fn(v);
s.push(' ');
s
}
fn fmt_currency(c: i64) -> String {
let neg = c < 0;
let abs = c.unsigned_abs();
let int = abs / 10_000;
let frac = abs % 10_000;
let mut s = if frac == 0 {
int.to_string()
} else {
let f = format!("{frac:04}");
let f = f.trim_end_matches('0');
if int == 0 {
format!(".{f}")
} else {
format!("{int}.{f}")
}
};
if neg {
s.insert(0, '-');
}
s
}
/// Gleitkomma mit maximal `sig` signifikanten Stellen; Exponentialform
/// (Marke `E` bzw. `D`), wenn der Dezimalexponent < 7 oder ≥ `sig` ist
/// (Schwelle: Festlegung der Matrix in docs/tbvm-design.md; Verifikation
/// gegen die Original-Hilfe ist in PLAN.md Phase 3 eingeplant).
fn fmt_float(x: f64, sig: usize, expch: char) -> String {
if x == 0.0 {
return "0".to_string();
}
if x.is_nan() {
return "NaN".to_string();
}
if x.is_infinite() {
return if x < 0.0 { "-1E+38".into() } else { "1E+38".into() };
}
let neg = x < 0.0;
let ax = x.abs();
// Mantisse/Exponent mit `sig` Stellen bestimmen.
let e = format!("{:.*e}", sig - 1, ax); // z. B. "1.500000e0"
let (mant, exp) = e.split_once('e').unwrap();
let exp: i32 = exp.parse().unwrap();
let mut digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
// Rundungsüberlauf ("9.99…e5" → "10.0e5") normalisiert `format!` bereits.
// Nachgestellte Nullen der Mantisse entfernen.
while digits.len() > 1 && digits.ends_with('0') {
digits.pop();
}
let body = if exp < -7 || exp >= sig as i32 {
// Exponentialform: D. Mantisse "d[.rest]"
let mut m = String::new();
m.push(digits.as_bytes()[0] as char);
if digits.len() > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
format!("{m}{expch}{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
} else if exp >= 0 {
let e = exp as usize;
if (e + 1) >= digits.len() {
// Ganzzahl, ggf. Nullen anhängen
let mut s = digits.clone();
s.push_str(&"0".repeat(e + 1 - digits.len()));
s
} else {
format!("{}.{}", &digits[..e + 1], &digits[e + 1..])
}
} else {
// 0 > exp >= -7: ".0…digits" ohne führende Null
let zeros = (-exp - 1) as usize;
let mut s = String::from(".");
s.push_str(&"0".repeat(zeros));
s.push_str(&digits);
s
};
if neg {
format!("-{body}")
} else {
body
}
}
/// `VAL`: liest das führende Zahlenpräfix (Leerraum wird übersprungen;
/// `&H`/`&O`-Präfixe wie beim Vorbild), ignoriert Restzeichen.
pub fn val(s: &str) -> f64 {
let t = s.trim_start();
// Hex/Oktal
if let Some(rest) = t.strip_prefix("&H").or_else(|| t.strip_prefix("&h")) {
let hex: String = rest.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
return i64::from_str_radix(&hex, 16).unwrap_or(0) as f64;
}
if let Some(rest) = t.strip_prefix("&O").or_else(|| t.strip_prefix("&o")) {
let oct: String = rest.chars().take_while(|c| ('0'..='7').contains(c)).collect();
return i64::from_str_radix(&oct, 8).unwrap_or(0) as f64;
}
let bytes: Vec<char> = t.chars().collect();
let mut i = 0;
let mut num = String::new();
if i < bytes.len() && (bytes[i] == '+' || bytes[i] == '-') {
num.push(bytes[i]);
i += 1;
}
let mut seen_digit = false;
let mut seen_dot = false;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_digit() {
seen_digit = true;
num.push(c);
i += 1;
} else if c == '.' && !seen_dot {
seen_dot = true;
num.push(c);
i += 1;
} else if c == ' ' || c == '\t' {
// Vorbild: VAL ignoriert eingebetteten Leerraum
i += 1;
} else {
break;
}
}
// Exponent (E/D)
if seen_digit && i < bytes.len() && matches!(bytes[i], 'e' | 'E' | 'd' | 'D') {
let mut j = i + 1;
let mut exp = String::new();
if j < bytes.len() && (bytes[j] == '+' || bytes[j] == '-') {
exp.push(bytes[j]);
j += 1;
}
let mut exp_digits = false;
while j < bytes.len() && bytes[j].is_ascii_digit() {
exp.push(bytes[j]);
exp_digits = true;
j += 1;
}
if exp_digits {
num.push('e');
num.push_str(&exp);
}
}
if !seen_digit {
return 0.0;
}
num.parse().unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Value;
#[test]
fn ganzzahlen() {
assert_eq!(format_number(&Value::Int(42)), "42");
assert_eq!(format_number(&Value::Int(-7)), "-7");
assert_eq!(format_print(&Value::Int(1)), " 1 ");
assert_eq!(format_print(&Value::Int(-2)), "-2 ");
assert_eq!(format_str_fn(&Value::Int(42)), " 42");
}
#[test]
fn single_darstellung() {
assert_eq!(format_number(&Value::Sng(1.5)), "1.5");
assert_eq!(format_number(&Value::Sng(-2.5)), "-2.5");
assert_eq!(format_number(&Value::Sng(0.5)), ".5");
assert_eq!(format_number(&Value::Sng(3.0)), "3");
assert_eq!(format_number(&Value::Sng(1.0 / 3.0)), ".3333333");
assert_eq!(format_number(&Value::Sng(1e8)), "1E+08");
assert_eq!(format_number(&Value::Sng(9_999_999.0)), "9999999");
assert_eq!(format_number(&Value::Sng(1e7)), "1E+07");
}
#[test]
fn double_darstellung() {
assert_eq!(format_number(&Value::Dbl(1.5)), "1.5");
assert_eq!(format_number(&Value::Dbl(1e16)), "1D+16");
assert_eq!(
format_number(&Value::Dbl(0.3333333333333333)),
".3333333333333333"
);
}
#[test]
fn currency_darstellung() {
assert_eq!(format_number(&Value::Cur(15_000)), "1.5");
assert_eq!(format_number(&Value::Cur(10_000)), "1");
assert_eq!(format_number(&Value::Cur(-12_345)), "-1.2345");
assert_eq!(format_number(&Value::Cur(2_500)), ".25");
}
#[test]
fn val_parsen() {
assert_eq!(val(" 12.5abc"), 12.5);
assert_eq!(val("-3"), -3.0);
assert_eq!(val("1e2"), 100.0);
assert_eq!(val("1D2"), 100.0);
assert_eq!(val("&HFF"), 255.0);
assert_eq!(val("&O10"), 8.0);
assert_eq!(val("abc"), 0.0);
assert_eq!(val(" 1 2 3"), 123.0); // eingebetteter Leerraum
}
}

View File

@@ -0,0 +1,92 @@
//! `Host`-Abstraktion für Konsolen-E/A (Design-Entscheidung D3):
//! Alle Konsolenwirkungen der VM laufen über dieses Trait. Host-Aufrufe
//! dürfen blockieren; die Abbruchprüfung (Strg+Untbr) obliegt dem Host.
use std::collections::VecDeque;
use std::io::{BufRead, Write as _};
pub trait Host {
/// Text ausgeben (ohne implizite Zeilenumbrüche).
fn write(&mut self, s: &str);
/// Eine Eingabezeile lesen (ohne Zeilenende); `None` = Eingabeende.
fn read_line(&mut self) -> Option<String>;
/// Abbruchwunsch (Strg+Untbr)? Wird an Anweisungsgrenzen geprüft.
fn interrupted(&mut self) -> bool {
false
}
/// `SLEEP` — blockierend im Host.
fn sleep(&mut self, _secs: f64) {}
}
/// Konsolen-Host für `tbc run`: stdout/stdin.
#[derive(Default)]
pub struct ConsoleHost;
impl Host for ConsoleHost {
fn write(&mut self, s: &str) {
let mut out = std::io::stdout().lock();
let _ = out.write_all(s.as_bytes());
let _ = out.flush();
}
fn read_line(&mut self) -> Option<String> {
let mut line = String::new();
match std::io::stdin().lock().read_line(&mut line) {
Ok(0) => None,
Ok(_) => {
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
Some(line)
}
Err(_) => None,
}
}
fn sleep(&mut self, secs: f64) {
if secs > 0.0 {
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
}
}
}
/// Capture-Host für Tests: zeichnet die Ausgabe byte-genau auf und
/// liefert vorbereitete Eingabezeilen.
#[derive(Default)]
pub struct CaptureHost {
pub output: String,
pub input: VecDeque<String>,
}
impl CaptureHost {
pub fn with_input(lines: &[&str]) -> Self {
CaptureHost {
output: String::new(),
input: lines.iter().map(|s| s.to_string()).collect(),
}
}
}
impl Host for CaptureHost {
fn write(&mut self, s: &str) {
self.output.push_str(s);
}
fn read_line(&mut self) -> Option<String> {
self.input.pop_front()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_host_zeichnet_bytegenau_auf() {
let mut h = CaptureHost::default();
h.write(" 1 2 ");
h.write("\n");
h.write("x");
assert_eq!(h.output, " 1 2 \nx");
}
}

View File

@@ -4,8 +4,13 @@
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive //! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3). //! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING … pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE … pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod datetime; // DATE$, TIME$, TIMER
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod host; // Host-Trait (Konsole, Capture) — Entscheidung D3
pub mod datetime; // DATE$, TIME$, TIMER
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix

View File

@@ -0,0 +1,283 @@
//! Laufzeitwerte der TBVM: Tagged Enum (Entwurf docs/tbvm-design.md),
//! `Rc` statt GC (der Dialekt kennt keine Zyklen), Arrays/Records als
//! geteilte Handles, Referenzwerte für BYREF-Parameter.
//!
//! Hier lebt außerdem die **Zahlenkonvertierungs-Matrix** (Banker's
//! Rounding, Überlauf → Fehler 6) — die einzige Implementierung der in
//! docs/tbvm-design.md dokumentierten Semantik.
use crate::errors::RuntimeError;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub enum Value {
Int(i16),
Lng(i32),
Sng(f32),
Dbl(f64),
Cur(i64),
Str(Rc<str>),
Arr(Rc<RefCell<ArrayObj>>),
Rec(Rc<RefCell<RecordObj>>),
/// Referenz (BYREF-Parameter-Slot).
Ref(VarRef),
/// Nicht-initialisiertes Array-/Record-Handle (Auto-DIM bei Zugriff).
Empty,
}
/// Referenzziel eines BYREF-Parameters.
#[derive(Debug, Clone)]
pub enum VarRef {
Global(u16),
/// Absoluter Index in den Locals-Stack der VM.
Stack(u32),
/// Arrayelement (flacher Index).
Elem(Rc<RefCell<ArrayObj>>, u32),
/// Record-Feldpfad.
Field(Rc<RefCell<RecordObj>>, Vec<u16>),
}
/// Initialisierungstyp eines Slots/Elements (serialisierbar im `.tbc`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeInit {
Int,
Lng,
Sng,
Dbl,
Cur,
Str,
FixedStr(u32),
Udt(u16),
/// Array-/Record-Slot ohne Vorbelegung (Auto-DIM).
Empty,
}
/// UDT-Layout für die Default-Erzeugung.
#[derive(Debug, Clone)]
pub struct UdtLayout {
pub name: String,
pub fields: Vec<TypeInit>,
}
pub fn default_value(init: &TypeInit, udts: &[UdtLayout]) -> Value {
match init {
TypeInit::Int => Value::Int(0),
TypeInit::Lng => Value::Lng(0),
TypeInit::Sng => Value::Sng(0.0),
TypeInit::Dbl => Value::Dbl(0.0),
TypeInit::Cur => Value::Cur(0),
TypeInit::Str => Value::Str(Rc::from("")),
TypeInit::FixedStr(n) => Value::Str(Rc::from(" ".repeat(*n as usize).as_str())),
TypeInit::Udt(id) => {
let layout = &udts[*id as usize];
let fields = layout
.fields
.iter()
.map(|f| default_value(f, udts))
.collect();
Value::Rec(Rc::new(RefCell::new(RecordObj { fields })))
}
TypeInit::Empty => Value::Empty,
}
}
#[derive(Debug)]
pub struct ArrayObj {
pub elem: TypeInit,
/// (Untergrenze, Obergrenze) je Dimension.
pub dims: Vec<(i32, i32)>,
pub data: Vec<Value>,
}
impl ArrayObj {
pub fn new(elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout]) -> Result<Self, RuntimeError> {
let mut len: usize = 1;
for (lo, hi) in &dims {
if hi < lo {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let n = (*hi as i64 - *lo as i64 + 1) as usize;
len = len.checked_mul(n).ok_or(RuntimeError::OUT_OF_MEMORY)?;
if len > 64 * 1024 * 1024 {
return Err(RuntimeError::OUT_OF_MEMORY);
}
}
let mut data = Vec::with_capacity(len);
for _ in 0..len {
data.push(default_value(&elem, udts));
}
Ok(ArrayObj { elem, dims, data })
}
/// Flacher Index (zeilenweise, letzte Dimension läuft am schnellsten);
/// Bereichsprüfung → Fehler 9.
pub fn flat_index(&self, idx: &[i32]) -> Result<u32, RuntimeError> {
if idx.len() != self.dims.len() {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let mut flat: u64 = 0;
for (i, (lo, hi)) in idx.iter().zip(self.dims.iter()) {
if i < lo || i > hi {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let span = (*hi as i64 - *lo as i64 + 1) as u64;
flat = flat * span + (*i as i64 - *lo as i64) as u64;
}
Ok(flat as u32)
}
}
#[derive(Debug)]
pub struct RecordObj {
pub fields: Vec<Value>,
}
// ---- Konvertierungsmatrix ---------------------------------------------------
/// Kaufmännische Rundung zur nächsten geraden Zahl (Banker's Rounding,
/// Verhalten von CINT/CLNG des Vorbilds).
pub fn banker_round(x: f64) -> f64 {
let floor = x.floor();
let diff = x - floor;
if diff > 0.5 {
floor + 1.0
} else if diff < 0.5 {
floor
} else {
// exakt .5 → zur geraden Zahl
if (floor as i64) % 2 == 0 {
floor
} else {
floor + 1.0
}
}
}
pub fn f64_to_i16(x: f64) -> Result<i16, RuntimeError> {
let r = banker_round(x);
if !(i16::MIN as f64..=i16::MAX as f64).contains(&r) {
return Err(RuntimeError::OVERFLOW);
}
Ok(r as i16)
}
pub fn f64_to_i32(x: f64) -> Result<i32, RuntimeError> {
let r = banker_round(x);
if !(i32::MIN as f64..=i32::MAX as f64).contains(&r) {
return Err(RuntimeError::OVERFLOW);
}
Ok(r as i32)
}
pub fn f64_to_f32(x: f64) -> Result<f32, RuntimeError> {
if x.is_finite() && x.abs() > f32::MAX as f64 {
return Err(RuntimeError::OVERFLOW);
}
Ok(x as f32)
}
/// f64 → CURRENCY (Festkomma ×10 000, Banker's auf der 4. Nachkommastelle).
pub fn f64_to_cur(x: f64) -> Result<i64, RuntimeError> {
let scaled = banker_round(x * 10_000.0);
if !((i64::MIN as f64) < scaled && scaled < (i64::MAX as f64)) {
return Err(RuntimeError::OVERFLOW);
}
Ok(scaled as i64)
}
/// CURRENCY → Ganzzahl (÷10 000, Banker's; ganzzahlig exakt).
pub fn cur_to_i64(c: i64) -> i64 {
let q = c.div_euclid(10_000);
let r = c.rem_euclid(10_000);
// r in 0..10000; runde halb-zu-gerade
if r > 5_000 {
q + 1
} else if r < 5_000 {
q
} else if q % 2 == 0 {
q
} else {
q + 1
}
}
pub fn cur_to_i16(c: i64) -> Result<i16, RuntimeError> {
let v = cur_to_i64(c);
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
pub fn cur_to_i32(c: i64) -> Result<i32, RuntimeError> {
let v = cur_to_i64(c);
i32::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
pub fn cur_to_f64(c: i64) -> f64 {
c as f64 / 10_000.0
}
/// i32 → i16 mit Bereichsprüfung.
pub fn i32_to_i16(v: i32) -> Result<i16, RuntimeError> {
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
/// Numerischer Wert als f64 (für Builtins, die per Tag dispatchen).
pub fn as_f64(v: &Value) -> f64 {
match v {
Value::Int(x) => *x as f64,
Value::Lng(x) => *x as f64,
Value::Sng(x) => *x as f64,
Value::Dbl(x) => *x,
Value::Cur(x) => cur_to_f64(*x),
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn banker_rounding() {
assert_eq!(banker_round(0.5), 0.0);
assert_eq!(banker_round(1.5), 2.0);
assert_eq!(banker_round(2.5), 2.0);
assert_eq!(banker_round(-0.5), 0.0);
assert_eq!(banker_round(-1.5), -2.0);
assert_eq!(banker_round(2.4), 2.0);
assert_eq!(banker_round(2.6), 3.0);
}
#[test]
fn overflow_bei_konvertierung() {
assert!(f64_to_i16(40_000.0).is_err());
assert!(f64_to_i16(32_767.4).is_ok());
assert!(f64_to_i32(3e9).is_err());
assert!(f64_to_f32(1e39).is_err());
}
#[test]
fn currency_rundung() {
assert_eq!(f64_to_cur(1.5).unwrap(), 15_000);
assert_eq!(f64_to_cur(-2.25).unwrap(), -22_500);
// Halb-zu-gerade nur bei exakt darstellbarem .5-Fall (Skalierung
// 10000.5 ist binär exakt, wenn der Ausgangswert es hergibt):
assert_eq!(banker_round(10_000.5), 10_000.0);
assert_eq!(cur_to_i64(15_000), 2); // 1.5 → 2
assert_eq!(cur_to_i64(25_000), 2); // 2.5 → 2
assert_eq!(cur_to_i64(-15_000), -2);
}
#[test]
fn array_indexpruefung() {
let a = ArrayObj::new(TypeInit::Int, vec![(0, 10)], &[]).unwrap();
assert_eq!(a.flat_index(&[0]).unwrap(), 0);
assert_eq!(a.flat_index(&[10]).unwrap(), 10);
assert!(a.flat_index(&[11]).is_err());
assert!(a.flat_index(&[-1]).is_err());
let b = ArrayObj::new(TypeInit::Int, vec![(1, 3), (1, 2)], &[]).unwrap();
assert_eq!(b.flat_index(&[1, 1]).unwrap(), 0);
assert_eq!(b.flat_index(&[1, 2]).unwrap(), 1);
assert_eq!(b.flat_index(&[2, 1]).unwrap(), 2);
}
}

View File

@@ -10,3 +10,12 @@ authors.workspace = true
tb-frontend.workspace = true tb-frontend.workspace = true
thiserror.workspace = true thiserror.workspace = true
log.workspace = true log.workspace = true
tb-runtime.workspace = true
[[bench]]
name = "compile"
harness = false
[[bench]]
name = "vm"
harness = false

View File

@@ -0,0 +1,88 @@
//! Compile-Budget-Benchmark (Instant-Compile-Anforderung, PLAN.md):
//! Projekt mit ~50.000 Zeilen < 1 s, einzelnes Modul (~500 Zeilen) < 50 ms
//! (Release-Build). Eigener Harness (kein Criterion — keine
//! Fremdabhängigkeit nötig); Lauf mit `cargo bench -p tb-vm`.
use std::fmt::Write as _;
use std::time::Instant;
/// Realistisch gemischtes Modul mit `n_blocks` Codeblöcken
/// (~10 Zeilen je Block) plus Prozeduren.
fn generate_module(n_blocks: usize, seed: usize) -> String {
let mut src = String::new();
let _ = writeln!(src, "' Generiertes Benchmark-Modul {seed}");
let _ = writeln!(src, "DIM feld{seed}%(100)");
for i in 0..n_blocks {
let v = format!("v{seed}x{i}");
let _ = writeln!(src, "{v}% = {i} MOD 100");
let _ = writeln!(src, "{v}tot# = {v}% * 1.5 + SQR({v}% + 1)");
let _ = writeln!(src, "IF {v}% > 50 THEN");
let _ = writeln!(src, " {v}s$ = \"gross\" + STR$({v}%)");
let _ = writeln!(src, "ELSE");
let _ = writeln!(src, " {v}s$ = LEFT$(\"klein\", 3)");
let _ = writeln!(src, "END IF");
let _ = writeln!(src, "FOR {v}i% = 1 TO 10");
let _ = writeln!(src, " feld{seed}%({v}i% MOD 100) = {v}i%");
let _ = writeln!(src, "NEXT");
}
// Prozeduren am Modulende
for p in 0..(n_blocks / 20).max(1) {
let _ = writeln!(src, "SUB Tu{seed}p{p} (a%, b#)");
let _ = writeln!(src, " b# = a% * 2 + b#");
let _ = writeln!(src, "END SUB");
}
src
}
fn compile_all(sources: &[(String, String)]) -> usize {
let mut total = 0;
for (name, src) in sources {
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren");
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>();
}
total
}
fn main() {
// Einzelnes Modul: ~500 Zeilen (Budget < 50 ms).
let single = generate_module(50, 0);
let single_lines = single.lines().count();
// Projekt: ~50.000 Zeilen über 20 Module (Budget < 1 s).
let modules: Vec<(String, String)> = (0..20)
.map(|i| (format!("MOD{i}"), generate_module(245, i)))
.collect();
let project_lines: usize = modules.iter().map(|(_, s)| s.lines().count()).sum();
// Aufwärmen
let _ = compile_all(&[("WARM".into(), single.clone())]);
let t = Instant::now();
let mut best_single = f64::MAX;
for _ in 0..10 {
let t1 = Instant::now();
let _ = compile_all(&[("EINZEL".into(), single.clone())]);
best_single = best_single.min(t1.elapsed().as_secs_f64());
}
let _ = t;
let t2 = Instant::now();
let instrs = compile_all(&modules);
let project_secs = t2.elapsed().as_secs_f64();
println!("Compile-Budget-Benchmark (Release):");
println!(
" Einzelmodul: {single_lines} Zeilen in {:.2} ms (Budget 50 ms) {}",
best_single * 1000.0,
if best_single < 0.050 { "OK" } else { "VERFEHLT" }
);
println!(
" Projekt: {project_lines} Zeilen in {:.0} ms (Budget 1000 ms) {}{instrs} Instruktionen",
project_secs * 1000.0,
if project_secs < 1.0 { "OK" } else { "VERFEHLT" }
);
let lps = project_lines as f64 / project_secs;
println!(" Durchsatz: {:.0} Zeilen/s", lps);
assert!(best_single < 0.050, "Einzelmodul-Budget verfehlt");
assert!(project_secs < 1.0, "Projekt-Budget verfehlt");
}

View File

@@ -0,0 +1,57 @@
//! VM-Durchsatz-Benchmark (Messlatte docs/tbvm-design.md, „Performance"):
//! Schleifen- und String-Lasten. Eigener Harness; `cargo bench -p tb-vm`.
use std::time::Instant;
use tb_runtime::host::CaptureHost;
use tb_vm::interp::{RunEvent, Vm};
fn run_timed(name: &str, src: &str, work_units: f64, unit: &str) {
let module = tb_vm::compile_source("BENCH", src).expect("kompiliert");
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
let t = Instant::now();
let ev = vm.run(&mut host);
let secs = t.elapsed().as_secs_f64();
assert_eq!(ev, RunEvent::Ended, "{ev:?}");
println!(
" {name}: {:.0} ms ({:.1} Mio {unit}/s)",
secs * 1000.0,
work_units / secs / 1e6
);
}
fn main() {
println!("VM-Durchsatz-Benchmark (Release):");
// Ganzzahl-Schleife: 10 Mio Iterationen mit Arithmetik.
run_timed(
"INTEGER-Schleife (10 Mio)",
"s& = 0\nFOR i& = 1 TO 10000000\ns& = (s& + i& MOD 7) MOD 100000\nNEXT\nPRINT s&",
10_000_000.0,
"Iterationen",
);
// Gleitkomma-Schleife.
run_timed(
"DOUBLE-Schleife (5 Mio)",
"d# = 0\nFOR i& = 1 TO 5000000\nd# = d# + i& * 1.000001\nNEXT\nPRINT CINT(d# / 1000000000000#)",
5_000_000.0,
"Iterationen",
);
// Prozeduraufrufe.
run_timed(
"SUB-Aufrufe (1 Mio, BYREF)",
"SUB Inc (x&)\nx& = x& + 1\nEND SUB\nn& = 0\nFOR i& = 1 TO 1000000\nInc n&\nNEXT\nPRINT n&",
1_000_000.0,
"Aufrufe",
);
// String-Last: MID$/INSTR/Verkettung.
run_timed(
"String-Funktionen (200k)",
"s$ = \"Terminal Basic Benchmark\"\nn& = 0\nFOR i& = 1 TO 200000\nt$ = MID$(s$, (i& MOD 10) + 1, 8) + \"x\"\nn& = n& + INSTR(t$, \"a\") + LEN(t$)\nNEXT\nPRINT n& > 0",
200_000.0,
"Runden",
);
}

View File

@@ -1,3 +1,680 @@
//! Bytecode-Format der TBVM: Opcodes, Konstantenpool, Modul-/Prozedurtabellen. //! Bytecode-Definition und `.tbc`-Serialisierung.
//!
//! In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, Enum
//! mit eingebetteten Operanden — Wort-Dispatch); die Serialisierung
//! bildet jede Instruktion auf 1 Opcode-Byte + Operanden (little-endian)
//! ab. Opcode-Bytes sind **stabil** und gruppenweise mit Lücken vergeben
//! (Phase 3 ergänzt in den Lücken). Dokumentation: docs/tbvm-design.md.
// Platzhalter — wird in Phase 2 ausgearbeitet (siehe PLAN.md) use std::fmt;
use std::rc::Rc;
use tb_runtime::value::{TypeInit, UdtLayout};
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
pub const TBC_VERSION: u16 = 1;
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CmpOp {
Eq = 0,
Ne = 1,
Lt = 2,
Le = 3,
Gt = 4,
Ge = 5,
}
impl CmpOp {
fn from_u8(v: u8) -> Result<Self, LoadError> {
Ok(match v {
0 => CmpOp::Eq,
1 => CmpOp::Ne,
2 => CmpOp::Lt,
3 => CmpOp::Le,
4 => CmpOp::Gt,
5 => CmpOp::Ge,
_ => return Err(LoadError::Corrupt("CmpOp")),
})
}
}
#[derive(Debug)]
pub enum LoadError {
BadMagic,
/// Unbekannte Formatversion (enthaltene Version).
Version(u16),
Corrupt(&'static str),
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
LoadError::Version(v) => {
write!(f, "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})")
}
LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
}
}
}
// ---- Encoder/Decoder-Hilfen -------------------------------------------------
pub struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Reader { buf, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
if self.pos + n > self.buf.len() {
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
}
let s = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, LoadError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, LoadError> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Result<u32, LoadError> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn string(&mut self) -> Result<String, LoadError> {
let n = self.u32()? as usize;
let b = self.take(n)?;
String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
}
}
trait Enc: Sized {
fn enc(&self, out: &mut Vec<u8>);
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
}
macro_rules! enc_prim {
($t:ty, $n:literal) => {
impl Enc for $t {
fn enc(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.to_le_bytes());
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(<$t>::from_le_bytes(r.take($n)?.try_into().unwrap()))
}
}
};
}
enc_prim!(u16, 2);
enc_prim!(u32, 4);
enc_prim!(i16, 2);
enc_prim!(i32, 4);
enc_prim!(i64, 8);
enc_prim!(f32, 4);
enc_prim!(f64, 8);
impl Enc for u8 {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
r.u8()
}
}
impl Enc for bool {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self as u8);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(r.u8()? != 0)
}
}
impl Enc for CmpOp {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self as u8);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
CmpOp::from_u8(r.u8()?)
}
}
impl Enc for TypeInit {
fn enc(&self, out: &mut Vec<u8>) {
let (tag, extra): (u8, u32) = match self {
TypeInit::Int => (0, 0),
TypeInit::Lng => (1, 0),
TypeInit::Sng => (2, 0),
TypeInit::Dbl => (3, 0),
TypeInit::Cur => (4, 0),
TypeInit::Str => (5, 0),
TypeInit::FixedStr(n) => (6, *n),
TypeInit::Udt(id) => (7, *id as u32),
TypeInit::Empty => (8, 0),
};
out.push(tag);
out.extend_from_slice(&extra.to_le_bytes());
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
let tag = r.u8()?;
let extra = r.u32()?;
Ok(match tag {
0 => TypeInit::Int,
1 => TypeInit::Lng,
2 => TypeInit::Sng,
3 => TypeInit::Dbl,
4 => TypeInit::Cur,
5 => TypeInit::Str,
6 => TypeInit::FixedStr(extra),
7 => TypeInit::Udt(extra as u16),
8 => TypeInit::Empty,
_ => return Err(LoadError::Corrupt("TypeInit")),
})
}
}
// ---- Instruktionssatz ---------------------------------------------------------
macro_rules! instrs {
($( $op:literal $name:ident $(( $($fname:ident : $ft:ty),+ ))? ; )+) => {
/// Eine dekodierte Instruktion. Serialisiert: Opcode-Byte + Operanden.
#[derive(Debug, Clone, PartialEq)]
pub enum Instr {
$( $name $(( $($ft),+ ))? , )+
}
impl Instr {
pub fn encode(&self, out: &mut Vec<u8>) {
match self {
$( Instr::$name $(( $($fname),+ ))? => {
out.push($op);
$( $( Enc::enc($fname, out); )+ )?
} )+
}
}
pub fn decode(r: &mut Reader) -> Result<Instr, LoadError> {
let op = r.u8()?;
Ok(match op {
$( $op => Instr::$name $(( $( <$ft as Enc>::dec(r)? ),+ ))? , )+
_ => return Err(LoadError::Corrupt("Opcode")),
})
}
}
};
}
instrs! {
// 0x00 — Anweisungsgrenzen und Kontrolle
0x00 Stmt(a: u32); // Quellzeile; Tick-Prüfung, Resume-Punkt
0x01 SetErl(a: u32); // numerische Zeilennummer durchlaufen
0x02 End;
0x03 StopInstr;
0x04 SystemInstr;
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
// 0x10 — Konstanten und Stack
0x10 PushInt(a: i16);
0x11 PushLng(a: i32);
0x12 PushSng(a: f32);
0x13 PushDbl(a: f64);
0x14 PushCur(a: i64);
0x15 PushStr(a: u16);
0x16 Dup;
0x17 Pop;
// 0x20 — Variablen und Referenzen
0x20 LoadGlobal(a: u16);
0x21 StoreGlobal(a: u16);
0x22 LoadLocal(a: u16);
0x23 StoreLocal(a: u16);
0x24 LoadRef(a: u16); // durch Referenz in lokalem Slot lesen
0x25 StoreRef(a: u16);
0x26 MakeRefGlobal(a: u16);
0x27 MakeRefLocal(a: u16);
0x28 MakeRefElem(a: u8); // Handle+Indizes → Elementreferenz
0x29 MakeRefField(a: u16); // Rec/Feldreferenz → tiefere Feldreferenz
// 0x30 — Arrays und Records
0x30 LoadArr(a: bool, b: u16, c: u8, d: TypeInit); // Slot sichern (Auto-DIM) + Handle
0x31 LoadElem(a: u8);
0x32 StoreElem(a: u8);
0x33 DimArr(a: bool, b: u16, c: u8, d: TypeInit);
0x34 RedimArr(a: bool, b: u16, c: u8, d: TypeInit);
0x35 EraseSlot(a: bool, b: u16);
0x36 LoadField(a: u16);
0x37 StoreField(a: u16);
0x38 CopyRec;
0x39 ArrBound(a: bool); // true = LBOUND
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
// 0x40 — Arithmetik (monomorph)
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
0x4A MulI2; 0x4B MulI4; 0x4C MulR4; 0x4D MulR8; 0x4E MulCy;
0x4F NegI2; 0x50 NegI4; 0x51 NegR4; 0x52 NegR8; 0x53 NegCy;
0x54 DivR4; 0x55 DivR8;
0x56 IDivI2; 0x57 IDivI4;
0x58 ModI2; 0x59 ModI4;
0x5A PowR8;
0x5B Concat;
// 0x60 — Konvertierungen (Matrix)
0x60 ConvI2I4; 0x61 ConvI2R4; 0x62 ConvI2R8; 0x63 ConvI2Cy;
0x64 ConvI4I2; 0x65 ConvI4R4; 0x66 ConvI4R8; 0x67 ConvI4Cy;
0x68 ConvR4I2; 0x69 ConvR4I4; 0x6A ConvR4R8; 0x6B ConvR4Cy;
0x6C ConvR8I2; 0x6D ConvR8I4; 0x6E ConvR8R4; 0x6F ConvR8Cy;
0x70 ConvCyI2; 0x71 ConvCyI4; 0x72 ConvCyR4; 0x73 ConvCyR8;
// 0x80 — Logik (bitweise)
0x80 NotI2; 0x81 NotI4;
0x82 AndI2; 0x83 AndI4;
0x84 OrI2; 0x85 OrI4;
0x86 XorI2; 0x87 XorI4;
0x88 EqvI2; 0x89 EqvI4;
0x8A ImpI2; 0x8B ImpI4;
// 0x90 — Vergleiche (Ergebnis INTEGER 1/0)
0x90 CmpI2(a: CmpOp);
0x91 CmpI4(a: CmpOp);
0x92 CmpR4(a: CmpOp);
0x93 CmpR8(a: CmpOp);
0x94 CmpCy(a: CmpOp);
0x95 CmpStr(a: CmpOp);
// 0xA0 — Kontrollfluss
0xA0 Jump(a: u32);
0xA1 JumpIfFalse(a: u32);
0xA2 JumpIfTrue(a: u32);
0xA3 Gosub(a: u32);
0xA4 RetGosub;
0xA5 RetGosubTo(a: u32);
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
// 0xB0 — Prozeduren und Builtins
0xB0 Call(a: u16, b: u8);
0xB1 RetProc;
0xB2 RetFn;
0xB3 CallBuiltin(a: u16, b: u8);
// 0xC0 — Fehlerbehandlung
0xC0 OnErrorGoto(a: u32);
0xC1 OnErrorLocal(a: u32);
0xC2 OnErrorDisable;
0xC3 OnErrorLocalDisable;
0xC4 OnErrorResumeNext(a: bool);
0xC5 Resume0;
0xC6 ResumeNext;
0xC7 ResumeLabel(a: u32);
0xC8 RaiseError; // Code vom Stack (ERROR n)
0xC9 LoadErr;
0xCA LoadErl;
// 0xD0 — DATA und Eingabe
0xD0 ReadData(a: u8); // nächstes DATA-Element; 0 = String, 1 = Zahl (DOUBLE)
0xD1 Restore(a: u32);
0xD2 Input(a: u8, b: bool, c: u16, d: bool); // argc, line_mode, prompt (0xFFFF=ohne), '?'
}
// ---- Modulstruktur ------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct ProcCode {
pub name: String,
pub n_params: u16,
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
/// wird beim Aufruf durch die Argumente ersetzt).
pub locals_init: Vec<TypeInit>,
/// Slot-Namen (Debugger-Inspektion).
pub local_names: Vec<String>,
pub code: Vec<Instr>,
}
#[derive(Debug, Clone)]
pub struct DataItem {
pub text: String,
pub line: u32,
}
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
#[derive(Debug)]
pub struct CompiledModule {
pub name: String,
/// `OPTION BASE` (Untergrenze impliziter Arrays).
pub option_base: u8,
/// Deduplizierter Stringpool.
pub strings: Vec<Rc<str>>,
pub globals_init: Vec<TypeInit>,
pub global_names: Vec<String>,
pub udts: Vec<UdtLayout>,
/// Prozeduren; Index 0 ist das Hauptprogramm (modul-qualifiziert über
/// `name` des Moduls + Prozedurname).
pub procs: Vec<ProcCode>,
pub data: Vec<DataItem>,
/// Sprungtabellen für `ON n GOTO/GOSUB`.
pub jump_tables: Vec<Vec<u32>>,
}
fn w_string(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}
impl CompiledModule {
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
pub fn to_tbc(&self) -> Vec<u8> {
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
let mut modn = Vec::new();
w_string(&mut modn, &self.name);
modn.push(self.option_base);
sections.push((*b"MODN", modn));
let mut cons = Vec::new();
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
for s in &self.strings {
w_string(&mut cons, s);
}
sections.push((*b"CONS", cons));
let mut typs = Vec::new();
typs.extend_from_slice(&(self.udts.len() as u32).to_le_bytes());
for u in &self.udts {
w_string(&mut typs, &u.name);
typs.extend_from_slice(&(u.fields.len() as u32).to_le_bytes());
for f in &u.fields {
f.enc(&mut typs);
}
}
sections.push((*b"TYPS", typs));
let mut glob = Vec::new();
glob.extend_from_slice(&(self.globals_init.len() as u32).to_le_bytes());
for (init, name) in self.globals_init.iter().zip(&self.global_names) {
init.enc(&mut glob);
w_string(&mut glob, name);
}
sections.push((*b"GLOB", glob));
let mut proc = Vec::new();
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
for p in &self.procs {
w_string(&mut proc, &p.name);
proc.extend_from_slice(&p.n_params.to_le_bytes());
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
init.enc(&mut proc);
w_string(&mut proc, name);
}
let mut code = Vec::new();
for i in &p.code {
i.encode(&mut code);
}
proc.extend_from_slice(&(p.code.len() as u32).to_le_bytes());
proc.extend_from_slice(&(code.len() as u32).to_le_bytes());
proc.extend_from_slice(&code);
}
sections.push((*b"PROC", proc));
let mut data = Vec::new();
data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
for d in &self.data {
w_string(&mut data, &d.text);
data.extend_from_slice(&d.line.to_le_bytes());
}
sections.push((*b"DATA", data));
let mut jmpt = Vec::new();
jmpt.extend_from_slice(&(self.jump_tables.len() as u32).to_le_bytes());
for t in &self.jump_tables {
jmpt.extend_from_slice(&(t.len() as u32).to_le_bytes());
for target in t {
jmpt.extend_from_slice(&target.to_le_bytes());
}
}
sections.push((*b"JMPT", jmpt));
// Header + Abschnittstabelle
let mut out = Vec::new();
out.extend_from_slice(TBC_MAGIC);
out.extend_from_slice(&TBC_VERSION.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); // Flags
out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
let table_start = out.len();
// Platzhalter für Tabelle
for _ in 0..sections.len() {
out.extend_from_slice(&[0u8; 12]);
}
let mut offsets = Vec::new();
for (_, payload) in &sections {
offsets.push((out.len() as u32, payload.len() as u32));
out.extend_from_slice(payload);
}
for (i, ((id, _), (off, len))) in sections.iter().zip(&offsets).enumerate() {
let at = table_start + i * 12;
out[at..at + 4].copy_from_slice(id);
out[at + 4..at + 8].copy_from_slice(&off.to_le_bytes());
out[at + 8..at + 12].copy_from_slice(&len.to_le_bytes());
}
out
}
pub fn from_tbc(buf: &[u8]) -> Result<CompiledModule, LoadError> {
let mut r = Reader::new(buf);
if r.take(4)? != TBC_MAGIC {
return Err(LoadError::BadMagic);
}
let version = r.u16()?;
if version != TBC_VERSION {
return Err(LoadError::Version(version));
}
let _flags = r.u16()?;
let n_sections = r.u32()? as usize;
let mut table = Vec::new();
for _ in 0..n_sections {
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
let off = r.u32()? as usize;
let len = r.u32()? as usize;
table.push((id, off, len));
}
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
for (sid, off, len) in &table {
if sid == id {
if off + len > buf.len() {
return Err(LoadError::Corrupt("Abschnittstabelle"));
}
return Ok(Reader::new(&buf[*off..*off + *len]));
}
}
Err(LoadError::Corrupt("Abschnitt fehlt"))
};
let mut r = section(b"MODN")?;
let name = r.string()?;
let option_base = r.u8()?;
let mut r = section(b"CONS")?;
let n = r.u32()? as usize;
let mut strings = Vec::with_capacity(n);
for _ in 0..n {
strings.push(Rc::from(r.string()?.as_str()));
}
let mut r = section(b"TYPS")?;
let n = r.u32()? as usize;
let mut udts = Vec::with_capacity(n);
for _ in 0..n {
let name = r.string()?;
let nf = r.u32()? as usize;
let mut fields = Vec::with_capacity(nf);
for _ in 0..nf {
fields.push(TypeInit::dec(&mut r)?);
}
udts.push(UdtLayout { name, fields });
}
let mut r = section(b"GLOB")?;
let n = r.u32()? as usize;
let mut globals_init = Vec::with_capacity(n);
let mut global_names = Vec::with_capacity(n);
for _ in 0..n {
globals_init.push(TypeInit::dec(&mut r)?);
global_names.push(r.string()?);
}
let mut r = section(b"PROC")?;
let n = r.u32()? as usize;
let mut procs = Vec::with_capacity(n);
for _ in 0..n {
let name = r.string()?;
let n_params = r.u16()?;
let nl = r.u32()? as usize;
let mut locals_init = Vec::with_capacity(nl);
let mut local_names = Vec::with_capacity(nl);
for _ in 0..nl {
locals_init.push(TypeInit::dec(&mut r)?);
local_names.push(r.string()?);
}
let n_instr = r.u32()? as usize;
let code_len = r.u32()? as usize;
let code_bytes = r.take(code_len)?;
let mut cr = Reader::new(code_bytes);
let mut code = Vec::with_capacity(n_instr);
for _ in 0..n_instr {
code.push(Instr::decode(&mut cr)?);
}
procs.push(ProcCode { name, n_params, locals_init, local_names, code });
}
let mut r = section(b"DATA")?;
let n = r.u32()? as usize;
let mut data = Vec::with_capacity(n);
for _ in 0..n {
let text = r.string()?;
let line = r.u32()?;
data.push(DataItem { text, line });
}
let mut r = section(b"JMPT")?;
let n = r.u32()? as usize;
let mut jump_tables = Vec::with_capacity(n);
for _ in 0..n {
let m = r.u32()? as usize;
let mut t = Vec::with_capacity(m);
for _ in 0..m {
t.push(r.u32()?);
}
jump_tables.push(t);
}
Ok(CompiledModule {
name,
option_base,
strings,
globals_init,
global_names,
udts,
procs,
data,
jump_tables,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instr_roundtrip() {
let samples = vec![
Instr::Stmt(42),
Instr::PushInt(-7),
Instr::PushDbl(1.5),
Instr::PushCur(-12_345),
Instr::LoadArr(true, 3, 2, TypeInit::FixedStr(30)),
Instr::CmpR8(CmpOp::Le),
Instr::OnJump(1, true),
Instr::Call(2, 3),
Instr::Input(2, false, 0xFFFF, true),
Instr::ConvCyR8,
Instr::RetFn,
];
let mut buf = Vec::new();
for i in &samples {
i.encode(&mut buf);
}
let mut r = Reader::new(&buf);
for want in &samples {
let got = Instr::decode(&mut r).unwrap();
assert_eq!(&got, want);
}
}
#[test]
fn tbc_roundtrip() {
let m = CompiledModule {
name: "TEST".into(),
option_base: 1,
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
globals_init: vec![TypeInit::Int, TypeInit::Str],
global_names: vec!["a".into(), "s".into()],
udts: vec![UdtLayout {
name: "Kunde".into(),
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
}],
procs: vec![ProcCode {
name: "TEST".into(),
n_params: 0,
locals_init: vec![],
local_names: vec![],
code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
}],
data: vec![DataItem { text: "1.5".into(), line: 3 }],
jump_tables: vec![vec![4, 9]],
};
let bytes = m.to_tbc();
let back = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(back.name, "TEST");
assert_eq!(back.strings.len(), 2);
assert_eq!(&*back.strings[0], "Hallo");
assert_eq!(back.globals_init, m.globals_init);
assert_eq!(back.udts[0].fields, m.udts[0].fields);
assert_eq!(back.procs[0].code, m.procs[0].code);
assert_eq!(back.data[0].text, "1.5");
assert_eq!(back.jump_tables, m.jump_tables);
}
#[test]
fn unbekannte_version_wird_abgelehnt() {
let m = CompiledModule {
name: "T".into(),
option_base: 0,
strings: vec![],
globals_init: vec![],
global_names: vec![],
udts: vec![],
procs: vec![],
data: vec![],
jump_tables: vec![],
};
let mut bytes = m.to_tbc();
bytes[4] = 0xFF; // Version hochsetzen
bytes[5] = 0x7F;
match CompiledModule::from_tbc(&bytes) {
Err(LoadError::Version(v)) => assert_eq!(v, 0x7FFF),
other => panic!("Version-Fehler erwartet, war {other:?}"),
}
let msg = LoadError::Version(0x7FFF).to_string();
assert!(msg.contains("32767"), "Meldung nennt die Version: {msg}");
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -11,3 +11,19 @@
pub mod bytecode; pub mod bytecode;
pub mod codegen; pub mod codegen;
pub mod interp; pub mod interp;
use tb_frontend::Diagnostic;
/// Komplette Übersetzung: Quelltext → Bytecode-Modul.
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
pub fn compile_source(
module_name: &str,
source: &str,
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
let analysis = tb_frontend::analyze_source(module_name, source);
if !analysis.diagnostics.is_empty() {
return Err(analysis.diagnostics);
}
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
Ok(codegen::compile(&hir))
}

457
crates/tb-vm/tests/vm.rs Normal file
View File

@@ -0,0 +1,457 @@
//! Interpreter-Tests: Spec-Szenarien aus vm-ausfuehrung und
//! vm-fehlerbehandlung (Phase-2-Änderung).
use tb_runtime::host::CaptureHost;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};
fn run(src: &str) -> (RunEvent, String) {
run_with_input(src, &[])
}
fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) {
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
panic!("Compile-Fehler: {d:?}");
});
let mut vm = Vm::new(module);
let mut host = CaptureHost::with_input(input);
let ev = vm.run(&mut host);
(ev, host.output)
}
fn out(src: &str) -> String {
let (ev, output) = run(src);
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{output}");
output
}
fn err_code(src: &str) -> u16 {
match run(src).0 {
RunEvent::Error { code, .. } => code,
other => panic!("Fehler erwartet, war {other:?}"),
}
}
// ---- 5.1 Ausdrücke und Konvertierungsmatrix --------------------------------
#[test]
fn print_hallo_welt() {
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
}
#[test]
fn banker_rounding_cint() {
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
assert_eq!(out("PRINT CINT(0.5); CINT(1.5); CINT(2.5)"), " 0 2 2 \n");
}
#[test]
fn overflow_bei_zuweisung() {
// Spec-Szenario: INTEGER-Variable = 40000 → Fehler 6
assert_eq!(err_code("i% = 40000"), 6);
}
#[test]
fn intdiv_rundet_operanden_vor() {
// Spec-Szenario: PRINT 7.5 \ 2 → 8 \ 2 = 4
assert_eq!(out("PRINT 7.5 \\ 2"), " 4 \n");
}
#[test]
fn gemischte_arithmetik() {
assert_eq!(out("i% = 2\nd# = i% + 1.5#\nPRINT d#"), " 3.5 \n");
assert_eq!(out("PRINT 1 / 3"), " .3333333 \n"); // SINGLE-Division
assert_eq!(out("PRINT 7 MOD 3; -7 MOD 3"), " 1 -1 \n"); // Vorzeichen wie Dividend
assert_eq!(out("PRINT 2 ^ 10"), " 1024 \n");
}
#[test]
fn logik_bitweise() {
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
}
#[test]
fn division_durch_null() {
assert_eq!(err_code("PRINT 1 / 0"), 11);
assert_eq!(err_code("PRINT 1 \\ 0"), 11);
assert_eq!(err_code("PRINT 0 ^ -1"), 11);
}
#[test]
fn stringvergleich_und_verkettung() {
assert_eq!(out("PRINT \"a\" + \"b\""), "ab\n");
assert_eq!(out("PRINT (\"abc\" < \"abd\")"), "-1 \n");
}
// ---- 5.2 Kontrollfluss -------------------------------------------------------
#[test]
fn for_ohne_durchlauf() {
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
}
#[test]
fn for_mit_negativem_step() {
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
}
#[test]
fn for_mit_dynamischem_step() {
assert_eq!(
out("s% = -2\nFOR i% = 5 TO 1 STEP s%\nPRINT i%;\nNEXT\nPRINT"),
" 5 3 1 \n"
);
}
#[test]
fn select_case_bereiche() {
let src = "FOR i% = 1 TO 4\nSELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3: PRINT \"mittel\"\nCASE ELSE: PRINT \"rest\"\nEND SELECT\nNEXT";
assert_eq!(out(src), "eins\nmittel\nmittel\nrest\n");
}
#[test]
fn do_loop_varianten() {
assert_eq!(
out("n% = 3\nDO WHILE n% > 0\nPRINT n%;\nn% = n% - 1\nLOOP\nPRINT"),
" 3 2 1 \n"
);
assert_eq!(
out("n% = 0\nDO\nn% = n% + 1\nLOOP UNTIL n% >= 3\nPRINT n%"),
" 3 \n"
);
assert_eq!(
out("n% = 5\nWHILE n% > 3\nn% = n% - 1\nWEND\nPRINT n%"),
" 3 \n"
);
}
#[test]
fn exit_for_und_do() {
assert_eq!(
out("FOR i% = 1 TO 10\nIF i% = 3 THEN EXIT FOR\nNEXT\nPRINT i%"),
" 3 \n"
);
}
#[test]
fn kontrollfluss_korpusdatei() {
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
)
.unwrap();
let want = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
)
.unwrap();
assert_eq!(out(&src), want);
}
#[test]
fn gosub_return_und_fehler_3() {
assert_eq!(
out("GOSUB U\nPRINT \"zurueck\"\nEND\nU:\nPRINT \"unten\"\nRETURN"),
"unten\nzurueck\n"
);
// Spec-Szenario: RETURN ohne GOSUB → Fehler 3
assert_eq!(err_code("RETURN"), 3);
}
#[test]
fn on_goto_berechnet() {
let src = "FOR i% = 0 TO 3\nON i% GOTO A, B\nPRINT \"kein\";\nGOTO W\nA:\nPRINT \"a\";\nGOTO W\nB:\nPRINT \"b\";\nW:\nNEXT\nPRINT";
assert_eq!(out(src), "keinabkein\n");
}
// ---- 5.3 Prozeduren ------------------------------------------------------------
#[test]
fn byref_wirkt_zurueck() {
// Spec-Szenario BYREF
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nPRINT n%"),
" 2 \n"
);
}
#[test]
fn klammern_erzwingen_byval() {
// Spec-Szenario BYVAL
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc (n%)\nPRINT n%"),
" 1 \n"
);
}
#[test]
fn byref_auf_arrayelement() {
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nDIM a%(5)\na%(2) = 7\nInc a%(2)\nPRINT a%(2)"),
" 8 \n"
);
}
#[test]
fn function_und_rekursion() {
assert_eq!(
out("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\nPRINT Quad(3)"),
" 9 \n"
);
let fak = "FUNCTION Fak& (n%)\nIF n% <= 1 THEN\nFak& = 1\nELSE\nFak& = n% * Fak&(n% - 1)\nEND IF\nEND FUNCTION\nPRINT Fak&(10)";
assert_eq!(out(fak), " 3628800 \n");
}
#[test]
fn static_behaelt_werte() {
let src = "SUB Zaehl\nSTATIC n%\nn% = n% + 1\nPRINT n%;\nEND SUB\nZaehl\nZaehl\nZaehl\nPRINT";
assert_eq!(out(src), " 1 2 3 \n");
}
#[test]
fn def_fn_im_modulkontext() {
// DEF FN: Parameter lokal (BYVAL), freie Namen binden an Modulvariablen.
let src = "faktor = 10\nDEF FNmal (x) = x * faktor\nPRINT FNmal(3)";
assert_eq!(out(src), " 30 \n");
}
#[test]
fn ganzes_array_uebergeben() {
let src = "SUB Summe (a%(), s%)\ns% = 0\nFOR i% = LBOUND(a%) TO UBOUND(a%)\ns% = s% + a%(i%)\nNEXT\nEND SUB\nDIM w%(3)\nFOR i% = 0 TO 3\nw%(i%) = i%\nNEXT\nSumme w%(), erg%\nPRINT erg%";
assert_eq!(out(src), " 6 \n");
}
#[test]
fn udt_wertsemantik_und_felder() {
let src = "TYPE Punkt\nx AS INTEGER\ny AS INTEGER\nEND TYPE\nDIM a AS Punkt, b AS Punkt\na.x = 1\nb = a\nb.x = 9\nPRINT a.x; b.x";
assert_eq!(out(src), " 1 9 \n");
}
// ---- 5.4 DATA/READ/RESTORE -----------------------------------------------------
#[test]
fn data_read_restore() {
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
}
#[test]
fn out_of_data_fehler_4() {
assert_eq!(err_code("DATA 1\nREAD a%, b%"), 4);
}
#[test]
fn data_typkonflikt_fehler_13() {
assert_eq!(err_code("DATA hallo\nREAD a%"), 13);
}
#[test]
fn restore_mit_label() {
let src = "DATA 1\nMarke:\nDATA 2\nREAD a%\nRESTORE Marke\nREAD b%\nPRINT a%; b%";
assert_eq!(out(src), " 1 2 \n");
}
// ---- 5.5 Fehlerbehandlung -------------------------------------------------------
#[test]
fn modulweiter_handler_faengt_prozedurfehler() {
// Spec-Szenario: Hauptprogramm setzt Handler, SUB löst Fehler 6 aus.
let src = "SUB Knall\ni% = 40000\nEND SUB\nON ERROR GOTO Fehler\nKnall\nPRINT \"nie\"\nEND\nFehler:\nPRINT \"ERR=\"; ERR\nEND";
assert_eq!(out(src), "ERR= 6 \n");
}
#[test]
fn lokaler_handler_verdeckt_modulweiten() {
let src = "SUB Tu\nON LOCAL ERROR GOTO L\nERROR 5\nEXIT SUB\nL:\nPRINT \"lokal\"; ERR\nRESUME Weiter\nWeiter:\nEND SUB\nON ERROR GOTO M\nTu\nEND\nM:\nPRINT \"modul\"\nEND";
assert_eq!(out(src), "lokal 5 \n");
}
#[test]
fn ohne_handler_bricht_ab() {
// Spec-Szenario: Fehler 9 ohne Handler → Abbruch mit Meldung.
let (ev, _) = run("DIM a%(3)\nPRINT a%(7)");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 9);
assert_eq!(message, "Subscript out of range");
}
other => panic!("{other:?}"),
}
}
#[test]
fn erl_liefert_zeilennummer() {
let src = "ON ERROR GOTO H\n10 ERROR 5\nEND\nH:\nPRINT ERL\nEND";
assert_eq!(out(src), " 10 \n");
}
#[test]
fn erl_null_ohne_zeilennummern() {
// Spec-Szenario: keine numerischen Zeilennummern → ERL = 0
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\nPRINT ERL\nEND";
assert_eq!(out(src), " 0 \n");
}
#[test]
fn error_anweisung() {
// Spec-Szenario: ERROR 53 → Handler mit ERR = 53
let src = "ON ERROR GOTO H\nERROR 53\nEND\nH:\nPRINT ERR\nEND";
assert_eq!(out(src), " 53 \n");
}
#[test]
fn resume_wiederholt_anweisung() {
let src = "ON ERROR GOTO H\nn% = 0\nversuch% = 0\n10 versuch% = versuch% + 1\nIF versuch% < 3 THEN ERROR 5\nPRINT versuch%\nEND\nH:\nRESUME";
// RESUME wiederholt die IF-Anweisung; versuch% bleibt 1? Nein:
// Fehler in IF-Zeile, RESUME wiederholt IF — Endlosschleife ohne
// Zählerénderung wäre falsch. Zähler steht in Zeile 10, daher: der
// Fehler passiert im IF, RESUME wiederholt das IF, versuch% ist noch
// < 3 … Um Determinismus zu sichern, zählt der Handler mit.
let _ = src;
let src2 = "ON ERROR GOTO H\nversuch% = 0\nERROR 5\nPRINT \"nach\"; versuch%\nEND\nH:\nversuch% = versuch% + 1\nIF versuch% < 3 THEN RESUME\nRESUME NEXT";
assert_eq!(out(src2), "nach 3 \n");
}
#[test]
fn resume_next_faehrt_fort() {
// Spec-Szenario: RESUME NEXT nach Division durch 0.
let src = "ON ERROR GOTO H\nx = 1 / 0\nPRINT \"weiter\"\nEND\nH:\nRESUME NEXT";
assert_eq!(out(src), "weiter\n");
}
#[test]
fn resume_ohne_fehler_20() {
assert_eq!(err_code("RESUME"), 20);
}
#[test]
fn fehler_im_handler_ist_fatal() {
// Spec-Szenario: kein Kaskadieren.
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\ni% = 40000\nEND";
let (ev, _) = run(src);
match ev {
RunEvent::Error { code, .. } => assert_eq!(code, 6),
other => panic!("{other:?}"),
}
}
#[test]
fn on_error_goto_0_deaktiviert() {
let src = "ON ERROR GOTO H\nON ERROR GOTO 0\nERROR 5\nEND\nH:\nPRINT \"nie\"\nEND";
assert_eq!(err_code(src), 5);
}
// ---- 5.6 Unterbrechbarkeit -------------------------------------------------------
#[test]
fn breakpoint_haelt_an_und_setzt_fort() {
let module =
tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nc% = 3\nPRINT a% + b% + c%").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
vm.add_breakpoint(3);
// Spec-Szenario: hält VOR der Anweisung in Zeile 3.
match vm.run(&mut host) {
RunEvent::Breakpoint { line } => assert_eq!(line, 3),
other => panic!("{other:?}"),
}
// Spec-Szenario Inspektion: b% ist gesetzt, c% noch 0.
assert!(matches!(vm.inspect("b%"), Some(Value::Int(2))));
assert!(matches!(vm.inspect("c%"), Some(Value::Int(0))));
// Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut).
vm.remove_breakpoint(3);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(host.output, " 6 \n");
}
#[test]
fn einzelschritt() {
let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nEND").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
vm.set_step(true);
let mut lines = Vec::new();
loop {
match vm.run(&mut host) {
RunEvent::Stepped { line } => lines.push(line),
RunEvent::Ended => break,
other => panic!("{other:?}"),
}
}
assert_eq!(lines, vec![1, 2, 3]);
}
// ---- 5.7 Programmende --------------------------------------------------------------
#[test]
fn stop_liefert_zeile_und_ist_fortsetzbar() {
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nSTOP\nPRINT \"b\"").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
match vm.run(&mut host) {
RunEvent::Stopped { line } => assert_eq!(line, 2),
other => panic!("{other:?}"),
}
// IDE-Semantik: CONT = weiterlaufen.
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(host.output, "a\nb\n");
}
#[test]
fn end_und_system() {
assert_eq!(run("PRINT \"x\"\nEND").0, RunEvent::Ended);
assert_eq!(run("SYSTEM").0, RunEvent::Ended);
}
// ---- Eingabe -----------------------------------------------------------------------
#[test]
fn input_mit_redo() {
let (ev, output) = run_with_input(
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
&["abc", "21"],
);
assert_eq!(ev, RunEvent::Ended);
assert!(output.contains("Redo from start"));
assert!(output.ends_with(" 42 \n"), "{output}");
}
#[test]
fn line_input_liest_ganze_zeile() {
let (ev, output) = run_with_input("LINE INPUT s$\nPRINT s$", &["a, b, c"]);
assert_eq!(ev, RunEvent::Ended);
assert!(output.ends_with("a, b, c\n"));
}
// ---- Strings/PRINT über die VM -------------------------------------------------------
#[test]
fn mid_anweisung_mutiert() {
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
}
#[test]
fn print_zonen_und_tab() {
assert_eq!(out("PRINT \"a\", \"b\""), "a b\n");
assert_eq!(out("PRINT TAB(5); \"x\""), " x\n");
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
}
#[test]
fn unsupported_feature_fehler_73() {
// Dokumentiert, aber Phase 3: Datei-E/A → Laufzeitfehler 73.
let (ev, _) = run("OPEN \"x.txt\" FOR INPUT AS #1");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73);
// Katalogtext des Vorbilds (VBDOS) für Code 73:
assert_eq!(message, "Feature unavailable");
}
other => panic!("{other:?}"),
}
}

View File

@@ -84,7 +84,10 @@ stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
- **Konvertierung:** implizit zwischen numerischen Typen mit Rundung - **Konvertierung:** implizit zwischen numerischen Typen mit Rundung
(Banker's Rounding bei `CINT`/`CLNG` und Zuweisung an Ganzzahl); Überlauf (Banker's Rounding bei `CINT`/`CLNG` und Zuweisung an Ganzzahl); Überlauf
→ Fehler 6. Keine implizite Konvertierung Zahl ↔ String (Fehler 13, → Fehler 6. Keine implizite Konvertierung Zahl ↔ String (Fehler 13,
Type mismatch). Type mismatch). Die vollständige Konvertierungsmatrix (Ergebnistypen
der Operatoren, Rundungs- und Überlaufregeln, Textdarstellung) steht
in docs/tbvm-design.md und ist über `tests/compat/konvertierung.bas`
ausführbar verankert (Phase 2, 2026-09-02).
## 3. Deklarationen und Sichtbarkeit ## 3. Deklarationen und Sichtbarkeit
@@ -133,7 +136,11 @@ INTEGER 1 (wahr) / 0 (falsch).
- `ON n GOTO liste`, `ON n GOSUB liste` (berechneter Sprung, 1-basiert; - `ON n GOTO liste`, `ON n GOSUB liste` (berechneter Sprung, 1-basiert;
0 oder > Anzahl: kein Sprung; negativ/>255: Fehler 5) 0 oder > Anzahl: kein Sprung; negativ/>255: Fehler 5)
- `END` (Programmende), `STOP` (Unterbrechung → im IDE-Kontext Debugger), - `END` (Programmende), `STOP` (Unterbrechung → im IDE-Kontext Debugger),
`SYSTEM` (Programmende, im Vorbild „zurück zu DOS") `SYSTEM` (Programmende, im Vorbild „zurück zu DOS").
Runner-Verhalten (Entscheidung 2026-09-02): außerhalb der IDE beendet
`STOP` das Programm mit der Meldung `STOP in line n` und Exit-Code ≠ 0;
`END`/`SYSTEM`/Programmende liefern Exit-Code 0. `CONT` existiert nur
als Direktmodus-Befehl der IDE (Phase 5) und ist nicht kompilierbar.
- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten — - `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten —
zentral für Forms) zentral für Forms)

View File

@@ -59,16 +59,29 @@ enum Value {
die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein
eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt. eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt.
## `.tbc`-Container (Entwurf) ## `.tbc`-Container (Stand Phase 2, Formatversion 1)
``` ```
Magic "TBC\0" · Formatversion u16 · Flags Magic "TBC\0" · Formatversion u16 · Flags u16 · Abschnittsanzahl u32
Abschnittstabelle: [ (Kennung, Offset, Länge) ] Abschnittstabelle: [ (Kennung 4 Byte, Offset u32, Länge u32) ]
Abschnitte: CONSTS, TYPES, PROCS, CODE, DATA, LINES, FORMS (serialisierte .FRM-Beschreibungen) 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, Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig,
Format bleibt stabil und dokumentiert). 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 ## Eigenständige Executables
@@ -111,8 +124,148 @@ Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender.
Originalhardware; Richtwert grob Lua-Interpreter-Klasse, gemessen und Originalhardware; Richtwert grob Lua-Interpreter-Klasse, gemessen und
dokumentiert statt geraten. 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 Exponentialform `mE±xx`
(exakte Schwelle `TODO verify`; umgesetzt: Exponentialform, wenn der
Dezimalexponent < 7 oder ≥ 7 ist).
- DOUBLE: wie SINGLE mit bis 16 signifikanten Stellen und `D±xx` als
Exponentmarke.
- CURRENCY: Festformat mit bis zu 4 Nachkommastellen, nachgestellte
Nullen entfallen (`TODO verify`).
- `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<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 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 ## Offene Punkte
- Opcode-Satz konkret ausformulieren (mit Phase 2) Keine — die Punkte aus Phase 0 (Opcode-Satz, Konvertierungsmatrix,
- Zahlenkonvertierungs-Matrix (implizite Casts, Rundung, Überlauf) als Tabelle `STOP`/`CONT` im Runner) sind oben ausgearbeitet; verbleibende
- Verhalten von `STOP`/`CONT` im Runner (ohne IDE): Abbruch mit Meldung? Detailunsicherheiten sind als `TODO verify` direkt an den betroffenen
Zellen markiert und über den Testkorpus abgesichert.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-02

View File

@@ -0,0 +1,129 @@
# Design — Phase 2: Bytecode und VM
## Context
Motivation siehe proposal.md („Why"). Ausgangslage: `tb-frontend` liefert
AST + Diagnosen, wirft aber alle Sema-Ergebnisse (Typen, Slots,
Konstanten) weg; `tb-vm` ist ein Stub. Rahmenbedingungen aus PLAN.md und
docs/tbvm-design.md: Instant-Compile (Single-Pass, keine
Optimierungspasses), Interpreter-Tempo in Lua-Klasse, Werte-Modell als
Tagged Enum mit `Rc` (kein GC — der Dialekt kennt keine Zyklen),
`step()`-basierte, unterbrechbare VM. Entscheidungen dieses Dokuments
wurden am 2026-09-02 in einer Explore-Session getroffen und bestätigt.
## Goals / Non-Goals
**Goals:**
- Durchgängige Pipeline Quelltext → HIR → Bytecode → Ausführung mit
byte-genauer Korpus-Ausgabe.
- Debugger-taugliche VM-Architektur, ohne dass der Debugger selbst
(Phase 5) schon gebaut wird.
- Builtin-ABI, die Phase 3 nur noch „auffüllt".
**Non-Goals:**
- Mehrmodul-Linking und `COMMON`-Verknüpfung über Moduleinheiten (die
Prozedurtabelle ist modul-qualifiziert angelegt, verknüpft wird ab
Phase 5 mit `.MAK`-Projekten).
- Datei-E/A, Ereigniszustellung (Hooks über Tick-Flag-Wort genügen),
`tbc build --exe`, Optimierungspasses jeder Art.
## Decisions
### D1 — Typisiertes HIR als Sema-Ausgabe (statt Doppel-Resolution oder Side-Tables)
Die Sema wird vom reinen Checker zum Lowering-Pass: Ausgabe ist ein
eigener HIR-Baum (Namen → Slots, Typen an jedem Ausdruck, Konstanten
gefaltet, implizite Konvertierungen als explizite Knoten). Verworfen:
(a) Codegen resolved selbst nochmal — dupliziert die Typisierungslogik,
Divergenz-Bugs; (b) Side-Tables am AST — der AST hat keine Node-IDs,
Pointer-Keys sind fragil. Das HIR hält das Single-Pass-Budget ein (alle
Pässe linear, keine globale Analyse) und macht den Codegen zum dummen
Tree-Walk.
### D2 — Monomorpher Opcode-Satz, Konvertierungen als eigene Opcodes
Da das HIR jede Konvertierung materialisiert, sind alle Arithmetik-,
Vergleichs- und Konvertierungs-Opcodes typrein (`ADD_I16`, `ADD_F64`,
`CONV_I16_F64`, `STR_EQ` …). Kein Tag-Dispatch in heißen Pfaden; die
Konvertierungsmatrix lebt an genau einer Stelle (HIR-Erzeugung), der
Interpreter bleibt eine enge `match`-Schleife. Preis: breiter
Opcode-Satz (~150200) — Fleiß, keine Komplexität. Encoding: Opcode
1 Byte; Slots u16; Konstantenpool-Index u16 (Wide-Variante bei Bedarf);
Sprünge u32-absolut innerhalb der Prozedur (fixup-freundlich).
### D3 — Host-Trait mit blockierenden Aufrufen (statt Yield-Modell)
Alle Konsolenwirkungen laufen über ein `Host`-Trait (Ausgabe, Zeile
lesen, Abbruchprüfung). Host-Calls dürfen blockieren und sind aus
VM-Sicht atomar; Ticks (Breakpoints, Einzelschritt, Abbruch) greifen an
Anweisungsgrenzen. Verworfen: VM „yieldet" bei E/A und der Einbetter
erfüllt Requests — debuggerfreundlicher, aber deutlich komplexer; für
den IDE-Debugger reicht die Anweisungsgranularität. Implementierungen:
Konsolen-Host (`tbc run`), Capture-Host (Test-Harness), später
Zellenpuffer-Host (Phase 3/5).
### D4 — Fehlerbehandlung: Handler-Zustand modul-global + pro Frame
`ON ERROR GOTO` setzt den modulweiten Handler, `ON LOCAL ERROR` einen
Frame-lokalen, der ihn verdeckt. Beim Fehler: aktiven Handler suchen
(erst Frame-Kette nach LOCAL-Handlern, sonst Modul-Handler), Stack bis
zum Handler-Frame abwickeln, gemerkten Anweisungs-Offset + Frame für
`RESUME` sichern, `ERR`/`ERL` setzen. Fehler bei aktiver Behandlung →
sofortiger Abbruch (keine Kaskade). Grundlage ist die Original-Hilfe
(Topics `ON ERROR`, `RESUME`); ein Emulator-Spike ist nur nötig, falls
die Doku Lücken lässt.
### D5 — GOSUB-Stack pro Frame
`GOSUB` legt die Rücksprungadresse auf einen separaten Stack im
aktuellen Frame (kein Call-Frame); `RETURN` bedient zuerst diesen
Stack. Prozedurende mit nicht-leerem GOSUB-Stack verwirft ihn (wie beim
Vorbild beim Verlassen der Prozedur).
### D6 — Runner-Verhalten `STOP`/`CONT`
`STOP` außerhalb der IDE terminiert mit Meldung + Exit-Code ≠ 0; `END`/
Programmende → Exit-Code 0. `CONT` ist Direktmodus-Konzept der IDE
(Phase 5) — die `step()`-Architektur liefert Suspend/Resume dort gratis.
### D7 — `.tbc` mit eigenem Writer/Reader (kein serde)
Format bleibt stabil, dokumentiert und unabhängig von Crate-Versionen;
Abschnittstabelle erlaubt spätere Abschnitte (FORMS ab Phase 4) ohne
Versionsbruch. Prozedurtabelle trägt von Beginn an die Modulkennung.
### D8 — Benchmarks als früher Bestandteil, nicht als Abschluss
Das Compile-Budget-Harness (generiertes ~50k-Zeilen-Projekt) entsteht
früh in der Phase, damit Architekturfehler (z. B. quadratische
Fixup-Listen) sofort auffallen. Criterion-Benches für VM-Durchsatz
(Schleifen/Strings); Ergebnisse werden in docs/tbvm-design.md
festgehalten. Ein Dispatch-Mikrobenchmark vorab wurde verworfen
(vorzeitige Optimierung — gemessen wird am echten Interpreter).
## Risks / Trade-offs
- [Sema-Umbau destabilisiert Phase-1-Ergebnis] → Diagnostik-Verhalten
ist durch die 33 Frontend-Tests + Korpus fixiert; Umbau gilt erst als
fertig, wenn alle Bestandstests grün sind.
- [Opcode-Breite macht Interpreter-Match unübersichtlich] → Opcodes
systematisch gruppieren (Makro/Tabelle generiert Emit + Dispatch
konsistent); Dokumentationspflicht in tbvm-design.md.
- [Konvertierungsmatrix enthält falsch rekonstruierte Zellen] →
jede Zelle mit Fundstelle in der Original-Hilfe belegen; unklare
Zellen als `TODO verify` im Korpus markieren (Konvention besteht).
- [PRINT-Zahlformatierung (SINGLE/DOUBLE-Stellen) weicht ab] →
byte-genaue Korpustests decken Randfälle ab; Formatierungsregeln aus
der Original-Hilfe ableiten, Abweichungen dokumentieren.
- [Blockierende Host-Calls verhindern Abbruch während INPUT] →
akzeptiert für Phase 2 (Abbruchprüfung obliegt dem Host); die
Ereignisschleifen-Architektur (Phase 4) kann den Host asynchroner
gestalten, ohne die VM zu ändern.
## Migration Plan
Rein additiv (neue Crate-Inhalte, neue Sema-Ausgabe neben bestehender
Diagnostik). Kein Rollback-Plan nötig; PLAN.md-Haken und
docs/tbvm-design.md-Fortschreibung erfolgen beim Abschluss der Phase.
## Open Questions
- Exakte Ergebnistyp-Regel für `^` sowie Stellenzahl der
SINGLE/DOUBLE-Textdarstellung: wird beim Ausformulieren der Matrix
aus der Original-Hilfe übernommen; ändert weder Ansatz noch
Task-Zuschnitt.
- Ob `ERL` beim Vorbild auch Zeilennummern aus `GOSUB`-Zielen o. ä.
Sonderfällen meldet: bei Doku-Lücke entscheidet ein kurzer
Emulator-Check; betrifft nur eine Matrix-/Handlerzelle.

View File

@@ -0,0 +1,85 @@
# Phase 2 — Bytecode und VM (`tb-vm`)
## Why
Das Sprach-Frontend (Phase 1) liefert einen typgeprüften AST, aber es gibt
keine Ausführungsschicht: `tb-vm` besteht aus leeren Stub-Modulen, der
Kompatibilitäts-Testkorpus ist nur eine ruhende Verhaltensspezifikation.
Phase 2 macht aus dem Frontend einen Compiler mit lauffähigem Ergebnis —
Bytecode-Erzeugung, serialisierbares Kompilat (`.tbc`) und die TBVM als
Interpreter — und löst damit den Plan-Meilenstein ein: der Konsolen-Korpus
läuft per `tbc run` mit byte-genau korrekter Ausgabe.
## What Changes
- **Typisiertes HIR als Sema-Ausgabe** (Entscheidung 2026-09-02, Explore-
Session): Die semantische Analyse behält ihr Wissen — sie liefert neben
Diagnostics einen abgesenkten, typisierten Zwischenbaum (Namen → Slots,
Typen explizit, Konstanten gefaltet, implizite Konvertierungen als
explizite Knoten). Der Codegen wird ein einfacher Tree-Walk.
- **Bytecode-Feindesign** (Eingangsaufgabe aus Phase 0): monomorpher,
typisierter Opcode-Satz; Zahlenkonvertierungs-Matrix (implizite Casts,
Banker's Rounding, Überlauf → Fehler 6) als Tabelle in
docs/tbvm-design.md und als Korpustest verankert.
- **`.tbc`-Container**: Serialisierung mit eigenem Writer/Reader (Magic,
Formatversion, Abschnittstabelle: CONSTS, TYPES, PROCS, CODE, DATA,
LINES); Prozedurtabelle von Beginn an modul-qualifiziert.
- **TBVM-Interpreter**: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`,
Schleifen, `GOTO`/`GOSUB`), Prozeduraufrufe mit BYREF-Semantik,
`DEF FN`, `DATA`/`READ`/`RESTORE`.
- **Fehlerbehandlung**: `ON ERROR GOTO` (modulweit), `ON LOCAL ERROR`
(prozedurlokal), `RESUME [0|NEXT|label]`, `ERR`/`ERL`, Propagation die
Aufrufkette hoch, Fehler im Handler fatal.
- **Unterbrechbarkeit**: `step()`-basierte VM, Tick-Prüfung an
Anweisungsgrenzen (ein Flag-Wort), Breakpoints, Einzelschritt,
Variableninspektion — Grundlage für den IDE-Debugger (Phase 5).
- **Runner-Verhalten** (Entscheidung 2026-09-02): `STOP` außerhalb der IDE
terminiert mit Meldung und Exit-Code ≠ 0, `END` mit Exit-Code 0;
`CONT` bleibt IDE-Konzept (Phase 5).
- **Konsolen-Basisbibliothek (vorgezogene Phase-3-Scheibe)**: Builtin-ABI
(Dispatch-Tabelle in `tb-runtime`), `Host`-Trait für Konsolen-E/A
(blockierende Host-Calls, kein Yield-Modell), PRINT-Zahlenformatierung
und Druckzonen sowie die vom Korpus benötigten Stringfunktionen.
- **Test-Harness**: `cargo test` führt jede Korpusdatei per `tbc run` aus
und vergleicht byte-genau gegen `.out`.
- **Benchmarks** in `benches/`: Compile-Budget (~50k Zeilen < 1 s, Modul
< 50 ms) und VM-Durchsatz; Ergebnisse in docs/tbvm-design.md.
**Non-Goals:** Mehrmodul-Linking/`COMMON`-Verknüpfung über Moduleinheiten
(kommt mit Projekten, Phase 5); Datei-E/A, Ereignissteuerung, `tbc build
--exe` (spätere Phasen); vollständige Standardbibliothek (Phase 3).
## Capabilities
### New Capabilities
- `bytecode-kompilat`: Codegen HIR → Bytecode, Opcode-Satz,
`.tbc`-Format und Serialisierung, Instant-Compile-Budget.
- `vm-ausfuehrung`: Interpreter-Semantik — Ausdrücke inkl.
Zahlenkonvertierungs-Matrix, Kontrollfluss, Prozeduraufrufe/BYREF,
`GOSUB`, `DEF FN`, `DATA`/`READ`, Runner-Verhalten (`STOP`/`END`) und
Unterbrechbarkeit (Ticks, Breakpoints, Einzelschritt).
- `vm-fehlerbehandlung`: `ON [LOCAL] ERROR`, `RESUME`, `ERR`/`ERL`,
Handler-Scoping und Fehlerpropagation.
- `konsolen-basisbibliothek`: `Host`-Trait, Builtin-ABI,
PRINT-Formatierung/Druckzonen, Korpus-Stringfunktionen.
### Modified Capabilities
- `sprach-frontend`: Die Sema SHALL zusätzlich zum Diagnostik-Ergebnis
ein typisiertes HIR liefern (neue Ausgabe-Anforderung des Frontends).
- `kompat-testkorpus`: Neuer Laufzeitvergleich — jede Korpusdatei läuft
per `tbc run`, Ausgabe wird byte-genau gegen `.out` geprüft.
## Impact
- `crates/tb-frontend`: Sema-Umbau (HIR-Ausgabe), neues HIR-Modul.
- `crates/tb-vm`: `bytecode.rs`, `codegen.rs`, `interp.rs` von Stub zu
Vollimplementierung; neue Benchmarks.
- `crates/tb-runtime`: Builtin-Dispatch-Tabelle, PRINT-Formatierung,
Stringfunktionen (Scheibe).
- `crates/tb-cli`: `tbc run` (Kompilieren + Ausführen), Konsolen-`Host`.
- `tests/compat`: Harness + neue Korpusdateien (u. a. Konvertierungs-
matrix, Fehlerbehandlung); `docs/tbvm-design.md` wird fortgeschrieben
(Opcode-Satz, Matrix, Benchmark-Ergebnisse), PLAN.md-Haken folgen bei
Abschluss.

View File

@@ -0,0 +1,64 @@
## Purpose
Das Bytecode-Kompilat ist das serialisierbare Ergebnis der Übersetzung:
Der Codegenerator senkt das typisierte HIR in einen monomorphen
Opcode-Satz ab, der `.tbc`-Container hält es als stabiles, dokumentiertes
Format, und das Instant-Compile-Budget sichert den schnellen
Edit-Run-Zyklus.
## ADDED Requirements
### Requirement: Monomorpher Opcode-Satz
Der Codegenerator SHALL jedes HIR-Konstrukt in typisierte, monomorphe
Opcodes übersetzen: Arithmetik, Vergleiche und Konvertierungen tragen den
Operandentyp im Opcode (kein Tag-Dispatch über Werte zur Laufzeit);
implizite Konvertierungen erscheinen als explizite Konvertierungs-Opcodes
an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in
docs/tbvm-design.md vollständig dokumentiert sein.
#### Scenario: Gemischter Ausdruck wird monomorph
- **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel)
- **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode
#### Scenario: Namen sind zur Laufzeit aufgelöst
- **WHEN** eine Variable oder Prozedur im Bytecode referenziert wird
- **THEN** geschieht das über Slot- bzw. Tabellenindizes, nicht über Namens-Lookups
### Requirement: `.tbc`-Containerformat
Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein:
Magic `TBC\0`, Formatversion, Abschnittstabelle mit den Abschnitten
CONSTS (deduplizierter Konstantenpool), TYPES (TYPE-Layouts), PROCS
(modul-qualifizierte Prozedurtabelle mit Signatur, Locals-Anzahl,
Code-Offset), CODE, DATA (`READ`/`RESTORE`-Segment) und LINES
(Zeilentabelle). Laden und erneutes Serialisieren MUST verlustfrei sein;
eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung
abgewiesen werden.
#### Scenario: Roundtrip
- **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird
- **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung)
#### Scenario: Unbekannte Version
- **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird
- **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt
### Requirement: Zeilentabelle für Fehlerortung
Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile)
zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und
Einzelschritt die Quellzeile exakt benennen können.
#### Scenario: Fehler nennt Zeile
- **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt
- **THEN** nennt die Fehlermeldung Zeile 42
### Requirement: Instant-Compile-Budget
Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses
auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein
Benchmark MUST das Budget nachweisbar machen: ein Projekt von ~50.000
Zeilen kompiliert in unter 1 s, ein einzelnes Modul in unter 50 ms
(Release-Build, Referenzrechner); die Messwerte werden in
docs/tbvm-design.md festgehalten.
#### Scenario: Budget wird gemessen
- **WHEN** die Benchmarks in `benches/` laufen
- **THEN** wird die Kompilierzeit für das ~50k-Zeilen-Projekt und für ein Einzelmodul ausgewiesen und gegen das Budget verglichen

View File

@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Laufzeitvergleich über den Korpus
Ein automatischer Test SHALL jede Korpusdatei `tests/compat/*.bas`
kompilieren, über die VM mit Capture-Host ausführen und die Ausgabe
byte-genau (inklusive nachgestellter Leerzeichen und LF-Zeilenenden)
gegen die zugehörige `.out`-Datei vergleichen. Bei Abweichung MUST der
Test Datei, erste abweichende Zeile sowie Soll und Ist nennen.
#### Scenario: Korpus läuft mit korrekter Ausgabe
- **WHEN** die Laufzeit-Testsuite läuft
- **THEN** stimmt die Ausgabe jeder Korpusdatei byte-genau mit ihrer `.out` überein
#### Scenario: Abweichung wird benannt
- **WHEN** ein Korpusprogramm eine abweichende Ausgabe erzeugt
- **THEN** schlägt der Test fehl und nennt Datei, Zeilennummer, Soll- und Ist-Zeile
### Requirement: Korpusabdeckung der Phase-2-Semantik
Der Korpus SHALL um Referenzprogramme mit dokumentierter Sollausgabe für
die Zahlenkonvertierungs-Matrix (Rundung, Ganzzahldivision, logische
Operatoren, Überlauf-Randfälle über Fehlerbehandlung), die
Fehlerbehandlung (`ON ERROR`, `RESUME`-Varianten, `ERR`/`ERL`) sowie
Prozeduraufrufe mit BYREF-/BYVAL-Wirkung erweitert werden.
#### Scenario: Konvertierungsmatrix als Korpustest
- **WHEN** die Laufzeit-Testsuite läuft
- **THEN** existiert ein Korpusprogramm, das die Matrix-Randfälle ausgibt, und seine Ausgabe entspricht der dokumentierten Sollausgabe

View File

@@ -0,0 +1,66 @@
## Purpose
Die Konsolen-Basisbibliothek ist die in Phase 2 vorgezogene Scheibe der
Laufzeitbibliothek: die Builtin-Aufrufschnittstelle der VM, ein
`Host`-Abstraktion für Konsolen-E/A sowie PRINT-Formatierung und die vom
Testkorpus benötigten Stringfunktionen.
## ADDED Requirements
### Requirement: Builtin-Aufrufschnittstelle
Die VM SHALL Bibliotheksfunktionen und -anweisungen über eine indizierte
Dispatch-Tabelle aufrufen (Argumente auf dem Operandenstack, Index im
Opcode); die Tabelle lebt in der Laufzeitbibliothek und ist ohne
Änderung am Opcode-Satz erweiterbar (Phase 3 füllt sie auf).
#### Scenario: Builtin über Tabelle
- **WHEN** `PRINT LEN("abc")` ausgeführt wird
- **THEN** wird `LEN` über seinen Tabellenindex aufgerufen und liefert 3
### Requirement: Host-Abstraktion für Konsolen-E/A
Alle Konsolenwirkungen der VM SHALL über eine austauschbare
Host-Schnittstelle laufen (Textausgabe, zeilenweises Lesen für `INPUT`,
Abbruchprüfung). Host-Aufrufe dürfen blockieren; die Abbruchprüfung
(Strg+Untbr) obliegt dem Host. `tbc run` verwendet einen Konsolen-Host
(stdout/stdin); Tests MUST einen Capture-Host verwenden können, der die
Ausgabe byte-genau aufzeichnet.
#### Scenario: Capture-Host im Test
- **WHEN** ein Korpusprogramm im Test-Harness läuft
- **THEN** entsteht die komplette Ausgabe im Capture-Host und ist byte-genau vergleichbar
### Requirement: PRINT-Formatierung
`PRINT` SHALL Zahlen im Format des Vorbilds ausgeben: führendes
Leerzeichen für nicht-negative bzw. `-` für negative Werte,
nachgestelltes Leerzeichen (auch am Zeilenende); Zahlendarstellung je
Typ wie beim Vorbild (u. a. kein Dezimalpunkt bei ganzzahligen Werten,
signifikante Stellen je nach SINGLE/DOUBLE). `;` schließt direkt an,
`,` springt zur nächsten 14-Zeichen-Druckzone (belegt ein Element die
Zone ganz, zur übernächsten); ohne abschließendes `;`/`,` folgt ein
Zeilenumbruch.
#### Scenario: Zahlen mit Vorzeichenspalte
- **WHEN** `PRINT 1; -2; 3.5` ausgeführt wird
- **THEN** lautet die Ausgabe ` 1 -2 3.5 ` mit Zeilenumbruch
#### Scenario: Druckzonen
- **WHEN** `PRINT "12345678901234", "x"` ausgeführt wird
- **THEN** beginnt `x` in Spalte 29 (Zone nach der belegten übersprungen)
### Requirement: Korpus-Stringfunktionen
Die Bibliothek SHALL mindestens folgende Funktionen mit der Semantik des
Vorbilds bereitstellen (Zeichen = Unicode-Codepoints, dokumentierte
UTF-8-Abweichung): `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion),
`INSTR`, `UCASE$`, `LCASE$`, `STRING$`, `SPACE$`, `CHR$`, `ASC`,
`STR$`, `VAL`. Randfälle folgen dem Vorbild, u. a.: `VAL` liest das
führende Zahlenpräfix und ignoriert Leerraum; `STR$` stellt
nicht-negativen Zahlen ein Leerzeichen voran; `ASC("")` löst Fehler 5
„Illegal function call" aus.
#### Scenario: Korpusdatei strings.bas
- **WHEN** `tests/compat/strings.bas` ausgeführt wird
- **THEN** entspricht die Ausgabe byte-genau `strings.out`
#### Scenario: VAL mit Restzeichen
- **WHEN** `VAL(" 12.5abc")` ausgewertet wird
- **THEN** ist das Ergebnis 12.5

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Typisiertes HIR als Sema-Ausgabe
Die semantische Analyse SHALL neben den Diagnosen einen typisierten,
abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet:
Namen sind zu Slot-/Tabellenindizes aufgelöst, jeder Ausdrucksknoten
trägt seinen Ergebnistyp, `CONST`-Werte sind gefaltet, `DEFtype`-Regeln
angewandt und implizite Konvertierungen als explizite
Konvertierungsknoten materialisiert. Bei Diagnosen mit Fehlercharakter
darf kein HIR für das betroffene Konstrukt garantiert werden; ein
diagnose-freies Modul MUST ein vollständiges HIR ergeben.
#### Scenario: Konvertierung wird materialisiert
- **WHEN** `d# = i% + 1.5#` semantisch analysiert wird
- **THEN** enthält das HIR einen expliziten Konvertierungsknoten INTEGER→DOUBLE unter dem Additionsknoten und die Zuweisung trägt den Zieltyp DOUBLE
#### Scenario: Diagnose-freies Modul liefert HIR
- **WHEN** ein Korpusmodul ohne Diagnosen analysiert wird
- **THEN** liefert das Frontend ein HIR, aus dem der Codegen ohne erneute Namens- oder Typauflösung Bytecode erzeugen kann

View File

@@ -0,0 +1,115 @@
## Purpose
Die TBVM führt das Bytecode-Kompilat mit der exakten Semantik des
Dialekts aus: Ausdrücke mit der Zahlenkonvertierungs-Matrix des Vorbilds,
Kontrollfluss, Prozeduraufrufe mit BYREF, `GOSUB`, unterbrechbare
`step()`-Ausführung als Grundlage für Debugger und Ereignisschleife.
## ADDED Requirements
### Requirement: Zahlenkonvertierungs-Matrix
Die VM SHALL implizite und explizite Zahlenkonvertierungen nach der in
docs/tbvm-design.md dokumentierten Matrix ausführen, insbesondere:
Gleitkomma→Ganzzahl rundet kaufmännisch zur nächsten geraden Zahl
(Banker's Rounding); Bereichsüberschreitung löst Fehler 6 „Overflow"
aus; `/` liefert stets Gleitkomma, `\` und `MOD` konvertieren ihre
Operanden vorab zu Ganzzahlen; logische Operatoren (`AND`, `OR`, `XOR`,
`EQV`, `IMP`) arbeiten bitweise auf zu Ganzzahlen konvertierten
Operanden; CURRENCY rechnet als Festkomma ×10 000. Die Matrix SHALL
durch ein Korpusprogramm mit dokumentierter Sollausgabe abgedeckt sein.
#### Scenario: Banker's Rounding
- **WHEN** `PRINT CINT(0.5); CINT(1.5); CINT(2.5)` ausgeführt wird
- **THEN** lautet die Ausgabe ` 0 2 2 `
#### Scenario: Überlauf bei Konvertierung
- **WHEN** einer INTEGER-Variablen der Wert 40000 zugewiesen wird
- **THEN** tritt Laufzeitfehler 6 „Overflow" auf
#### Scenario: Ganzzahldivision rundet Operanden vor
- **WHEN** `PRINT 7.5 \ 2` ausgeführt wird
- **THEN** wird 7.5 zuerst zu 8 gerundet und die Ausgabe ist ` 4 `
### Requirement: Kontrollfluss
Die VM SHALL `IF`/`ELSEIF`/`ELSE`, `SELECT CASE` (Werte, Bereiche,
`IS`-Vergleiche, `CASE ELSE`), `FOR`/`NEXT` (inkl. `STEP`, null
Durchläufen bei unpassender Grenze), `DO`/`LOOP` (alle vier
Bedingungsformen), `WHILE`/`WEND`, `GOTO`, `ON expr GOTO/GOSUB` und
`EXIT FOR/DO` mit der Semantik des Vorbilds ausführen.
#### Scenario: FOR ohne Durchlauf
- **WHEN** `FOR i% = 3 TO 1: PRINT i%: NEXT` ausgeführt wird
- **THEN** wird der Schleifenkörper nicht betreten
#### Scenario: Korpus Kontrollfluss
- **WHEN** `tests/compat/kontrollfluss.bas` ausgeführt wird
- **THEN** entspricht die Ausgabe byte-genau `kontrollfluss.out`
### Requirement: GOSUB/RETURN neben Prozeduraufrufen
`GOSUB` SHALL die Rücksprungadresse auf einem GOSUB-Stack im aktuellen
Frame ablegen (kein eigener Frame); `RETURN` kehrt dorthin zurück,
`RETURN label` an das benannte Ziel. Ein `RETURN` ohne anhängigen
`GOSUB` MUST Fehler 3 „RETURN without GOSUB" auslösen.
#### Scenario: RETURN ohne GOSUB
- **WHEN** ein Programm `RETURN` ohne vorangehendes `GOSUB` ausführt
- **THEN** tritt Laufzeitfehler 3 auf
### Requirement: Prozeduraufrufe mit BYREF-Semantik
SUB-/FUNCTION-Aufrufe SHALL eigene Frames mit Locals-Slots verwenden.
Variablen, Arrayelemente und UDT-Felder werden BYREF übergeben
(Änderungen im Rumpf wirken auf das Argument zurück); ein geklammertes
Argument oder ein Ausdruck wird als temporärer Wert übergeben (BYVAL-
Wirkung). `STATIC`-Prozeduren behalten ihre Locals zwischen Aufrufen;
rekursive Aufrufe nicht-statischer Prozeduren erhalten frische Locals.
`DEF FN`-Funktionen werten im Modulkontext aus (Zugriff auf
Modulvariablen), ihre Parameter wirken lokal.
#### Scenario: BYREF wirkt zurück
- **WHEN** `SUB Inc(x%) : x% = x% + 1 : END SUB` mit `Inc n%` aufgerufen wird
- **THEN** ist `n%` danach um 1 erhöht
#### Scenario: Klammern erzwingen Wertübergabe
- **WHEN** dieselbe SUB mit `Inc (n%)` aufgerufen wird
- **THEN** bleibt `n%` unverändert
### Requirement: DATA/READ/RESTORE
`READ` SHALL die `DATA`-Konstanten des Moduls in Deklarationsreihenfolge
konsumieren und in den Zieltyp konvertieren; `RESTORE [label]` setzt den
Lesezeiger auf den Anfang bzw. auf die erste `DATA`-Anweisung ab dem
Label. Lesen hinter dem letzten Element MUST Fehler 4 „Out of DATA"
auslösen; eine nicht konvertierbare Konstante MUST Fehler 13 „Type
mismatch" auslösen.
#### Scenario: Out of DATA
- **WHEN** ein Programm mehr Werte liest als `DATA` bereitstellt
- **THEN** tritt Laufzeitfehler 4 auf
### Requirement: Unterbrechbare step()-Ausführung
Die VM SHALL als zustandsbehaftete, vom Einbetter getriebene Struktur
ausführbar sein: An Anweisungsgrenzen prüft sie ein Flag-Wort und kann
anhalten (Breakpoint auf (Modul, Zeile), Einzelschritt, Abbruchwunsch).
Im angehaltenen Zustand MUST der Einbetter Variableninhalte (Skalare,
Arrayelemente, UDT-Felder) inspizieren und die Ausführung fortsetzen
können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro
Opcode.
#### Scenario: Breakpoint hält an
- **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht
- **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile
#### Scenario: Inspektion im Halt
- **WHEN** die VM an einem Breakpoint steht
- **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen
### Requirement: Programmende im Runner
Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den
Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung
`STOP in line n` (bzw. Modul/Zeile) und einem Exit-Code ungleich 0
terminieren (Projektentscheidung 2026-09-02). `SYSTEM` verhält sich wie
`END`. `CONT` existiert nur als IDE-Konzept (Phase 5) und ist kein
Programmbestandteil.
#### Scenario: STOP im Runner
- **WHEN** ein per `tbc run` gestartetes Programm `STOP` in Zeile 7 ausführt
- **THEN** endet der Prozess mit Exit-Code ungleich 0 und die Meldung nennt Zeile 7

View File

@@ -0,0 +1,71 @@
## Purpose
Die BASIC-Fehlersemantik (`ON ERROR GOTO`, `ON LOCAL ERROR`, `RESUME`,
`ERR`/`ERL`) ist Teil des Kompatibilitätsvertrags: Programme des Vorbilds
verlassen sich auf Handler-Scoping, Propagation und Wiederaufnahme exakt
wie dokumentiert.
## ADDED Requirements
### Requirement: Handler-Scoping nach Vorbild
`ON ERROR GOTO label` SHALL einen modulweiten Handler aktivieren (auch
wenn die Anweisung in einer Prozedur steht); `ON LOCAL ERROR GOTO label`
SHALL einen prozedurlokalen Handler aktivieren, der den modulweiten für
die Dauer des Prozedurlaufs verdeckt. `ON [LOCAL] ERROR GOTO 0`
deaktiviert den jeweiligen Handler. Tritt ein Fehler in einer Prozedur
ohne zuständigen Handler auf, SHALL er die Aufrufkette hochpropagieren,
bis ein aktiver Handler gefunden ist; ohne Handler bricht das Programm
mit der Fehlermeldung und der Quellzeile ab.
#### Scenario: Modulweiter Handler fängt Prozedurfehler
- **WHEN** das Hauptprogramm `ON ERROR GOTO Fehler` gesetzt hat und eine gerufene SUB Fehler 6 auslöst
- **THEN** springt die Ausführung in den Handler `Fehler` und `ERR` ist 6
#### Scenario: Lokaler Handler verdeckt modulweiten
- **WHEN** eine Prozedur `ON LOCAL ERROR GOTO L` gesetzt hat und in ihr ein Fehler auftritt
- **THEN** behandelt `L` den Fehler, nicht der modulweite Handler
#### Scenario: Ohne Handler bricht das Programm ab
- **WHEN** Fehler 9 ohne aktiven Handler auftritt
- **THEN** endet das Programm mit der Meldung „Subscript out of range" und der Quellzeile
### Requirement: ERR und ERL
Beim Eintritt in einen Handler SHALL `ERR` den Fehlercode liefern und
`ERL` die zuletzt vor dem Fehler ausgeführte **numerische** Zeilennummer
(0, wenn keine numerischen Zeilennummern durchlaufen wurden —
alphanumerische Labels zählen nicht). `ERROR n` SHALL Fehler `n`
künstlich auslösen und dieselbe Maschinerie durchlaufen.
#### Scenario: ERL bei Label-Programm
- **WHEN** ein Programm ohne numerische Zeilennummern einen Fehler behandelt
- **THEN** liefert `ERL` 0
#### Scenario: ERROR-Anweisung
- **WHEN** `ERROR 53` bei aktivem Handler ausgeführt wird
- **THEN** wird der Handler mit `ERR` = 53 betreten
### Requirement: RESUME-Semantik
Im Handler SHALL `RESUME` (bzw. `RESUME 0`) die fehlerauslösende
Anweisung wiederholen, `RESUME NEXT` mit der Anweisung danach
fortfahren und `RESUME label` an das Ziel springen. `RESUME` außerhalb
einer aktiven Fehlerbehandlung MUST Fehler 20 „RESUME without error"
auslösen. Nach `RESUME` gilt die Fehlerbehandlung als abgeschlossen
(`ERR` wird zurückgesetzt).
#### Scenario: RESUME NEXT
- **WHEN** ein Handler nach Fehler 11 (`Division by zero`) `RESUME NEXT` ausführt
- **THEN** wird die auf die Division folgende Anweisung ausgeführt
#### Scenario: RESUME ohne Fehler
- **WHEN** `RESUME` außerhalb eines Handlers ausgeführt wird
- **THEN** tritt Laufzeitfehler 20 auf
### Requirement: Fehler im Handler ist fatal
Tritt ein Fehler auf, während die Fehlerbehandlung aktiv ist (zwischen
Handler-Eintritt und `RESUME`), SHALL das Programm ohne erneute
Handler-Suche mit der Fehlermeldung abbrechen (keine Fehlerkaskaden,
Verhalten des Vorbilds).
#### Scenario: Kein Kaskadieren
- **WHEN** im aktiven Handler selbst Fehler 6 auftritt
- **THEN** bricht das Programm mit „Overflow" ab, auch wenn ein Handler registriert ist

View File

@@ -0,0 +1,121 @@
# Tasks — Phase 2: Bytecode und VM
## 1. Feindesign-Dokumentation (Eingangsaufgabe aus Phase 0)
- [x] 1.1 Zahlenkonvertierungs-Matrix (implizite Casts, Banker's Rounding,
Überlauf, `/` vs. `\`/`MOD`, logische Operatoren, CURRENCY, `^`-Regel,
SINGLE/DOUBLE-Textdarstellung) als Tabelle in docs/tbvm-design.md
schreiben; jede Zelle mit Fundstelle in der Original-Hilfe belegen,
unklare Zellen als `TODO verify` markieren — verifizierbar: Abschnitt
existiert, keine Zelle ohne Beleg oder TODO-Marke
- [x] 1.2 Opcode-Satz vollständig in docs/tbvm-design.md ausformulieren
(Gruppen, Encoding, Operanden) und `STOP`/`CONT`-Runner-Entscheidung
sowie Fehlerbehandlungs-Scoping (design.md D4D6) dort festhalten —
verifizierbar: „Offene Punkte"-Abschnitt in tbvm-design.md ist leer
## 2. Frontend: Typisiertes HIR
- [x] 2.1 HIR-Datenmodell in `tb-frontend` definieren (Slots, typisierte
Ausdrücke, explizite Konvertierungsknoten, Prozedur-/Modulstruktur,
Quellzeilen-Anker) — verifizierbar: `cargo check` und Doku-Kommentare
je Knotentyp
- [x] 2.2 Sema zum Lowering-Pass umbauen: `analyze_source` liefert
zusätzlich HIR; Konstantenfaltung, `DEFtype`, implizite
Konvertierungen materialisieren — verifizierbar: alle 33 bestehenden
Frontend-Tests bleiben grün
- [x] 2.3 HIR-Tests: diagnose-freie Korpusmodule liefern vollständiges HIR;
Konvertierungsknoten-Szenario (`d# = i% + 1.5`) als Unit-Test —
verifizierbar: neue Tests in `crates/tb-frontend/tests` grün
## 3. Bytecode und Serialisierung
- [x] 3.1 Opcode-Definitionen und Modulstruktur (Konstantenpool, Typtabelle,
modul-qualifizierte Prozedurtabelle, DATA-Segment, Zeilentabelle) in
`tb-vm::bytecode` implementieren — verifizierbar: Unit-Tests für
Encoding/Decoding einzelner Opcodes
- [x] 3.2 `.tbc`-Writer/Reader (Magic, Formatversion, Abschnittstabelle)
implementieren — verifizierbar: Roundtrip-Test (schreiben → laden →
identisch) und Ablehnungstest für unbekannte Formatversion
## 4. Codegenerator
- [x] 4.1 Ausdrucks-Codegen (Literale, Slots, Arithmetik, Vergleiche,
logische Operatoren, Konvertierungsknoten, String-Konkatenation) —
verifizierbar: Unit-Tests HIR-Ausdruck → erwartete Opcode-Folge
- [x] 4.2 Kontrollfluss-Codegen (`IF`, `SELECT CASE`, `FOR`, `DO`, `WHILE`,
`GOTO`/`GOSUB`/`ON GOTO`, `EXIT`, Labels/Zeilennummern) mit
Fixup-Listen für Vorwärtsziele — verifizierbar: Unit-Tests je
Konstrukt
- [x] 4.3 Prozedur-Codegen (SUB/FUNCTION/`DEF FN`, BYREF-/BYVAL-Argumente,
`STATIC`, Frames/Locals), `DIM`/Arrays/UDTs, `DATA`-Segment und
Zeilentabelle emittieren — verifizierbar: Unit-Tests und
`cargo test -p tb-vm` grün
## 5. Interpreter (TBVM)
- [x] 5.1 VM-Grundgerüst: Werte-Modell, Operandenstack, Frames,
`step()`-Schleife mit Tick-Flag-Wort an Anweisungsgrenzen —
verifizierbar: Ausdrucks-Programme laufen in Unit-Tests
- [x] 5.2 Kontrollfluss und `GOSUB`-Stack pro Frame inkl. Fehler 3
(„RETURN without GOSUB") — verifizierbar: kontrollfluss.bas läuft im
Capture-Host mit korrekter Ausgabe
- [x] 5.3 Prozeduraufrufe (BYREF-Rückwirkung, Klammer-BYVAL, `STATIC`,
Rekursion, `DEF FN` im Modulkontext) — verifizierbar: Unit-Tests der
Spec-Szenarien aus vm-ausfuehrung
- [x] 5.4 `DATA`/`READ`/`RESTORE` inkl. Fehler 4 und 13 — verifizierbar:
Unit-Tests der Spec-Szenarien
- [x] 5.5 Fehlerbehandlung: Handler-Zustand (modul-global + Frame-lokal),
Unwinding, `RESUME`-Varianten, `ERR`/`ERL`, `ERROR n`, Fehler im
Handler fatal — verifizierbar: alle Szenarien aus vm-fehlerbehandlung
als Tests grün
- [x] 5.6 Unterbrechbarkeit: Breakpoints auf (Modul, Zeile), Einzelschritt,
Variableninspektion im Halt, Abbruchwunsch — verifizierbar:
Unit-Tests treiben die VM per `step()` und prüfen Halt/Inspektion
- [x] 5.7 Programmende: `END`/`SYSTEM`/Programmende vs. `STOP` mit Meldung
und Exit-Code-Unterscheidung — verifizierbar: Runner-Test prüft
Exit-Codes und Meldung
## 6. Konsolen-Basisbibliothek (`tb-runtime`-Scheibe)
- [x] 6.1 `Host`-Trait definieren; Konsolen-Host und Capture-Host
implementieren — verifizierbar: Unit-Test zeichnet Ausgabe byte-genau
auf
- [x] 6.2 Builtin-Dispatch-Tabelle + `CALL_BUILTIN`-ABI (Argumente auf dem
Stack) in VM und Runtime verdrahten — verifizierbar: `LEN`-Aufruf
läuft über die Tabelle (Spec-Szenario)
- [x] 6.3 PRINT-Formatierung: Zahlendarstellung je Typ, Vorzeichenspalte,
nachgestelltes Leerzeichen, `;`/`,`-Semantik, 14-Zeichen-Druckzonen —
verifizierbar: printzahlen.bas und printzonen.bas byte-genau korrekt
- [x] 6.4 Korpus-Stringfunktionen (`LEN`, `LEFT$`, `RIGHT$`, `MID$`,
`INSTR`, `UCASE$`, `LCASE$`, `STRING$`, `SPACE$`, `CHR$`, `ASC`,
`STR$`, `VAL`) mit Randfällen des Vorbilds — verifizierbar:
strings.bas byte-genau korrekt plus Randfall-Unit-Tests
(`VAL`-Präfixlesen, `STR$`-Leerzeichen, `ASC("")` → Fehler 5)
## 7. CLI und Test-Harness
- [x] 7.1 `tbc run <datei.bas>`: kompilieren, `.tbc` in Memory ausführen,
Konsolen-Host; Diagnosen bei Compile-Fehler, Exit-Codes nach D6 —
verifizierbar: `tbc run tests/compat/hello.bas` druckt `Hallo, Welt!`
- [x] 7.2 Harness in `tests/`: alle `tests/compat/*.bas` kompilieren, im
Capture-Host ausführen, byte-genau gegen `.out` vergleichen; bei
Abweichung Datei/Zeile/Soll/Ist nennen — verifizierbar: `cargo test`
führt den kompletten Korpus aus
- [x] 7.3 Korpus erweitern: Konvertierungsmatrix-Programm, Fehlerbehandlung
(`ON ERROR`/`RESUME`/`ERR`/`ERL`), BYREF/BYVAL-Programm mit
dokumentierter Sollausgabe (Belege bzw. `TODO verify`-Konvention) —
verifizierbar: neue `.bas`/`.out`-Paare laufen im Harness grün
## 8. Benchmarks und Abschluss
- [x] 8.1 Compile-Budget-Harness früh aufsetzen: Generator für
~50k-Zeilen-Projekt + Criterion-Bench; Budgetvergleich (< 1 s Projekt,
< 50 ms Modul) — verifizierbar: `cargo bench` weist die Zeiten aus
- [x] 8.2 VM-Durchsatz-Benches (Schleifen, Strings); Ergebnisse und
Benchmark-Stand in docs/tbvm-design.md „Performance" festhalten —
verifizierbar: Abschnitt enthält Messwerte mit Datum
- [x] 8.3 Meilenstein-Verifikation und Doku-Abschluss: kompletter Korpus
grün via `cargo test`, docs/sprachreferenz.md um Phase-2-Festlegungen
ergänzt (Rundung, `STOP`-Verhalten), PLAN.md-Phase-2-Haken gesetzt —
verifizierbar: alle Checkboxen der Phase in PLAN.md abgehakt,
Testsuite grün

30
tests/compat/byref.bas Normal file
View File

@@ -0,0 +1,30 @@
' BYREF wirkt auf das Argument zurueck; Klammern erzwingen
' Wertuebergabe (BYVAL); STATIC behaelt Werte; Rekursion ueber den
' Funktionsnamen (Spec vm-ausfuehrung).
n% = 1
Inc n%
PRINT n%
Inc (n%)
PRINT n%
PRINT Fak&(6)
Zaehl
Zaehl
END
SUB Inc (x%)
x% = x% + 1
END SUB
FUNCTION Fak& (n%)
IF n% <= 1 THEN
Fak& = 1
ELSE
Fak& = n% * Fak&(n% - 1)
END IF
END FUNCTION
SUB Zaehl
STATIC z%
z% = z% + 1
PRINT "zaehler"; z%
END SUB

5
tests/compat/byref.out Normal file
View File

@@ -0,0 +1,5 @@
2
2
720
zaehler 1
zaehler 2

View File

@@ -0,0 +1,14 @@
' ON ERROR/RESUME NEXT/ERR/ERL (Spec vm-fehlerbehandlung):
' Handler-Eintritt setzt ERR und ERL (letzte numerische Zeilennummer);
' RESUME NEXT setzt nach der fehlerausloesenden Anweisung fort.
ON ERROR GOTO Handler
10 ERROR 5
20 PRINT "nach RESUME NEXT"
x = 1 / 0
PRINT "weiter"
ON ERROR GOTO 0
PRINT "fertig"
END
Handler:
PRINT "ERR="; ERR; "ERL="; ERL
RESUME NEXT

View File

@@ -0,0 +1,5 @@
ERR= 5 ERL= 10
nach RESUME NEXT
ERR= 11 ERL= 20
weiter
fertig

View File

@@ -0,0 +1,19 @@
' Zahlenkonvertierungs-Matrix (docs/tbvm-design.md, Abschnitt
' "Zahlenkonvertierungs-Matrix"): Banker's Rounding (Original-Hilfe:
' CINT), Ganzzahldivision rundet Operanden vor, Logik bitweise auf
' gerundeten Ganzzahlen, Ueberlauf -> Fehler 6.
PRINT CINT(0.5); CINT(1.5); CINT(2.5); CINT(-0.5); CINT(-1.5)
PRINT 7.5 \ 2; 7 \ 2; -7 \ 2
PRINT 7 MOD 3; -7 MOD 3; 7.5 MOD 2
PRINT 1 / 4; 1 / 3
PRINT 2 ^ 10; 2 ^ 0.5
PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0; 1.5 AND 1
PRINT CLNG(2.5); CLNG(3.5)
ON ERROR GOTO Uebergelaufen
i% = 32767
i% = i% + 1
PRINT "nie"
END
Uebergelaufen:
PRINT "Fehler"; ERR
END

View File

@@ -0,0 +1,8 @@
0 2 2 0 -2
4 3 -3
1 -1 0
.25 .3333333
1024 1.414214
2 7 5 -1 0
2 4
Fehler 6