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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -508,6 +508,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"tb-frontend",
|
||||
"tb-runtime",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
||||
57
PLAN.md
57
PLAN.md
@@ -212,21 +212,38 @@ gefundene Frontend-Lücken sind Bugs im Sinne des Guiding Principle und
|
||||
werden sofort geschlossen (kein Wiedereröffnen der Phase nötig).
|
||||
|
||||
## Phase 2 — Bytecode und VM (`tb-vm`)
|
||||
- [ ] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
|
||||
|
||||
**Status: abgeschlossen (2026-09-02).** OpenSpec-Änderung
|
||||
`phase-2-bytecode-vm` (Proposal/Design/Specs/Tasks). Zusätzliche
|
||||
Entscheidung: Die Sema liefert ein **typisiertes HIR** (Namen → Slots,
|
||||
Konvertierungen explizit) als Codegen-Eingabe; der Codegenerator ist ein
|
||||
reiner Tree-Walk.
|
||||
|
||||
- [x] Eingangsaufgabe (aus Phase 0 übernommen): Bytecode-**Feindesign** —
|
||||
Opcode-Satz, Zahlenkonvertierungs-Matrix (implizite Casts, Rundung,
|
||||
Überlauf), `STOP`/`CONT`-Verhalten im Runner ohne IDE
|
||||
(offene Punkte in docs/tbvm-design.md abarbeiten)
|
||||
- [ ] Bytecode-Format und Serialisierung (`.tbc`)
|
||||
- [ ] Codegenerator AST → Bytecode
|
||||
- [ ] Interpreter: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`, Schleifen,
|
||||
`GOTO`/`GOSUB`), Prozeduraufrufe, `BYREF`-Semantik
|
||||
- [ ] Fehlerbehandlung: `ON ERROR GOTO/RESUME`, `ERR`/`ERL`, Fehlerkaskaden
|
||||
- [ ] Unterbrechbarkeit: Tick-Grenzen, Breakpoints, Einzelschritt,
|
||||
Variableninspektion (Grundlage für IDE-Debugger)
|
||||
- [ ] Benchmarks in `benches/`: Compile-Budget messen (Projekt ≈50k Zeilen
|
||||
< 1 s, einzelnes Modul < 50 ms) und VM-Durchsatz (Schleifen/Strings);
|
||||
Ergebnisse in docs/tbvm-design.md festhalten
|
||||
- [ ] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe (`tbc run`)
|
||||
(offene Punkte in docs/tbvm-design.md abgearbeitet)
|
||||
- [x] Bytecode-Format und Serialisierung (`.tbc`, Formatversion 1;
|
||||
Roundtrip- und Versionsablehnungs-Tests)
|
||||
- [x] Codegenerator HIR → Bytecode (monomorphe Opcodes, Fixup-Listen)
|
||||
- [x] Interpreter: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`, Schleifen,
|
||||
`GOTO`/`GOSUB`), Prozeduraufrufe, `BYREF`-Semantik (inkl.
|
||||
Klammer-BYVAL, STATIC, Rekursion, DEF FN, DATA/READ/RESTORE)
|
||||
- [x] Fehlerbehandlung: `ON [LOCAL] ERROR GOTO/RESUME`, `ERR`/`ERL`,
|
||||
Fehler im Handler fatal (keine Kaskaden), Propagation über Frames
|
||||
- [x] Unterbrechbarkeit: Tick-Grenzen, Breakpoints, Einzelschritt,
|
||||
Variableninspektion (Grundlage für IDE-Debugger); `STOP` ist in
|
||||
der Einbettung fortsetzbar (CONT-Grundlage)
|
||||
- [x] Benchmarks in `benches/`: Compile-Budget gemessen (49 760 Zeilen in
|
||||
124 ms; Einzelmodul 1,2 ms — Budgets eingehalten) und VM-Durchsatz
|
||||
(≈5 Mio Schleifeniterationen/s); Ergebnisse in docs/tbvm-design.md
|
||||
- [x] Meilenstein: Konsolen-Testkorpus läuft mit korrekter Ausgabe
|
||||
(`tbc run` + Harness `crates/tb-cli/tests/compat.rs`; Korpus um
|
||||
konvertierung/fehlerbehandlung/byref erweitert)
|
||||
|
||||
Vorgezogen aus Phase 3 (Meilenstein-Voraussetzung): `Host`-Trait
|
||||
(Konsole/Capture), Builtin-Dispatch-Tabelle, PRINT-Formatierung mit
|
||||
Druckzonen sowie die Korpus-Stringfunktionen (`tb-runtime`).
|
||||
|
||||
## Phase 3 — Laufzeitbibliothek (`tb-runtime`) und Bildschirm (`tb-ui::screen`)
|
||||
|
||||
@@ -261,6 +278,20 @@ Leitplanke Vollständigkeit); die Aufzählungen unten sind Beispiele.
|
||||
`CSRLIN`/`POS`, `WIDTH`, `VIEW PRINT` auf dem Zellenpuffer
|
||||
- [ ] Offene Detailfrage klären: Umfang der `KEY n`-Funktionstasten-Makros
|
||||
(`KEY LIST`/`ON`/`OFF`) und in docs/sprachreferenz.md festhalten
|
||||
- [ ] Aus Phase 2 verlagert — `ON ERROR GOTO`-Scoping des Vorbilds im
|
||||
Frontend abbilden: Handler-Label auf Modulebene ist auch aus
|
||||
Prozeduren heraus ansprechbar (derzeit wird nicht-lokales
|
||||
`ON ERROR` in einer Prozedur wie `ON LOCAL ERROR` behandelt,
|
||||
siehe sema.rs, lower_stmt/OnError)
|
||||
- [ ] Aus Phase 2 verlagert — DATA-Rohtext erhalten: unquotierte
|
||||
DATA-Elemente verlieren derzeit die Groß-/Kleinschreibung, weil
|
||||
der Lexer Bezeichner normalisiert; Rohtext-Erfassung im Lexer,
|
||||
Korpustest ergänzen
|
||||
- [ ] Aus Phase 2 verlagert — `TODO verify`-Zellen der
|
||||
Konvertierungsmatrix (docs/tbvm-design.md) gegen die
|
||||
Original-Hilfe auflösen: CURRENCY-Promotion mit Gleitkomma,
|
||||
`^`-Ergebnistyp, Exponentialform-Schwellen der Textdarstellung,
|
||||
CURRENCY-Ausgabeformat; zugehörige Korpustests nachschärfen
|
||||
- [ ] Meilenstein: klassische Konsolenprogramme laufen unverändert
|
||||
|
||||
## Phase 4 — Forms-Engine (`tb-ui::forms`)
|
||||
|
||||
@@ -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
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
crates/tb-cli/tests/compat.rs
Normal file
132
crates/tb-cli/tests/compat.rs
Normal 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);
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
459
crates/tb-frontend/src/hir.rs
Normal file
459
crates/tb-frontend/src/hir.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}");
|
||||
|
||||
623
crates/tb-runtime/src/builtins.rs
Normal file
623
crates/tb-runtime/src/builtins.rs
Normal 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 8–23 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);
|
||||
}
|
||||
}
|
||||
112
crates/tb-runtime/src/console.rs
Normal file
112
crates/tb-runtime/src/console.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
240
crates/tb-runtime/src/format.rs
Normal file
240
crates/tb-runtime/src/format.rs
Normal 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
|
||||
}
|
||||
}
|
||||
92
crates/tb-runtime/src/host.rs
Normal file
92
crates/tb-runtime/src/host.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
283
crates/tb-runtime/src/value.rs
Normal file
283
crates/tb-runtime/src/value.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
88
crates/tb-vm/benches/compile.rs
Normal file
88
crates/tb-vm/benches/compile.rs
Normal 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");
|
||||
}
|
||||
57
crates/tb-vm/benches/vm.rs
Normal file
57
crates/tb-vm/benches/vm.rs
Normal 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",
|
||||
);
|
||||
}
|
||||
@@ -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 §ions {
|
||||
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
@@ -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
457
crates/tb-vm/tests/vm.rs
Normal 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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,10 @@ stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
|
||||
- **Konvertierung:** implizit zwischen numerischen Typen mit Rundung
|
||||
(Banker's Rounding bei `CINT`/`CLNG` und Zuweisung an Ganzzahl); Überlauf
|
||||
→ Fehler 6. Keine implizite Konvertierung Zahl ↔ String (Fehler 13,
|
||||
Type mismatch).
|
||||
Type mismatch). Die vollständige Konvertierungsmatrix (Ergebnistypen
|
||||
der Operatoren, Rundungs- und Überlaufregeln, Textdarstellung) steht
|
||||
in docs/tbvm-design.md und ist über `tests/compat/konvertierung.bas`
|
||||
ausführbar verankert (Phase 2, 2026-09-02).
|
||||
|
||||
## 3. Deklarationen und Sichtbarkeit
|
||||
|
||||
@@ -133,7 +136,11 @@ INTEGER −1 (wahr) / 0 (falsch).
|
||||
- `ON n GOTO liste`, `ON n GOSUB liste` (berechneter Sprung, 1-basiert;
|
||||
0 oder > Anzahl: kein Sprung; negativ/>255: Fehler 5)
|
||||
- `END` (Programmende), `STOP` (Unterbrechung → im IDE-Kontext Debugger),
|
||||
`SYSTEM` (Programmende, im Vorbild „zurück zu DOS")
|
||||
`SYSTEM` (Programmende, im Vorbild „zurück zu DOS").
|
||||
Runner-Verhalten (Entscheidung 2026-09-02): außerhalb der IDE beendet
|
||||
`STOP` das Programm mit der Meldung `STOP in line n` und Exit-Code ≠ 0;
|
||||
`END`/`SYSTEM`/Programmende liefern Exit-Code 0. `CONT` existiert nur
|
||||
als Direktmodus-Befehl der IDE (Phase 5) und ist nicht kompilierbar.
|
||||
- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten —
|
||||
zentral für Forms)
|
||||
|
||||
|
||||
@@ -59,16 +59,29 @@ enum Value {
|
||||
die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein
|
||||
eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt.
|
||||
|
||||
## `.tbc`-Container (Entwurf)
|
||||
## `.tbc`-Container (Stand Phase 2, Formatversion 1)
|
||||
|
||||
```
|
||||
Magic "TBC\0" · Formatversion u16 · Flags
|
||||
Abschnittstabelle: [ (Kennung, Offset, Länge) ]
|
||||
Abschnitte: CONSTS, TYPES, PROCS, CODE, DATA, LINES, FORMS (serialisierte .FRM-Beschreibungen)
|
||||
Magic "TBC\0" · Formatversion u16 · Flags u16 · Abschnittsanzahl u32
|
||||
Abschnittstabelle: [ (Kennung 4 Byte, Offset u32, Länge u32) ]
|
||||
Abschnitte:
|
||||
MODN Modulname + OPTION BASE
|
||||
CONS deduplizierter Stringpool
|
||||
TYPS TYPE-Layouts (Feld-Initialisierungstypen)
|
||||
GLOB globale Slots (Initialisierungstyp + Name für Debugger)
|
||||
PROC Prozedurtabelle: Name, Parameteranzahl, Frame-Slots, Code
|
||||
(Zeileninfo liegt inline im Code: Stmt-/SetErl-Instruktionen —
|
||||
ersetzt den früher geplanten separaten LINES-Abschnitt)
|
||||
DATA DATA-Konstanten (Rohtext + Quellzeile)
|
||||
JMPT Sprungtabellen für ON n GOTO/GOSUB
|
||||
FORMS (ab Phase 4: serialisierte .FRM-Beschreibungen)
|
||||
```
|
||||
|
||||
Serialisierung mit einfachem eigenem Writer/Reader (kein serde nötig,
|
||||
Format bleibt stabil und dokumentiert).
|
||||
Format bleibt stabil und dokumentiert); unbekannte Formatversionen
|
||||
werden mit Meldung abgewiesen. Instruktions-Encoding: 1 Opcode-Byte +
|
||||
Operanden little-endian, Opcode-Bytes gruppenweise mit Lücken vergeben
|
||||
(`crates/tb-vm/src/bytecode.rs` ist die normative Liste).
|
||||
|
||||
## Eigenständige Executables
|
||||
|
||||
@@ -111,8 +124,148 @@ Nutzer — verworfen: erfordert Rust-Toolchain beim Anwender.
|
||||
Originalhardware; Richtwert grob Lua-Interpreter-Klasse, gemessen und
|
||||
dokumentiert statt geraten.
|
||||
|
||||
**Benchmark-Ergebnisse (2026-09-02, Release-Build, Entwicklerrechner
|
||||
Windows 11, `cargo bench -p tb-vm`):**
|
||||
|
||||
- Compile-Budget (`benches/compile.rs`):
|
||||
- Einzelmodul, 508 Zeilen: **1,2 ms** (Budget 50 ms — eingehalten)
|
||||
- Projekt, 49 760 Zeilen über 20 Module: **124 ms** (Budget 1 000 ms —
|
||||
eingehalten); Durchsatz ≈ 400 000 Zeilen/s
|
||||
- VM-Durchsatz (`benches/vm.rs`):
|
||||
- INTEGER-Schleife (Arithmetik + MOD): ≈ 4,8 Mio Iterationen/s
|
||||
- DOUBLE-Schleife: ≈ 5,2 Mio Iterationen/s
|
||||
- SUB-Aufrufe mit BYREF: ≈ 5,2 Mio Aufrufe/s
|
||||
- String-Lasten (MID$/INSTR/Verkettung): ≈ 0,8 Mio Runden/s
|
||||
- Einordnung: bei ≈ 13 Instruktionen je Schleifeniteration entspricht
|
||||
das ≈ 60 Mio Instruktionen/s — Vorbild-Programme auf 1992er-Hardware
|
||||
werden um weit mehr als den Faktor 100 übertroffen; für 80×25-
|
||||
Terminalprogramme mehr als ausreichend. Ein Performance-Pass ist erst
|
||||
bei Bedarf vorgesehen (Phase 6).
|
||||
|
||||
## Zahlenkonvertierungs-Matrix (Feindesign, 2026-09-02)
|
||||
|
||||
Grundlage: Original-Hilfe (Topics „Arithmetic Operators", „CINT/CLNG",
|
||||
„MOD", „Numeric Types"). Zellen ohne belastbare Fundstelle sind mit
|
||||
`TODO verify` markiert; der Korpustest `konvertierung.bas` verankert die
|
||||
Matrix ausführbar.
|
||||
|
||||
**Rangfolge für Promotion:** INTEGER < LONG < CURRENCY < SINGLE < DOUBLE.
|
||||
Ausnahme: CURRENCY gemischt mit SINGLE oder DOUBLE ergibt **DOUBLE**
|
||||
(nicht SINGLE), damit keine Festkomma-Präzision in f32 verloren geht
|
||||
(`TODO verify` gegen PDS-Hilfe; plausibelste Lesart).
|
||||
|
||||
**Ergebnistyp binärer Operatoren** (l op r; T = promoteter Typ):
|
||||
|
||||
| Operator | Ergebnistyp | Besonderheiten |
|
||||
|---|---|---|
|
||||
| `+ - *` | T; gleiche Typen bleiben erhalten | Ganzzahl-/CURRENCY-Überlauf → Fehler 6 (auch INTEGER+INTEGER promotet **nicht**) |
|
||||
| `/` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE | Division durch 0 → Fehler 11 |
|
||||
| `\` `MOD` | INTEGER, wenn beide Operanden INTEGER, sonst LONG | Operanden werden **vorher** kaufmännisch (banker's) auf Ganzzahl gerundet; außerhalb LONG → Fehler 6; Divisor 0 → Fehler 11; `MOD`-Vorzeichen wie Dividend |
|
||||
| `^` | DOUBLE, wenn ein Operand DOUBLE oder CURRENCY, sonst SINGLE (`TODO verify`) | Rechnung intern in DOUBLE; 0 ^ negativ → Fehler 11; negative Basis mit nicht-ganzem Exponent → Fehler 5 |
|
||||
| `= <> < <= > >=` | INTEGER (−1/0) | Vergleich im promoteten Typ; Strings codepoint-weise |
|
||||
| `NOT AND OR XOR EQV IMP` | INTEGER, wenn alle Operanden INTEGER, sonst LONG | bitweise; Operanden vorher banker's-gerundet auf Ganzzahl, außerhalb LONG → Fehler 6 |
|
||||
| unäres `-` | Typ des Operanden | −(−32768) INTEGER → Fehler 6 |
|
||||
|
||||
**Konvertierung Quelle → Ziel** (implizit bei Zuweisung/Argument, explizit
|
||||
via `CINT`/`CLNG`/`CSNG`/`CDBL`/`CCUR` — identische Semantik):
|
||||
|
||||
| von \ nach | INTEGER | LONG | SINGLE | DOUBLE | CURRENCY |
|
||||
|---|---|---|---|---|---|
|
||||
| INTEGER | — | exakt | exakt | exakt | ×10 000 |
|
||||
| LONG | Bereich prüfen → 6 | — | nächstgelegener f32 | exakt | ×10 000 |
|
||||
| SINGLE | banker's + Bereich → 6 | banker's + Bereich → 6 | — | exakt (Widening) | banker's auf 4 Nachkommastellen + Bereich → 6 |
|
||||
| DOUBLE | banker's + Bereich → 6 | banker's + Bereich → 6 | Bereich prüfen → 6, sonst nächstgelegener f32 | — | banker's auf 4 Nachkommastellen + Bereich → 6 |
|
||||
| CURRENCY | ÷10 000, banker's + Bereich → 6 | ÷10 000, banker's + Bereich → 6 | ÷10 000.0 | ÷10 000.0 | — |
|
||||
|
||||
- Banker's Rounding: zur nächsten Ganzzahl, bei .5 zur **geraden**
|
||||
(Original-Hilfe: CINT). Gilt für alle Gleitkomma→Ganzzahl-Übergänge
|
||||
inklusive `\`/`MOD`/Logik-Operanden und CURRENCY-Skalierung.
|
||||
- Zahl ↔ String nie implizit (Compile-Zeit „Type mismatch"); explizit nur
|
||||
über `STR$`/`VAL`.
|
||||
|
||||
**Textdarstellung (PRINT und STR$):**
|
||||
|
||||
- INTEGER/LONG: dezimal ohne Dezimalpunkt.
|
||||
- SINGLE: bis 7 signifikante Stellen, kürzeste Darstellung, keine
|
||||
führende Null vor dem Dezimalpunkt (`.5`), keine nachgestellten
|
||||
Nullen; außerhalb des Festformat-Bereichs Exponentialform `mE±xx`
|
||||
(exakte Schwelle `TODO verify`; umgesetzt: Exponentialform, wenn der
|
||||
Dezimalexponent < −7 oder ≥ 7 ist).
|
||||
- DOUBLE: wie SINGLE mit bis 16 signifikanten Stellen und `D±xx` als
|
||||
Exponentmarke.
|
||||
- CURRENCY: Festformat mit bis zu 4 Nachkommastellen, nachgestellte
|
||||
Nullen entfallen (`TODO verify`).
|
||||
- `PRINT` stellt nicht-negativen Zahlen ein Leerzeichen, negativen ein
|
||||
`-` voran und hängt stets ein Leerzeichen an; `STR$` nur das führende
|
||||
Leerzeichen/`-`, kein nachgestelltes.
|
||||
|
||||
## Opcode-Satz (Feindesign, 2026-09-02)
|
||||
|
||||
In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, ein Rust-
|
||||
Enum mit eingebetteten Operanden — Wort-Dispatch, keine Byte-Dekodierung
|
||||
im heißen Pfad); die `.tbc`-Serialisierung bildet jede Instruktion auf
|
||||
1 Opcode-Byte + Operanden (little-endian) ab. Typkürzel: `I2`=INTEGER,
|
||||
`I4`=LONG, `R4`=SINGLE, `R8`=DOUBLE, `CY`=CURRENCY, `STR`=STRING.
|
||||
|
||||
| Gruppe | Instruktionen | Bemerkung |
|
||||
|---|---|---|
|
||||
| Anweisungsgrenze | `Stmt(line:u32)` · `SetErl(n:u32)` | `Stmt` prüft das Tick-Flag-Wort (Breakpoint/Einzelschritt/Abbruch), aktualisiert Zeile und Resume-Punkt; `SetErl` bei numerischen Zeilennummern |
|
||||
| Konstanten/Stack | `PushInt(i16)` `PushLng(i32)` `PushSng(f32)` `PushDbl(f64)` `PushCur(i64)` `PushStr(pool:u16)` · `Dup` `Pop` | Stringpool dedupliziert |
|
||||
| Variablen | `LoadGlobal/StoreGlobal(u16)` · `LoadLocal/StoreLocal(u16)` · `MakeRefGlobal/MakeRefLocal(u16)` · `LoadRef/StoreRef(u16)` | Slots statisch aufgelöst; `*Ref` bedienen BYREF-Parameter (Referenz-Werte) |
|
||||
| Arrays | `DimGlobal/DimLocal{slot,dims,elem}` `RedimGlobal/RedimLocal{…}` `EraseGlobal/EraseLocal(u16)` · `LoadElem/StoreElem{dims:u8}` · `MakeRefElem{dims:u8}` | Grenzen auf dem Stack (lo/hi je Dimension als LONG); Indexprüfung → Fehler 9 |
|
||||
| UDT | `LoadField/StoreField(u16)` · `MakeRefField(u16)` | Feldindex aus Typtabelle; verschachtelt durch Verkettung |
|
||||
| Arithmetik | `Add/Sub/Mul{I2,I4,R4,R8,CY}` · `Neg{…}` · `Div{R4,R8}` · `IDiv/Mod{I2,I4}` · `PowR8` (`^` rechnet in DOUBLE, SINGLE-Ergebnis per Conv) | monomorph; Ganzzahl-/CY-Überlauf → Fehler 6, Division durch 0 → Fehler 11 |
|
||||
| Konvertierung | `Conv_<src>_<dst>` für alle 20 geordneten Paare aus {I2,I4,R4,R8,CY} | Semantik exakt nach Matrix (Rundung/Überlauf) |
|
||||
| Logik | `Not/And/Or/Xor/Eqv/Imp{I2,I4}` | bitweise |
|
||||
| Vergleich | `Cmp{Eq,Ne,Lt,Le,Gt,Ge}{I2,I4,R4,R8,CY,STR}` | Ergebnis INTEGER −1/0 |
|
||||
| Strings | `Concat` | `Rc<str>`, Kopien sind Pointer-Kopien |
|
||||
| Kontrollfluss | `Jump(u32)` `JumpIfFalse(u32)` `JumpIfTrue(u32)` | Ziele absolut (Instruktionsindex) innerhalb der Code-Einheit; Fixups beim Emit |
|
||||
| GOSUB | `Gosub(u32)` `RetGosub` `RetGosubTo(u32)` | GOSUB-Stack pro Frame; leer → Fehler 3 |
|
||||
| Berechnete Sprünge | `OnJump{table:u16,gosub:bool}` | Sprungtabellen im Modul; 0/zu groß: kein Sprung; negativ/>255 → Fehler 5 |
|
||||
| Prozeduren | `Call(proc:u16)` `RetProc` `RetFn` `CallBuiltin{id:u16,argc:u8}` | Argumente links→rechts auf dem Stack (Werte oder Referenzen); `RetFn` transportiert den Funktionswert über den Frame-Abbau |
|
||||
| Fehler | `OnErrorGoto(u32)` `OnErrorLocal(u32)` `OnErrorDisable` `OnErrorLocalDisable` · `Resume0` `ResumeNext` `ResumeLabel(u32)` · `RaiseError` | Scoping siehe unten |
|
||||
| DATA | `ReadData(art)` `Restore(u32)` | art 0 = String, 1 = Zahl (DOUBLE, dann Conv nach Matrix); Ende → Fehler 4, unkonvertierbar → Fehler 13 |
|
||||
| E/A | `Input{…}` sowie Builtins (`PRINT`-Familie über `CallBuiltin`) | Konsolenwirkung ausschließlich über das `Host`-Trait |
|
||||
| Ende | `End` `StopInstr` `SystemInstr` | Verhalten siehe Runner |
|
||||
|
||||
FOR/NEXT wird ohne Spezial-Opcodes kompiliert: Grenz-/Schrittwert einmal
|
||||
in versteckte Slots ausgewertet; bei konstantem STEP wählt der Codegen
|
||||
die Vergleichsrichtung zur Compilezeit, sonst verzweigt ein einmalig
|
||||
berechnetes Vorzeichen-Flag auf zwei Vergleichssequenzen. Inkrement mit
|
||||
regulärer typisierter Addition (Überlauf → Fehler 6, wie Vorbild).
|
||||
`SELECT CASE` wertet den Selektor in einen versteckten Slot aus und wird
|
||||
als Vergleichs-/Sprungkette abgesenkt.
|
||||
|
||||
## Fehlerbehandlung: Scoping (Feindesign, 2026-09-02)
|
||||
|
||||
- `ON ERROR GOTO x` setzt den **modulweiten** Handler — auch aus einer
|
||||
Prozedur heraus. `ON LOCAL ERROR GOTO x` setzt einen **Frame-lokalen**
|
||||
Handler, der den modulweiten für die Dauer des Prozedurlaufs verdeckt.
|
||||
`… GOTO 0` deaktiviert den jeweiligen Handler.
|
||||
- Fehlerfall: erst die Frame-Kette von innen nach außen nach
|
||||
LOCAL-Handlern absuchen, sonst modulweiter Handler; Stack wird bis zum
|
||||
Handler-Frame abgewickelt, Anweisungs-Offset und Frame für `RESUME`
|
||||
gemerkt, `ERR`/`ERL` gesetzt. Ohne Handler: Abbruch mit Meldung und
|
||||
Quellzeile.
|
||||
- Fehler **während aktiver Behandlung** (zwischen Handler-Eintritt und
|
||||
`RESUME`): sofortiger Abbruch, keine Kaskade (Vorbild).
|
||||
- `ERL` liefert die zuletzt durchlaufene **numerische** Zeilennummer
|
||||
(Labels zählen nicht; ohne numerische Zeilen: 0).
|
||||
|
||||
## Runner-Verhalten `STOP`/`CONT` (Entscheidung 2026-09-02)
|
||||
|
||||
- `END`, `SYSTEM` und das Erreichen des Programmendes beenden den
|
||||
Prozess mit Exit-Code 0.
|
||||
- `STOP` außerhalb der IDE terminiert mit der Meldung `STOP in line n`
|
||||
(bzw. Modul/Zeile) und Exit-Code ≠ 0. Offene Dateien werden wie bei
|
||||
`END` geschlossen.
|
||||
- `CONT` ist ein Direktmodus-Konzept der IDE (Phase 5): dort suspendiert
|
||||
`STOP` die VM (`step()`-Architektur), `CONT` setzt fort. Im Kompilat
|
||||
existiert kein `CONT`.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Opcode-Satz konkret ausformulieren (mit Phase 2)
|
||||
- Zahlenkonvertierungs-Matrix (implizite Casts, Rundung, Überlauf) als Tabelle
|
||||
- Verhalten von `STOP`/`CONT` im Runner (ohne IDE): Abbruch mit Meldung?
|
||||
Keine — die Punkte aus Phase 0 (Opcode-Satz, Konvertierungsmatrix,
|
||||
`STOP`/`CONT` im Runner) sind oben ausgearbeitet; verbleibende
|
||||
Detailunsicherheiten sind als `TODO verify` direkt an den betroffenen
|
||||
Zellen markiert und über den Testkorpus abgesichert.
|
||||
|
||||
2
openspec/changes/phase-2-bytecode-vm/.openspec.yaml
Normal file
2
openspec/changes/phase-2-bytecode-vm/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
129
openspec/changes/phase-2-bytecode-vm/design.md
Normal file
129
openspec/changes/phase-2-bytecode-vm/design.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Design — Phase 2: Bytecode und VM
|
||||
|
||||
## Context
|
||||
|
||||
Motivation siehe proposal.md („Why"). Ausgangslage: `tb-frontend` liefert
|
||||
AST + Diagnosen, wirft aber alle Sema-Ergebnisse (Typen, Slots,
|
||||
Konstanten) weg; `tb-vm` ist ein Stub. Rahmenbedingungen aus PLAN.md und
|
||||
docs/tbvm-design.md: Instant-Compile (Single-Pass, keine
|
||||
Optimierungspasses), Interpreter-Tempo in Lua-Klasse, Werte-Modell als
|
||||
Tagged Enum mit `Rc` (kein GC — der Dialekt kennt keine Zyklen),
|
||||
`step()`-basierte, unterbrechbare VM. Entscheidungen dieses Dokuments
|
||||
wurden am 2026-09-02 in einer Explore-Session getroffen und bestätigt.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Durchgängige Pipeline Quelltext → HIR → Bytecode → Ausführung mit
|
||||
byte-genauer Korpus-Ausgabe.
|
||||
- Debugger-taugliche VM-Architektur, ohne dass der Debugger selbst
|
||||
(Phase 5) schon gebaut wird.
|
||||
- Builtin-ABI, die Phase 3 nur noch „auffüllt".
|
||||
|
||||
**Non-Goals:**
|
||||
- Mehrmodul-Linking und `COMMON`-Verknüpfung über Moduleinheiten (die
|
||||
Prozedurtabelle ist modul-qualifiziert angelegt, verknüpft wird ab
|
||||
Phase 5 mit `.MAK`-Projekten).
|
||||
- Datei-E/A, Ereigniszustellung (Hooks über Tick-Flag-Wort genügen),
|
||||
`tbc build --exe`, Optimierungspasses jeder Art.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — Typisiertes HIR als Sema-Ausgabe (statt Doppel-Resolution oder Side-Tables)
|
||||
Die Sema wird vom reinen Checker zum Lowering-Pass: Ausgabe ist ein
|
||||
eigener HIR-Baum (Namen → Slots, Typen an jedem Ausdruck, Konstanten
|
||||
gefaltet, implizite Konvertierungen als explizite Knoten). Verworfen:
|
||||
(a) Codegen resolved selbst nochmal — dupliziert die Typisierungslogik,
|
||||
Divergenz-Bugs; (b) Side-Tables am AST — der AST hat keine Node-IDs,
|
||||
Pointer-Keys sind fragil. Das HIR hält das Single-Pass-Budget ein (alle
|
||||
Pässe linear, keine globale Analyse) und macht den Codegen zum dummen
|
||||
Tree-Walk.
|
||||
|
||||
### D2 — Monomorpher Opcode-Satz, Konvertierungen als eigene Opcodes
|
||||
Da das HIR jede Konvertierung materialisiert, sind alle Arithmetik-,
|
||||
Vergleichs- und Konvertierungs-Opcodes typrein (`ADD_I16`, `ADD_F64`,
|
||||
`CONV_I16_F64`, `STR_EQ` …). Kein Tag-Dispatch in heißen Pfaden; die
|
||||
Konvertierungsmatrix lebt an genau einer Stelle (HIR-Erzeugung), der
|
||||
Interpreter bleibt eine enge `match`-Schleife. Preis: breiter
|
||||
Opcode-Satz (~150–200) — Fleiß, keine Komplexität. Encoding: Opcode
|
||||
1 Byte; Slots u16; Konstantenpool-Index u16 (Wide-Variante bei Bedarf);
|
||||
Sprünge u32-absolut innerhalb der Prozedur (fixup-freundlich).
|
||||
|
||||
### D3 — Host-Trait mit blockierenden Aufrufen (statt Yield-Modell)
|
||||
Alle Konsolenwirkungen laufen über ein `Host`-Trait (Ausgabe, Zeile
|
||||
lesen, Abbruchprüfung). Host-Calls dürfen blockieren und sind aus
|
||||
VM-Sicht atomar; Ticks (Breakpoints, Einzelschritt, Abbruch) greifen an
|
||||
Anweisungsgrenzen. Verworfen: VM „yieldet" bei E/A und der Einbetter
|
||||
erfüllt Requests — debuggerfreundlicher, aber deutlich komplexer; für
|
||||
den IDE-Debugger reicht die Anweisungsgranularität. Implementierungen:
|
||||
Konsolen-Host (`tbc run`), Capture-Host (Test-Harness), später
|
||||
Zellenpuffer-Host (Phase 3/5).
|
||||
|
||||
### D4 — Fehlerbehandlung: Handler-Zustand modul-global + pro Frame
|
||||
`ON ERROR GOTO` setzt den modulweiten Handler, `ON LOCAL ERROR` einen
|
||||
Frame-lokalen, der ihn verdeckt. Beim Fehler: aktiven Handler suchen
|
||||
(erst Frame-Kette nach LOCAL-Handlern, sonst Modul-Handler), Stack bis
|
||||
zum Handler-Frame abwickeln, gemerkten Anweisungs-Offset + Frame für
|
||||
`RESUME` sichern, `ERR`/`ERL` setzen. Fehler bei aktiver Behandlung →
|
||||
sofortiger Abbruch (keine Kaskade). Grundlage ist die Original-Hilfe
|
||||
(Topics `ON ERROR`, `RESUME`); ein Emulator-Spike ist nur nötig, falls
|
||||
die Doku Lücken lässt.
|
||||
|
||||
### D5 — GOSUB-Stack pro Frame
|
||||
`GOSUB` legt die Rücksprungadresse auf einen separaten Stack im
|
||||
aktuellen Frame (kein Call-Frame); `RETURN` bedient zuerst diesen
|
||||
Stack. Prozedurende mit nicht-leerem GOSUB-Stack verwirft ihn (wie beim
|
||||
Vorbild beim Verlassen der Prozedur).
|
||||
|
||||
### D6 — Runner-Verhalten `STOP`/`CONT`
|
||||
`STOP` außerhalb der IDE terminiert mit Meldung + Exit-Code ≠ 0; `END`/
|
||||
Programmende → Exit-Code 0. `CONT` ist Direktmodus-Konzept der IDE
|
||||
(Phase 5) — die `step()`-Architektur liefert Suspend/Resume dort gratis.
|
||||
|
||||
### D7 — `.tbc` mit eigenem Writer/Reader (kein serde)
|
||||
Format bleibt stabil, dokumentiert und unabhängig von Crate-Versionen;
|
||||
Abschnittstabelle erlaubt spätere Abschnitte (FORMS ab Phase 4) ohne
|
||||
Versionsbruch. Prozedurtabelle trägt von Beginn an die Modulkennung.
|
||||
|
||||
### D8 — Benchmarks als früher Bestandteil, nicht als Abschluss
|
||||
Das Compile-Budget-Harness (generiertes ~50k-Zeilen-Projekt) entsteht
|
||||
früh in der Phase, damit Architekturfehler (z. B. quadratische
|
||||
Fixup-Listen) sofort auffallen. Criterion-Benches für VM-Durchsatz
|
||||
(Schleifen/Strings); Ergebnisse werden in docs/tbvm-design.md
|
||||
festgehalten. Ein Dispatch-Mikrobenchmark vorab wurde verworfen
|
||||
(vorzeitige Optimierung — gemessen wird am echten Interpreter).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Sema-Umbau destabilisiert Phase-1-Ergebnis] → Diagnostik-Verhalten
|
||||
ist durch die 33 Frontend-Tests + Korpus fixiert; Umbau gilt erst als
|
||||
fertig, wenn alle Bestandstests grün sind.
|
||||
- [Opcode-Breite macht Interpreter-Match unübersichtlich] → Opcodes
|
||||
systematisch gruppieren (Makro/Tabelle generiert Emit + Dispatch
|
||||
konsistent); Dokumentationspflicht in tbvm-design.md.
|
||||
- [Konvertierungsmatrix enthält falsch rekonstruierte Zellen] →
|
||||
jede Zelle mit Fundstelle in der Original-Hilfe belegen; unklare
|
||||
Zellen als `TODO verify` im Korpus markieren (Konvention besteht).
|
||||
- [PRINT-Zahlformatierung (SINGLE/DOUBLE-Stellen) weicht ab] →
|
||||
byte-genaue Korpustests decken Randfälle ab; Formatierungsregeln aus
|
||||
der Original-Hilfe ableiten, Abweichungen dokumentieren.
|
||||
- [Blockierende Host-Calls verhindern Abbruch während INPUT] →
|
||||
akzeptiert für Phase 2 (Abbruchprüfung obliegt dem Host); die
|
||||
Ereignisschleifen-Architektur (Phase 4) kann den Host asynchroner
|
||||
gestalten, ohne die VM zu ändern.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Rein additiv (neue Crate-Inhalte, neue Sema-Ausgabe neben bestehender
|
||||
Diagnostik). Kein Rollback-Plan nötig; PLAN.md-Haken und
|
||||
docs/tbvm-design.md-Fortschreibung erfolgen beim Abschluss der Phase.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Exakte Ergebnistyp-Regel für `^` sowie Stellenzahl der
|
||||
SINGLE/DOUBLE-Textdarstellung: wird beim Ausformulieren der Matrix
|
||||
aus der Original-Hilfe übernommen; ändert weder Ansatz noch
|
||||
Task-Zuschnitt.
|
||||
- Ob `ERL` beim Vorbild auch Zeilennummern aus `GOSUB`-Zielen o. ä.
|
||||
Sonderfällen meldet: bei Doku-Lücke entscheidet ein kurzer
|
||||
Emulator-Check; betrifft nur eine Matrix-/Handlerzelle.
|
||||
85
openspec/changes/phase-2-bytecode-vm/proposal.md
Normal file
85
openspec/changes/phase-2-bytecode-vm/proposal.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Phase 2 — Bytecode und VM (`tb-vm`)
|
||||
|
||||
## Why
|
||||
|
||||
Das Sprach-Frontend (Phase 1) liefert einen typgeprüften AST, aber es gibt
|
||||
keine Ausführungsschicht: `tb-vm` besteht aus leeren Stub-Modulen, der
|
||||
Kompatibilitäts-Testkorpus ist nur eine ruhende Verhaltensspezifikation.
|
||||
Phase 2 macht aus dem Frontend einen Compiler mit lauffähigem Ergebnis —
|
||||
Bytecode-Erzeugung, serialisierbares Kompilat (`.tbc`) und die TBVM als
|
||||
Interpreter — und löst damit den Plan-Meilenstein ein: der Konsolen-Korpus
|
||||
läuft per `tbc run` mit byte-genau korrekter Ausgabe.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Typisiertes HIR als Sema-Ausgabe** (Entscheidung 2026-09-02, Explore-
|
||||
Session): Die semantische Analyse behält ihr Wissen — sie liefert neben
|
||||
Diagnostics einen abgesenkten, typisierten Zwischenbaum (Namen → Slots,
|
||||
Typen explizit, Konstanten gefaltet, implizite Konvertierungen als
|
||||
explizite Knoten). Der Codegen wird ein einfacher Tree-Walk.
|
||||
- **Bytecode-Feindesign** (Eingangsaufgabe aus Phase 0): monomorpher,
|
||||
typisierter Opcode-Satz; Zahlenkonvertierungs-Matrix (implizite Casts,
|
||||
Banker's Rounding, Überlauf → Fehler 6) als Tabelle in
|
||||
docs/tbvm-design.md und als Korpustest verankert.
|
||||
- **`.tbc`-Container**: Serialisierung mit eigenem Writer/Reader (Magic,
|
||||
Formatversion, Abschnittstabelle: CONSTS, TYPES, PROCS, CODE, DATA,
|
||||
LINES); Prozedurtabelle von Beginn an modul-qualifiziert.
|
||||
- **TBVM-Interpreter**: Ausdrücke, Kontrollfluss (`IF`, `SELECT CASE`,
|
||||
Schleifen, `GOTO`/`GOSUB`), Prozeduraufrufe mit BYREF-Semantik,
|
||||
`DEF FN`, `DATA`/`READ`/`RESTORE`.
|
||||
- **Fehlerbehandlung**: `ON ERROR GOTO` (modulweit), `ON LOCAL ERROR`
|
||||
(prozedurlokal), `RESUME [0|NEXT|label]`, `ERR`/`ERL`, Propagation die
|
||||
Aufrufkette hoch, Fehler im Handler fatal.
|
||||
- **Unterbrechbarkeit**: `step()`-basierte VM, Tick-Prüfung an
|
||||
Anweisungsgrenzen (ein Flag-Wort), Breakpoints, Einzelschritt,
|
||||
Variableninspektion — Grundlage für den IDE-Debugger (Phase 5).
|
||||
- **Runner-Verhalten** (Entscheidung 2026-09-02): `STOP` außerhalb der IDE
|
||||
terminiert mit Meldung und Exit-Code ≠ 0, `END` mit Exit-Code 0;
|
||||
`CONT` bleibt IDE-Konzept (Phase 5).
|
||||
- **Konsolen-Basisbibliothek (vorgezogene Phase-3-Scheibe)**: Builtin-ABI
|
||||
(Dispatch-Tabelle in `tb-runtime`), `Host`-Trait für Konsolen-E/A
|
||||
(blockierende Host-Calls, kein Yield-Modell), PRINT-Zahlenformatierung
|
||||
und Druckzonen sowie die vom Korpus benötigten Stringfunktionen.
|
||||
- **Test-Harness**: `cargo test` führt jede Korpusdatei per `tbc run` aus
|
||||
und vergleicht byte-genau gegen `.out`.
|
||||
- **Benchmarks** in `benches/`: Compile-Budget (~50k Zeilen < 1 s, Modul
|
||||
< 50 ms) und VM-Durchsatz; Ergebnisse in docs/tbvm-design.md.
|
||||
|
||||
**Non-Goals:** Mehrmodul-Linking/`COMMON`-Verknüpfung über Moduleinheiten
|
||||
(kommt mit Projekten, Phase 5); Datei-E/A, Ereignissteuerung, `tbc build
|
||||
--exe` (spätere Phasen); vollständige Standardbibliothek (Phase 3).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `bytecode-kompilat`: Codegen HIR → Bytecode, Opcode-Satz,
|
||||
`.tbc`-Format und Serialisierung, Instant-Compile-Budget.
|
||||
- `vm-ausfuehrung`: Interpreter-Semantik — Ausdrücke inkl.
|
||||
Zahlenkonvertierungs-Matrix, Kontrollfluss, Prozeduraufrufe/BYREF,
|
||||
`GOSUB`, `DEF FN`, `DATA`/`READ`, Runner-Verhalten (`STOP`/`END`) und
|
||||
Unterbrechbarkeit (Ticks, Breakpoints, Einzelschritt).
|
||||
- `vm-fehlerbehandlung`: `ON [LOCAL] ERROR`, `RESUME`, `ERR`/`ERL`,
|
||||
Handler-Scoping und Fehlerpropagation.
|
||||
- `konsolen-basisbibliothek`: `Host`-Trait, Builtin-ABI,
|
||||
PRINT-Formatierung/Druckzonen, Korpus-Stringfunktionen.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `sprach-frontend`: Die Sema SHALL zusätzlich zum Diagnostik-Ergebnis
|
||||
ein typisiertes HIR liefern (neue Ausgabe-Anforderung des Frontends).
|
||||
- `kompat-testkorpus`: Neuer Laufzeitvergleich — jede Korpusdatei läuft
|
||||
per `tbc run`, Ausgabe wird byte-genau gegen `.out` geprüft.
|
||||
|
||||
## Impact
|
||||
|
||||
- `crates/tb-frontend`: Sema-Umbau (HIR-Ausgabe), neues HIR-Modul.
|
||||
- `crates/tb-vm`: `bytecode.rs`, `codegen.rs`, `interp.rs` von Stub zu
|
||||
Vollimplementierung; neue Benchmarks.
|
||||
- `crates/tb-runtime`: Builtin-Dispatch-Tabelle, PRINT-Formatierung,
|
||||
Stringfunktionen (Scheibe).
|
||||
- `crates/tb-cli`: `tbc run` (Kompilieren + Ausführen), Konsolen-`Host`.
|
||||
- `tests/compat`: Harness + neue Korpusdateien (u. a. Konvertierungs-
|
||||
matrix, Fehlerbehandlung); `docs/tbvm-design.md` wird fortgeschrieben
|
||||
(Opcode-Satz, Matrix, Benchmark-Ergebnisse), PLAN.md-Haken folgen bei
|
||||
Abschluss.
|
||||
@@ -0,0 +1,64 @@
|
||||
## Purpose
|
||||
|
||||
Das Bytecode-Kompilat ist das serialisierbare Ergebnis der Übersetzung:
|
||||
Der Codegenerator senkt das typisierte HIR in einen monomorphen
|
||||
Opcode-Satz ab, der `.tbc`-Container hält es als stabiles, dokumentiertes
|
||||
Format, und das Instant-Compile-Budget sichert den schnellen
|
||||
Edit-Run-Zyklus.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Monomorpher Opcode-Satz
|
||||
Der Codegenerator SHALL jedes HIR-Konstrukt in typisierte, monomorphe
|
||||
Opcodes übersetzen: Arithmetik, Vergleiche und Konvertierungen tragen den
|
||||
Operandentyp im Opcode (kein Tag-Dispatch über Werte zur Laufzeit);
|
||||
implizite Konvertierungen erscheinen als explizite Konvertierungs-Opcodes
|
||||
an den vom HIR bestimmten Stellen. Der Opcode-Satz SHALL in
|
||||
docs/tbvm-design.md vollständig dokumentiert sein.
|
||||
|
||||
#### Scenario: Gemischter Ausdruck wird monomorph
|
||||
- **WHEN** `d# = i% + 1.5#` übersetzt wird (INTEGER-Variable, DOUBLE-Ziel)
|
||||
- **THEN** enthält der Bytecode einen Konvertierungs-Opcode INTEGER→DOUBLE und eine DOUBLE-Addition, keinen generischen Additions-Opcode
|
||||
|
||||
#### Scenario: Namen sind zur Laufzeit aufgelöst
|
||||
- **WHEN** eine Variable oder Prozedur im Bytecode referenziert wird
|
||||
- **THEN** geschieht das über Slot- bzw. Tabellenindizes, nicht über Namens-Lookups
|
||||
|
||||
### Requirement: `.tbc`-Containerformat
|
||||
Das Kompilat SHALL als `.tbc`-Datei serialisierbar und wieder ladbar sein:
|
||||
Magic `TBC\0`, Formatversion, Abschnittstabelle mit den Abschnitten
|
||||
CONSTS (deduplizierter Konstantenpool), TYPES (TYPE-Layouts), PROCS
|
||||
(modul-qualifizierte Prozedurtabelle mit Signatur, Locals-Anzahl,
|
||||
Code-Offset), CODE, DATA (`READ`/`RESTORE`-Segment) und LINES
|
||||
(Zeilentabelle). Laden und erneutes Serialisieren MUST verlustfrei sein;
|
||||
eine unbekannte Formatversion MUST mit einer klaren Fehlermeldung
|
||||
abgewiesen werden.
|
||||
|
||||
#### Scenario: Roundtrip
|
||||
- **WHEN** ein kompiliertes Modul als `.tbc` geschrieben und wieder geladen wird
|
||||
- **THEN** ist das geladene Kompilat funktional identisch (gleiche Ausführung, gleiche Zeilenzuordnung)
|
||||
|
||||
#### Scenario: Unbekannte Version
|
||||
- **WHEN** eine `.tbc`-Datei mit höherer Formatversion geladen wird
|
||||
- **THEN** wird das Laden mit einer Meldung abgelehnt, die die Version nennt
|
||||
|
||||
### Requirement: Zeilentabelle für Fehlerortung
|
||||
Der Bytecode SHALL jede Anweisung ihrem Ursprung (Moduldatei, Zeile)
|
||||
zuordnen, sodass Laufzeitfehlermeldungen, `ERL`, Breakpoints und
|
||||
Einzelschritt die Quellzeile exakt benennen können.
|
||||
|
||||
#### Scenario: Fehler nennt Zeile
|
||||
- **WHEN** in Zeile 42 eines Programms ein Laufzeitfehler ohne Handler auftritt
|
||||
- **THEN** nennt die Fehlermeldung Zeile 42
|
||||
|
||||
### Requirement: Instant-Compile-Budget
|
||||
Die Übersetzung (Lexen bis Bytecode) SHALL ohne globale Analysepasses
|
||||
auskommen; Vorwärtsreferenzen werden über Fixups aufgelöst. Ein
|
||||
Benchmark MUST das Budget nachweisbar machen: ein Projekt von ~50.000
|
||||
Zeilen kompiliert in unter 1 s, ein einzelnes Modul in unter 50 ms
|
||||
(Release-Build, Referenzrechner); die Messwerte werden in
|
||||
docs/tbvm-design.md festgehalten.
|
||||
|
||||
#### Scenario: Budget wird gemessen
|
||||
- **WHEN** die Benchmarks in `benches/` laufen
|
||||
- **THEN** wird die Kompilierzeit für das ~50k-Zeilen-Projekt und für ein Einzelmodul ausgewiesen und gegen das Budget verglichen
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Laufzeitvergleich über den Korpus
|
||||
Ein automatischer Test SHALL jede Korpusdatei `tests/compat/*.bas`
|
||||
kompilieren, über die VM mit Capture-Host ausführen und die Ausgabe
|
||||
byte-genau (inklusive nachgestellter Leerzeichen und LF-Zeilenenden)
|
||||
gegen die zugehörige `.out`-Datei vergleichen. Bei Abweichung MUST der
|
||||
Test Datei, erste abweichende Zeile sowie Soll und Ist nennen.
|
||||
|
||||
#### Scenario: Korpus läuft mit korrekter Ausgabe
|
||||
- **WHEN** die Laufzeit-Testsuite läuft
|
||||
- **THEN** stimmt die Ausgabe jeder Korpusdatei byte-genau mit ihrer `.out` überein
|
||||
|
||||
#### Scenario: Abweichung wird benannt
|
||||
- **WHEN** ein Korpusprogramm eine abweichende Ausgabe erzeugt
|
||||
- **THEN** schlägt der Test fehl und nennt Datei, Zeilennummer, Soll- und Ist-Zeile
|
||||
|
||||
### Requirement: Korpusabdeckung der Phase-2-Semantik
|
||||
Der Korpus SHALL um Referenzprogramme mit dokumentierter Sollausgabe für
|
||||
die Zahlenkonvertierungs-Matrix (Rundung, Ganzzahldivision, logische
|
||||
Operatoren, Überlauf-Randfälle über Fehlerbehandlung), die
|
||||
Fehlerbehandlung (`ON ERROR`, `RESUME`-Varianten, `ERR`/`ERL`) sowie
|
||||
Prozeduraufrufe mit BYREF-/BYVAL-Wirkung erweitert werden.
|
||||
|
||||
#### Scenario: Konvertierungsmatrix als Korpustest
|
||||
- **WHEN** die Laufzeit-Testsuite läuft
|
||||
- **THEN** existiert ein Korpusprogramm, das die Matrix-Randfälle ausgibt, und seine Ausgabe entspricht der dokumentierten Sollausgabe
|
||||
@@ -0,0 +1,66 @@
|
||||
## Purpose
|
||||
|
||||
Die Konsolen-Basisbibliothek ist die in Phase 2 vorgezogene Scheibe der
|
||||
Laufzeitbibliothek: die Builtin-Aufrufschnittstelle der VM, ein
|
||||
`Host`-Abstraktion für Konsolen-E/A sowie PRINT-Formatierung und die vom
|
||||
Testkorpus benötigten Stringfunktionen.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Builtin-Aufrufschnittstelle
|
||||
Die VM SHALL Bibliotheksfunktionen und -anweisungen über eine indizierte
|
||||
Dispatch-Tabelle aufrufen (Argumente auf dem Operandenstack, Index im
|
||||
Opcode); die Tabelle lebt in der Laufzeitbibliothek und ist ohne
|
||||
Änderung am Opcode-Satz erweiterbar (Phase 3 füllt sie auf).
|
||||
|
||||
#### Scenario: Builtin über Tabelle
|
||||
- **WHEN** `PRINT LEN("abc")` ausgeführt wird
|
||||
- **THEN** wird `LEN` über seinen Tabellenindex aufgerufen und liefert 3
|
||||
|
||||
### Requirement: Host-Abstraktion für Konsolen-E/A
|
||||
Alle Konsolenwirkungen der VM SHALL über eine austauschbare
|
||||
Host-Schnittstelle laufen (Textausgabe, zeilenweises Lesen für `INPUT`,
|
||||
Abbruchprüfung). Host-Aufrufe dürfen blockieren; die Abbruchprüfung
|
||||
(Strg+Untbr) obliegt dem Host. `tbc run` verwendet einen Konsolen-Host
|
||||
(stdout/stdin); Tests MUST einen Capture-Host verwenden können, der die
|
||||
Ausgabe byte-genau aufzeichnet.
|
||||
|
||||
#### Scenario: Capture-Host im Test
|
||||
- **WHEN** ein Korpusprogramm im Test-Harness läuft
|
||||
- **THEN** entsteht die komplette Ausgabe im Capture-Host und ist byte-genau vergleichbar
|
||||
|
||||
### Requirement: PRINT-Formatierung
|
||||
`PRINT` SHALL Zahlen im Format des Vorbilds ausgeben: führendes
|
||||
Leerzeichen für nicht-negative bzw. `-` für negative Werte,
|
||||
nachgestelltes Leerzeichen (auch am Zeilenende); Zahlendarstellung je
|
||||
Typ wie beim Vorbild (u. a. kein Dezimalpunkt bei ganzzahligen Werten,
|
||||
signifikante Stellen je nach SINGLE/DOUBLE). `;` schließt direkt an,
|
||||
`,` springt zur nächsten 14-Zeichen-Druckzone (belegt ein Element die
|
||||
Zone ganz, zur übernächsten); ohne abschließendes `;`/`,` folgt ein
|
||||
Zeilenumbruch.
|
||||
|
||||
#### Scenario: Zahlen mit Vorzeichenspalte
|
||||
- **WHEN** `PRINT 1; -2; 3.5` ausgeführt wird
|
||||
- **THEN** lautet die Ausgabe ` 1 -2 3.5 ` mit Zeilenumbruch
|
||||
|
||||
#### Scenario: Druckzonen
|
||||
- **WHEN** `PRINT "12345678901234", "x"` ausgeführt wird
|
||||
- **THEN** beginnt `x` in Spalte 29 (Zone nach der belegten übersprungen)
|
||||
|
||||
### Requirement: Korpus-Stringfunktionen
|
||||
Die Bibliothek SHALL mindestens folgende Funktionen mit der Semantik des
|
||||
Vorbilds bereitstellen (Zeichen = Unicode-Codepoints, dokumentierte
|
||||
UTF-8-Abweichung): `LEN`, `LEFT$`, `RIGHT$`, `MID$` (Funktion),
|
||||
`INSTR`, `UCASE$`, `LCASE$`, `STRING$`, `SPACE$`, `CHR$`, `ASC`,
|
||||
`STR$`, `VAL`. Randfälle folgen dem Vorbild, u. a.: `VAL` liest das
|
||||
führende Zahlenpräfix und ignoriert Leerraum; `STR$` stellt
|
||||
nicht-negativen Zahlen ein Leerzeichen voran; `ASC("")` löst Fehler 5
|
||||
„Illegal function call" aus.
|
||||
|
||||
#### Scenario: Korpusdatei strings.bas
|
||||
- **WHEN** `tests/compat/strings.bas` ausgeführt wird
|
||||
- **THEN** entspricht die Ausgabe byte-genau `strings.out`
|
||||
|
||||
#### Scenario: VAL mit Restzeichen
|
||||
- **WHEN** `VAL(" 12.5abc")` ausgewertet wird
|
||||
- **THEN** ist das Ergebnis 12.5
|
||||
@@ -0,0 +1,19 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Typisiertes HIR als Sema-Ausgabe
|
||||
Die semantische Analyse SHALL neben den Diagnosen einen typisierten,
|
||||
abgesenkten Zwischenbaum (HIR) liefern, der die Codegen-Eingabe bildet:
|
||||
Namen sind zu Slot-/Tabellenindizes aufgelöst, jeder Ausdrucksknoten
|
||||
trägt seinen Ergebnistyp, `CONST`-Werte sind gefaltet, `DEFtype`-Regeln
|
||||
angewandt und implizite Konvertierungen als explizite
|
||||
Konvertierungsknoten materialisiert. Bei Diagnosen mit Fehlercharakter
|
||||
darf kein HIR für das betroffene Konstrukt garantiert werden; ein
|
||||
diagnose-freies Modul MUST ein vollständiges HIR ergeben.
|
||||
|
||||
#### Scenario: Konvertierung wird materialisiert
|
||||
- **WHEN** `d# = i% + 1.5#` semantisch analysiert wird
|
||||
- **THEN** enthält das HIR einen expliziten Konvertierungsknoten INTEGER→DOUBLE unter dem Additionsknoten und die Zuweisung trägt den Zieltyp DOUBLE
|
||||
|
||||
#### Scenario: Diagnose-freies Modul liefert HIR
|
||||
- **WHEN** ein Korpusmodul ohne Diagnosen analysiert wird
|
||||
- **THEN** liefert das Frontend ein HIR, aus dem der Codegen ohne erneute Namens- oder Typauflösung Bytecode erzeugen kann
|
||||
@@ -0,0 +1,115 @@
|
||||
## Purpose
|
||||
|
||||
Die TBVM führt das Bytecode-Kompilat mit der exakten Semantik des
|
||||
Dialekts aus: Ausdrücke mit der Zahlenkonvertierungs-Matrix des Vorbilds,
|
||||
Kontrollfluss, Prozeduraufrufe mit BYREF, `GOSUB`, unterbrechbare
|
||||
`step()`-Ausführung als Grundlage für Debugger und Ereignisschleife.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Zahlenkonvertierungs-Matrix
|
||||
Die VM SHALL implizite und explizite Zahlenkonvertierungen nach der in
|
||||
docs/tbvm-design.md dokumentierten Matrix ausführen, insbesondere:
|
||||
Gleitkomma→Ganzzahl rundet kaufmännisch zur nächsten geraden Zahl
|
||||
(Banker's Rounding); Bereichsüberschreitung löst Fehler 6 „Overflow"
|
||||
aus; `/` liefert stets Gleitkomma, `\` und `MOD` konvertieren ihre
|
||||
Operanden vorab zu Ganzzahlen; logische Operatoren (`AND`, `OR`, `XOR`,
|
||||
`EQV`, `IMP`) arbeiten bitweise auf zu Ganzzahlen konvertierten
|
||||
Operanden; CURRENCY rechnet als Festkomma ×10 000. Die Matrix SHALL
|
||||
durch ein Korpusprogramm mit dokumentierter Sollausgabe abgedeckt sein.
|
||||
|
||||
#### Scenario: Banker's Rounding
|
||||
- **WHEN** `PRINT CINT(0.5); CINT(1.5); CINT(2.5)` ausgeführt wird
|
||||
- **THEN** lautet die Ausgabe ` 0 2 2 `
|
||||
|
||||
#### Scenario: Überlauf bei Konvertierung
|
||||
- **WHEN** einer INTEGER-Variablen der Wert 40000 zugewiesen wird
|
||||
- **THEN** tritt Laufzeitfehler 6 „Overflow" auf
|
||||
|
||||
#### Scenario: Ganzzahldivision rundet Operanden vor
|
||||
- **WHEN** `PRINT 7.5 \ 2` ausgeführt wird
|
||||
- **THEN** wird 7.5 zuerst zu 8 gerundet und die Ausgabe ist ` 4 `
|
||||
|
||||
### Requirement: Kontrollfluss
|
||||
Die VM SHALL `IF`/`ELSEIF`/`ELSE`, `SELECT CASE` (Werte, Bereiche,
|
||||
`IS`-Vergleiche, `CASE ELSE`), `FOR`/`NEXT` (inkl. `STEP`, null
|
||||
Durchläufen bei unpassender Grenze), `DO`/`LOOP` (alle vier
|
||||
Bedingungsformen), `WHILE`/`WEND`, `GOTO`, `ON expr GOTO/GOSUB` und
|
||||
`EXIT FOR/DO` mit der Semantik des Vorbilds ausführen.
|
||||
|
||||
#### Scenario: FOR ohne Durchlauf
|
||||
- **WHEN** `FOR i% = 3 TO 1: PRINT i%: NEXT` ausgeführt wird
|
||||
- **THEN** wird der Schleifenkörper nicht betreten
|
||||
|
||||
#### Scenario: Korpus Kontrollfluss
|
||||
- **WHEN** `tests/compat/kontrollfluss.bas` ausgeführt wird
|
||||
- **THEN** entspricht die Ausgabe byte-genau `kontrollfluss.out`
|
||||
|
||||
### Requirement: GOSUB/RETURN neben Prozeduraufrufen
|
||||
`GOSUB` SHALL die Rücksprungadresse auf einem GOSUB-Stack im aktuellen
|
||||
Frame ablegen (kein eigener Frame); `RETURN` kehrt dorthin zurück,
|
||||
`RETURN label` an das benannte Ziel. Ein `RETURN` ohne anhängigen
|
||||
`GOSUB` MUST Fehler 3 „RETURN without GOSUB" auslösen.
|
||||
|
||||
#### Scenario: RETURN ohne GOSUB
|
||||
- **WHEN** ein Programm `RETURN` ohne vorangehendes `GOSUB` ausführt
|
||||
- **THEN** tritt Laufzeitfehler 3 auf
|
||||
|
||||
### Requirement: Prozeduraufrufe mit BYREF-Semantik
|
||||
SUB-/FUNCTION-Aufrufe SHALL eigene Frames mit Locals-Slots verwenden.
|
||||
Variablen, Arrayelemente und UDT-Felder werden BYREF übergeben
|
||||
(Änderungen im Rumpf wirken auf das Argument zurück); ein geklammertes
|
||||
Argument oder ein Ausdruck wird als temporärer Wert übergeben (BYVAL-
|
||||
Wirkung). `STATIC`-Prozeduren behalten ihre Locals zwischen Aufrufen;
|
||||
rekursive Aufrufe nicht-statischer Prozeduren erhalten frische Locals.
|
||||
`DEF FN`-Funktionen werten im Modulkontext aus (Zugriff auf
|
||||
Modulvariablen), ihre Parameter wirken lokal.
|
||||
|
||||
#### Scenario: BYREF wirkt zurück
|
||||
- **WHEN** `SUB Inc(x%) : x% = x% + 1 : END SUB` mit `Inc n%` aufgerufen wird
|
||||
- **THEN** ist `n%` danach um 1 erhöht
|
||||
|
||||
#### Scenario: Klammern erzwingen Wertübergabe
|
||||
- **WHEN** dieselbe SUB mit `Inc (n%)` aufgerufen wird
|
||||
- **THEN** bleibt `n%` unverändert
|
||||
|
||||
### Requirement: DATA/READ/RESTORE
|
||||
`READ` SHALL die `DATA`-Konstanten des Moduls in Deklarationsreihenfolge
|
||||
konsumieren und in den Zieltyp konvertieren; `RESTORE [label]` setzt den
|
||||
Lesezeiger auf den Anfang bzw. auf die erste `DATA`-Anweisung ab dem
|
||||
Label. Lesen hinter dem letzten Element MUST Fehler 4 „Out of DATA"
|
||||
auslösen; eine nicht konvertierbare Konstante MUST Fehler 13 „Type
|
||||
mismatch" auslösen.
|
||||
|
||||
#### Scenario: Out of DATA
|
||||
- **WHEN** ein Programm mehr Werte liest als `DATA` bereitstellt
|
||||
- **THEN** tritt Laufzeitfehler 4 auf
|
||||
|
||||
### Requirement: Unterbrechbare step()-Ausführung
|
||||
Die VM SHALL als zustandsbehaftete, vom Einbetter getriebene Struktur
|
||||
ausführbar sein: An Anweisungsgrenzen prüft sie ein Flag-Wort und kann
|
||||
anhalten (Breakpoint auf (Modul, Zeile), Einzelschritt, Abbruchwunsch).
|
||||
Im angehaltenen Zustand MUST der Einbetter Variableninhalte (Skalare,
|
||||
Arrayelemente, UDT-Felder) inspizieren und die Ausführung fortsetzen
|
||||
können. Die Tick-Prüfung erfolgt nur an Anweisungsgrenzen, nicht pro
|
||||
Opcode.
|
||||
|
||||
#### Scenario: Breakpoint hält an
|
||||
- **WHEN** ein Breakpoint auf Zeile 10 gesetzt ist und die Ausführung Zeile 10 erreicht
|
||||
- **THEN** hält die VM vor der Anweisung an und meldet Modul und Zeile
|
||||
|
||||
#### Scenario: Inspektion im Halt
|
||||
- **WHEN** die VM an einem Breakpoint steht
|
||||
- **THEN** liefert die Inspektions-API den aktuellen Wert einer benannten Variablen
|
||||
|
||||
### Requirement: Programmende im Runner
|
||||
Außerhalb der IDE SHALL `END` (und das Erreichen des Programmendes) den
|
||||
Prozess mit Exit-Code 0 beenden; `STOP` SHALL mit der Meldung
|
||||
`STOP in line n` (bzw. Modul/Zeile) und einem Exit-Code ungleich 0
|
||||
terminieren (Projektentscheidung 2026-09-02). `SYSTEM` verhält sich wie
|
||||
`END`. `CONT` existiert nur als IDE-Konzept (Phase 5) und ist kein
|
||||
Programmbestandteil.
|
||||
|
||||
#### Scenario: STOP im Runner
|
||||
- **WHEN** ein per `tbc run` gestartetes Programm `STOP` in Zeile 7 ausführt
|
||||
- **THEN** endet der Prozess mit Exit-Code ungleich 0 und die Meldung nennt Zeile 7
|
||||
@@ -0,0 +1,71 @@
|
||||
## Purpose
|
||||
|
||||
Die BASIC-Fehlersemantik (`ON ERROR GOTO`, `ON LOCAL ERROR`, `RESUME`,
|
||||
`ERR`/`ERL`) ist Teil des Kompatibilitätsvertrags: Programme des Vorbilds
|
||||
verlassen sich auf Handler-Scoping, Propagation und Wiederaufnahme exakt
|
||||
wie dokumentiert.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Handler-Scoping nach Vorbild
|
||||
`ON ERROR GOTO label` SHALL einen modulweiten Handler aktivieren (auch
|
||||
wenn die Anweisung in einer Prozedur steht); `ON LOCAL ERROR GOTO label`
|
||||
SHALL einen prozedurlokalen Handler aktivieren, der den modulweiten für
|
||||
die Dauer des Prozedurlaufs verdeckt. `ON [LOCAL] ERROR GOTO 0`
|
||||
deaktiviert den jeweiligen Handler. Tritt ein Fehler in einer Prozedur
|
||||
ohne zuständigen Handler auf, SHALL er die Aufrufkette hochpropagieren,
|
||||
bis ein aktiver Handler gefunden ist; ohne Handler bricht das Programm
|
||||
mit der Fehlermeldung und der Quellzeile ab.
|
||||
|
||||
#### Scenario: Modulweiter Handler fängt Prozedurfehler
|
||||
- **WHEN** das Hauptprogramm `ON ERROR GOTO Fehler` gesetzt hat und eine gerufene SUB Fehler 6 auslöst
|
||||
- **THEN** springt die Ausführung in den Handler `Fehler` und `ERR` ist 6
|
||||
|
||||
#### Scenario: Lokaler Handler verdeckt modulweiten
|
||||
- **WHEN** eine Prozedur `ON LOCAL ERROR GOTO L` gesetzt hat und in ihr ein Fehler auftritt
|
||||
- **THEN** behandelt `L` den Fehler, nicht der modulweite Handler
|
||||
|
||||
#### Scenario: Ohne Handler bricht das Programm ab
|
||||
- **WHEN** Fehler 9 ohne aktiven Handler auftritt
|
||||
- **THEN** endet das Programm mit der Meldung „Subscript out of range" und der Quellzeile
|
||||
|
||||
### Requirement: ERR und ERL
|
||||
Beim Eintritt in einen Handler SHALL `ERR` den Fehlercode liefern und
|
||||
`ERL` die zuletzt vor dem Fehler ausgeführte **numerische** Zeilennummer
|
||||
(0, wenn keine numerischen Zeilennummern durchlaufen wurden —
|
||||
alphanumerische Labels zählen nicht). `ERROR n` SHALL Fehler `n`
|
||||
künstlich auslösen und dieselbe Maschinerie durchlaufen.
|
||||
|
||||
#### Scenario: ERL bei Label-Programm
|
||||
- **WHEN** ein Programm ohne numerische Zeilennummern einen Fehler behandelt
|
||||
- **THEN** liefert `ERL` 0
|
||||
|
||||
#### Scenario: ERROR-Anweisung
|
||||
- **WHEN** `ERROR 53` bei aktivem Handler ausgeführt wird
|
||||
- **THEN** wird der Handler mit `ERR` = 53 betreten
|
||||
|
||||
### Requirement: RESUME-Semantik
|
||||
Im Handler SHALL `RESUME` (bzw. `RESUME 0`) die fehlerauslösende
|
||||
Anweisung wiederholen, `RESUME NEXT` mit der Anweisung danach
|
||||
fortfahren und `RESUME label` an das Ziel springen. `RESUME` außerhalb
|
||||
einer aktiven Fehlerbehandlung MUST Fehler 20 „RESUME without error"
|
||||
auslösen. Nach `RESUME` gilt die Fehlerbehandlung als abgeschlossen
|
||||
(`ERR` wird zurückgesetzt).
|
||||
|
||||
#### Scenario: RESUME NEXT
|
||||
- **WHEN** ein Handler nach Fehler 11 (`Division by zero`) `RESUME NEXT` ausführt
|
||||
- **THEN** wird die auf die Division folgende Anweisung ausgeführt
|
||||
|
||||
#### Scenario: RESUME ohne Fehler
|
||||
- **WHEN** `RESUME` außerhalb eines Handlers ausgeführt wird
|
||||
- **THEN** tritt Laufzeitfehler 20 auf
|
||||
|
||||
### Requirement: Fehler im Handler ist fatal
|
||||
Tritt ein Fehler auf, während die Fehlerbehandlung aktiv ist (zwischen
|
||||
Handler-Eintritt und `RESUME`), SHALL das Programm ohne erneute
|
||||
Handler-Suche mit der Fehlermeldung abbrechen (keine Fehlerkaskaden,
|
||||
Verhalten des Vorbilds).
|
||||
|
||||
#### Scenario: Kein Kaskadieren
|
||||
- **WHEN** im aktiven Handler selbst Fehler 6 auftritt
|
||||
- **THEN** bricht das Programm mit „Overflow" ab, auch wenn ein Handler registriert ist
|
||||
121
openspec/changes/phase-2-bytecode-vm/tasks.md
Normal file
121
openspec/changes/phase-2-bytecode-vm/tasks.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Tasks — Phase 2: Bytecode und VM
|
||||
|
||||
## 1. Feindesign-Dokumentation (Eingangsaufgabe aus Phase 0)
|
||||
|
||||
- [x] 1.1 Zahlenkonvertierungs-Matrix (implizite Casts, Banker's Rounding,
|
||||
Überlauf, `/` vs. `\`/`MOD`, logische Operatoren, CURRENCY, `^`-Regel,
|
||||
SINGLE/DOUBLE-Textdarstellung) als Tabelle in docs/tbvm-design.md
|
||||
schreiben; jede Zelle mit Fundstelle in der Original-Hilfe belegen,
|
||||
unklare Zellen als `TODO verify` markieren — verifizierbar: Abschnitt
|
||||
existiert, keine Zelle ohne Beleg oder TODO-Marke
|
||||
- [x] 1.2 Opcode-Satz vollständig in docs/tbvm-design.md ausformulieren
|
||||
(Gruppen, Encoding, Operanden) und `STOP`/`CONT`-Runner-Entscheidung
|
||||
sowie Fehlerbehandlungs-Scoping (design.md D4–D6) dort festhalten —
|
||||
verifizierbar: „Offene Punkte"-Abschnitt in tbvm-design.md ist leer
|
||||
|
||||
## 2. Frontend: Typisiertes HIR
|
||||
|
||||
- [x] 2.1 HIR-Datenmodell in `tb-frontend` definieren (Slots, typisierte
|
||||
Ausdrücke, explizite Konvertierungsknoten, Prozedur-/Modulstruktur,
|
||||
Quellzeilen-Anker) — verifizierbar: `cargo check` und Doku-Kommentare
|
||||
je Knotentyp
|
||||
- [x] 2.2 Sema zum Lowering-Pass umbauen: `analyze_source` liefert
|
||||
zusätzlich HIR; Konstantenfaltung, `DEFtype`, implizite
|
||||
Konvertierungen materialisieren — verifizierbar: alle 33 bestehenden
|
||||
Frontend-Tests bleiben grün
|
||||
- [x] 2.3 HIR-Tests: diagnose-freie Korpusmodule liefern vollständiges HIR;
|
||||
Konvertierungsknoten-Szenario (`d# = i% + 1.5`) als Unit-Test —
|
||||
verifizierbar: neue Tests in `crates/tb-frontend/tests` grün
|
||||
|
||||
## 3. Bytecode und Serialisierung
|
||||
|
||||
- [x] 3.1 Opcode-Definitionen und Modulstruktur (Konstantenpool, Typtabelle,
|
||||
modul-qualifizierte Prozedurtabelle, DATA-Segment, Zeilentabelle) in
|
||||
`tb-vm::bytecode` implementieren — verifizierbar: Unit-Tests für
|
||||
Encoding/Decoding einzelner Opcodes
|
||||
- [x] 3.2 `.tbc`-Writer/Reader (Magic, Formatversion, Abschnittstabelle)
|
||||
implementieren — verifizierbar: Roundtrip-Test (schreiben → laden →
|
||||
identisch) und Ablehnungstest für unbekannte Formatversion
|
||||
|
||||
## 4. Codegenerator
|
||||
|
||||
- [x] 4.1 Ausdrucks-Codegen (Literale, Slots, Arithmetik, Vergleiche,
|
||||
logische Operatoren, Konvertierungsknoten, String-Konkatenation) —
|
||||
verifizierbar: Unit-Tests HIR-Ausdruck → erwartete Opcode-Folge
|
||||
- [x] 4.2 Kontrollfluss-Codegen (`IF`, `SELECT CASE`, `FOR`, `DO`, `WHILE`,
|
||||
`GOTO`/`GOSUB`/`ON GOTO`, `EXIT`, Labels/Zeilennummern) mit
|
||||
Fixup-Listen für Vorwärtsziele — verifizierbar: Unit-Tests je
|
||||
Konstrukt
|
||||
- [x] 4.3 Prozedur-Codegen (SUB/FUNCTION/`DEF FN`, BYREF-/BYVAL-Argumente,
|
||||
`STATIC`, Frames/Locals), `DIM`/Arrays/UDTs, `DATA`-Segment und
|
||||
Zeilentabelle emittieren — verifizierbar: Unit-Tests und
|
||||
`cargo test -p tb-vm` grün
|
||||
|
||||
## 5. Interpreter (TBVM)
|
||||
|
||||
- [x] 5.1 VM-Grundgerüst: Werte-Modell, Operandenstack, Frames,
|
||||
`step()`-Schleife mit Tick-Flag-Wort an Anweisungsgrenzen —
|
||||
verifizierbar: Ausdrucks-Programme laufen in Unit-Tests
|
||||
- [x] 5.2 Kontrollfluss und `GOSUB`-Stack pro Frame inkl. Fehler 3
|
||||
(„RETURN without GOSUB") — verifizierbar: kontrollfluss.bas läuft im
|
||||
Capture-Host mit korrekter Ausgabe
|
||||
- [x] 5.3 Prozeduraufrufe (BYREF-Rückwirkung, Klammer-BYVAL, `STATIC`,
|
||||
Rekursion, `DEF FN` im Modulkontext) — verifizierbar: Unit-Tests der
|
||||
Spec-Szenarien aus vm-ausfuehrung
|
||||
- [x] 5.4 `DATA`/`READ`/`RESTORE` inkl. Fehler 4 und 13 — verifizierbar:
|
||||
Unit-Tests der Spec-Szenarien
|
||||
- [x] 5.5 Fehlerbehandlung: Handler-Zustand (modul-global + Frame-lokal),
|
||||
Unwinding, `RESUME`-Varianten, `ERR`/`ERL`, `ERROR n`, Fehler im
|
||||
Handler fatal — verifizierbar: alle Szenarien aus vm-fehlerbehandlung
|
||||
als Tests grün
|
||||
- [x] 5.6 Unterbrechbarkeit: Breakpoints auf (Modul, Zeile), Einzelschritt,
|
||||
Variableninspektion im Halt, Abbruchwunsch — verifizierbar:
|
||||
Unit-Tests treiben die VM per `step()` und prüfen Halt/Inspektion
|
||||
- [x] 5.7 Programmende: `END`/`SYSTEM`/Programmende vs. `STOP` mit Meldung
|
||||
und Exit-Code-Unterscheidung — verifizierbar: Runner-Test prüft
|
||||
Exit-Codes und Meldung
|
||||
|
||||
## 6. Konsolen-Basisbibliothek (`tb-runtime`-Scheibe)
|
||||
|
||||
- [x] 6.1 `Host`-Trait definieren; Konsolen-Host und Capture-Host
|
||||
implementieren — verifizierbar: Unit-Test zeichnet Ausgabe byte-genau
|
||||
auf
|
||||
- [x] 6.2 Builtin-Dispatch-Tabelle + `CALL_BUILTIN`-ABI (Argumente auf dem
|
||||
Stack) in VM und Runtime verdrahten — verifizierbar: `LEN`-Aufruf
|
||||
läuft über die Tabelle (Spec-Szenario)
|
||||
- [x] 6.3 PRINT-Formatierung: Zahlendarstellung je Typ, Vorzeichenspalte,
|
||||
nachgestelltes Leerzeichen, `;`/`,`-Semantik, 14-Zeichen-Druckzonen —
|
||||
verifizierbar: printzahlen.bas und printzonen.bas byte-genau korrekt
|
||||
- [x] 6.4 Korpus-Stringfunktionen (`LEN`, `LEFT$`, `RIGHT$`, `MID$`,
|
||||
`INSTR`, `UCASE$`, `LCASE$`, `STRING$`, `SPACE$`, `CHR$`, `ASC`,
|
||||
`STR$`, `VAL`) mit Randfällen des Vorbilds — verifizierbar:
|
||||
strings.bas byte-genau korrekt plus Randfall-Unit-Tests
|
||||
(`VAL`-Präfixlesen, `STR$`-Leerzeichen, `ASC("")` → Fehler 5)
|
||||
|
||||
## 7. CLI und Test-Harness
|
||||
|
||||
- [x] 7.1 `tbc run <datei.bas>`: kompilieren, `.tbc` in Memory ausführen,
|
||||
Konsolen-Host; Diagnosen bei Compile-Fehler, Exit-Codes nach D6 —
|
||||
verifizierbar: `tbc run tests/compat/hello.bas` druckt `Hallo, Welt!`
|
||||
- [x] 7.2 Harness in `tests/`: alle `tests/compat/*.bas` kompilieren, im
|
||||
Capture-Host ausführen, byte-genau gegen `.out` vergleichen; bei
|
||||
Abweichung Datei/Zeile/Soll/Ist nennen — verifizierbar: `cargo test`
|
||||
führt den kompletten Korpus aus
|
||||
- [x] 7.3 Korpus erweitern: Konvertierungsmatrix-Programm, Fehlerbehandlung
|
||||
(`ON ERROR`/`RESUME`/`ERR`/`ERL`), BYREF/BYVAL-Programm mit
|
||||
dokumentierter Sollausgabe (Belege bzw. `TODO verify`-Konvention) —
|
||||
verifizierbar: neue `.bas`/`.out`-Paare laufen im Harness grün
|
||||
|
||||
## 8. Benchmarks und Abschluss
|
||||
|
||||
- [x] 8.1 Compile-Budget-Harness früh aufsetzen: Generator für
|
||||
~50k-Zeilen-Projekt + Criterion-Bench; Budgetvergleich (< 1 s Projekt,
|
||||
< 50 ms Modul) — verifizierbar: `cargo bench` weist die Zeiten aus
|
||||
- [x] 8.2 VM-Durchsatz-Benches (Schleifen, Strings); Ergebnisse und
|
||||
Benchmark-Stand in docs/tbvm-design.md „Performance" festhalten —
|
||||
verifizierbar: Abschnitt enthält Messwerte mit Datum
|
||||
- [x] 8.3 Meilenstein-Verifikation und Doku-Abschluss: kompletter Korpus
|
||||
grün via `cargo test`, docs/sprachreferenz.md um Phase-2-Festlegungen
|
||||
ergänzt (Rundung, `STOP`-Verhalten), PLAN.md-Phase-2-Haken gesetzt —
|
||||
verifizierbar: alle Checkboxen der Phase in PLAN.md abgehakt,
|
||||
Testsuite grün
|
||||
30
tests/compat/byref.bas
Normal file
30
tests/compat/byref.bas
Normal file
@@ -0,0 +1,30 @@
|
||||
' BYREF wirkt auf das Argument zurueck; Klammern erzwingen
|
||||
' Wertuebergabe (BYVAL); STATIC behaelt Werte; Rekursion ueber den
|
||||
' Funktionsnamen (Spec vm-ausfuehrung).
|
||||
n% = 1
|
||||
Inc n%
|
||||
PRINT n%
|
||||
Inc (n%)
|
||||
PRINT n%
|
||||
PRINT Fak&(6)
|
||||
Zaehl
|
||||
Zaehl
|
||||
END
|
||||
|
||||
SUB Inc (x%)
|
||||
x% = x% + 1
|
||||
END SUB
|
||||
|
||||
FUNCTION Fak& (n%)
|
||||
IF n% <= 1 THEN
|
||||
Fak& = 1
|
||||
ELSE
|
||||
Fak& = n% * Fak&(n% - 1)
|
||||
END IF
|
||||
END FUNCTION
|
||||
|
||||
SUB Zaehl
|
||||
STATIC z%
|
||||
z% = z% + 1
|
||||
PRINT "zaehler"; z%
|
||||
END SUB
|
||||
5
tests/compat/byref.out
Normal file
5
tests/compat/byref.out
Normal file
@@ -0,0 +1,5 @@
|
||||
2
|
||||
2
|
||||
720
|
||||
zaehler 1
|
||||
zaehler 2
|
||||
14
tests/compat/fehlerbehandlung.bas
Normal file
14
tests/compat/fehlerbehandlung.bas
Normal file
@@ -0,0 +1,14 @@
|
||||
' ON ERROR/RESUME NEXT/ERR/ERL (Spec vm-fehlerbehandlung):
|
||||
' Handler-Eintritt setzt ERR und ERL (letzte numerische Zeilennummer);
|
||||
' RESUME NEXT setzt nach der fehlerausloesenden Anweisung fort.
|
||||
ON ERROR GOTO Handler
|
||||
10 ERROR 5
|
||||
20 PRINT "nach RESUME NEXT"
|
||||
x = 1 / 0
|
||||
PRINT "weiter"
|
||||
ON ERROR GOTO 0
|
||||
PRINT "fertig"
|
||||
END
|
||||
Handler:
|
||||
PRINT "ERR="; ERR; "ERL="; ERL
|
||||
RESUME NEXT
|
||||
5
tests/compat/fehlerbehandlung.out
Normal file
5
tests/compat/fehlerbehandlung.out
Normal file
@@ -0,0 +1,5 @@
|
||||
ERR= 5 ERL= 10
|
||||
nach RESUME NEXT
|
||||
ERR= 11 ERL= 20
|
||||
weiter
|
||||
fertig
|
||||
19
tests/compat/konvertierung.bas
Normal file
19
tests/compat/konvertierung.bas
Normal file
@@ -0,0 +1,19 @@
|
||||
' Zahlenkonvertierungs-Matrix (docs/tbvm-design.md, Abschnitt
|
||||
' "Zahlenkonvertierungs-Matrix"): Banker's Rounding (Original-Hilfe:
|
||||
' CINT), Ganzzahldivision rundet Operanden vor, Logik bitweise auf
|
||||
' gerundeten Ganzzahlen, Ueberlauf -> Fehler 6.
|
||||
PRINT CINT(0.5); CINT(1.5); CINT(2.5); CINT(-0.5); CINT(-1.5)
|
||||
PRINT 7.5 \ 2; 7 \ 2; -7 \ 2
|
||||
PRINT 7 MOD 3; -7 MOD 3; 7.5 MOD 2
|
||||
PRINT 1 / 4; 1 / 3
|
||||
PRINT 2 ^ 10; 2 ^ 0.5
|
||||
PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0; 1.5 AND 1
|
||||
PRINT CLNG(2.5); CLNG(3.5)
|
||||
ON ERROR GOTO Uebergelaufen
|
||||
i% = 32767
|
||||
i% = i% + 1
|
||||
PRINT "nie"
|
||||
END
|
||||
Uebergelaufen:
|
||||
PRINT "Fehler"; ERR
|
||||
END
|
||||
8
tests/compat/konvertierung.out
Normal file
8
tests/compat/konvertierung.out
Normal file
@@ -0,0 +1,8 @@
|
||||
0 2 2 0 -2
|
||||
4 3 -3
|
||||
1 -1 0
|
||||
.25 .3333333
|
||||
1024 1.414214
|
||||
2 7 5 -1 0
|
||||
2 4
|
||||
Fehler 6
|
||||
Reference in New Issue
Block a user