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

- Sema zum Lowering-Pass umgebaut: typisiertes HIR (Slots, explizite
  Konvertierungsknoten) als Codegen-Eingabe; BYREF verlangt exakten Typ
- Bytecode-Feindesign umgesetzt: monomorpher Opcode-Satz,
  .tbc-Container (Formatversion 1) mit eigenem Writer/Reader
- Codegenerator HIR -> Bytecode (Fixup-Listen, keine globalen Passes)
- TBVM-Interpreter: Kontrollfluss, GOSUB-Stack je Frame, BYREF/BYVAL,
  STATIC, DEF FN, DATA/READ/RESTORE, ON [LOCAL] ERROR/RESUME/ERR/ERL,
  Breakpoints/Einzelschritt/Inspektion, STOP fortsetzbar
- Runtime-Scheibe: Host-Trait (Konsole/Capture), Builtin-Tabelle,
  Konvertierungsmatrix, PRINT-Formatierung/Druckzonen, Stringfunktionen
- tbc run/build/check mit Exit-Codes nach Entscheidung D6
- Korpus-Harness (byte-genauer Vergleich) + 3 neue Korpusdateien
  (konvertierung, fehlerbehandlung, byref); 137 Tests gruen
- Benchmarks: Einzelmodul 1,2 ms / Projekt 49.760 Zeilen 124 ms
  (Budgets eingehalten), VM ~5 Mio Schleifeniterationen/s
- Doku fortgeschrieben (tbvm-design, sprachreferenz, PLAN);
  verlagerte Punkte als explizite Aufgaben in Phase 3
- OpenSpec-Change phase-2-bytecode-vm (27/27 Tasks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:28:07 +02:00
parent da23d52036
commit f7e57b0bd8
42 changed files with 9225 additions and 484 deletions

View File

@@ -1,13 +1,110 @@
//! `tbc` — Standalone-Compiler von Terminal Basic.
//!
//! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle
//! (siehe PLAN.md):
//! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
//! Unterbefehle (Phase 2):
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
//! - `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<()> {
println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
Ok(())
use std::path::{Path, PathBuf};
use std::process::ExitCode;
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 },
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
/// Geklammerter Ausdruck. Semantisch transparent, aber als Argument
/// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF.
Paren(Box<Expr>),
/// Ausgelassenes Argument (`LOCATE , 5`).
Missing,
}
@@ -58,6 +61,7 @@ impl Expr {
Expr::Name { pos, .. }
| Expr::Unary { pos, .. }
| Expr::Binary { pos, .. } => *pos,
Expr::Paren(e) => e.pos(),
_ => SourcePos::default(),
}
}
@@ -242,9 +246,9 @@ pub enum Stmt {
Gosub { target: LabelRef, pos: SourcePos },
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
Return { target: Option<LabelRef>, pos: SourcePos },
End,
StopStmt,
System,
End(SourcePos),
StopStmt(SourcePos),
System(SourcePos),
Exit { kind: ExitKind, pos: SourcePos },
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,14 @@ fn korpus_parst_und_wird_typgeprueft() {
"{name}: {:?}",
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;
}
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
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod datetime; // DATE$, TIME$, TIMER
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
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
thiserror.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 codegen;
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:?}"),
}
}