From f7e57b0bd8437e8494f997a6627dee44bc0904f5 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Wed, 2 Sep 2026 11:28:07 +0200 Subject: [PATCH] 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 --- Cargo.lock | 1 + PLAN.md | 57 +- crates/tb-cli/src/main.rs | 115 +- crates/tb-cli/tests/compat.rs | 132 + crates/tb-frontend/src/ast.rs | 10 +- crates/tb-frontend/src/hir.rs | 459 +++ crates/tb-frontend/src/lib.rs | 10 +- crates/tb-frontend/src/parser.rs | 19 +- crates/tb-frontend/src/sema.rs | 2722 ++++++++++++++--- crates/tb-frontend/tests/corpus.rs | 8 + crates/tb-runtime/src/builtins.rs | 623 ++++ crates/tb-runtime/src/console.rs | 112 + crates/tb-runtime/src/format.rs | 240 ++ crates/tb-runtime/src/host.rs | 92 + crates/tb-runtime/src/lib.rs | 13 +- crates/tb-runtime/src/value.rs | 283 ++ crates/tb-vm/Cargo.toml | 9 + crates/tb-vm/benches/compile.rs | 88 + crates/tb-vm/benches/vm.rs | 57 + crates/tb-vm/src/bytecode.rs | 681 ++++- crates/tb-vm/src/codegen.rs | 1068 ++++++- crates/tb-vm/src/interp.rs | 1477 ++++++++- crates/tb-vm/src/lib.rs | 16 + crates/tb-vm/tests/vm.rs | 457 +++ docs/sprachreferenz.md | 11 +- docs/tbvm-design.md | 169 +- .../phase-2-bytecode-vm/.openspec.yaml | 2 + .../changes/phase-2-bytecode-vm/design.md | 129 + .../changes/phase-2-bytecode-vm/proposal.md | 85 + .../specs/bytecode-kompilat/spec.md | 64 + .../specs/kompat-testkorpus/spec.md | 27 + .../specs/konsolen-basisbibliothek/spec.md | 66 + .../specs/sprach-frontend/spec.md | 19 + .../specs/vm-ausfuehrung/spec.md | 115 + .../specs/vm-fehlerbehandlung/spec.md | 71 + openspec/changes/phase-2-bytecode-vm/tasks.md | 121 + tests/compat/byref.bas | 30 + tests/compat/byref.out | 5 + tests/compat/fehlerbehandlung.bas | 14 + tests/compat/fehlerbehandlung.out | 5 + tests/compat/konvertierung.bas | 19 + tests/compat/konvertierung.out | 8 + 42 files changed, 9225 insertions(+), 484 deletions(-) create mode 100644 crates/tb-cli/tests/compat.rs create mode 100644 crates/tb-frontend/src/hir.rs create mode 100644 crates/tb-runtime/src/builtins.rs create mode 100644 crates/tb-runtime/src/console.rs create mode 100644 crates/tb-runtime/src/format.rs create mode 100644 crates/tb-runtime/src/host.rs create mode 100644 crates/tb-runtime/src/value.rs create mode 100644 crates/tb-vm/benches/compile.rs create mode 100644 crates/tb-vm/benches/vm.rs create mode 100644 crates/tb-vm/tests/vm.rs create mode 100644 openspec/changes/phase-2-bytecode-vm/.openspec.yaml create mode 100644 openspec/changes/phase-2-bytecode-vm/design.md create mode 100644 openspec/changes/phase-2-bytecode-vm/proposal.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/bytecode-kompilat/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/kompat-testkorpus/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/konsolen-basisbibliothek/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/sprach-frontend/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/vm-ausfuehrung/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/specs/vm-fehlerbehandlung/spec.md create mode 100644 openspec/changes/phase-2-bytecode-vm/tasks.md create mode 100644 tests/compat/byref.bas create mode 100644 tests/compat/byref.out create mode 100644 tests/compat/fehlerbehandlung.bas create mode 100644 tests/compat/fehlerbehandlung.out create mode 100644 tests/compat/konvertierung.bas create mode 100644 tests/compat/konvertierung.out diff --git a/Cargo.lock b/Cargo.lock index 1f54bf7..54c63da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,6 +508,7 @@ version = "0.1.0" dependencies = [ "log", "tb-frontend", + "tb-runtime", "thiserror", ] diff --git a/PLAN.md b/PLAN.md index ada787d..63b80b9 100644 --- a/PLAN.md +++ b/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`) diff --git a/crates/tb-cli/src/main.rs b/crates/tb-cli/src/main.rs index cabb120..e3f2df0 100644 --- a/crates/tb-cli/src/main.rs +++ b/crates/tb-cli/src/main.rs @@ -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 ` Kompilieren zu binärem Ergebnis -//! - `tbc run ` Kompilieren und sofort ausführen -//! - `tbc check ` Nur Syntax-/Semantikprüfung +//! Unterbefehle (Phase 2): +//! - `tbc run ` Kompilieren und sofort ausführen +//! - `tbc build ` Kompilieren zu `datei.tbc` +//! - `tbc check ` 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 = 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 "); + 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 "); + 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) + } + } } diff --git a/crates/tb-cli/tests/compat.rs b/crates/tb-cli/tests/compat.rs new file mode 100644 index 0000000..2049c64 --- /dev/null +++ b/crates/tb-cli/tests/compat.rs @@ -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 = 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); +} diff --git a/crates/tb-frontend/src/ast.rs b/crates/tb-frontend/src/ast.rs index 6060a4d..f0b636f 100644 --- a/crates/tb-frontend/src/ast.rs +++ b/crates/tb-frontend/src/ast.rs @@ -48,6 +48,9 @@ pub enum Expr { }, Unary { op: UnOp, operand: Box, pos: SourcePos }, Binary { op: BinOp, lhs: Box, rhs: Box, pos: SourcePos }, + /// Geklammerter Ausdruck. Semantisch transparent, aber als Argument + /// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF. + Paren(Box), /// 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, gosub: bool, pos: SourcePos }, Return { target: Option, pos: SourcePos }, - End, - StopStmt, - System, + End(SourcePos), + StopStmt(SourcePos), + System(SourcePos), Exit { kind: ExitKind, pos: SourcePos }, Dim { shared: bool, redim: bool, decls: Vec, pos: SourcePos }, /// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen). diff --git a/crates/tb-frontend/src/hir.rs b/crates/tb-frontend/src/hir.rs new file mode 100644 index 0000000..d78d991 --- /dev/null +++ b/crates/tb-frontend/src/hir.rs @@ -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 { + 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, + /// Alle Frame-Slots; `params.len()` erste Einträge sind die Parameter. + pub locals: Vec, + /// Slot der Rückgabevariablen (Function/DefFn). + pub ret_slot: Option, + pub ret_ty: Option, + pub body: Vec, + /// 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, + pub udts: Vec, + /// Prozeduren; Index 0 ist das Hauptprogramm. + pub procs: Vec, + pub data: Vec, + 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, + /// UDT-Feldpfad (Feldindizes je Ebene). + pub fields: Vec, + /// 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), + /// Numerische Konvertierung nach Matrix (Rundung/Überlauf). + Conv { + from: NumTy, + to: NumTy, + arg: Box, + }, + /// Kürzen/Padden auf feste Stringlänge. + FixStr { + len: u32, + arg: Box, + }, + Neg { + ty: NumTy, + arg: Box, + }, + /// 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, + r: Box, + }, + Not { + ty: IntKind, + arg: Box, + }, + Logic { + op: HLogic, + ty: IntKind, + l: Box, + r: Box, + }, + /// Vergleich; Ergebnis ist INTEGER (−1/0). + Cmp { + op: HCmp, + ty: CmpKind, + l: Box, + r: Box, + }, + Concat(Box, Box), + /// FUNCTION-/DEF FN-Aufruf. + FnCall { + proc: u16, + args: Vec, + ret: HTy, + }, + Builtin { + b: Builtin, + args: Vec, + ret: HTy, + }, + /// LBOUND/UBOUND eines Arrays. + ArrayBound { + lower: bool, + place: Box, + dim: Box, + }, + 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, + /// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)? + trailing: bool, + }, + Input { + line_mode: bool, + prompt: Option, + /// Fragezeichen nach dem Prompt (`;`-Form). + question: bool, + targets: Vec, + }, + If { + cond: HExpr, + then: Vec, + els: Vec, + }, + /// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck). + Loop { + pre: Option<(bool, HExpr)>, + post: Option<(bool, HExpr)>, + body: Vec, + exit_label: LabelId, + }, + For { + var: HPlace, + ty: NumTy, + from: HExpr, + to: HExpr, + step: Option, + /// Versteckte Slots für Grenze/Schritt (Schritt nur wenn dynamisch). + limit_slot: VarSlot, + step_slot: Option, + body: Vec, + exit_label: LabelId, + }, + Goto(LabelId), + Gosub(LabelId), + OnGoto { + sel: HExpr, + gosub: bool, + targets: Vec, + }, + ReturnGosub(Option), + /// EXIT SUB/FUNCTION/DEF bzw. Rumpfende. + ExitProc, + CallSub { + proc: u16, + args: Vec, + }, + BuiltinStmt { + b: Builtin, + args: Vec, + }, + OnError { + local: bool, + /// `None` = `GOTO 0` (deaktivieren). + target: Option, + }, + OnErrorResumeNext { + local: bool, + }, + Resume(HResume), + /// `ERROR n`. + RaiseError(HExpr), + Read(Vec), + /// 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), + 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 { + 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, + } +} diff --git a/crates/tb-frontend/src/lib.rs b/crates/tb-frontend/src/lib.rs index a89a083..6aa6726 100644 --- a/crates/tb-frontend/src/lib.rs +++ b/crates/tb-frontend/src/lib.rs @@ -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, + /// Typisiertes HIR (vollständig nur bei leeren Diagnosen). + pub hir: Option, } -/// 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 } } diff --git a/crates/tb-frontend/src/parser.rs b/crates/tb-frontend/src/parser.rs index 96cbd41..0b24312 100644 --- a/crates/tb-frontend/src/parser.rs +++ b/crates/tb-frontend/src/parser.rs @@ -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 = 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(), _ => { diff --git a/crates/tb-frontend/src/sema.rs b/crates/tb-frontend/src/sema.rs index b0d4d59..67bcfdb 100644 --- a/crates/tb-frontend/src/sema.rs +++ b/crates/tb-frontend/src/sema.rs @@ -1,14 +1,19 @@ -//! Semantische Analyse: Symboltabellen, implizite Deklaration, +//! Semantische Analyse **und** HIR-Lowering (Design-Entscheidung D1 der +//! Phase-2-Änderung): Symboltabellen, implizite Deklaration, //! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl. -//! UDT-Feldtypen, Konstantenfaltung, Auflösung Array-Index vs. -//! Funktionsaufruf, Label-Prüfung, Builtin-Signaturen sowie die -//! Compile-Zeit-Abweisung deklarierter Non-Features. +//! UDT-Feldtypen, Konstantenfaltung, Builtin-Signaturen, Label-Prüfung, +//! Compile-Zeit-Abweisung deklarierter Non-Features — und in demselben +//! Durchlauf die Erzeugung des typisierten HIR (Slots statt Namen, +//! explizite Konvertierungsknoten nach der Matrix in docs/tbvm-design.md). +//! +//! Ein vollständiges HIR ist nur bei diagnose-freier Analyse garantiert; +//! bei Fehlern werden betroffene Konstrukte bestmöglich übersprungen. use crate::ast::*; +use crate::hir::{self, Builtin, HArg, HExpr, HPlace, HPrintItem, HStmt, HStmtKind, HTy, IntKind, LabelId, NumTy, VarSlot}; use crate::lexer::Suffix; use crate::{Diagnostic, SourcePos}; use std::collections::HashMap; -use std::collections::HashSet; #[derive(Debug, Clone, PartialEq)] pub enum Ty { @@ -18,7 +23,7 @@ pub enum Ty { Sng, Dbl, Str, - FixedStr, + FixedStr(u32), Udt(String), Unknown, } @@ -27,20 +32,53 @@ fn is_num(t: &Ty) -> bool { matches!(t, Ty::Int | Ty::Lng | Ty::Cur | Ty::Sng | Ty::Dbl | Ty::Unknown) } fn is_str(t: &Ty) -> bool { - matches!(t, Ty::Str | Ty::FixedStr | Ty::Unknown) + matches!(t, Ty::Str | Ty::FixedStr(_) | Ty::Unknown) } -fn rank(t: &Ty) -> u8 { + +/// Numerischer HIR-Typ eines `Ty` (Fallback SINGLE bei `Unknown`). +fn num_ty(t: &Ty) -> NumTy { match t { - Ty::Int => 1, - Ty::Lng => 2, - Ty::Cur => 3, - Ty::Sng => 4, - Ty::Dbl => 5, - _ => 0, + Ty::Int => NumTy::Int, + Ty::Lng => NumTy::Lng, + Ty::Cur => NumTy::Cur, + Ty::Dbl => NumTy::Dbl, + _ => NumTy::Sng, } } -fn promote(a: &Ty, b: &Ty) -> Ty { - if rank(a) >= rank(b) { a.clone() } else { b.clone() } + +fn ty_of_num(n: NumTy) -> Ty { + match n { + NumTy::Int => Ty::Int, + NumTy::Lng => Ty::Lng, + NumTy::Cur => Ty::Cur, + NumTy::Sng => Ty::Sng, + NumTy::Dbl => Ty::Dbl, + } +} + +fn rank(n: NumTy) -> u8 { + match n { + NumTy::Int => 1, + NumTy::Lng => 2, + NumTy::Cur => 3, + NumTy::Sng => 4, + NumTy::Dbl => 5, + } +} + +/// Promotion nach Matrix: Rangfolge INT < LNG < CUR < SNG < DBL, +/// CURRENCY gemischt mit Gleitkomma ergibt DOUBLE. +fn promote_num(a: NumTy, b: NumTy) -> NumTy { + if a == b { + return a; + } + let hi = if rank(a) >= rank(b) { a } else { b }; + let has_cur = a == NumTy::Cur || b == NumTy::Cur; + if has_cur && matches!(hi, NumTy::Sng | NumTy::Dbl) { + NumTy::Dbl + } else { + hi + } } fn suffix_ty(s: Suffix) -> Ty { @@ -62,7 +100,7 @@ fn type_name_ty(t: &TypeName) -> Ty { TypeName::Double => Ty::Dbl, TypeName::Currency => Ty::Cur, TypeName::Str => Ty::Str, - TypeName::FixedStr(_) => Ty::FixedStr, + TypeName::FixedStr(n) => Ty::FixedStr((*n).max(0) as u32), TypeName::Udt(n) => Ty::Udt(n.clone()), } } @@ -79,13 +117,42 @@ struct VarInfo { ty: Ty, array: bool, explicit: bool, + slot: u16, + global: bool, + /// Skalar-BYREF-Parameter (Slot enthält eine Referenz). + by_ref: bool, } #[derive(Default)] struct Scope { vars: HashMap, - labels: HashSet, - line_labels: HashSet, + labels: HashMap, + line_labels: HashMap, + next_label: u16, + /// Frame-Slots (leer im Modul-Scope: dort liegen Variablen global). + locals: Vec, + is_module: bool, + /// `DEF FN`: freie Namen binden an Modulvariablen. + is_def_fn: bool, + /// `STATIC`-Prozedur: Locals wandern in globale Slots. + is_static: bool, + /// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`. + loop_exits: Vec<(LoopKind, LabelId)>, + current_line: u32, +} + +#[derive(PartialEq, Clone, Copy)] +enum LoopKind { + For, + Do, +} + +impl Scope { + fn new_label(&mut self) -> LabelId { + let id = self.next_label; + self.next_label += 1; + id + } } #[derive(Debug, Clone)] @@ -93,6 +160,8 @@ struct ProcInfo { kind: ProcKind, ret: Ty, params: Vec<(Ty, bool)>, // (Typ, ist Array) + id: u16, + def_fn: bool, } // ---- Builtin-Signaturen ---------------------------------------------------- @@ -144,7 +213,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> { "LEN" => (1, 1, &[A], I), "LEFT$" | "RIGHT$" => (2, 2, &[S, N], St), "MID$" => (2, 3, &[S, N, N], St), - "INSTR" => (2, 3, &[A, A, A], I), // Sonderfall, s. check_builtin_args + "INSTR" => (2, 3, &[A, A, A], I), // Sonderfall, s. lower_builtin_fn "UCASE$" | "LCASE$" | "LTRIM$" | "RTRIM$" => (1, 1, &[S], St), "SPACE$" => (1, 1, &[N], St), "STRING$" => (2, 2, &[N, A], St), @@ -228,21 +297,35 @@ fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> { }) } -// ---- Prüfung --------------------------------------------------------------- +// ---- Einstiegspunkte ------------------------------------------------------- +/// Nur Diagnosen (Kompatibilitäts-API; nutzt intern das Lowering). pub fn check(module: &Module) -> Vec { + lower(module).1 +} + +/// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren +/// Diagnosen) und alle Diagnosen. +pub fn lower(module: &Module) -> (Option, Vec) { let mut s = Sema { diags: Vec::new(), deftypes: [const { None }; 26], explicit: false, option_base: 0, procs: HashMap::new(), - udts: HashMap::new(), + next_proc_id: 1, + udt_ids: HashMap::new(), + udt_defs: Vec::new(), consts: HashMap::new(), module_vars: HashMap::new(), + globals: Vec::new(), + data: Vec::new(), + data_marks_name: HashMap::new(), + data_marks_line: HashMap::new(), + hir_procs: Vec::new(), }; - s.run(module); - s.diags + let hir = s.run(module); + (hir, s.diags) } struct Sema { @@ -250,15 +333,25 @@ struct Sema { /// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE). deftypes: [Option; 26], explicit: bool, - /// `OPTION BASE` (0 oder 1) — relevant für die Codegenerierung. + /// `OPTION BASE` (0 oder 1). option_base: u8, procs: HashMap, - /// Benutzerdefinierte Typen mit Feldtypen. - udts: HashMap>, + next_proc_id: u16, + /// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet. + udt_ids: HashMap, + udt_defs: Vec, /// Konstanten: Typ und gefalteter Wert. consts: HashMap)>, - /// Modulvariablen nach dem Modulrumpf-Pass (für SHARED in Prozeduren). + /// Modulvariablen (Schlüssel wie `var_key`). module_vars: HashMap, + /// Globale Slots (Modulvariablen, STATICs, versteckte Temps). + globals: Vec, + /// DATA-Konstanten des Moduls (aus dem Prescan, statisch). + data: Vec, + data_marks_name: HashMap, + data_marks_line: HashMap, + /// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm). + hir_procs: Vec>, } impl Sema { @@ -266,24 +359,26 @@ impl Sema { self.diags.push(Diagnostic { pos, message: msg.into() }); } - fn run(&mut self, module: &Module) { - // Pass 1: Prozeduren, DECLAREs und TYPEs registrieren (in Reihenfolge, - // damit TYPE-Verweise „definiert vor Verwendung" geprüft werden). + fn run(&mut self, module: &Module) -> Option { + // Pass 1: Prozeduren, DECLAREs und TYPEs registrieren. for stmt in &module.body { match stmt { Stmt::Declare { sig, .. } => self.register_proc(sig), Stmt::TypeDecl { name, fields, pos } => { - let mut map = HashMap::new(); + let mut hfields = Vec::new(); for (fname, ftype) in fields { if let TypeName::Udt(n) = ftype { - if !self.udts.contains_key(n) { + if !self.udt_ids.contains_key(n) { self.err(*pos, "Type not defined"); } } - map.insert(fname.clone(), type_name_ty(ftype)); + hfields.push((fname.clone(), self.h_ty(&type_name_ty(ftype)))); } - if self.udts.insert(name.clone(), map).is_some() { + if self.udt_ids.contains_key(name) { self.err(*pos, "Duplicate definition"); + } else { + self.udt_ids.insert(name.clone(), self.udt_defs.len() as u16); + self.udt_defs.push(hir::HUdt { name: name.clone(), fields: hfields }); } } _ => {} @@ -292,36 +387,167 @@ impl Sema { for proc in &module.procs { self.register_proc(&proc.sig); } + self.hir_procs = Vec::new(); + self.hir_procs.resize_with(self.next_proc_id as usize, || None); - // Pass 2: Modulrumpf - let mut scope = Scope::default(); - collect_labels(&module.body, &mut scope); - for stmt in &module.body { - self.check_stmt(stmt, &mut scope); - } - self.module_vars = scope.vars.clone(); + // Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen + // werden vorab eingesammelt (RESTORE nach vorn, statisches DATA). + let mut scope = Scope { is_module: true, ..Scope::default() }; + self.prescan(&module.body, &mut scope, true); + let body = self.lower_body(&module.body, &mut scope); + let main = hir::HProc { + name: module.name.clone(), + kind: hir::HProcKind::Main, + params: Vec::new(), + locals: Vec::new(), + ret_slot: None, + ret_ty: None, + body, + label_count: scope.next_label, + }; + self.hir_procs.resize_with(self.next_proc_id as usize, || None); + self.hir_procs[0] = Some(main); - // Pass 3: Prozedurrümpfe + // Pass 3: Prozedurrümpfe. for proc in &module.procs { - let mut pscope = Scope::default(); - collect_labels(&proc.body, &mut pscope); - for p in &proc.sig.params { - let ty = self.param_ty(p); - let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some()); - pscope - .vars - .insert(key, VarInfo { ty, array: p.array, explicit: true }); - } - // Funktionsname als Rückgabe-Variable - if proc.sig.kind == ProcKind::Function { - let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix); - let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false); - pscope - .vars - .insert(key, VarInfo { ty: ret, array: false, explicit: true }); - } - for stmt in &proc.body { - self.check_stmt(stmt, &mut pscope); + self.lower_proc(proc); + } + + // Zusammensetzen: fehlende Rümpfe (nur DECLARE) als leere Stubs. + let procs: Vec = self + .hir_procs + .drain(..) + .enumerate() + .map(|(i, p)| { + p.unwrap_or_else(|| hir::HProc { + name: format!(""), + kind: hir::HProcKind::Sub, + params: Vec::new(), + locals: Vec::new(), + ret_slot: None, + ret_ty: None, + body: Vec::new(), + label_count: 0, + }) + }) + .collect(); + + Some(hir::HirModule { + name: module.name.clone(), + globals: std::mem::take(&mut self.globals), + udts: std::mem::take(&mut self.udt_defs), + procs, + data: std::mem::take(&mut self.data), + option_base: self.option_base, + }) + } + + fn lower_proc(&mut self, proc: &Proc) { + let info = self.procs.get(&proc.sig.name).cloned(); + let Some(info) = info else { return }; + let mut scope = Scope { + is_static: proc.is_static, + ..Scope::default() + }; + self.prescan(&proc.body, &mut scope, false); + let mut params = Vec::new(); + for p in &proc.sig.params { + let ty = self.param_ty(p); + let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some()); + let hty = self.h_ty(&ty); + let slot = scope.locals.len() as u16; + scope.locals.push(hir::HVar { name: p.name.clone(), ty: hty.clone(), array: p.array }); + let by_ref = !p.array && !matches!(ty, Ty::Udt(_)); + scope.vars.insert( + key, + VarInfo { ty: ty.clone(), array: p.array, explicit: true, slot, global: false, by_ref }, + ); + params.push(hir::HParam { name: p.name.clone(), ty: hty, array: p.array, by_ref }); + } + // Funktionsname als Rückgabe-Variable (immer frame-lokal). + let mut ret_slot = None; + let mut ret_ty = None; + if proc.sig.kind == ProcKind::Function { + let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix); + let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false); + let slot = scope.locals.len() as u16; + scope.locals.push(hir::HVar { + name: proc.sig.name.clone(), + ty: self.h_ty(&ret), + array: false, + }); + scope.vars.insert( + key, + VarInfo { ty: ret.clone(), array: false, explicit: true, slot, global: false, by_ref: false }, + ); + ret_slot = Some(VarSlot::Local(slot)); + ret_ty = Some(self.h_ty(&ret)); + } + let body = self.lower_body(&proc.body, &mut scope); + let hproc = hir::HProc { + name: proc.sig.name.clone(), + kind: match proc.sig.kind { + ProcKind::Sub => hir::HProcKind::Sub, + ProcKind::Function => hir::HProcKind::Function, + }, + params, + locals: std::mem::take(&mut scope.locals), + ret_slot, + ret_ty, + body, + label_count: scope.next_label, + }; + let idx = info.id as usize; + if idx < self.hir_procs.len() { + self.hir_procs[idx] = Some(hproc); + } + } + + /// Prescan eines Rumpfs: Labels/Zeilennummern erhalten `LabelId`s; + /// im Modulrumpf werden zusätzlich DATA-Konstanten (statisch, in + /// Quellreihenfolge) und RESTORE-Marken eingesammelt. + fn prescan(&mut self, stmts: &[Stmt], scope: &mut Scope, module_data: bool) { + for s in stmts { + match s { + Stmt::Label(n) => { + let id = scope.new_label(); + scope.labels.insert(n.clone(), id); + if module_data { + self.data_marks_name.insert(n.clone(), self.data.len() as u32); + } + } + Stmt::LineNumber(n) => { + let id = scope.new_label(); + scope.line_labels.insert(*n, id); + if module_data { + self.data_marks_line.insert(*n, self.data.len() as u32); + } + } + Stmt::Data { items, pos } => { + if module_data { + for it in items { + self.data.push(hir::DataItem { text: it.clone(), line: pos.line }); + } + } + } + Stmt::If { then_body, elseifs, else_body, .. } => { + self.prescan(then_body, scope, module_data); + for (_, b) in elseifs { + self.prescan(b, scope, module_data); + } + if let Some(b) = else_body { + self.prescan(b, scope, module_data); + } + } + Stmt::Select { arms, .. } => { + for a in arms { + self.prescan(&a.body, scope, module_data); + } + } + Stmt::For { body, .. } + | Stmt::DoLoop { body, .. } + | Stmt::While { body, .. } => self.prescan(body, scope, module_data), + _ => {} } } } @@ -340,8 +566,32 @@ impl Sema { ProcKind::Sub => Ty::Unknown, }; let params = sig.params.iter().map(|p| (self.param_ty(p), p.array)).collect(); - self.procs - .insert(sig.name.clone(), ProcInfo { kind: sig.kind, ret, params }); + let id = match self.procs.get(&sig.name) { + Some(p) => p.id, + None => { + let id = self.next_proc_id; + self.next_proc_id += 1; + id + } + }; + self.procs.insert( + sig.name.clone(), + ProcInfo { kind: sig.kind, ret, params, id, def_fn: false }, + ); + } + + fn h_ty(&self, t: &Ty) -> HTy { + match t { + Ty::Int => HTy::Num(NumTy::Int), + Ty::Lng => HTy::Num(NumTy::Lng), + Ty::Cur => HTy::Num(NumTy::Cur), + Ty::Sng => HTy::Num(NumTy::Sng), + Ty::Dbl => HTy::Num(NumTy::Dbl), + Ty::Str => HTy::Str, + Ty::FixedStr(n) => HTy::FixedStr(*n), + Ty::Udt(name) => HTy::Udt(*self.udt_ids.get(name).unwrap_or(&0)), + Ty::Unknown => HTy::Num(NumTy::Sng), + } } /// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE). @@ -382,32 +632,100 @@ impl Sema { } } - /// Feldtyp eines UDT-Zugriffs `basisvar.feld[.feld …]`. - fn member_ty(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> Ty { + // ---- Slot-Verwaltung --------------------------------------------------- + + fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 { + let slot = self.globals.len() as u16; + self.globals.push(hir::HVar { name: name.to_string(), ty: self.h_ty(ty), array }); + slot + } + + /// Variable im gegebenen Scope anlegen (Modul → global; Prozedur → + /// lokal, außer STATIC-Prozedur → globaler Slot mit Mangel-Namen). + fn alloc_var(&mut self, scope: &mut Scope, name: &str, ty: &Ty, array: bool) -> (u16, bool) { + if scope.is_module { + (self.alloc_global(name, ty, array), true) + } else if scope.is_static { + (self.alloc_global(&format!("STATIC.{name}"), ty, array), true) + } else { + let slot = scope.locals.len() as u16; + scope.locals.push(hir::HVar { name: name.to_string(), ty: self.h_ty(ty), array }); + (slot, false) + } + } + + /// Versteckter Temp-Slot (SELECT-Selektor, FOR-Grenzen, SWAP). + fn alloc_temp(&mut self, scope: &mut Scope, ty: &Ty) -> VarSlot { + let (slot, global) = self.alloc_var(scope, "", ty, false); + if global { + VarSlot::Global(slot) + } else { + VarSlot::Local(slot) + } + } + + fn vars_of<'a>(&'a self, scope: &'a Scope) -> &'a HashMap { + if scope.is_module { + &self.module_vars + } else { + &scope.vars + } + } + + fn insert_var(&mut self, scope: &mut Scope, key: String, info: VarInfo) { + if scope.is_module { + self.module_vars.insert(key, info); + } else { + scope.vars.insert(key, info); + } + } + + /// Feldpfad eines UDT-Zugriffs auflösen: liefert Typ und Feldindizes. + fn member_path( + &mut self, + base_ty: &Ty, + path: &[&str], + pos: SourcePos, + ) -> (Ty, Vec) { let mut cur = base_ty.clone(); + let mut idxs = Vec::new(); for seg in path { - match &cur { + match cur.clone() { Ty::Udt(udt_name) => { - match self.udts.get(udt_name).and_then(|f| f.get(*seg)) { - Some(t) => cur = t.clone(), + let found = self.udt_ids.get(&udt_name).and_then(|id| { + self.udt_defs[*id as usize] + .fields + .iter() + .position(|(n, _)| n == seg) + .map(|i| (i as u16, self.udt_defs[*id as usize].fields[i].1.clone())) + }); + match found { + Some((i, hty)) => { + idxs.push(i); + cur = match hty { + HTy::Num(n) => ty_of_num(n), + HTy::Str => Ty::Str, + HTy::FixedStr(n) => Ty::FixedStr(n), + HTy::Udt(id) => Ty::Udt(self.udt_defs[id as usize].name.clone()), + }; + } None => { self.err(pos, "Element not defined"); - return Ty::Unknown; + return (Ty::Unknown, idxs); } } } - Ty::Unknown => return Ty::Unknown, + Ty::Unknown => return (Ty::Unknown, idxs), _ => { self.err(pos, "Type mismatch"); - return Ty::Unknown; + return (Ty::Unknown, idxs); } } } - cur + (cur, idxs) } - /// Variable nachschlagen bzw. implizit deklarieren; löst auch - /// UDT-Feldzugriffe (`a.b.c`) auf. + /// Variable nachschlagen bzw. implizit deklarieren. fn resolve_var( &mut self, scope: &mut Scope, @@ -415,46 +733,49 @@ impl Sema { suffix: &Option, array: bool, pos: SourcePos, - ) -> Ty { + ) -> Option { // AS-deklarierte Variable hat Vorrang, wenn kein Suffix angegeben let as_key = format!("{name}\u{1}AS"); if suffix.is_none() { - if let Some(v) = scope.vars.get(&as_key) { - return v.ty.clone(); + if let Some(v) = self.vars_of(scope).get(&as_key) { + return Some(v.clone()); } - } else if scope.vars.contains_key(&as_key) { + if scope.is_def_fn { + if let Some(v) = self.module_vars.get(&as_key) { + return Some(v.clone()); + } + } + } else if self.vars_of(scope).contains_key(&as_key) { self.err(pos, "Duplicate definition"); - return Ty::Unknown; + return None; } let key = self.var_key(name, suffix, false); - if let Some(v) = scope.vars.get(&key) { - return v.ty.clone(); + if let Some(v) = self.vars_of(scope).get(&key) { + return Some(v.clone()); } - // UDT-Feldzugriff: Basis vor dem ersten Punkt auflösen - if name.contains('.') { - let parts: Vec<&str> = name.split('.').collect(); - let base = parts[0]; - let base_as = format!("{base}\u{1}AS"); - let base_info = scope - .vars - .get(&base_as) - .or_else(|| scope.vars.get(&self.var_key(base, &None, false))) - .cloned(); - if let Some(info) = base_info { - if matches!(info.ty, Ty::Udt(_)) { - return self.member_ty(&info.ty, &parts[1..], pos); - } + if scope.is_def_fn { + if let Some(v) = self.module_vars.get(&key) { + return Some(v.clone()); } } if self.explicit { self.err(pos, "Variable not defined"); - return self.name_ty(name, suffix); + return None; } let ty = self.name_ty(name, suffix); - scope - .vars - .insert(key, VarInfo { ty: ty.clone(), array, explicit: false }); - ty + // DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik). + let info = if scope.is_def_fn { + let slot = self.alloc_global(name, &ty, array); + let info = VarInfo { ty, array, explicit: false, slot, global: true, by_ref: false }; + self.module_vars.insert(key, info.clone()); + info + } else { + let (slot, global) = self.alloc_var(scope, name, &ty, array); + let info = VarInfo { ty, array, explicit: false, slot, global, by_ref: false }; + self.insert_var(scope, key, info.clone()); + info + }; + Some(info) } /// Konstantenfaltung für `CONST`-Ausdrücke. @@ -466,6 +787,7 @@ impl Sema { Expr::DoubleLit(v) => Some(ConstVal::Num(*v)), Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)), Expr::StrLit(s) => Some(ConstVal::Str(s.clone())), + Expr::Paren(e) => self.fold_const(e), Expr::Name { name, args: None, .. } => { self.consts.get(name).and_then(|(_, v)| v.clone()) } @@ -520,13 +842,99 @@ impl Sema { } } + // ---- Konvertierungen --------------------------------------------------- + + /// Numerische Konvertierung als expliziter HIR-Knoten (Matrix). + fn conv_num(&mut self, e: HExpr, from: &Ty, to: NumTy) -> HExpr { + let f = num_ty(from); + if f == to { + return e; + } + HExpr::Conv { from: f, to, arg: Box::new(e) } + } + + /// Wert an Zieltyp anpassen (Zuweisung, BYVAL-Argument). + fn coerce(&mut self, e: HExpr, from: &Ty, to: &Ty, pos: SourcePos) -> HExpr { + match (is_num(to), is_str(to)) { + (true, _) if is_num(from) => { + if matches!(to, Ty::Unknown) || matches!(from, Ty::Unknown) { + e + } else { + self.conv_num(e, from, num_ty(to)) + } + } + (_, true) if is_str(from) => match to { + Ty::FixedStr(n) => HExpr::FixStr { len: *n, arg: Box::new(e) }, + _ => e, + }, + _ => { + if !matches!(to, Ty::Udt(_)) && !matches!(from, Ty::Udt(_)) { + self.err(pos, "Type mismatch"); + } + e + } + } + } + // ---- Anweisungen ------------------------------------------------------- - fn check_stmt(&mut self, stmt: &Stmt, scope: &mut Scope) { + fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec { + let mut out = Vec::new(); + for stmt in stmts { + self.lower_stmt(stmt, scope, &mut out); + } + out + } + + fn push(&mut self, out: &mut Vec, scope: &Scope, kind: HStmtKind) { + out.push(HStmt { line: scope.current_line, kind }); + } + + fn lower_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, out: &mut Vec) { + // Quellzeile für Anweisungsgrenzen/Fehlerortung mitführen. + let pos = stmt_pos(stmt); + if pos.line > 0 { + scope.current_line = pos.line; + } match stmt { - Stmt::Label(_) | Stmt::LineNumber(_) | Stmt::End | Stmt::StopStmt - | Stmt::System | Stmt::Data { .. } | Stmt::Exit { .. } - | Stmt::Include { .. } | Stmt::MetaArrays { .. } => {} + Stmt::Data { .. } | Stmt::Include { .. } | Stmt::MetaArrays { .. } => {} + Stmt::Label(n) => { + if let Some(id) = scope.labels.get(n).copied() { + self.push(out, scope, HStmtKind::Label(id)); + } + } + Stmt::LineNumber(n) => { + if let Some(id) = scope.line_labels.get(n).copied() { + self.push(out, scope, HStmtKind::Label(id)); + } + self.push(out, scope, HStmtKind::SetErl(*n)); + } + Stmt::End(_) => self.push(out, scope, HStmtKind::End), + Stmt::StopStmt(_) => self.push(out, scope, HStmtKind::Stop), + Stmt::System(_) => self.push(out, scope, HStmtKind::System), + Stmt::Exit { kind, pos } => match kind { + ExitKind::For => { + match scope.loop_exits.iter().rev().find(|(k, _)| *k == LoopKind::For) { + Some((_, id)) => { + let id = *id; + self.push(out, scope, HStmtKind::Goto(id)); + } + None => self.err(*pos, "EXIT not within FOR...NEXT"), + } + } + ExitKind::Do => { + match scope.loop_exits.iter().rev().find(|(k, _)| *k == LoopKind::Do) { + Some((_, id)) => { + let id = *id; + self.push(out, scope, HStmtKind::Goto(id)); + } + None => self.err(*pos, "EXIT DO not within DO...LOOP"), + } + } + ExitKind::Sub | ExitKind::Function | ExitKind::Def => { + self.push(out, scope, HStmtKind::ExitProc); + } + }, Stmt::Assign { target, value, pos } => { // MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck @@ -541,167 +949,333 @@ impl Sema { if args.is_empty() || args.len() > 3 { self.err(*pos, "Argument-count mismatch"); } + let mut place = None; if let Some(sv) = args.first() { - let t = self.lvalue_ty(sv, scope); + let (p, t) = self.lower_place(sv, scope); if !is_str(&t) { self.err(sv.pos(), "Type mismatch"); } + place = p; } - for a in args.iter().skip(1) { - self.want_num(a, scope); + let start = args.get(1).map(|a| self.lower_num_as(a, scope, NumTy::Lng)); + let len = args.get(2).map(|a| self.lower_num_as(a, scope, NumTy::Lng)); + let (ve, vt) = self.lower_expr(value, scope); + if !is_str(&vt) { + self.err(value.pos(), "Type mismatch"); + } + if let Some(place) = place { + let cur = HExpr::Load(Box::new(place.clone())); + let call = HExpr::Builtin { + b: Builtin::MidAssign, + args: vec![ + cur, + start.unwrap_or(HExpr::Lng(1)), + len.unwrap_or(HExpr::Lng(-1)), + ve, + ], + ret: HTy::Str, + }; + self.push(out, scope, HStmtKind::Assign { place, value: call }); } - self.want_str(value, scope); return; } } - let tt = self.lvalue_ty(target, scope); - let vt = self.expr_ty(value, scope); + let (place, tt) = self.lower_place(target, scope); + let (ve, vt) = self.lower_expr(value, scope); self.check_assign(&tt, &vt, *pos); + if let Some(place) = place { + let value = self.coerce_silent(ve, &vt, &tt); + self.push(out, scope, HStmtKind::Assign { place, value }); + } } - Stmt::Print { file, using, items, .. } => { + Stmt::Print { printer, file, using, items, .. } => { if let Some(f) = file { self.want_num(f, scope); } if let Some(u) = using { self.want_str(u, scope); } - for item in items { - if let PrintItem::Expr(e) = item { - self.expr_ty(e, scope); - } - } - } - Stmt::Input { file, vars, .. } => { - if let Some(f) = file { - self.want_num(f, scope); - } - for v in vars { - self.lvalue_ty(v, scope); - } - } - Stmt::If { cond, then_body, elseifs, else_body, .. } => { - self.want_num(cond, scope); - for s in then_body { - self.check_stmt(s, scope); - } - for (c, b) in elseifs { - self.want_num(c, scope); - for s in b { - self.check_stmt(s, scope); - } - } - if let Some(b) = else_body { - for s in b { - self.check_stmt(s, scope); - } - } - } - Stmt::Select { expr, arms, .. } => { - let st = self.expr_ty(expr, scope); - for arm in arms { - for spec in &arm.specs { - let specs: Vec<&Expr> = match spec { - CaseSpec::Expr(e) => vec![e], - CaseSpec::Range(a, b) => vec![a, b], - CaseSpec::Is(_, e) => vec![e], - }; - for e in specs { - let et = self.expr_ty(e, scope); - let ok = (is_num(&st) && is_num(&et)) - || (is_str(&st) && is_str(&et)); - if !ok { - self.err(e.pos(), "Type mismatch"); + let mut hitems = Vec::new(); + let mut trailing = false; + for (i, item) in items.iter().enumerate() { + let last = i + 1 == items.len(); + match item { + PrintItem::Expr(e) => { + trailing = false; + // TAB(n)/SPC(n) sind Positionssteuerungen. + if let Expr::Name { name, suffix: None, args: Some(a), .. } = e { + if (name == "TAB" || name == "SPC") && a.len() == 1 { + let n = self.lower_num_as(&a[0], scope, NumTy::Lng); + hitems.push(if name == "TAB" { + HPrintItem::Tab(n) + } else { + HPrintItem::Spc(n) + }); + // TAB/SPC wirken wie `;` danach + trailing = last; + continue; + } } + let (he, _t) = self.lower_expr(e, scope); + hitems.push(HPrintItem::Val(he)); + } + PrintItem::Comma => { + hitems.push(HPrintItem::Comma); + trailing = last; + } + PrintItem::Semicolon => { + trailing = last; } } - for s in &arm.body { - self.check_stmt(s, scope); - } + } + if *printer { + self.push(out, scope, HStmtKind::Unsupported("LPRINT")); + } else if file.is_some() { + self.push(out, scope, HStmtKind::Unsupported("PRINT #")); + } else if using.is_some() { + self.push(out, scope, HStmtKind::Unsupported("PRINT USING")); + } else { + self.push(out, scope, HStmtKind::Print { items: hitems, trailing }); } } + Stmt::Input { line, file, keep_cursor: _, prompt, vars, .. } => { + if let Some(f) = file { + self.want_num(f, scope); + for v in vars { + self.lower_place(v, scope); + } + self.push(out, scope, HStmtKind::Unsupported("INPUT #")); + return; + } + let mut targets = Vec::new(); + for v in vars { + if let (Some(p), _) = self.lower_place(v, scope) { + targets.push(p); + } + } + let (ptext, question) = match prompt { + Some((t, q)) => (Some(t.clone()), *q), + None => (None, true), + }; + self.push( + out, + scope, + HStmtKind::Input { line_mode: *line, prompt: ptext, question, targets }, + ); + } + Stmt::If { cond, then_body, elseifs, else_body, .. } => { + let c = self.lower_cond(cond, scope); + let then = self.lower_body(then_body, scope); + // ELSEIF-Kette zu verschachteltem If absenken (von hinten). + let mut els = match else_body { + Some(b) => self.lower_body(b, scope), + None => Vec::new(), + }; + for (ec, eb) in elseifs.iter().rev() { + let c2 = self.lower_cond(ec, scope); + let body2 = self.lower_body(eb, scope); + let stmt = HStmt { + line: scope.current_line, + kind: HStmtKind::If { cond: c2, then: body2, els }, + }; + els = vec![stmt]; + } + self.push(out, scope, HStmtKind::If { cond: c, then, els }); + } + Stmt::Select { expr, arms, .. } => { + self.lower_select(expr, arms, scope, out); + } Stmt::For { var, from, to, step, body, pos } => { - let vt = self.lvalue_ty(var, scope); + let (place, vt) = self.lower_place(var, scope); if !is_num(&vt) { self.err(*pos, "Type mismatch"); } - self.want_num(from, scope); - self.want_num(to, scope); - if let Some(s) = step { - self.want_num(s, scope); - } - for s in body { - self.check_stmt(s, scope); + let ty = num_ty(&vt); + let (fe, ft) = self.lower_expr_num(from, scope); + let (te, tt2) = self.lower_expr_num(to, scope); + let from_e = self.conv_num(fe, &ft, ty); + let to_e = self.conv_num(te, &tt2, ty); + let step_e = step.as_ref().map(|s| { + let (se, st) = self.lower_expr_num(s, scope); + self.conv_num(se, &st, ty) + }); + let limit_slot = self.alloc_temp(scope, &ty_of_num(ty)); + // Dynamischer STEP braucht einen Slot; konstante Literale + // bettet der Codegen direkt ein. + let step_slot = match &step_e { + Some(e) if literal_value(e).is_none() => { + Some(self.alloc_temp(scope, &ty_of_num(ty))) + } + _ => None, + }; + let exit_label = scope.new_label(); + scope.loop_exits.push((LoopKind::For, exit_label)); + let hbody = self.lower_body(body, scope); + scope.loop_exits.pop(); + if let Some(place) = place { + self.push( + out, + scope, + HStmtKind::For { + var: place, + ty, + from: from_e, + to: to_e, + step: step_e, + limit_slot, + step_slot, + body: hbody, + exit_label, + }, + ); } } Stmt::DoLoop { pre, post, body, .. } => { - if let Some((_, c)) = pre { - self.want_num(c, scope); - } - if let Some((_, c)) = post { - self.want_num(c, scope); - } - for s in body { - self.check_stmt(s, scope); - } + let pre_c = pre + .as_ref() + .map(|(u, c)| (*u, self.lower_cond(c, scope))); + let exit_label = scope.new_label(); + scope.loop_exits.push((LoopKind::Do, exit_label)); + let hbody = self.lower_body(body, scope); + scope.loop_exits.pop(); + let post_c = post + .as_ref() + .map(|(u, c)| (*u, self.lower_cond(c, scope))); + self.push( + out, + scope, + HStmtKind::Loop { pre: pre_c, post: post_c, body: hbody, exit_label }, + ); } Stmt::While { cond, body, .. } => { - self.want_num(cond, scope); - for s in body { - self.check_stmt(s, scope); + let c = self.lower_cond(cond, scope); + let exit_label = scope.new_label(); + let hbody = self.lower_body(body, scope); + self.push( + out, + scope, + HStmtKind::Loop { + pre: Some((false, c)), + post: None, + body: hbody, + exit_label, + }, + ); + } + Stmt::Goto { target, pos } => { + if let Some(id) = self.label_id(target, scope, *pos) { + self.push(out, scope, HStmtKind::Goto(id)); } } - Stmt::Goto { target, pos } | Stmt::Gosub { target, pos } => { - self.check_label(target, scope, *pos); + Stmt::Gosub { target, pos } => { + if let Some(id) = self.label_id(target, scope, *pos) { + self.push(out, scope, HStmtKind::Gosub(id)); + } } - Stmt::OnGoto { expr, targets, pos, .. } => { - self.want_num(expr, scope); + Stmt::OnGoto { expr, targets, gosub, pos } => { + let sel = self.lower_num_as(expr, scope, NumTy::Int); + let mut ids = Vec::new(); for t in targets { - self.check_label(t, scope, *pos); + if let Some(id) = self.label_id(t, scope, *pos) { + ids.push(id); + } } + self.push(out, scope, HStmtKind::OnGoto { sel, gosub: *gosub, targets: ids }); } Stmt::Return { target, pos } => { - if let Some(t) = target { - self.check_label(t, scope, *pos); - } + let id = match target { + Some(t) => match self.label_id(t, scope, *pos) { + Some(id) => Some(id), + None => return, + }, + None => None, + }; + self.push(out, scope, HStmtKind::ReturnGosub(id)); } - Stmt::OnError { action, pos, .. } => { - if let OnErrorAction::Goto(t) = action { - self.check_label(t, scope, *pos); + Stmt::OnError { local, action, pos } => { + // Nicht-lokales ON ERROR in einer Prozedur: das Sprungziel + // liegt zwingend im Prozedurrumpf (Label-Scoping) — wird wie + // ON LOCAL ERROR behandelt. Die Scoping-Regel des Vorbilds + // (Handler-Label auf Modulebene) ist als Aufgabe in PLAN.md + // Phase 3 eingeplant. + let local = *local || !scope.is_module; + match action { + OnErrorAction::Goto(t) => { + if let Some(id) = self.label_id(t, scope, *pos) { + self.push(out, scope, HStmtKind::OnError { local, target: Some(id) }); + } + } + OnErrorAction::Disable => { + self.push(out, scope, HStmtKind::OnError { local, target: None }); + } + OnErrorAction::ResumeNext => { + self.push(out, scope, HStmtKind::OnErrorResumeNext { local }); + } } } Stmt::Resume { kind, pos } => { - if let ResumeKind::Label(t) = kind { - self.check_label(t, scope, *pos); - } + let k = match kind { + ResumeKind::Retry => hir::HResume::Retry, + ResumeKind::Next => hir::HResume::Next, + ResumeKind::Label(t) => match self.label_id(t, scope, *pos) { + Some(id) => hir::HResume::Label(id), + None => return, + }, + }; + self.push(out, scope, HStmtKind::Resume(k)); + } + Stmt::ErrorStmt { code, .. } => { + let c = self.lower_num_as(code, scope, NumTy::Int); + self.push(out, scope, HStmtKind::RaiseError(c)); } - Stmt::ErrorStmt { code, .. } => self.want_num(code, scope), Stmt::Dim { redim, decls, .. } => { for d in decls { - self.declare(d, *redim, scope); + self.declare(d, *redim, scope, out); } } - Stmt::StaticDecl { decls, .. } | Stmt::CommonDecl { decls, .. } => { + Stmt::StaticDecl { decls, .. } => { + // STATIC-Variablen leben in globalen Slots (eine Instanz). for d in decls { - self.declare(d, false, scope); + let ty = self.decl_ty(d); + let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some()); + if !scope.vars.contains_key(&key) { + let slot = + self.alloc_global(&format!("STATIC.{}", d.name), &ty, d.dims.is_some()); + scope.vars.insert( + key, + VarInfo { + ty, + array: d.dims.is_some(), + explicit: true, + slot, + global: true, + by_ref: false, + }, + ); + } + } + } + Stmt::CommonDecl { decls, .. } => { + for d in decls { + self.declare(d, false, scope, out); } } Stmt::SharedDecl { decls, .. } => { - // Zugriff auf Modulvariablen aus einer Prozedur heraus: - // vorhandene Modulvariable importieren, sonst dort anlegen. + // Zugriff auf Modulvariablen aus einer Prozedur heraus. for d in decls { let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some()); let info = match self.module_vars.get(&key) { Some(v) => v.clone(), None => { - let ty = if let Some(t) = &d.as_type { - type_name_ty(t) - } else { - self.name_ty(&d.name, &d.suffix) - }; + let ty = self.decl_ty(d); + let slot = self.alloc_global(&d.name, &ty, d.dims.is_some()); let v = VarInfo { ty, array: d.dims.is_some(), explicit: true, + slot, + global: true, + by_ref: false, }; self.module_vars.insert(key.clone(), v.clone()); v @@ -711,11 +1285,19 @@ impl Sema { } } Stmt::Erase { names, .. } => { + let mut slots = Vec::new(); for n in names { if let Expr::Name { name, suffix, pos, .. } = n { - self.resolve_var(scope, name, suffix, true, *pos); + if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) { + slots.push(if v.global { + VarSlot::Global(v.slot) + } else { + VarSlot::Local(v.slot) + }); + } } } + self.push(out, scope, HStmtKind::Erase(slots)); } Stmt::ConstDecl { items, pos } => { for (name, suffix, value) in items { @@ -753,35 +1335,41 @@ impl Sema { Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert Stmt::Declare { .. } => {} // bereits in Pass 1 registriert Stmt::Call { name, args, pos, .. } => { - self.check_call(name, args, scope, *pos); + self.lower_call_stmt(name, args, scope, *pos, out); } Stmt::ReadStmt { vars, .. } => { + let mut places = Vec::new(); for v in vars { - self.lvalue_ty(v, scope); + if let (Some(p), _) = self.lower_place(v, scope) { + places.push(p); + } } + self.push(out, scope, HStmtKind::Read(places)); } Stmt::Restore { target, pos } => { - if let Some(t) = target { - self.check_label(t, scope, *pos); - } + let idx = match target { + None => 0, + Some(t) => { + self.check_label_exists(t, scope, *pos); + match t { + LabelRef::Name(n) => { + self.data_marks_name.get(n).copied().unwrap_or(0) + } + LabelRef::Line(n) => { + self.data_marks_line.get(n).copied().unwrap_or(0) + } + } + } + }; + self.push(out, scope, HStmtKind::Restore(idx)); } - Stmt::DefFn { name, suffix, params, body, .. } => { - self.register_def_fn(name, suffix, params); - let mut fscope = self.def_fn_scope(name, suffix, params); - self.expr_ty(body, &mut fscope); + Stmt::DefFn { name, suffix, params, body, pos } => { + self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos); } - Stmt::DefFnBlock { name, suffix, params, body, .. } => { - self.register_def_fn(name, suffix, params); - let mut fscope = self.def_fn_scope(name, suffix, params); - collect_labels(body, &mut fscope); - // Modul-Labels bleiben aus DEF FN heraus ansprechbar - fscope.labels.extend(scope.labels.iter().cloned()); - fscope.line_labels.extend(scope.line_labels.iter().copied()); - for s in body { - self.check_stmt(s, &mut fscope); - } + Stmt::DefFnBlock { name, suffix, params, body, pos } => { + self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos); } - // ---- Datei-E/A ---- + // ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ---- Stmt::Open { file, isam, number, len, pos, .. } => { self.want_str(file, scope); self.want_num(number, scope); @@ -789,10 +1377,11 @@ impl Sema { self.want_num(l, scope); } if let Some((ty_name, _table)) = isam { - if !ty_name.is_empty() && !self.udts.contains_key(ty_name) { + if !ty_name.is_empty() && !self.udt_ids.contains_key(ty_name) { self.err(*pos, "Type not defined"); } } + self.push(out, scope, HStmtKind::Unsupported("OPEN")); } Stmt::OpenLegacy { mode, number, file, len, .. } => { self.want_str(mode, scope); @@ -801,34 +1390,38 @@ impl Sema { if let Some(l) = len { self.want_num(l, scope); } + self.push(out, scope, HStmtKind::Unsupported("OPEN")); } Stmt::CloseStmt { files, .. } => { for f in files { self.want_num(f, scope); } + self.push(out, scope, HStmtKind::Unsupported("CLOSE")); } Stmt::FieldStmt { file, fields, .. } => { self.want_num(file, scope); for (width, var) in fields { self.want_num(width, scope); - let t = self.lvalue_ty(var, scope); + let (_, t) = self.lower_place(var, scope); if !is_str(&t) { self.err(var.pos(), "Type mismatch"); } } + self.push(out, scope, HStmtKind::Unsupported("FIELD")); } - Stmt::GetPut { file, recnum, var, .. } => { + Stmt::GetPut { put, file, recnum, var, .. } => { self.want_num(file, scope); if let Some(r) = recnum { self.want_num(r, scope); } if let Some(v) = var { - self.lvalue_ty(v, scope); + self.lower_place(v, scope); } + self.push(out, scope, HStmtKind::Unsupported(if *put { "PUT" } else { "GET" })); } - Stmt::LsetRset { target, value, pos, .. } => { - let tt = self.lvalue_ty(target, scope); - let vt = self.expr_ty(value, scope); + Stmt::LsetRset { rset, target, value, pos } => { + let (_, tt) = self.lower_place(target, scope); + let (_, vt) = self.lower_expr(value, scope); let ok = (is_str(&tt) && is_str(&vt)) || matches!((&tt, &vt), (Ty::Udt(a), Ty::Udt(b)) if a == b) || tt == Ty::Unknown @@ -836,20 +1429,23 @@ impl Sema { if !ok { self.err(*pos, "Type mismatch"); } + self.push(out, scope, HStmtKind::Unsupported(if *rset { "RSET" } else { "LSET" })); } Stmt::WriteStmt { file, items, .. } => { if let Some(f) = file { self.want_num(f, scope); } for e in items { - self.expr_ty(e, scope); + self.lower_expr(e, scope); } + self.push(out, scope, HStmtKind::Unsupported("WRITE")); } Stmt::SeekStmt { file, position, .. } => { self.want_num(file, scope); self.want_num(position, scope); + self.push(out, scope, HStmtKind::Unsupported("SEEK")); } - Stmt::LockStmt { file, from, to, .. } => { + Stmt::LockStmt { unlock, file, from, to, .. } => { self.want_num(file, scope); if let Some(f) = from { self.want_num(f, scope); @@ -857,10 +1453,16 @@ impl Sema { if let Some(t) = to { self.want_num(t, scope); } + self.push( + out, + scope, + HStmtKind::Unsupported(if *unlock { "UNLOCK" } else { "LOCK" }), + ); } Stmt::NameStmt { old, new, .. } => { self.want_str(old, scope); self.want_str(new, scope); + self.push(out, scope, HStmtKind::Unsupported("NAME")); } // ---- Bildschirm/Ereignisse ---- Stmt::ViewPrint { top, bottom, .. } => { @@ -870,6 +1472,7 @@ impl Sema { if let Some(b) = bottom { self.want_num(b, scope); } + self.push(out, scope, HStmtKind::Unsupported("VIEW PRINT")); } Stmt::EventControl { device, index, pos, .. } => { match device.as_str() { @@ -879,79 +1482,348 @@ impl Sema { if let Some(i) = index { self.want_num(i, scope); } + self.push(out, scope, HStmtKind::Unsupported("Ereignissteuerung")); } } } - fn register_def_fn(&mut self, name: &str, suffix: &Option, params: &[Param]) { - let ret = self.name_ty(name, suffix); - let param_infos = params.iter().map(|p| (self.param_ty(p), p.array)).collect(); - self.procs.insert( - name.to_string(), - ProcInfo { kind: ProcKind::Function, ret, params: param_infos }, - ); - } - - fn def_fn_scope(&mut self, name: &str, suffix: &Option, params: &[Param]) -> Scope { - let mut fscope = Scope::default(); - for p in params { - let ty = self.param_ty(p); - let key = self.var_key(&p.name, &p.suffix, false); - fscope.vars.insert(key, VarInfo { ty, array: false, explicit: true }); - } - let ret = self.name_ty(name, suffix); - let key = self.var_key(name, suffix, false); - fscope.vars.insert(key, VarInfo { ty: ret, array: false, explicit: true }); - fscope - } - - fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope) { - let ty = if let Some(t) = &d.as_type { + fn decl_ty(&mut self, d: &VarDecl) -> Ty { + if let Some(t) = &d.as_type { if let TypeName::Udt(n) = t { - if !self.udts.contains_key(n) { + if !self.udt_ids.contains_key(n) { self.err(d.pos, "Type not defined"); } } type_name_ty(t) } else { self.name_ty(&d.name, &d.suffix) + } + } + + fn lower_select( + &mut self, + expr: &Expr, + arms: &[CaseArm], + scope: &mut Scope, + out: &mut Vec, + ) { + let (se, st) = self.lower_expr(expr, scope); + // Selektor einmal auswerten (versteckter Temp-Slot). + let sel_ty = if st == Ty::Unknown { Ty::Sng } else { st.clone() }; + let temp = self.alloc_temp(scope, &sel_ty); + let temp_place = HPlace { + base: temp, + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&sel_ty), + array_elem: None, }; + self.push( + out, + scope, + HStmtKind::Assign { place: temp_place.clone(), value: se }, + ); + + // Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert. + let mut cases: Vec<(HExpr, Vec)> = Vec::new(); + let mut else_body: Vec = Vec::new(); + for arm in arms { + let body = self.lower_body(&arm.body, scope); + if arm.specs.is_empty() { + else_body = body; + continue; + } + let mut cond: Option = None; + for spec in &arm.specs { + let c = self.lower_case_spec(spec, &temp_place, &st, scope); + cond = Some(match cond { + None => c, + Some(prev) => HExpr::Logic { + op: hir::HLogic::Or, + ty: IntKind::I2, + l: Box::new(prev), + r: Box::new(c), + }, + }); + } + cases.push((cond.unwrap(), body)); + } + // Von hinten zu verschachteltem If zusammensetzen. + let mut els = else_body; + for (cond, body) in cases.into_iter().rev() { + let stmt = HStmt { + line: scope.current_line, + kind: HStmtKind::If { cond, then: body, els }, + }; + els = vec![stmt]; + } + out.extend(els); + } + + fn lower_case_spec( + &mut self, + spec: &CaseSpec, + temp: &HPlace, + sel_ty: &Ty, + scope: &mut Scope, + ) -> HExpr { + let load = || HExpr::Load(Box::new(temp.clone())); + let cmp = |s: &mut Self, op: hir::HCmp, e: &Expr, scope: &mut Scope| -> HExpr { + let (he, et) = s.lower_expr(e, scope); + let ok = (is_num(sel_ty) && is_num(&et)) || (is_str(sel_ty) && is_str(&et)); + if !ok { + s.err(e.pos(), "Type mismatch"); + } + if is_str(sel_ty) && is_str(&et) { + HExpr::Cmp { + op, + ty: hir::CmpKind::Str, + l: Box::new(load()), + r: Box::new(he), + } + } else { + let common = promote_num(num_ty(sel_ty), num_ty(&et)); + let l = s.conv_num(load(), sel_ty, common); + let r = s.conv_num(he, &et, common); + HExpr::Cmp { + op, + ty: hir::CmpKind::Num(common), + l: Box::new(l), + r: Box::new(r), + } + } + }; + match spec { + CaseSpec::Expr(e) => cmp(self, hir::HCmp::Eq, e, scope), + CaseSpec::Is(op, e) => { + let hop = match op { + BinOp::Eq => hir::HCmp::Eq, + BinOp::Ne => hir::HCmp::Ne, + BinOp::Lt => hir::HCmp::Lt, + BinOp::Le => hir::HCmp::Le, + BinOp::Gt => hir::HCmp::Gt, + BinOp::Ge => hir::HCmp::Ge, + _ => hir::HCmp::Eq, + }; + cmp(self, hop, e, scope) + } + CaseSpec::Range(a, b) => { + let lo = cmp(self, hir::HCmp::Ge, a, scope); + let hi = cmp(self, hir::HCmp::Le, b, scope); + HExpr::Logic { + op: hir::HLogic::And, + ty: IntKind::I2, + l: Box::new(lo), + r: Box::new(hi), + } + } + } + } + + fn lower_def_fn( + &mut self, + name: &str, + suffix: &Option, + params: &[Param], + block: Option<&[Stmt]>, + single: Option<&Expr>, + scope: &mut Scope, + pos: SourcePos, + ) { + // Registrierung (mit Prozedur-Id). + let ret = self.name_ty(name, suffix); + let param_infos: Vec<(Ty, bool)> = + params.iter().map(|p| (self.param_ty(p), p.array)).collect(); + let id = match self.procs.get(name) { + Some(p) => p.id, + None => { + let id = self.next_proc_id; + self.next_proc_id += 1; + self.hir_procs.push(None); + id + } + }; + self.procs.insert( + name.to_string(), + ProcInfo { + kind: ProcKind::Function, + ret: ret.clone(), + params: param_infos, + id, + def_fn: true, + }, + ); + let _ = pos; + + // Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal, + // freie Namen binden an Modulvariablen. + let mut fscope = Scope { is_def_fn: true, ..Scope::default() }; + let mut hparams = Vec::new(); + for p in params { + let ty = self.param_ty(p); + let key = self.var_key(&p.name, &p.suffix, false); + let slot = fscope.locals.len() as u16; + fscope.locals.push(hir::HVar { + name: p.name.clone(), + ty: self.h_ty(&ty), + array: false, + }); + fscope.vars.insert( + key, + VarInfo { ty: ty.clone(), array: false, explicit: true, slot, global: false, by_ref: false }, + ); + hparams.push(hir::HParam { + name: p.name.clone(), + ty: self.h_ty(&ty), + array: false, + by_ref: false, + }); + } + let ret_key = self.var_key(name, suffix, false); + let ret_slot_idx = fscope.locals.len() as u16; + fscope.locals.push(hir::HVar { + name: name.to_string(), + ty: self.h_ty(&ret), + array: false, + }); + fscope.vars.insert( + ret_key, + VarInfo { + ty: ret.clone(), + array: false, + explicit: true, + slot: ret_slot_idx, + global: false, + by_ref: false, + }, + ); + + let body = match (block, single) { + (Some(stmts), _) => { + self.prescan(stmts, &mut fscope, false); + self.lower_body(stmts, &mut fscope) + } + (_, Some(expr)) => { + let (e, et) = self.lower_expr(expr, &mut fscope); + let value = self.coerce(e, &et, &ret, expr.pos()); + vec![HStmt { + line: fscope.current_line, + kind: HStmtKind::Assign { + place: HPlace { + base: VarSlot::Local(ret_slot_idx), + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&ret), + array_elem: None, + }, + value, + }, + }] + } + _ => Vec::new(), + }; + + let hproc = hir::HProc { + name: name.to_string(), + kind: hir::HProcKind::DefFn, + params: hparams, + locals: std::mem::take(&mut fscope.locals), + ret_slot: Some(VarSlot::Local(ret_slot_idx)), + ret_ty: Some(self.h_ty(&ret)), + body, + label_count: fscope.next_label, + }; + // Modul-Scope-Zeile weiterführen. + scope.current_line = scope.current_line.max(fscope.current_line); + while self.hir_procs.len() <= id as usize { + self.hir_procs.push(None); + } + self.hir_procs[id as usize] = Some(hproc); + } + + fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec) { + let ty = self.decl_ty(d); + let mut hdims = Vec::new(); if let Some(dims) = &d.dims { for (lo, hi) in dims { - if let Some(l) = lo { - self.want_num(l, scope); - } - self.want_num(hi, scope); + let lo_e = match lo { + Some(l) => self.lower_num_as(l, scope, NumTy::Lng), + None => HExpr::Lng(self.option_base as i32), + }; + let hi_e = self.lower_num_as(hi, scope, NumTy::Lng); + hdims.push((lo_e, hi_e)); } } let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some()); let is_array = d.dims.is_some(); - if let Some(existing) = scope.vars.get(&key) { + if let Some(existing) = self.vars_of(scope).get(&key).cloned() { let redim_ok = redim && existing.array && is_array; if existing.explicit && !redim_ok { self.err(d.pos, "Duplicate definition"); return; } + if redim_ok && !hdims.is_empty() { + let slot = if existing.global { + VarSlot::Global(existing.slot) + } else { + VarSlot::Local(existing.slot) + }; + self.push( + out, + scope, + HStmtKind::Dim { slot, elem: self.h_ty(&ty), dims: hdims, redim: true }, + ); + return; + } + } + let (slot, global) = self.alloc_var(scope, &d.name, &ty, is_array); + self.insert_var( + scope, + key, + VarInfo { ty: ty.clone(), array: is_array, explicit: true, slot, global, by_ref: false }, + ); + if is_array && !hdims.is_empty() { + let vslot = if global { VarSlot::Global(slot) } else { VarSlot::Local(slot) }; + self.push( + out, + scope, + HStmtKind::Dim { slot: vslot, elem: self.h_ty(&ty), dims: hdims, redim }, + ); } - scope.vars.insert(key, VarInfo { ty, array: is_array, explicit: true }); } - fn check_label(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) { + fn check_label_exists(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) { let found = match target { - LabelRef::Name(n) => scope.labels.contains(n), - LabelRef::Line(n) => scope.line_labels.contains(n), + LabelRef::Name(n) => scope.labels.contains_key(n), + LabelRef::Line(n) => scope.line_labels.contains_key(n), }; if !found { self.err(pos, "Label not defined"); } } - fn check_call( + fn label_id(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) -> Option { + let id = match target { + LabelRef::Name(n) => scope.labels.get(n).copied(), + LabelRef::Line(n) => scope.line_labels.get(n).copied(), + }; + if id.is_none() { + self.err(pos, "Label not defined"); + } + id + } + + // ---- Aufrufe ----------------------------------------------------------- + + fn lower_call_stmt( &mut self, name: &str, args: &[Expr], scope: &mut Scope, pos: SourcePos, + out: &mut Vec, ) { if let Some(info) = self.procs.get(name).cloned() { if info.kind != ProcKind::Sub { @@ -961,27 +1833,17 @@ impl Sema { if args.len() != info.params.len() { self.err(pos, "Argument-count mismatch"); } - for (i, a) in args.iter().enumerate() { - let at = self.expr_ty(a, scope); - if let Some((pt, _)) = info.params.get(i) { - let ok = (is_num(pt) && is_num(&at)) - || (is_str(pt) && is_str(&at)) - || matches!(pt, Ty::Udt(_) | Ty::Unknown) - || at == Ty::Unknown; - if !ok { - self.err(a.pos(), "Parameter type mismatch"); - } - } - } + let hargs = self.lower_call_args(&info, args, scope); + self.push(out, scope, HStmtKind::CallSub { proc: info.id, args: hargs }); return; } if let Some((min, max, spec)) = builtin_stmt(name) { - self.check_builtin_args(name, args, min, max, spec, scope, pos); + self.lower_builtin_stmt(name, args, min, max, spec, scope, pos, out); return; } if banned_feature(name) { for a in args { - self.expr_ty(a, scope); + self.lower_expr(a, scope); } self.err(pos, "Feature unavailable"); return; @@ -989,7 +1851,148 @@ impl Sema { self.err(pos, "Subprogram not defined"); } - fn check_builtin_args( + /// Argumente eines SUB-/FUNCTION-Aufrufs: Variablen BYREF (exakter + /// Typ), Ausdrücke/Klammern BYVAL mit Konvertierung. + fn lower_call_args(&mut self, info: &ProcInfo, args: &[Expr], scope: &mut Scope) -> Vec { + let mut out = Vec::new(); + for (i, a) in args.iter().enumerate() { + let (pt, p_array) = match info.params.get(i) { + Some(p) => (p.0.clone(), p.1), + None => (Ty::Unknown, false), + }; + // DEF FN: alle Parameter BYVAL. + let force_byval = info.def_fn; + // Array-Parameter: ganzes Array als Referenz. + if p_array { + if let Expr::Name { name, suffix, args: idx, pos } = a { + if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) { + if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) { + let base = if v.global { + VarSlot::Global(v.slot) + } else { + VarSlot::Local(v.slot) + }; + out.push(HArg::ArrayRef(HPlace { + base, + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&v.ty), + array_elem: None, + })); + continue; + } + } + } + self.err(a.pos(), "Parameter type mismatch"); + continue; + } + // BYREF-fähig: einfacher Variablen-/Element-/Feldzugriff. + let byref_candidate = !force_byval && self.is_place_expr(a, scope); + if byref_candidate { + let (place, at) = self.lower_place(a, scope); + let kind_ok = (is_num(&pt) && is_num(&at)) + || (is_str(&pt) && is_str(&at)) + || matches!((&pt, &at), (Ty::Udt(x), Ty::Udt(y)) if x == y) + || pt == Ty::Unknown + || at == Ty::Unknown; + if !kind_ok { + self.err(a.pos(), "Parameter type mismatch"); + continue; + } + // BYREF verlangt exakten Typ (Vorbild: „Parameter type + // mismatch" bei abweichendem numerischen Typ). + if pt != Ty::Unknown && at != Ty::Unknown && pt != at { + if is_num(&pt) && is_num(&at) { + self.err(a.pos(), "Parameter type mismatch"); + continue; + } + // FixedStr → STRING-Parameter: BYVAL-Kopie. + if let Some(place) = place { + let e = HExpr::Load(Box::new(place)); + let e = self.coerce(e, &at, &pt, a.pos()); + out.push(HArg::ByVal(e)); + } + continue; + } + if let Some(place) = place { + out.push(HArg::ByRef(place)); + } + continue; + } + // BYVAL: Ausdruck auswerten und konvertieren. + let (e, at) = self.lower_expr(a, scope); + let kind_ok = (is_num(&pt) && is_num(&at)) + || (is_str(&pt) && is_str(&at)) + || pt == Ty::Unknown + || at == Ty::Unknown; + if !kind_ok { + self.err(a.pos(), "Parameter type mismatch"); + continue; + } + let e = self.coerce(e, &at, &pt, a.pos()); + out.push(HArg::ByVal(e)); + } + out + } + + /// Ist der Ausdruck ein L-Wert (Variable/Element/Feld), keine + /// Konstante, Funktion oder Klammerung? + fn is_place_expr(&self, e: &Expr, scope: &Scope) -> bool { + match e { + Expr::Name { name, suffix, args, .. } => { + if self.consts.contains_key(name) { + return false; + } + match args { + None => { + // Variable oder parameterlose Funktion/Builtin? + let as_key = format!("{name}\u{1}AS"); + let key = self.var_key(name, suffix, false); + let declared = self.vars_of(scope).contains_key(&key) + || (suffix.is_none() && self.vars_of(scope).contains_key(&as_key)) + || (scope.is_def_fn + && (self.module_vars.contains_key(&key) + || (suffix.is_none() + && self.module_vars.contains_key(&as_key)))); + if declared { + return true; + } + let full_name = match suffix { + Some(s) => format!("{name}{}", s.as_char()), + None => name.to_string(), + }; + if banned_feature(&full_name) || builtin_fn(&full_name).is_some() { + return false; + } + if let Some(info) = self.procs.get(name) { + if info.kind == ProcKind::Function && info.params.is_empty() { + return false; + } + } + true // implizite Variable + } + Some(_) => { + // Nur deklarierte Arrays sind L-Werte. + let as_key = format!("{name}\u{1}AS"); + let key = self.var_key(name, suffix, false); + let v = if suffix.is_none() { + self.vars_of(scope) + .get(&as_key) + .or_else(|| self.vars_of(scope).get(&key)) + } else { + self.vars_of(scope).get(&key) + }; + v.map(|v| v.array).unwrap_or(false) + } + } + } + _ => false, + } + } + + #[allow(clippy::too_many_arguments)] + fn lower_builtin_stmt( &mut self, name: &str, args: &[Expr], @@ -998,7 +2001,112 @@ impl Sema { spec: &[ArgK], scope: &mut Scope, pos: SourcePos, + out: &mut Vec, ) { + // Argumentprüfung wie bisher. + let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos); + + match name { + "BEEP" => self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Beep, args: vec![] }), + "DOEVENTS" => { + self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Doevents, args: vec![] }) + } + "RANDOMIZE" => { + let a = lowered + .into_iter() + .next() + .map(|(e, t)| { + let conv = self.conv_num(e, &t, NumTy::Dbl); + vec![conv] + }) + .unwrap_or_default(); + self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Randomize, args: a }); + } + "SLEEP" => { + let a = lowered + .into_iter() + .next() + .map(|(e, t)| { + let conv = self.conv_num(e, &t, NumTy::Dbl); + vec![conv] + }) + .unwrap_or_default(); + self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Sleep, args: a }); + } + "SWAP" => { + // SWAP a, b → t = a : a = b : b = t (versteckter Temp). + if args.len() == 2 { + let (pa, ta) = self.lower_place(&args[0], scope); + let (pb, tb) = self.lower_place(&args[1], scope); + let ok = (is_num(&ta) && is_num(&tb)) + || (is_str(&ta) && is_str(&tb)) + || matches!((&ta, &tb), (Ty::Udt(x), Ty::Udt(y)) if x == y) + || ta == Ty::Unknown + || tb == Ty::Unknown; + if !ok { + self.err(pos, "Type mismatch"); + return; + } + if let (Some(pa), Some(pb)) = (pa, pb) { + let tmp = self.alloc_temp(scope, &ta); + let tmp_place = HPlace { + base: tmp, + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&ta), + array_elem: None, + }; + let load_a = HExpr::Load(Box::new(pa.clone())); + let load_b = HExpr::Load(Box::new(pb.clone())); + let b_conv = self.coerce_silent(load_b, &tb, &ta); + let t_load = HExpr::Load(Box::new(tmp_place.clone())); + let t_conv = self.coerce_silent(t_load, &ta, &tb); + self.push(out, scope, HStmtKind::Assign { place: tmp_place, value: load_a }); + self.push(out, scope, HStmtKind::Assign { place: pa, value: b_conv }); + self.push(out, scope, HStmtKind::Assign { place: pb, value: t_conv }); + } + } + } + // Bildschirm-/Datei-/System-Anweisungen späterer Phasen. + _ => self.push(out, scope, HStmtKind::Unsupported(match name { + "CLS" => "CLS", + "COLOR" => "COLOR", + "LOCATE" => "LOCATE", + "WIDTH" => "WIDTH", + "SCREEN" => "SCREEN", + "KEY" => "KEY", + "CLEAR" => "CLEAR", + "FILES" => "FILES", + "KILL" => "KILL", + "CHDIR" => "CHDIR", + "MKDIR" => "MKDIR", + "RMDIR" => "RMDIR", + "SHELL" => "SHELL", + "RUN" => "RUN", + "ENVIRON" => "ENVIRON", + "RESET" => "RESET", + "TRON" => "TRON", + "TROFF" => "TROFF", + "MSGBOX" => "MSGBOX", + "SETUEVENT" => "SETUEVENT", + _ => "Anweisung", + })), + } + } + + /// Zählt/prüft Builtin-Argumente und liefert die abgesenkten Ausdrücke. + #[allow(clippy::too_many_arguments)] + fn check_and_lower_builtin_args( + &mut self, + name: &str, + args: &[Expr], + min: u8, + max: u8, + spec: &[ArgK], + scope: &mut Scope, + pos: SourcePos, + ) -> Vec<(HExpr, Ty)> { let real: Vec<&Expr> = args.iter().filter(|a| !matches!(a, Expr::Missing)).collect(); if real.len() < min as usize || args.len() > max as usize { @@ -1006,11 +2114,13 @@ impl Sema { } // Sonderfall INSTR([start%,] s$, such$) let instr_with_start = name == "INSTR" && args.len() == 3; + let mut out = Vec::new(); for (i, a) in args.iter().enumerate() { if matches!(a, Expr::Missing) { + out.push((HExpr::Int(0), Ty::Unknown)); continue; } - let at = self.expr_ty(a, scope); + let (he, at) = self.lower_expr(a, scope); let kind = if instr_with_start { match i { 0 => ArgK::N, @@ -1029,22 +2139,66 @@ impl Sema { if !ok { self.err(a.pos(), "Type mismatch"); } + out.push((he, at)); } + out } // ---- Ausdrücke --------------------------------------------------------- - fn want_num(&mut self, e: &Expr, scope: &mut Scope) { - let t = self.expr_ty(e, scope); + fn want_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) { + let (he, t) = self.lower_expr(e, scope); if !is_num(&t) { self.err(e.pos(), "Type mismatch"); } + (he, t) } - fn want_str(&mut self, e: &Expr, scope: &mut Scope) { - let t = self.expr_ty(e, scope); + fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) { + let (he, t) = self.lower_expr(e, scope); if !is_str(&t) { self.err(e.pos(), "Type mismatch"); } + (he, t) + } + + /// Numerischer Ausdruck, konvertiert auf Zieltyp. + fn lower_num_as(&mut self, e: &Expr, scope: &mut Scope, to: NumTy) -> HExpr { + let (he, t) = self.want_num(e, scope); + if t == Ty::Unknown { + he + } else { + self.conv_num(he, &t, to) + } + } + + fn lower_expr_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) { + self.want_num(e, scope) + } + + /// Bedingung: numerisch; 0 = falsch. Der Codegen testet auf ≠ 0 im + /// jeweiligen Typ (kein Konvertierungszwang nötig — auf INTEGER + /// konvertieren würde bei großen Werten fälschlich Fehler 6 auslösen, + /// daher Vergleich im Operandentyp). + fn lower_cond(&mut self, e: &Expr, scope: &mut Scope) -> HExpr { + let (he, t) = self.want_num(e, scope); + if t == Ty::Unknown || num_ty(&t) == NumTy::Int { + return he; + } + // vergleiche ≠ 0 im Operandentyp → INTEGER-Ergebnis + let nt = num_ty(&t); + let zero = match nt { + NumTy::Int => HExpr::Int(0), + NumTy::Lng => HExpr::Lng(0), + NumTy::Cur => HExpr::Cur(0), + NumTy::Sng => HExpr::Sng(0.0), + NumTy::Dbl => HExpr::Dbl(0.0), + }; + HExpr::Cmp { + op: hir::HCmp::Ne, + ty: hir::CmpKind::Num(nt), + l: Box::new(he), + r: Box::new(zero), + } } fn check_assign(&mut self, target: &Ty, value: &Ty, pos: SourcePos) { @@ -1058,119 +2212,388 @@ impl Sema { } } - fn lvalue_ty(&mut self, e: &Expr, scope: &mut Scope) -> Ty { + /// Wie `coerce`, aber ohne neue Diagnose (Prüfung lief bereits). + fn coerce_silent(&mut self, e: HExpr, from: &Ty, to: &Ty) -> HExpr { + if is_num(to) && is_num(from) && *to != Ty::Unknown && *from != Ty::Unknown { + return self.conv_num(e, from, num_ty(to)); + } + if let Ty::FixedStr(n) = to { + if is_str(from) { + return HExpr::FixStr { len: *n, arg: Box::new(e) }; + } + } + e + } + + /// L-Wert absenken: liefert Platz und Typ. + fn lower_place(&mut self, e: &Expr, scope: &mut Scope) -> (Option, Ty) { match e { Expr::Name { name, suffix, args, pos } => { - if let Some(idx) = args { - for a in idx { - self.want_num(a, scope); + let indices = match args { + Some(idx) => { + let mut v = Vec::new(); + for a in idx { + v.push(self.lower_num_as(a, scope, NumTy::Lng)); + } + Some(v) + } + None => None, + }; + // UDT-Feldzugriff über Punktpfad? + if args.is_none() && name.contains('.') { + let parts: Vec<&str> = name.split('.').collect(); + let base = parts[0]; + let base_as = format!("{base}\u{1}AS"); + let base_info = self + .vars_of(scope) + .get(&base_as) + .or_else(|| self.vars_of(scope).get(&self.var_key(base, &None, false))) + .cloned() + .or_else(|| { + if scope.is_def_fn { + self.module_vars + .get(&base_as) + .or_else(|| { + self.module_vars.get(&self.var_key(base, &None, false)) + }) + .cloned() + } else { + None + } + }); + if let Some(info) = base_info { + if matches!(info.ty, Ty::Udt(_)) { + let (fty, fpath) = self.member_path(&info.ty, &parts[1..], *pos); + let hty = self.h_ty(&fty); + let base_slot = if info.global { + VarSlot::Global(info.slot) + } else { + VarSlot::Local(info.slot) + }; + return ( + Some(HPlace { + base: base_slot, + base_is_ref: false, + indices: Vec::new(), + fields: fpath, + ty: hty, + array_elem: None, + }), + fty, + ); + } } - self.resolve_var(scope, name, suffix, true, *pos) - } else { - self.resolve_var(scope, name, suffix, false, *pos) } + let is_array_access = indices.is_some(); + let info = self.resolve_var(scope, name, suffix, is_array_access, *pos); + let Some(info) = info else { + return (None, self.name_ty(name, suffix)); + }; + let base = if info.global { + VarSlot::Global(info.slot) + } else { + VarSlot::Local(info.slot) + }; + let hty = self.h_ty(&info.ty); + let idx_vec = indices.unwrap_or_default(); + let array_elem = if !idx_vec.is_empty() { + Some((hty.clone(), idx_vec.len() as u8)) + } else { + None + }; + ( + Some(HPlace { + base, + base_is_ref: info.by_ref, + indices: idx_vec, + fields: Vec::new(), + ty: hty, + array_elem, + }), + info.ty, + ) } _ => { self.err(e.pos(), "Variable required"); - Ty::Unknown + (None, Ty::Unknown) } } } - fn expr_ty(&mut self, e: &Expr, scope: &mut Scope) -> Ty { + fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) { match e { - Expr::IntLit(_) => Ty::Int, - Expr::LongLit(_) => Ty::Lng, - Expr::SingleLit(_) => Ty::Sng, - Expr::DoubleLit(_) => Ty::Dbl, - Expr::CurrencyLit(_) => Ty::Cur, - Expr::StrLit(_) => Ty::Str, - Expr::Missing => Ty::Unknown, + Expr::IntLit(v) => (HExpr::Int(*v), Ty::Int), + Expr::LongLit(v) => (HExpr::Lng(*v), Ty::Lng), + Expr::SingleLit(v) => (HExpr::Sng(*v), Ty::Sng), + Expr::DoubleLit(v) => (HExpr::Dbl(*v), Ty::Dbl), + Expr::CurrencyLit(v) => (HExpr::Cur(*v), Ty::Cur), + Expr::StrLit(s) => (HExpr::Str(s.clone()), Ty::Str), + Expr::Paren(inner) => self.lower_expr(inner, scope), + Expr::Missing => (HExpr::Int(0), Ty::Unknown), Expr::Unary { op, operand, pos } => { - let t = self.expr_ty(operand, scope); + let (he, t) = self.lower_expr(operand, scope); if !is_num(&t) { self.err(*pos, "Type mismatch"); } match op { - UnOp::Neg => t, - UnOp::Not => Ty::Lng, + UnOp::Neg => { + let nt = num_ty(&t); + // Literal direkt negieren (hilft FOR STEP -1). + match (&he, nt) { + (HExpr::Int(v), NumTy::Int) if *v != i16::MIN => { + (HExpr::Int(-v), t.clone()) + } + (HExpr::Lng(v), NumTy::Lng) if *v != i32::MIN => { + (HExpr::Lng(-v), t.clone()) + } + (HExpr::Sng(v), NumTy::Sng) => (HExpr::Sng(-v), t.clone()), + (HExpr::Dbl(v), NumTy::Dbl) => (HExpr::Dbl(-v), t.clone()), + _ => (HExpr::Neg { ty: nt, arg: Box::new(he) }, t.clone()), + } + } + UnOp::Not => { + let kind = if num_ty(&t) == NumTy::Int { + IntKind::I2 + } else { + IntKind::I4 + }; + let target = if kind == IntKind::I2 { NumTy::Int } else { NumTy::Lng }; + let conv = if t == Ty::Unknown { he } else { self.conv_num(he, &t, target) }; + ( + HExpr::Not { ty: kind, arg: Box::new(conv) }, + if kind == IntKind::I2 { Ty::Int } else { Ty::Lng }, + ) + } } } Expr::Binary { op, lhs, rhs, pos } => { - let lt = self.expr_ty(lhs, scope); - let rt = self.expr_ty(rhs, scope); - match op { - BinOp::Add => { - if is_str(<) && is_str(&rt) { - Ty::Str - } else if is_num(<) && is_num(&rt) { - promote(<, &rt) - } else { - self.err(*pos, "Type mismatch"); - Ty::Unknown - } - } - BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt - | BinOp::Ge => { - let ok = (is_num(<) && is_num(&rt)) - || (is_str(<) && is_str(&rt)); - if !ok { - self.err(*pos, "Type mismatch"); - } - Ty::Int - } - BinOp::And | BinOp::Or | BinOp::Xor | BinOp::Eqv - | BinOp::Imp => { - if !is_num(<) || !is_num(&rt) { - self.err(*pos, "Type mismatch"); - } - Ty::Lng - } - BinOp::IntDiv | BinOp::Mod => { - if !is_num(<) || !is_num(&rt) { - self.err(*pos, "Type mismatch"); - } - Ty::Lng - } - BinOp::Div | BinOp::Pow => { - if !is_num(<) || !is_num(&rt) { - self.err(*pos, "Type mismatch"); - } - Ty::Dbl - } - BinOp::Mul | BinOp::Sub => { - if !is_num(<) || !is_num(&rt) { - self.err(*pos, "Type mismatch"); - } - promote(<, &rt) - } - } + let (le, lt) = self.lower_expr(lhs, scope); + let (re, rt) = self.lower_expr(rhs, scope); + self.lower_binary(*op, le, lt, re, rt, *pos) } Expr::Name { name, suffix, args, pos } => { - self.name_expr_ty(name, suffix, args, *pos, scope) + self.lower_name_expr(name, suffix, args, *pos, scope) } } } - fn name_expr_ty( + fn lower_binary( + &mut self, + op: BinOp, + le: HExpr, + lt: Ty, + re: HExpr, + rt: Ty, + pos: SourcePos, + ) -> (HExpr, Ty) { + use hir::{HArith, HCmp, HLogic}; + match op { + BinOp::Add => { + if is_str(<) && is_str(&rt) { + (HExpr::Concat(Box::new(le), Box::new(re)), Ty::Str) + } else if is_num(<) && is_num(&rt) { + self.arith(HArith::Add, le, <, re, &rt) + } else { + self.err(pos, "Type mismatch"); + (HExpr::Int(0), Ty::Unknown) + } + } + BinOp::Sub | BinOp::Mul => { + if !is_num(<) || !is_num(&rt) { + self.err(pos, "Type mismatch"); + return (HExpr::Int(0), Ty::Unknown); + } + let a = if op == BinOp::Sub { HArith::Sub } else { HArith::Mul }; + self.arith(a, le, <, re, &rt) + } + BinOp::Div => { + if !is_num(<) || !is_num(&rt) { + self.err(pos, "Type mismatch"); + return (HExpr::Int(0), Ty::Unknown); + } + // `/`: DOUBLE bei DOUBLE-/CURRENCY-Operand, sonst SINGLE. + let out = if matches!(num_ty(<), NumTy::Dbl | NumTy::Cur) + || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur) + { + NumTy::Dbl + } else { + NumTy::Sng + }; + let l = self.conv_num(le, <, out); + let r = self.conv_num(re, &rt, out); + ( + HExpr::Bin { op: HArith::Div, ty: out, l: Box::new(l), r: Box::new(r) }, + ty_of_num(out), + ) + } + BinOp::Pow => { + if !is_num(<) || !is_num(&rt) { + self.err(pos, "Type mismatch"); + return (HExpr::Int(0), Ty::Unknown); + } + // `^` rechnet in DOUBLE; Ergebnis SINGLE außer bei + // DOUBLE-/CURRENCY-Operand (Festlegung: Matrix in + // docs/tbvm-design.md; Verifikation gegen die + // Original-Hilfe ist in PLAN.md Phase 3 eingeplant). + let out = if matches!(num_ty(<), NumTy::Dbl | NumTy::Cur) + || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur) + { + NumTy::Dbl + } else { + NumTy::Sng + }; + let l = self.conv_num(le, <, NumTy::Dbl); + let r = self.conv_num(re, &rt, NumTy::Dbl); + let p = HExpr::Bin { + op: HArith::Pow, + ty: NumTy::Dbl, + l: Box::new(l), + r: Box::new(r), + }; + if out == NumTy::Dbl { + (p, Ty::Dbl) + } else { + ( + HExpr::Conv { from: NumTy::Dbl, to: NumTy::Sng, arg: Box::new(p) }, + Ty::Sng, + ) + } + } + BinOp::IntDiv | BinOp::Mod => { + if !is_num(<) || !is_num(&rt) { + self.err(pos, "Type mismatch"); + return (HExpr::Int(0), Ty::Unknown); + } + // Operanden vorab auf Ganzzahl gerundet; INTEGER nur wenn + // beide Operanden INTEGER sind. + let out = if num_ty(<) == NumTy::Int && num_ty(&rt) == NumTy::Int { + NumTy::Int + } else { + NumTy::Lng + }; + let l = self.conv_num(le, <, out); + let r = self.conv_num(re, &rt, out); + let a = if op == BinOp::IntDiv { HArith::IDiv } else { HArith::Mod }; + ( + HExpr::Bin { op: a, ty: out, l: Box::new(l), r: Box::new(r) }, + ty_of_num(out), + ) + } + BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => { + let hop = match op { + BinOp::Eq => HCmp::Eq, + BinOp::Ne => HCmp::Ne, + BinOp::Lt => HCmp::Lt, + BinOp::Le => HCmp::Le, + BinOp::Gt => HCmp::Gt, + _ => HCmp::Ge, + }; + if is_str(<) && is_str(&rt) { + ( + HExpr::Cmp { + op: hop, + ty: hir::CmpKind::Str, + l: Box::new(le), + r: Box::new(re), + }, + Ty::Int, + ) + } else if is_num(<) && is_num(&rt) { + let common = promote_num(num_ty(<), num_ty(&rt)); + let l = self.conv_num(le, <, common); + let r = self.conv_num(re, &rt, common); + ( + HExpr::Cmp { + op: hop, + ty: hir::CmpKind::Num(common), + l: Box::new(l), + r: Box::new(r), + }, + Ty::Int, + ) + } else { + self.err(pos, "Type mismatch"); + (HExpr::Int(0), Ty::Int) + } + } + BinOp::And | BinOp::Or | BinOp::Xor | BinOp::Eqv | BinOp::Imp => { + if !is_num(<) || !is_num(&rt) { + self.err(pos, "Type mismatch"); + return (HExpr::Int(0), Ty::Unknown); + } + let hop = match op { + BinOp::And => HLogic::And, + BinOp::Or => HLogic::Or, + BinOp::Xor => HLogic::Xor, + BinOp::Eqv => HLogic::Eqv, + _ => HLogic::Imp, + }; + // INTEGER-Ergebnis nur wenn beide Operanden INTEGER sind. + let kind = if num_ty(<) == NumTy::Int && num_ty(&rt) == NumTy::Int { + IntKind::I2 + } else { + IntKind::I4 + }; + let target = if kind == IntKind::I2 { NumTy::Int } else { NumTy::Lng }; + let l = self.conv_num(le, <, target); + let r = self.conv_num(re, &rt, target); + ( + HExpr::Logic { op: hop, ty: kind, l: Box::new(l), r: Box::new(r) }, + if kind == IntKind::I2 { Ty::Int } else { Ty::Lng }, + ) + } + } + } + + /// Arithmetik `+ - *` im promoteten Typ. + fn arith( + &mut self, + op: hir::HArith, + le: HExpr, + lt: &Ty, + re: HExpr, + rt: &Ty, + ) -> (HExpr, Ty) { + if *lt == Ty::Unknown || *rt == Ty::Unknown { + return (le, Ty::Unknown); + } + let out = promote_num(num_ty(lt), num_ty(rt)); + let l = self.conv_num(le, lt, out); + let r = self.conv_num(re, rt, out); + ( + HExpr::Bin { op, ty: out, l: Box::new(l), r: Box::new(r) }, + ty_of_num(out), + ) + } + + fn lower_name_expr( &mut self, name: &str, suffix: &Option, args: &Option>, pos: SourcePos, scope: &mut Scope, - ) -> Ty { + ) -> (HExpr, Ty) { let full_name = match suffix { Some(s) => format!("{name}{}", s.as_char()), None => name.to_string(), }; // 1. Konstante - if let Some((t, _)) = self.consts.get(name).cloned() { + if let Some((t, v)) = self.consts.get(name).cloned() { if args.is_some() { self.err(pos, "Syntax error"); } - return t; + let e = match (&t, v) { + (_, Some(ConstVal::Str(s))) => HExpr::Str(s), + (Ty::Int, Some(ConstVal::Num(n))) => HExpr::Int(n as i16), + (Ty::Lng, Some(ConstVal::Num(n))) => HExpr::Lng(n as i32), + (Ty::Sng, Some(ConstVal::Num(n))) => HExpr::Sng(n as f32), + (Ty::Cur, Some(ConstVal::Num(n))) => HExpr::Cur((n * 10_000.0).round() as i64), + (_, Some(ConstVal::Num(n))) => HExpr::Dbl(n), + _ => HExpr::Int(0), + }; + return (e, t); } match args { @@ -1179,32 +2602,67 @@ impl Sema { let as_key = format!("{name}\u{1}AS"); let key = self.var_key(name, suffix, false); let existing = if suffix.is_none() { - scope.vars.get(&as_key).or_else(|| scope.vars.get(&key)) + self.vars_of(scope) + .get(&as_key) + .or_else(|| self.vars_of(scope).get(&key)) } else { - scope.vars.get(&key) + self.vars_of(scope).get(&key) }; - if let Some(v) = existing.cloned() { - if v.array { - for a in idx { - self.want_num(a, scope); + let existing = existing.cloned().or_else(|| { + if scope.is_def_fn { + if suffix.is_none() { + self.module_vars + .get(&as_key) + .or_else(|| self.module_vars.get(&key)) + .cloned() + } else { + self.module_vars.get(&key).cloned() } - return v.ty; + } else { + None + } + }); + if let Some(v) = existing { + if v.array { + let (place, ty) = self.lower_place( + &Expr::Name { + name: name.to_string(), + suffix: *suffix, + args: Some(idx.clone()), + pos, + }, + scope, + ); + let e = match place { + Some(p) => HExpr::Load(Box::new(p)), + None => HExpr::Int(0), + }; + return (e, ty); + } + // Rekursion: In einer FUNCTION ist der Name mit + // Argumentliste der (rekursive) Aufruf, ohne Argumente + // die Rückgabevariable. + let is_fn_call = self + .procs + .get(name) + .map(|p| p.kind == ProcKind::Function) + .unwrap_or(false); + if !is_fn_call { + self.err(pos, "Duplicate definition"); + return (HExpr::Int(0), Ty::Unknown); } - self.err(pos, "Duplicate definition"); - return Ty::Unknown; } // 3. Nicht unterstützte Features → Compile-Fehler if banned_feature(&full_name) { for a in idx { - self.expr_ty(a, scope); + self.lower_expr(a, scope); } self.err(pos, "Feature unavailable"); - return Ty::Unknown; + return (HExpr::Int(0), Ty::Unknown); } // 4. Builtin-Funktion - if let Some((min, max, spec, ret)) = builtin_fn(&full_name) { - self.check_builtin_args(&full_name, idx, min, max, spec, scope, pos); - return ret_ty(ret); + if builtin_fn(&full_name).is_some() { + return self.lower_builtin_fn(&full_name, idx, scope, pos); } // 5. FUNCTION / DEF FN if let Some(info) = self.procs.get(name).cloned() { @@ -1212,91 +2670,406 @@ impl Sema { if idx.len() != info.params.len() { self.err(pos, "Argument-count mismatch"); } - for a in idx { - self.expr_ty(a, scope); - } - return info.ret; + let hargs = self.lower_call_args(&info, idx, scope); + let ret = info.ret.clone(); + return ( + HExpr::FnCall { + proc: info.id, + args: hargs, + ret: self.h_ty(&ret), + }, + ret, + ); } self.err(pos, "Duplicate definition"); - return Ty::Unknown; + return (HExpr::Int(0), Ty::Unknown); } // 6. Implizites Array (klassisch: DIM x(10) implizit) - for a in idx { - self.want_num(a, scope); - } if self.explicit { + for a in idx { + self.want_num(a, scope); + } self.err(pos, "Variable not defined"); - return self.name_ty(name, suffix); + return (HExpr::Int(0), self.name_ty(name, suffix)); } - let ty = self.name_ty(name, suffix); - let key = self.var_key(name, suffix, false); - scope - .vars - .insert(key, VarInfo { ty: ty.clone(), array: true, explicit: false }); - ty + let (place, ty) = self.lower_place( + &Expr::Name { + name: name.to_string(), + suffix: *suffix, + args: Some(idx.clone()), + pos, + }, + scope, + ); + let e = match place { + Some(p) => HExpr::Load(Box::new(p)), + None => HExpr::Int(0), + }; + (e, ty) } None => { // 2. Parameterlose Builtins (RND, ERR, INKEY$, TIMER …) let as_key = format!("{name}\u{1}AS"); let key = self.var_key(name, suffix, false); - let declared = scope.vars.contains_key(&key) - || (suffix.is_none() && scope.vars.contains_key(&as_key)); + let declared = self.vars_of(scope).contains_key(&key) + || (suffix.is_none() && self.vars_of(scope).contains_key(&as_key)) + || (scope.is_def_fn + && (self.module_vars.contains_key(&key) + || (suffix.is_none() && self.module_vars.contains_key(&as_key)))); if !declared { if banned_feature(&full_name) { self.err(pos, "Feature unavailable"); - return Ty::Unknown; + return (HExpr::Int(0), Ty::Unknown); } - if let Some((0, _, _, ret)) = builtin_fn(&full_name) { - return ret_ty(ret); + if let Some((0, _, _, _)) = builtin_fn(&full_name) { + return self.lower_builtin_fn(&full_name, &[], scope, pos); } if let Some(info) = self.procs.get(name).cloned() { if info.kind == ProcKind::Function && info.params.is_empty() { - return info.ret; + let ret = info.ret.clone(); + return ( + HExpr::FnCall { + proc: info.id, + args: Vec::new(), + ret: self.h_ty(&ret), + }, + ret, + ); } } } // 3. Variable (ggf. implizit deklarieren) - self.resolve_var(scope, name, suffix, false, pos) + let (place, ty) = self.lower_place( + &Expr::Name { name: name.to_string(), suffix: *suffix, args: None, pos }, + scope, + ); + let e = match place { + Some(p) => HExpr::Load(Box::new(p)), + None => HExpr::Int(0), + }; + (e, ty) + } + } + } + + /// Builtin-Funktionsaufruf absenken (inkl. Argument-Konvertierungen). + fn lower_builtin_fn( + &mut self, + full_name: &str, + args: &[Expr], + scope: &mut Scope, + pos: SourcePos, + ) -> (HExpr, Ty) { + let (min, max, spec, retk) = builtin_fn(full_name).unwrap(); + + // LBOUND/UBOUND brauchen das Array selbst. + if full_name == "LBOUND" || full_name == "UBOUND" { + if args.is_empty() || args.len() > 2 { + self.err(pos, "Argument-count mismatch"); + return (HExpr::Lng(0), Ty::Lng); + } + let place = if let Expr::Name { name, suffix, args: idx, pos } = &args[0] { + if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) { + self.resolve_var(scope, name, suffix, true, *pos).map(|v| HPlace { + base: if v.global { + VarSlot::Global(v.slot) + } else { + VarSlot::Local(v.slot) + }, + base_is_ref: false, + indices: Vec::new(), + fields: Vec::new(), + ty: self.h_ty(&v.ty), + array_elem: None, + }) + } else { + None + } + } else { + None + }; + let dim = match args.get(1) { + Some(d) => self.lower_num_as(d, scope, NumTy::Lng), + None => HExpr::Lng(1), + }; + match place { + Some(p) => { + return ( + HExpr::ArrayBound { + lower: full_name == "LBOUND", + place: Box::new(p), + dim: Box::new(dim), + }, + Ty::Lng, + ) + } + None => { + self.err(args[0].pos(), "Type mismatch"); + return (HExpr::Lng(0), Ty::Lng); + } + } + } + + let lowered = self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos); + let get = |i: usize| -> Option<&(HExpr, Ty)> { lowered.get(i) }; + let take = |l: &[(HExpr, Ty)], i: usize| -> (HExpr, Ty) { + l.get(i).cloned().unwrap_or((HExpr::Int(0), Ty::Unknown)) + }; + + macro_rules! conv_arg { + ($i:expr, $to:expr) => {{ + let (e, t) = take(&lowered, $i); + if t == Ty::Unknown { + e + } else { + self.conv_num(e, &t, $to) + } + }}; + } + + let b = |b: Builtin, args: Vec, ret: Ty, s: &Sema| -> (HExpr, Ty) { + let rty = s.h_ty(&ret); + (HExpr::Builtin { b, args, ret: rty }, ret) + }; + + match full_name { + // --- Strings --- + "LEN" => { + let (e, t) = take(&lowered, 0); + if is_str(&t) && t != Ty::Unknown { + b(Builtin::Len, vec![e], Ty::Int, self) + } else { + // LEN(zahlvariable) = Bytegröße des Typs + let size: i16 = match num_ty(&t) { + NumTy::Int => 2, + NumTy::Lng | NumTy::Sng => 4, + NumTy::Cur | NumTy::Dbl => 8, + }; + (HExpr::Int(size), Ty::Int) + } + } + "LEFT$" => { + let (s, _) = take(&lowered, 0); + let n = conv_arg!(1, NumTy::Lng); + b(Builtin::LeftS, vec![s, n], Ty::Str, self) + } + "RIGHT$" => { + let (s, _) = take(&lowered, 0); + let n = conv_arg!(1, NumTy::Lng); + b(Builtin::RightS, vec![s, n], Ty::Str, self) + } + "MID$" => { + let (s, _) = take(&lowered, 0); + let start = conv_arg!(1, NumTy::Lng); + let len = if get(2).is_some() { + conv_arg!(2, NumTy::Lng) + } else { + HExpr::Lng(-1) + }; + b(Builtin::MidS, vec![s, start, len], Ty::Str, self) + } + "INSTR" => { + let (start, s, t) = if args.len() == 3 { + let st = conv_arg!(0, NumTy::Lng); + (st, take(&lowered, 1).0, take(&lowered, 2).0) + } else { + (HExpr::Lng(1), take(&lowered, 0).0, take(&lowered, 1).0) + }; + b(Builtin::InstrF, vec![start, s, t], Ty::Int, self) + } + "UCASE$" => b(Builtin::UcaseS, vec![take(&lowered, 0).0], Ty::Str, self), + "LCASE$" => b(Builtin::LcaseS, vec![take(&lowered, 0).0], Ty::Str, self), + "LTRIM$" => b(Builtin::LtrimS, vec![take(&lowered, 0).0], Ty::Str, self), + "RTRIM$" => b(Builtin::RtrimS, vec![take(&lowered, 0).0], Ty::Str, self), + "SPACE$" => { + let n = conv_arg!(0, NumTy::Lng); + b(Builtin::SpaceS, vec![n], Ty::Str, self) + } + "STRING$" => { + let n = conv_arg!(0, NumTy::Lng); + let (ch, cht) = take(&lowered, 1); + let ch = if is_num(&cht) && cht != Ty::Unknown { + self.conv_num(ch, &cht, NumTy::Lng) + } else { + ch + }; + b(Builtin::StringS, vec![n, ch], Ty::Str, self) + } + "CHR$" => { + let n = conv_arg!(0, NumTy::Lng); + b(Builtin::ChrS, vec![n], Ty::Str, self) + } + "ASC" => b(Builtin::Asc, vec![take(&lowered, 0).0], Ty::Int, self), + "STR$" => { + // Typ bleibt erhalten (Formatierung je Tag). + b(Builtin::StrS, vec![take(&lowered, 0).0], Ty::Str, self) + } + "VAL" => b(Builtin::Val, vec![take(&lowered, 0).0], Ty::Dbl, self), + "HEX$" => { + let n = conv_arg!(0, NumTy::Lng); + b(Builtin::HexS, vec![n], Ty::Str, self) + } + "OCT$" => { + let n = conv_arg!(0, NumTy::Lng); + b(Builtin::OctS, vec![n], Ty::Str, self) + } + // --- Konvertierungsfunktionen → Conv-Knoten --- + "CINT" | "CLNG" | "CSNG" | "CDBL" | "CCUR" => { + let (e, t) = take(&lowered, 0); + let to = match full_name { + "CINT" => NumTy::Int, + "CLNG" => NumTy::Lng, + "CSNG" => NumTy::Sng, + "CDBL" => NumTy::Dbl, + _ => NumTy::Cur, + }; + let e = if t == Ty::Unknown { e } else { self.conv_num(e, &t, to) }; + (e, ty_of_num(to)) + } + // --- Mathematik --- + "ABS" | "FIX" | "INT" => { + let (e, t) = take(&lowered, 0); + let bt = match full_name { + "ABS" => Builtin::Abs, + "FIX" => Builtin::Fix, + _ => Builtin::IntF, + }; + // Ergebnistyp = Operandentyp (Vorbild). + let rt = if t == Ty::Unknown { Ty::Sng } else { ty_of_num(num_ty(&t)) }; + b(bt, vec![e], rt, self) + } + "SGN" => b(Builtin::Sgn, vec![take(&lowered, 0).0], Ty::Int, self), + "SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => { + let e = conv_arg!(0, NumTy::Dbl); + let bt = match full_name { + "SQR" => Builtin::Sqr, + "EXP" => Builtin::Exp, + "LOG" => Builtin::Log, + "SIN" => Builtin::Sin, + "COS" => Builtin::Cos, + "TAN" => Builtin::Tan, + _ => Builtin::Atn, + }; + b(bt, vec![e], Ty::Dbl, self) + } + "RND" => { + let a = if !lowered.is_empty() { + vec![conv_arg!(0, NumTy::Sng)] + } else { + vec![] + }; + b(Builtin::Rnd, a, Ty::Sng, self) + } + // --- Fehlerstatus --- + "ERR" => (HExpr::Err, Ty::Lng), + "ERL" => (HExpr::Erl, Ty::Lng), + // --- Sonstiges (Phase-2-Scheibe) --- + "TIMER" => b(Builtin::Timer, vec![], Ty::Sng, self), + "DATE$" => b(Builtin::DateS, vec![], Ty::Str, self), + "TIME$" => b(Builtin::TimeS, vec![], Ty::Str, self), + "COMMAND$" => b(Builtin::CommandS, vec![], Ty::Str, self), + "DOEVENTS" => b(Builtin::Doevents, vec![], Ty::Int, self), + // --- Spätere Phasen: dokumentiert, aber noch nicht verfügbar --- + _ => { + let name: &'static str = match full_name { + "INKEY$" => "INKEY$", + "INPUT$" => "INPUT$", + "CSRLIN" => "CSRLIN", + "POS" => "POS", + "SCREEN" => "SCREEN", + "TAB" => "TAB", + "SPC" => "SPC", + "NOW" => "NOW", + "DATESERIAL" => "DATESERIAL", + "TIMESERIAL" => "TIMESERIAL", + "DATEVALUE" => "DATEVALUE", + "TIMEVALUE" => "TIMEVALUE", + "DAY" => "DAY", + "MONTH" => "MONTH", + "YEAR" => "YEAR", + "WEEKDAY" => "WEEKDAY", + "HOUR" => "HOUR", + "MINUTE" => "MINUTE", + "SECOND" => "SECOND", + "FORMAT$" => "FORMAT$", + "FRE" => "FRE", + "EOF" => "EOF", + "LOF" => "LOF", + "LOC" => "LOC", + "SEEK" => "SEEK", + "FREEFILE" => "FREEFILE", + "FILEATTR" => "FILEATTR", + "ENVIRON$" => "ENVIRON$", + "MSGBOX" => "MSGBOX", + "INPUTBOX$" => "INPUTBOX$", + _ => "Funktion", + }; + (HExpr::Unsupported(name), ret_ty(retk)) } } } } -/// Labels rekursiv einsammeln (Sprungziele gelten je Rumpf). -fn collect_labels(stmts: &[Stmt], scope: &mut Scope) { - for s in stmts { - match s { - Stmt::Label(n) => { - scope.labels.insert(n.clone()); - } - Stmt::LineNumber(n) => { - scope.line_labels.insert(*n); - } - Stmt::If { then_body, elseifs, else_body, .. } => { - collect_labels(then_body, scope); - for (_, b) in elseifs { - collect_labels(b, scope); - } - if let Some(b) = else_body { - collect_labels(b, scope); - } - } - Stmt::Select { arms, .. } => { - for a in arms { - collect_labels(&a.body, scope); - } - } - Stmt::For { body, .. } - | Stmt::DoLoop { body, .. } - | Stmt::While { body, .. } => collect_labels(body, scope), - _ => {} - } +/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen). +fn stmt_pos(stmt: &Stmt) -> SourcePos { + match stmt { + Stmt::Assign { pos, .. } + | Stmt::Print { pos, .. } + | Stmt::Input { pos, .. } + | Stmt::If { pos, .. } + | Stmt::Select { pos, .. } + | Stmt::For { pos, .. } + | Stmt::DoLoop { pos, .. } + | Stmt::While { pos, .. } + | Stmt::Goto { pos, .. } + | Stmt::Gosub { pos, .. } + | Stmt::OnGoto { pos, .. } + | Stmt::Return { pos, .. } + | Stmt::Exit { pos, .. } + | Stmt::Dim { pos, .. } + | Stmt::SharedDecl { pos, .. } + | Stmt::StaticDecl { pos, .. } + | Stmt::CommonDecl { pos, .. } + | Stmt::Erase { pos, .. } + | Stmt::ConstDecl { pos, .. } + | Stmt::DefType { pos, .. } + | Stmt::OptionStmt { pos, .. } + | Stmt::TypeDecl { pos, .. } + | Stmt::Declare { pos, .. } + | Stmt::Call { pos, .. } + | Stmt::OnError { pos, .. } + | Stmt::Resume { pos, .. } + | Stmt::ErrorStmt { pos, .. } + | Stmt::Data { pos, .. } + | Stmt::ReadStmt { pos, .. } + | Stmt::Restore { pos, .. } + | Stmt::DefFn { pos, .. } + | Stmt::DefFnBlock { pos, .. } + | Stmt::Open { pos, .. } + | Stmt::OpenLegacy { pos, .. } + | Stmt::CloseStmt { pos, .. } + | Stmt::FieldStmt { pos, .. } + | Stmt::GetPut { pos, .. } + | Stmt::LsetRset { pos, .. } + | Stmt::WriteStmt { pos, .. } + | Stmt::SeekStmt { pos, .. } + | Stmt::LockStmt { pos, .. } + | Stmt::NameStmt { pos, .. } + | Stmt::ViewPrint { pos, .. } + | Stmt::EventControl { pos, .. } + | Stmt::Include { pos, .. } + | Stmt::MetaArrays { pos, .. } + | Stmt::End(pos) + | Stmt::StopStmt(pos) + | Stmt::System(pos) => *pos, + Stmt::Label(_) | Stmt::LineNumber(_) => SourcePos::default(), } } +use crate::hir::literal_value; + #[cfg(test)] mod tests { - use super::*; use crate::analyze_source; fn diags(src: &str) -> Vec { @@ -1429,4 +3202,95 @@ mod tests { assert!(diags(src).is_empty(), "{:?}", diags(src)); assert!(diags("PEN ON").contains(&"Feature unavailable".to_string())); } + + // ---- HIR-Lowering ------------------------------------------------------ + + use crate::hir::{HExpr, HStmtKind, NumTy}; + + fn hir_of(src: &str) -> crate::hir::HirModule { + let a = analyze_source("TEST", src); + assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics); + a.hir.expect("HIR erwartet") + } + + #[test] + fn hir_konvertierung_materialisiert() { + // d# = i% + 1.5# → Conv(INTEGER→DOUBLE) unter der DOUBLE-Addition + let h = hir_of("i% = 2\nd# = i% + 1.5#"); + let main = &h.procs[0]; + let HStmtKind::Assign { place, value } = &main.body[1].kind else { + panic!("Assign erwartet: {:?}", main.body[1].kind); + }; + assert_eq!(place.ty, crate::hir::HTy::Num(NumTy::Dbl)); + let HExpr::Bin { ty, l, .. } = value else { + panic!("Bin erwartet: {value:?}"); + }; + assert_eq!(*ty, NumTy::Dbl); + assert!( + matches!(**l, HExpr::Conv { from: NumTy::Int, to: NumTy::Dbl, .. }), + "Conv INTEGER→DOUBLE erwartet: {l:?}" + ); + } + + #[test] + fn hir_slots_aufgeloest() { + let h = hir_of("a% = 1\nb% = a%"); + assert_eq!(h.globals.len(), 2); + let HStmtKind::Assign { value, .. } = &h.procs[0].body[1].kind else { + panic!(); + }; + assert!(matches!(value, HExpr::Load(_))); + } + + #[test] + fn hir_intdiv_rundet_operanden() { + // 7.5 \ 2 → beide Operanden Conv → LONG (SINGLE-Operand) + let h = hir_of("x& = 7.5 \\ 2"); + let HStmtKind::Assign { value, .. } = &h.procs[0].body[0].kind else { + panic!(); + }; + let HExpr::Bin { op, ty, l, .. } = value else { + panic!("Bin erwartet: {value:?}"); + }; + assert_eq!(*op, crate::hir::HArith::IDiv); + assert_eq!(*ty, NumTy::Lng); + assert!(matches!(**l, HExpr::Conv { to: NumTy::Lng, .. })); + } + + #[test] + fn hir_select_wird_abgesenkt() { + let h = hir_of("SELECT CASE 2\nCASE 1\nPRINT \"a\"\nCASE ELSE\nPRINT \"b\"\nEND SELECT"); + // Temp-Zuweisung + If-Kette + let main = &h.procs[0]; + assert!(matches!(main.body[0].kind, HStmtKind::Assign { .. })); + assert!(matches!(main.body[1].kind, HStmtKind::If { .. })); + } + + #[test] + fn hir_byref_und_byval() { + let h = hir_of("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)"); + let main = &h.procs[0]; + let calls: Vec<_> = main + .body + .iter() + .filter_map(|s| match &s.kind { + HStmtKind::CallSub { args, .. } => Some(args), + _ => None, + }) + .collect(); + assert_eq!(calls.len(), 2); + assert!(matches!(calls[0][0], crate::hir::HArg::ByRef(_))); + assert!(matches!(calls[1][0], crate::hir::HArg::ByVal(_))); + } + + #[test] + fn byref_verlangt_exakten_typ() { + let d = diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo n%"); + assert!( + d.contains(&"Parameter type mismatch".to_string()), + "BYREF mit abweichendem Typ muss abgelehnt werden: {d:?}" + ); + // BYVAL (Klammern) konvertiert stattdessen + assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty()); + } } diff --git a/crates/tb-frontend/tests/corpus.rs b/crates/tb-frontend/tests/corpus.rs index 2520282..fe81884 100644 --- a/crates/tb-frontend/tests/corpus.rs +++ b/crates/tb-frontend/tests/corpus.rs @@ -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}"); diff --git a/crates/tb-runtime/src/builtins.rs b/crates/tb-runtime/src/builtins.rs new file mode 100644 index 0000000..5e91cba --- /dev/null +++ b/crates/tb-runtime/src/builtins.rs @@ -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, 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, RuntimeError> { + match args.get(i) { + Some(Value::Str(s)) => Ok(s.clone()), + _ => Err(RuntimeError::TYPE_MISMATCH), + } +} + +fn arg_i32(args: &[Value], i: usize) -> Result { + 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 { + match args.get(i) { + Some(v) => Ok(as_f64(v)), + None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL), + } +} + +fn s_ok(s: String) -> Result, RuntimeError> { + Ok(Some(Value::Str(Rc::from(s.as_str())))) +} + +// ---- Strings ---------------------------------------------------------------- + +fn bi_len(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, 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, 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, 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, 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, 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 = 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, RuntimeError> { + let s = arg_str(a, 0)?; + s_ok(s.to_uppercase()) +} + +fn bi_lcase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { + let s = arg_str(a, 0)?; + s_ok(s.to_lowercase()) +} + +fn bi_ltrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = target.chars().collect(); + let rchars: Vec = 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { + Ok(Some(Value::Dbl(arg_f64(a, 0)?.sin()))) +} +fn bi_cos(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { + Ok(Some(Value::Dbl(arg_f64(a, 0)?.cos()))) +} +fn bi_tan(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { + Ok(Some(Value::Dbl(arg_f64(a, 0)?.tan()))) +} +fn bi_atn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, RuntimeError> { + Ok(Some(Value::Dbl(arg_f64(a, 0)?.atan()))) +} + +fn bi_rnd(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result, 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, 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, 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, RuntimeError> { + st.print.print_comma(host); + Ok(None) +} + +fn bi_print_tab(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result, 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, 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, RuntimeError> { + st.print.print_newline(host); + Ok(None) +} + +// ---- Sonstiges ------------------------------------------------------------------ + +fn bi_timer(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result, 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, 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, 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, RuntimeError> { + s_ok(st.command.clone()) +} + +fn bi_doevents(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result, RuntimeError> { + // Ereigniszustellung kommt mit Phase 4. + Ok(Some(Value::Int(0))) +} + +fn bi_sleep(_: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result, 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, RuntimeError> { + host.write("\u{0007}"); + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host::CaptureHost; + + fn call(id: u16, args: Vec) -> Result, 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); + } +} diff --git a/crates/tb-runtime/src/console.rs b/crates/tb-runtime/src/console.rs new file mode 100644 index 0000000..d55e856 --- /dev/null +++ b/crates/tb-runtime/src/console.rs @@ -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"); + } +} diff --git a/crates/tb-runtime/src/format.rs b/crates/tb-runtime/src/format.rs new file mode 100644 index 0000000..0c6417d --- /dev/null +++ b/crates/tb-runtime/src/format.rs @@ -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 = 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 + } +} diff --git a/crates/tb-runtime/src/host.rs b/crates/tb-runtime/src/host.rs new file mode 100644 index 0000000..1eab45f --- /dev/null +++ b/crates/tb-runtime/src/host.rs @@ -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; + /// 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 { + 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, +} + +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 { + 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"); + } +} diff --git a/crates/tb-runtime/src/lib.rs b/crates/tb-runtime/src/lib.rs index 0607ad2..6a8ed9f 100644 --- a/crates/tb-runtime/src/lib.rs +++ b/crates/tb-runtime/src/lib.rs @@ -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 diff --git a/crates/tb-runtime/src/value.rs b/crates/tb-runtime/src/value.rs new file mode 100644 index 0000000..b1db71b --- /dev/null +++ b/crates/tb-runtime/src/value.rs @@ -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), + Arr(Rc>), + Rec(Rc>), + /// 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>, u32), + /// Record-Feldpfad. + Field(Rc>, Vec), +} + +/// 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, +} + +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, +} + +impl ArrayObj { + pub fn new(elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout]) -> Result { + 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 { + 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, +} + +// ---- 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 { + 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 { + 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 { + 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 { + 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 { + let v = cur_to_i64(c); + i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW) +} + +pub fn cur_to_i32(c: i64) -> Result { + 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::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); + } +} diff --git a/crates/tb-vm/Cargo.toml b/crates/tb-vm/Cargo.toml index fd2bbe0..b0e89d0 100644 --- a/crates/tb-vm/Cargo.toml +++ b/crates/tb-vm/Cargo.toml @@ -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 diff --git a/crates/tb-vm/benches/compile.rs b/crates/tb-vm/benches/compile.rs new file mode 100644 index 0000000..4d36a84 --- /dev/null +++ b/crates/tb-vm/benches/compile.rs @@ -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::(); + } + 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"); +} diff --git a/crates/tb-vm/benches/vm.rs b/crates/tb-vm/benches/vm.rs new file mode 100644 index 0000000..62d9b8e --- /dev/null +++ b/crates/tb-vm/benches/vm.rs @@ -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", + ); +} diff --git a/crates/tb-vm/src/bytecode.rs b/crates/tb-vm/src/bytecode.rs index d8358b0..cd3d21e 100644 --- a/crates/tb-vm/src/bytecode.rs +++ b/crates/tb-vm/src/bytecode.rs @@ -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`, 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 { + 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 { + Ok(self.take(1)?[0]) + } + fn u16(&mut self) -> Result { + Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap())) + } + fn u32(&mut self) -> Result { + Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap())) + } + fn string(&mut self) -> Result { + 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); + fn dec(r: &mut Reader) -> Result; +} + +macro_rules! enc_prim { + ($t:ty, $n:literal) => { + impl Enc for $t { + fn enc(&self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()); + } + fn dec(r: &mut Reader) -> Result { + 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) { + out.push(*self); + } + fn dec(r: &mut Reader) -> Result { + r.u8() + } +} + +impl Enc for bool { + fn enc(&self, out: &mut Vec) { + out.push(*self as u8); + } + fn dec(r: &mut Reader) -> Result { + Ok(r.u8()? != 0) + } +} + +impl Enc for CmpOp { + fn enc(&self, out: &mut Vec) { + out.push(*self as u8); + } + fn dec(r: &mut Reader) -> Result { + CmpOp::from_u8(r.u8()?) + } +} + +impl Enc for TypeInit { + fn enc(&self, out: &mut Vec) { + 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 { + 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) { + match self { + $( Instr::$name $(( $($fname),+ ))? => { + out.push($op); + $( $( Enc::enc($fname, out); )+ )? + } )+ + } + } + + pub fn decode(r: &mut Reader) -> Result { + 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, + /// Slot-Namen (Debugger-Inspektion). + pub local_names: Vec, + pub code: Vec, +} + +#[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>, + pub globals_init: Vec, + pub global_names: Vec, + pub udts: Vec, + /// Prozeduren; Index 0 ist das Hauptprogramm (modul-qualifiziert über + /// `name` des Moduls + Prozedurname). + pub procs: Vec, + pub data: Vec, + /// Sprungtabellen für `ON n GOTO/GOSUB`. + pub jump_tables: Vec>, +} + +fn w_string(out: &mut Vec, 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 { + let mut sections: Vec<([u8; 4], Vec)> = 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 { + 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 { + 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}"); + } +} diff --git a/crates/tb-vm/src/codegen.rs b/crates/tb-vm/src/codegen.rs index 555083e..e257bba 100644 --- a/crates/tb-vm/src/codegen.rs +++ b/crates/tb-vm/src/codegen.rs @@ -1,3 +1,1067 @@ -//! Codegenerator: übersetzt den typgeprüften AST aus `tb-frontend` in TBVM-Bytecode. +//! Codegenerator: typisiertes HIR → Bytecode. +//! +//! Dummer Tree-Walk (Design D1): Typen und Konvertierungen sind im HIR +//! bereits explizit, hier passiert nur noch Instruktionsauswahl und +//! Label-Fixup (Vorwärtsziele über Fixup-Listen, kein zweiter Pass). -// Platzhalter — wird in Phase 2 ausgearbeitet (siehe PLAN.md) +use crate::bytecode::{CmpOp, CompiledModule, DataItem, Instr, ProcCode}; +use std::collections::HashMap; +use std::rc::Rc; +use tb_frontend::hir::{ + self, Builtin, CmpKind, HArg, HArith, HCmp, HExpr, HLogic, HPlace, HPrintItem, HProcKind, + HResume, HStmt, HStmtKind, HTy, HirModule, IntKind, NumTy, VarSlot, +}; +use tb_runtime::builtins::ids; +use tb_runtime::value::TypeInit; + +pub fn compile(hir: &HirModule) -> CompiledModule { + let mut cg = Codegen { + strings: Vec::new(), + string_ids: HashMap::new(), + jump_tables: Vec::new(), + }; + let mut procs = Vec::new(); + for proc in &hir.procs { + procs.push(cg.compile_proc(proc)); + } + CompiledModule { + name: hir.name.clone(), + option_base: hir.option_base, + strings: cg.strings, + globals_init: hir.globals.iter().map(|g| slot_init(g)).collect(), + global_names: hir.globals.iter().map(|g| g.name.clone()).collect(), + udts: hir + .udts + .iter() + .map(|u| tb_runtime::value::UdtLayout { + name: u.name.clone(), + fields: u.fields.iter().map(|(_, t)| type_init(t)).collect(), + }) + .collect(), + procs, + data: hir + .data + .iter() + .map(|d| DataItem { text: d.text.clone(), line: d.line }) + .collect(), + jump_tables: cg.jump_tables, + } +} + +/// Slot-Vorbelegung: Arrays und UDT-Handles starten leer (Auto-Init). +fn slot_init(v: &hir::HVar) -> TypeInit { + if v.array { + TypeInit::Empty + } else { + type_init(&v.ty) + } +} + +fn type_init(t: &HTy) -> TypeInit { + match t { + HTy::Num(NumTy::Int) => TypeInit::Int, + HTy::Num(NumTy::Lng) => TypeInit::Lng, + HTy::Num(NumTy::Sng) => TypeInit::Sng, + HTy::Num(NumTy::Dbl) => TypeInit::Dbl, + HTy::Num(NumTy::Cur) => TypeInit::Cur, + HTy::Str => TypeInit::Str, + HTy::FixedStr(n) => TypeInit::FixedStr(*n), + HTy::Udt(id) => TypeInit::Udt(*id), + } +} + +struct Codegen { + strings: Vec>, + string_ids: HashMap, + jump_tables: Vec>, +} + +struct ProcCtx { + code: Vec, + /// LabelId → Instruktionsindex. + label_pc: Vec>, + /// (Instruktionsindex, LabelId) — nach dem Emit gepatcht. + fixups: Vec<(usize, u16)>, + /// (Tabellenindex, Labels) — Sprungtabellen nach dem Emit auflösen. + table_fixups: Vec<(usize, Vec)>, +} + +impl ProcCtx { + fn here(&self) -> u32 { + self.code.len() as u32 + } + fn bind(&mut self, label: u16) { + let pc = self.here(); + if (label as usize) >= self.label_pc.len() { + self.label_pc.resize(label as usize + 1, None); + } + self.label_pc[label as usize] = Some(pc); + } + fn emit(&mut self, i: Instr) { + self.code.push(i); + } + /// Sprunginstruktion mit noch unbekanntem Ziel emittieren. + fn emit_jump(&mut self, i: Instr, label: u16) { + self.fixups.push((self.code.len(), label)); + self.code.push(i); + } + /// Frisches internes Label (zusätzlich zu den HIR-Labels). + fn new_label(&mut self) -> u16 { + let id = self.label_pc.len() as u16; + self.label_pc.push(None); + id + } +} + +impl Codegen { + fn pool(&mut self, s: &str) -> u16 { + if let Some(id) = self.string_ids.get(s) { + return *id; + } + let id = self.strings.len() as u16; + self.strings.push(Rc::from(s)); + self.string_ids.insert(s.to_string(), id); + id + } + + fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode { + let mut ctx = ProcCtx { + code: Vec::new(), + label_pc: vec![None; proc.label_count as usize], + fixups: Vec::new(), + table_fixups: Vec::new(), + }; + for stmt in &proc.body { + self.stmt(&mut ctx, proc, stmt); + } + // Rumpfende + self.emit_proc_exit(&mut ctx, proc); + + // Fixups patchen + for (idx, label) in std::mem::take(&mut ctx.fixups) { + let pc = ctx.label_pc[label as usize].expect("Label ohne Position"); + match &mut ctx.code[idx] { + Instr::Jump(t) + | Instr::JumpIfFalse(t) + | Instr::JumpIfTrue(t) + | Instr::Gosub(t) + | Instr::RetGosubTo(t) + | Instr::ResumeLabel(t) + | Instr::OnErrorGoto(t) + | Instr::OnErrorLocal(t) => *t = pc, + other => unreachable!("Fixup auf {other:?}"), + } + } + for (table, labels) in std::mem::take(&mut ctx.table_fixups) { + let pcs: Vec = labels + .iter() + .map(|l| ctx.label_pc[*l as usize].expect("Label ohne Position")) + .collect(); + self.jump_tables[table] = pcs; + } + + ProcCode { + name: proc.name.clone(), + n_params: proc.params.len() as u16, + locals_init: proc.locals.iter().map(slot_init).collect(), + local_names: proc.locals.iter().map(|l| l.name.clone()).collect(), + code: ctx.code, + } + } + + fn emit_proc_exit(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc) { + match proc.kind { + HProcKind::Main => ctx.emit(Instr::End), + HProcKind::Sub => ctx.emit(Instr::RetProc), + HProcKind::Function | HProcKind::DefFn => { + if let Some(VarSlot::Local(slot)) = proc.ret_slot { + ctx.emit(Instr::LoadLocal(slot)); + } else { + ctx.emit(Instr::PushInt(0)); + } + ctx.emit(Instr::RetFn); + } + } + } + + // ---- Anweisungen ------------------------------------------------------- + + fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) { + match &stmt.kind { + HStmtKind::Label(l) => { + ctx.bind(*l); + return; + } + _ => ctx.emit(Instr::Stmt(stmt.line)), + } + match &stmt.kind { + HStmtKind::Label(_) => unreachable!(), + HStmtKind::SetErl(n) => ctx.emit(Instr::SetErl(*n)), + HStmtKind::Assign { place, value } => { + self.store_place(ctx, place, |cg, ctx| cg.expr(ctx, value)); + } + HStmtKind::Print { items, trailing } => { + for item in items { + match item { + HPrintItem::Val(e) => { + self.expr(ctx, e); + ctx.emit(Instr::CallBuiltin(ids::PRINT_VAL, 1)); + } + HPrintItem::Tab(e) => { + self.expr(ctx, e); + ctx.emit(Instr::CallBuiltin(ids::PRINT_TAB, 1)); + } + HPrintItem::Spc(e) => { + self.expr(ctx, e); + ctx.emit(Instr::CallBuiltin(ids::PRINT_SPC, 1)); + } + HPrintItem::Comma => { + ctx.emit(Instr::CallBuiltin(ids::PRINT_COMMA, 0)); + } + } + } + if !trailing { + ctx.emit(Instr::CallBuiltin(ids::PRINT_NEWLINE, 0)); + } + } + HStmtKind::Input { line_mode, prompt, question, targets } => { + for t in targets { + self.make_ref(ctx, t); + } + let prompt_idx = match prompt { + Some(p) => self.pool(p), + None => 0xFFFF, + }; + ctx.emit(Instr::Input( + targets.len() as u8, + *line_mode, + prompt_idx, + *question, + )); + } + HStmtKind::If { cond, then, els } => { + self.expr(ctx, cond); + let l_else = ctx.new_label(); + ctx.emit_jump(Instr::JumpIfFalse(0), l_else); + for s in then { + self.stmt(ctx, proc, s); + } + if els.is_empty() { + ctx.bind(l_else); + } else { + let l_end = ctx.new_label(); + ctx.emit_jump(Instr::Jump(0), l_end); + ctx.bind(l_else); + for s in els { + self.stmt(ctx, proc, s); + } + ctx.bind(l_end); + } + } + HStmtKind::Loop { pre, post, body, exit_label } => { + let l_start = ctx.new_label(); + ctx.bind(l_start); + if let Some((is_until, cond)) = pre { + self.expr(ctx, cond); + if *is_until { + ctx.emit_jump(Instr::JumpIfTrue(0), *exit_label); + } else { + ctx.emit_jump(Instr::JumpIfFalse(0), *exit_label); + } + } + for s in body { + self.stmt(ctx, proc, s); + } + match post { + Some((is_until, cond)) => { + self.expr(ctx, cond); + if *is_until { + // LOOP UNTIL: weiter, solange falsch + ctx.emit_jump(Instr::JumpIfFalse(0), l_start); + } else { + ctx.emit_jump(Instr::JumpIfTrue(0), l_start); + } + } + None => ctx.emit_jump(Instr::Jump(0), l_start), + } + ctx.bind(*exit_label); + } + HStmtKind::For { + var, + ty, + from, + to, + step, + limit_slot, + step_slot, + body, + exit_label, + } => { + self.gen_for( + ctx, proc, var, *ty, from, to, step.as_ref(), *limit_slot, *step_slot, body, + *exit_label, + ); + } + HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l), + HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(0), *l), + HStmtKind::OnGoto { sel, gosub, targets } => { + self.expr(ctx, sel); + let table = self.jump_tables.len(); + self.jump_tables.push(Vec::new()); + ctx.table_fixups.push((table, targets.clone())); + ctx.emit(Instr::OnJump(table as u16, *gosub)); + } + HStmtKind::ReturnGosub(target) => match target { + None => ctx.emit(Instr::RetGosub), + Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l), + }, + HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc), + HStmtKind::CallSub { proc: id, args } => { + for a in args { + self.arg(ctx, a); + } + ctx.emit(Instr::Call(*id, args.len() as u8)); + } + HStmtKind::BuiltinStmt { b, args } => { + for a in args { + self.expr(ctx, a); + } + let id = builtin_id(*b); + ctx.emit(Instr::CallBuiltin(id, args.len() as u8)); + if builtin_returns_value(*b) { + ctx.emit(Instr::Pop); + } + } + HStmtKind::OnError { local, target } => match (local, target) { + (false, Some(l)) => ctx.emit_jump(Instr::OnErrorGoto(0), *l), + (true, Some(l)) => ctx.emit_jump(Instr::OnErrorLocal(0), *l), + (false, None) => ctx.emit(Instr::OnErrorDisable), + (true, None) => ctx.emit(Instr::OnErrorLocalDisable), + }, + HStmtKind::OnErrorResumeNext { local } => { + ctx.emit(Instr::OnErrorResumeNext(*local)); + } + HStmtKind::Resume(kind) => match kind { + HResume::Retry => ctx.emit(Instr::Resume0), + HResume::Next => ctx.emit(Instr::ResumeNext), + HResume::Label(l) => ctx.emit_jump(Instr::ResumeLabel(0), *l), + }, + HStmtKind::RaiseError(code) => { + self.expr(ctx, code); + ctx.emit(Instr::RaiseError); + } + HStmtKind::Read(places) => { + for p in places { + let numeric = matches!(p.ty, HTy::Num(_)); + if numeric { + self.store_place(ctx, p, |_cg, ctx| { + ctx.emit(Instr::ReadData(1)); + emit_conv(ctx, NumTy::Dbl, p.ty.num().unwrap_or(NumTy::Dbl)); + }); + } else { + self.store_place(ctx, p, |_cg, ctx| { + ctx.emit(Instr::ReadData(0)); + if let HTy::FixedStr(n) = &p.ty { + ctx.emit(Instr::FixStr(*n)); + } + }); + } + } + } + HStmtKind::Restore(idx) => ctx.emit(Instr::Restore(*idx)), + HStmtKind::Dim { slot, elem, dims, redim } => { + for (lo, hi) in dims { + self.expr(ctx, lo); + self.expr(ctx, hi); + } + let (global, s) = slot_parts(*slot); + let init = type_init(elem); + if *redim { + ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init)); + } else { + ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init)); + } + } + HStmtKind::Erase(slots) => { + for slot in slots { + let (global, s) = slot_parts(*slot); + ctx.emit(Instr::EraseSlot(global, s)); + } + } + HStmtKind::End => ctx.emit(Instr::End), + HStmtKind::Stop => ctx.emit(Instr::StopInstr), + HStmtKind::System => ctx.emit(Instr::SystemInstr), + HStmtKind::Unsupported(name) => { + let idx = self.pool(name); + ctx.emit(Instr::Unsupported(idx)); + } + } + } + + #[allow(clippy::too_many_arguments)] + fn gen_for( + &mut self, + ctx: &mut ProcCtx, + proc: &hir::HProc, + var: &HPlace, + ty: NumTy, + from: &HExpr, + to: &HExpr, + step: Option<&HExpr>, + limit_slot: VarSlot, + step_slot: Option, + body: &[HStmt], + exit_label: u16, + ) { + // Startwert, Grenze, ggf. Schritt einmal auswerten. + self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from)); + self.expr(ctx, to); + emit_store_slot(ctx, limit_slot); + if let (Some(step_e), Some(sslot)) = (step, step_slot) { + self.expr(ctx, step_e); + emit_store_slot(ctx, sslot); + } + let const_step = match step { + None => Some(1.0), + Some(e) => hir::literal_value(e), + }; + + let l_test = ctx.new_label(); + let l_body = ctx.new_label(); + ctx.bind(l_test); + match const_step { + Some(s) => { + // Vergleichsrichtung zur Compilezeit. + let op = if s >= 0.0 { CmpOp::Le } else { CmpOp::Ge }; + self.load_place(ctx, var); + emit_load_slot(ctx, limit_slot); + ctx.emit(cmp_instr(ty, op)); + ctx.emit_jump(Instr::JumpIfFalse(0), exit_label); + } + None => { + // Vorzeichen des Schritts zur Laufzeit prüfen. + let sslot = step_slot.expect("dynamischer STEP ohne Slot"); + let l_neg = ctx.new_label(); + emit_load_slot(ctx, sslot); + push_zero(ctx, ty); + ctx.emit(cmp_instr(ty, CmpOp::Ge)); + ctx.emit_jump(Instr::JumpIfFalse(0), l_neg); + self.load_place(ctx, var); + emit_load_slot(ctx, limit_slot); + ctx.emit(cmp_instr(ty, CmpOp::Le)); + ctx.emit_jump(Instr::JumpIfFalse(0), exit_label); + ctx.emit_jump(Instr::Jump(0), l_body); + ctx.bind(l_neg); + self.load_place(ctx, var); + emit_load_slot(ctx, limit_slot); + ctx.emit(cmp_instr(ty, CmpOp::Ge)); + ctx.emit_jump(Instr::JumpIfFalse(0), exit_label); + } + } + ctx.bind(l_body); + for s in body { + self.stmt(ctx, proc, s); + } + // NEXT: inkrementieren, zurück zum Test. + self.store_place(ctx, var, |cg, ctx| { + cg.load_place(ctx, var); + match (step, step_slot) { + (Some(e), None) => cg.expr(ctx, e), // konstanter STEP + (Some(_), Some(sslot)) => emit_load_slot(ctx, sslot), + (None, _) => push_one(ctx, ty), + } + ctx.emit(add_instr(ty)); + }); + ctx.emit_jump(Instr::Jump(0), l_test); + ctx.bind(exit_label); + } + + // ---- Ausdrücke --------------------------------------------------------- + + fn expr(&mut self, ctx: &mut ProcCtx, e: &HExpr) { + match e { + HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)), + HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)), + HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)), + HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)), + HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)), + HExpr::Str(s) => { + let idx = self.pool(s); + ctx.emit(Instr::PushStr(idx)); + } + HExpr::Load(p) => self.load_place(ctx, p), + HExpr::Conv { from, to, arg } => { + self.expr(ctx, arg); + emit_conv(ctx, *from, *to); + } + HExpr::FixStr { len, arg } => { + self.expr(ctx, arg); + ctx.emit(Instr::FixStr(*len)); + } + HExpr::Neg { ty, arg } => { + self.expr(ctx, arg); + ctx.emit(match ty { + NumTy::Int => Instr::NegI2, + NumTy::Lng => Instr::NegI4, + NumTy::Sng => Instr::NegR4, + NumTy::Dbl => Instr::NegR8, + NumTy::Cur => Instr::NegCy, + }); + } + HExpr::Bin { op, ty, l, r } => { + self.expr(ctx, l); + self.expr(ctx, r); + ctx.emit(arith_instr(*op, *ty)); + } + HExpr::Not { ty, arg } => { + self.expr(ctx, arg); + ctx.emit(match ty { + IntKind::I2 => Instr::NotI2, + IntKind::I4 => Instr::NotI4, + }); + } + HExpr::Logic { op, ty, l, r } => { + self.expr(ctx, l); + self.expr(ctx, r); + ctx.emit(logic_instr(*op, *ty)); + } + HExpr::Cmp { op, ty, l, r } => { + self.expr(ctx, l); + self.expr(ctx, r); + let cop = cmp_op(*op); + ctx.emit(match ty { + CmpKind::Num(NumTy::Int) => Instr::CmpI2(cop), + CmpKind::Num(NumTy::Lng) => Instr::CmpI4(cop), + CmpKind::Num(NumTy::Sng) => Instr::CmpR4(cop), + CmpKind::Num(NumTy::Dbl) => Instr::CmpR8(cop), + CmpKind::Num(NumTy::Cur) => Instr::CmpCy(cop), + CmpKind::Str => Instr::CmpStr(cop), + }); + } + HExpr::Concat(l, r) => { + self.expr(ctx, l); + self.expr(ctx, r); + ctx.emit(Instr::Concat); + } + HExpr::FnCall { proc, args, .. } => { + for a in args { + self.arg(ctx, a); + } + ctx.emit(Instr::Call(*proc, args.len() as u8)); + } + HExpr::Builtin { b, args, .. } => { + for a in args { + self.expr(ctx, a); + } + ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8)); + } + HExpr::ArrayBound { lower, place, dim } => { + self.load_array_handle(ctx, place); + self.expr(ctx, dim); + ctx.emit(Instr::ArrBound(*lower)); + } + HExpr::Err => ctx.emit(Instr::LoadErr), + HExpr::Erl => ctx.emit(Instr::LoadErl), + HExpr::Unsupported(name) => { + let idx = self.pool(name); + ctx.emit(Instr::Unsupported(idx)); + } + } + } + + fn arg(&mut self, ctx: &mut ProcCtx, a: &HArg) { + match a { + HArg::ByVal(e) => self.expr(ctx, e), + HArg::ByRef(p) => self.make_ref(ctx, p), + HArg::ArrayRef(p) => self.load_array_handle(ctx, p), + } + } + + // ---- Plätze (L-Werte) ---------------------------------------------------- + + /// Array-Handle eines Platzes laden (mit Auto-DIM-Information). + fn load_array_handle(&mut self, ctx: &mut ProcCtx, p: &HPlace) { + let (global, slot) = slot_parts(p.base); + let (elem, dims) = match &p.array_elem { + Some((t, d)) => (type_init(t), *d), + None => (type_init(&p.ty), 1), + }; + ctx.emit(Instr::LoadArr(global, slot, dims, elem)); + } + + fn load_place(&mut self, ctx: &mut ProcCtx, p: &HPlace) { + let (global, slot) = slot_parts(p.base); + if !p.indices.is_empty() { + self.load_array_handle(ctx, p); + for i in &p.indices { + self.expr(ctx, i); + } + ctx.emit(Instr::LoadElem(p.indices.len() as u8)); + } else if p.base_is_ref { + ctx.emit(Instr::LoadRef(slot)); + } else if global { + ctx.emit(Instr::LoadGlobal(slot)); + } else { + ctx.emit(Instr::LoadLocal(slot)); + } + for f in &p.fields { + ctx.emit(Instr::LoadField(*f)); + } + } + + /// Platz speichern; `value` emittiert den Wert auf den Stack. + fn store_place( + &mut self, + ctx: &mut ProcCtx, + p: &HPlace, + value: impl FnOnce(&mut Self, &mut ProcCtx), + ) { + let (global, slot) = slot_parts(p.base); + if !p.fields.is_empty() { + // Basis-Handle (ggf. Element) laden, Feldpfad bis vorletzte Ebene. + if !p.indices.is_empty() { + self.load_array_handle(ctx, p); + for i in &p.indices { + self.expr(ctx, i); + } + ctx.emit(Instr::LoadElem(p.indices.len() as u8)); + } else if p.base_is_ref { + ctx.emit(Instr::LoadRef(slot)); + } else if global { + ctx.emit(Instr::LoadGlobal(slot)); + } else { + ctx.emit(Instr::LoadLocal(slot)); + } + for f in &p.fields[..p.fields.len() - 1] { + ctx.emit(Instr::LoadField(*f)); + } + value(self, ctx); + ctx.emit(Instr::StoreField(*p.fields.last().unwrap())); + return; + } + if !p.indices.is_empty() { + self.load_array_handle(ctx, p); + for i in &p.indices { + self.expr(ctx, i); + } + value(self, ctx); + ctx.emit(Instr::StoreElem(p.indices.len() as u8)); + return; + } + value(self, ctx); + if matches!(p.ty, HTy::Udt(_)) { + // UDT-Zuweisung kopiert Inhalte (Wertsemantik): Ziel-Handle + // laden und Quellfelder hineinkopieren. + if p.base_is_ref { + ctx.emit(Instr::LoadRef(slot)); + } else if global { + ctx.emit(Instr::LoadGlobal(slot)); + } else { + ctx.emit(Instr::LoadLocal(slot)); + } + ctx.emit(Instr::CopyRec); + return; + } + if p.base_is_ref { + ctx.emit(Instr::StoreRef(slot)); + } else if global { + ctx.emit(Instr::StoreGlobal(slot)); + } else { + ctx.emit(Instr::StoreLocal(slot)); + } + } + + /// Referenz auf einen Platz erzeugen (BYREF-Argumente, INPUT-Ziele). + fn make_ref(&mut self, ctx: &mut ProcCtx, p: &HPlace) { + let (global, slot) = slot_parts(p.base); + if !p.indices.is_empty() && p.fields.is_empty() { + self.load_array_handle(ctx, p); + for i in &p.indices { + self.expr(ctx, i); + } + ctx.emit(Instr::MakeRefElem(p.indices.len() as u8)); + return; + } + if !p.fields.is_empty() { + if !p.indices.is_empty() { + self.load_array_handle(ctx, p); + for i in &p.indices { + self.expr(ctx, i); + } + ctx.emit(Instr::LoadElem(p.indices.len() as u8)); + } else if p.base_is_ref { + ctx.emit(Instr::LoadRef(slot)); + } else if global { + ctx.emit(Instr::LoadGlobal(slot)); + } else { + ctx.emit(Instr::LoadLocal(slot)); + } + for f in &p.fields { + ctx.emit(Instr::MakeRefField(*f)); + } + return; + } + if p.base_is_ref { + // Referenz weiterreichen: der Slot enthält bereits eine Referenz. + ctx.emit(Instr::LoadLocal(slot)); + } else if global { + ctx.emit(Instr::MakeRefGlobal(slot)); + } else { + ctx.emit(Instr::MakeRefLocal(slot)); + } + } +} + +// ---- Instruktionsauswahl-Hilfen ---------------------------------------------- + +fn slot_parts(s: VarSlot) -> (bool, u16) { + match s { + VarSlot::Global(i) => (true, i), + VarSlot::Local(i) => (false, i), + } +} + +fn emit_store_slot(ctx: &mut ProcCtx, s: VarSlot) { + match s { + VarSlot::Global(i) => ctx.emit(Instr::StoreGlobal(i)), + VarSlot::Local(i) => ctx.emit(Instr::StoreLocal(i)), + } +} + +fn emit_load_slot(ctx: &mut ProcCtx, s: VarSlot) { + match s { + VarSlot::Global(i) => ctx.emit(Instr::LoadGlobal(i)), + VarSlot::Local(i) => ctx.emit(Instr::LoadLocal(i)), + } +} + +fn push_zero(ctx: &mut ProcCtx, ty: NumTy) { + ctx.emit(match ty { + NumTy::Int => Instr::PushInt(0), + NumTy::Lng => Instr::PushLng(0), + NumTy::Sng => Instr::PushSng(0.0), + NumTy::Dbl => Instr::PushDbl(0.0), + NumTy::Cur => Instr::PushCur(0), + }); +} + +fn push_one(ctx: &mut ProcCtx, ty: NumTy) { + ctx.emit(match ty { + NumTy::Int => Instr::PushInt(1), + NumTy::Lng => Instr::PushLng(1), + NumTy::Sng => Instr::PushSng(1.0), + NumTy::Dbl => Instr::PushDbl(1.0), + NumTy::Cur => Instr::PushCur(10_000), + }); +} + +fn add_instr(ty: NumTy) -> Instr { + match ty { + NumTy::Int => Instr::AddI2, + NumTy::Lng => Instr::AddI4, + NumTy::Sng => Instr::AddR4, + NumTy::Dbl => Instr::AddR8, + NumTy::Cur => Instr::AddCy, + } +} + +fn arith_instr(op: HArith, ty: NumTy) -> Instr { + use NumTy::*; + match (op, ty) { + (HArith::Add, Int) => Instr::AddI2, + (HArith::Add, Lng) => Instr::AddI4, + (HArith::Add, Sng) => Instr::AddR4, + (HArith::Add, Dbl) => Instr::AddR8, + (HArith::Add, Cur) => Instr::AddCy, + (HArith::Sub, Int) => Instr::SubI2, + (HArith::Sub, Lng) => Instr::SubI4, + (HArith::Sub, Sng) => Instr::SubR4, + (HArith::Sub, Dbl) => Instr::SubR8, + (HArith::Sub, Cur) => Instr::SubCy, + (HArith::Mul, Int) => Instr::MulI2, + (HArith::Mul, Lng) => Instr::MulI4, + (HArith::Mul, Sng) => Instr::MulR4, + (HArith::Mul, Dbl) => Instr::MulR8, + (HArith::Mul, Cur) => Instr::MulCy, + (HArith::Div, Sng) => Instr::DivR4, + (HArith::Div, Dbl) => Instr::DivR8, + (HArith::IDiv, Int) => Instr::IDivI2, + (HArith::IDiv, Lng) => Instr::IDivI4, + (HArith::Mod, Int) => Instr::ModI2, + (HArith::Mod, Lng) => Instr::ModI4, + (HArith::Pow, Dbl) => Instr::PowR8, + (op, ty) => unreachable!("arith {op:?} auf {ty:?}"), + } +} + +fn logic_instr(op: HLogic, ty: IntKind) -> Instr { + match (op, ty) { + (HLogic::And, IntKind::I2) => Instr::AndI2, + (HLogic::And, IntKind::I4) => Instr::AndI4, + (HLogic::Or, IntKind::I2) => Instr::OrI2, + (HLogic::Or, IntKind::I4) => Instr::OrI4, + (HLogic::Xor, IntKind::I2) => Instr::XorI2, + (HLogic::Xor, IntKind::I4) => Instr::XorI4, + (HLogic::Eqv, IntKind::I2) => Instr::EqvI2, + (HLogic::Eqv, IntKind::I4) => Instr::EqvI4, + (HLogic::Imp, IntKind::I2) => Instr::ImpI2, + (HLogic::Imp, IntKind::I4) => Instr::ImpI4, + } +} + +fn cmp_op(op: HCmp) -> CmpOp { + match op { + HCmp::Eq => CmpOp::Eq, + HCmp::Ne => CmpOp::Ne, + HCmp::Lt => CmpOp::Lt, + HCmp::Le => CmpOp::Le, + HCmp::Gt => CmpOp::Gt, + HCmp::Ge => CmpOp::Ge, + } +} + +fn cmp_instr(ty: NumTy, op: CmpOp) -> Instr { + match ty { + NumTy::Int => Instr::CmpI2(op), + NumTy::Lng => Instr::CmpI4(op), + NumTy::Sng => Instr::CmpR4(op), + NumTy::Dbl => Instr::CmpR8(op), + NumTy::Cur => Instr::CmpCy(op), + } +} + +fn emit_conv(ctx: &mut ProcCtx, from: NumTy, to: NumTy) { + use NumTy::*; + if from == to { + return; + } + ctx.emit(match (from, to) { + (Int, Lng) => Instr::ConvI2I4, + (Int, Sng) => Instr::ConvI2R4, + (Int, Dbl) => Instr::ConvI2R8, + (Int, Cur) => Instr::ConvI2Cy, + (Lng, Int) => Instr::ConvI4I2, + (Lng, Sng) => Instr::ConvI4R4, + (Lng, Dbl) => Instr::ConvI4R8, + (Lng, Cur) => Instr::ConvI4Cy, + (Sng, Int) => Instr::ConvR4I2, + (Sng, Lng) => Instr::ConvR4I4, + (Sng, Dbl) => Instr::ConvR4R8, + (Sng, Cur) => Instr::ConvR4Cy, + (Dbl, Int) => Instr::ConvR8I2, + (Dbl, Lng) => Instr::ConvR8I4, + (Dbl, Sng) => Instr::ConvR8R4, + (Dbl, Cur) => Instr::ConvR8Cy, + (Cur, Int) => Instr::ConvCyI2, + (Cur, Lng) => Instr::ConvCyI4, + (Cur, Sng) => Instr::ConvCyR4, + (Cur, Dbl) => Instr::ConvCyR8, + _ => unreachable!(), + }); +} + +/// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit. +/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege. +fn builtin_id(b: Builtin) -> u16 { + match b { + Builtin::Len => ids::LEN, + Builtin::LeftS => ids::LEFT_S, + Builtin::RightS => ids::RIGHT_S, + Builtin::MidS => ids::MID_S, + Builtin::InstrF => ids::INSTR, + Builtin::UcaseS => ids::UCASE_S, + Builtin::LcaseS => ids::LCASE_S, + Builtin::LtrimS => ids::LTRIM_S, + Builtin::RtrimS => ids::RTRIM_S, + Builtin::SpaceS => ids::SPACE_S, + Builtin::StringS => ids::STRING_S, + Builtin::ChrS => ids::CHR_S, + Builtin::Asc => ids::ASC, + Builtin::StrS => ids::STR_S, + Builtin::Val => ids::VAL, + Builtin::HexS => ids::HEX_S, + Builtin::OctS => ids::OCT_S, + Builtin::MidAssign => ids::MID_ASSIGN, + Builtin::Abs => ids::ABS, + Builtin::Sgn => ids::SGN, + Builtin::IntF => ids::INT_F, + Builtin::Fix => ids::FIX, + Builtin::Sqr => ids::SQR, + Builtin::Exp => ids::EXP, + Builtin::Log => ids::LOG, + Builtin::Sin => ids::SIN, + Builtin::Cos => ids::COS, + Builtin::Tan => ids::TAN, + Builtin::Atn => ids::ATN, + Builtin::Rnd => ids::RND, + Builtin::Randomize => ids::RANDOMIZE, + Builtin::PrintVal => ids::PRINT_VAL, + Builtin::PrintStrLit => ids::PRINT_STR_LIT, + Builtin::PrintComma => ids::PRINT_COMMA, + Builtin::PrintTab => ids::PRINT_TAB, + Builtin::PrintSpc => ids::PRINT_SPC, + Builtin::PrintNewline => ids::PRINT_NEWLINE, + Builtin::Timer => ids::TIMER, + Builtin::DateS => ids::DATE_S, + Builtin::TimeS => ids::TIME_S, + Builtin::CommandS => ids::COMMAND_S, + Builtin::Doevents => ids::DOEVENTS, + Builtin::Sleep => ids::SLEEP, + Builtin::Beep => ids::BEEP, + } +} + +/// Liefert der Builtin in Anweisungsposition einen Wert (→ `Pop`)? +fn builtin_returns_value(b: Builtin) -> bool { + matches!(b, Builtin::Doevents) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn compile_src(src: &str) -> CompiledModule { + let a = tb_frontend::analyze_source("TEST", src); + assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics); + compile(&a.hir.unwrap()) + } + + fn main_code(m: &CompiledModule) -> &[Instr] { + &m.procs[0].code + } + + #[test] + fn ausdruck_monomorph_mit_conv() { + // d# = i% + 1.5# → LoadGlobal, ConvI2R8, PushDbl, AddR8, StoreGlobal + let m = compile_src("i% = 2\nd# = i% + 1.5#"); + let code = main_code(&m); + let want = [ + Instr::LoadGlobal(0), + Instr::ConvI2R8, + Instr::PushDbl(1.5), + Instr::AddR8, + Instr::StoreGlobal(1), + ]; + assert!( + code.windows(want.len()).any(|w| w == want), + "erwartete Sequenz nicht gefunden: {code:?}" + ); + } + + #[test] + fn vergleich_und_logik() { + let m = compile_src("a% = 1\nb% = a% > 0 AND a% < 5"); + let code = main_code(&m); + assert!(code.contains(&Instr::CmpI2(CmpOp::Gt))); + assert!(code.contains(&Instr::CmpI2(CmpOp::Lt))); + assert!(code.contains(&Instr::AndI2)); + } + + #[test] + fn string_konkatenation() { + let m = compile_src("s$ = \"a\" + \"b\""); + assert!(main_code(&m).contains(&Instr::Concat)); + } + + #[test] + fn if_mit_fixup() { + let m = compile_src("IF 1 THEN\nPRINT \"a\"\nELSE\nPRINT \"b\"\nEND IF"); + let code = main_code(&m); + // Es gibt einen bedingten Sprung und einen unbedingten, beide gepatcht (≠ 0). + let jf = code.iter().find_map(|i| match i { + Instr::JumpIfFalse(t) => Some(*t), + _ => None, + }); + assert!(jf.is_some() && jf.unwrap() > 0, "{code:?}"); + } + + #[test] + fn for_mit_konstantem_step() { + let m = compile_src("FOR i% = 1 TO 3\nPRINT i%\nNEXT"); + let code = main_code(&m); + assert!(code.contains(&Instr::CmpI2(CmpOp::Le)), "{code:?}"); + assert!(code.contains(&Instr::AddI2)); + // Kein Laufzeit-Vorzeichentest bei konstantem Schritt: + assert!(!code.contains(&Instr::CmpI2(CmpOp::Ge))); + } + + #[test] + fn for_mit_dynamischem_step() { + let m = compile_src("s% = -1\nFOR i% = 3 TO 1 STEP s%\nNEXT"); + let code = main_code(&m); + // Vorzeichentest → beide Vergleichsrichtungen vorhanden + assert!(code.contains(&Instr::CmpI2(CmpOp::Le))); + assert!(code.contains(&Instr::CmpI2(CmpOp::Ge))); + } + + #[test] + fn gosub_und_on_goto() { + let m = compile_src("GOSUB U\nON 2 GOTO A, B\nA:\nB:\nU:\nRETURN"); + let code = main_code(&m); + assert!(code.iter().any(|i| matches!(i, Instr::Gosub(_)))); + assert!(code.iter().any(|i| matches!(i, Instr::OnJump(0, false)))); + assert_eq!(m.jump_tables.len(), 1); + assert_eq!(m.jump_tables[0].len(), 2); + assert!(code.contains(&Instr::RetGosub)); + } + + #[test] + fn prozedur_und_byref() { + let m = compile_src("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)"); + let code = main_code(&m); + assert!(code.contains(&Instr::MakeRefGlobal(0))); + assert!(code.iter().filter(|i| matches!(i, Instr::Call(1, 1))).count() == 2); + // Prozedurrumpf liest/schreibt über Referenz + let sub = &m.procs[1].code; + assert!(sub.contains(&Instr::LoadRef(0))); + assert!(sub.contains(&Instr::StoreRef(0))); + assert!(sub.last() == Some(&Instr::RetProc)); + } + + #[test] + fn function_liefert_wert() { + let m = compile_src("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)"); + let f = &m.procs[1].code; + assert!(f.contains(&Instr::RetFn)); + let code = main_code(&m); + assert!(code.iter().any(|i| matches!(i, Instr::Call(1, 1)))); + } + + #[test] + fn arrays_dim_und_zugriff() { + let m = compile_src("DIM a%(10)\na%(3) = 7\nPRINT a%(3)"); + let code = main_code(&m); + assert!(code + .iter() + .any(|i| matches!(i, Instr::DimArr(true, 0, 1, TypeInit::Int)))); + assert!(code.iter().any(|i| matches!(i, Instr::StoreElem(1)))); + assert!(code.iter().any(|i| matches!(i, Instr::LoadElem(1)))); + } + + #[test] + fn data_read_restore() { + let m = compile_src("DATA 1, 2\nREAD a%, b%\nRESTORE\nREAD c%"); + let code = main_code(&m); + assert_eq!(m.data.len(), 2); + assert!(code.iter().filter(|i| matches!(i, Instr::ReadData(1))).count() == 3); + assert!(code.contains(&Instr::Restore(0))); + assert!(code.contains(&Instr::ConvR8I2)); + } + + #[test] + fn fehlerbehandlung_emit() { + let m = compile_src("ON ERROR GOTO H\nERROR 5\nEND\nH:\nRESUME NEXT"); + let code = main_code(&m); + assert!(code.iter().any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0))); + assert!(code.contains(&Instr::RaiseError)); + assert!(code.contains(&Instr::ResumeNext)); + } + + #[test] + fn tbc_roundtrip_ueber_codegen() { + let m = compile_src("PRINT \"Hallo\""); + let bytes = m.to_tbc(); + let back = CompiledModule::from_tbc(&bytes).unwrap(); + assert_eq!(back.procs[0].code, m.procs[0].code); + } +} diff --git a/crates/tb-vm/src/interp.rs b/crates/tb-vm/src/interp.rs index e330d5c..229d8a1 100644 --- a/crates/tb-vm/src/interp.rs +++ b/crates/tb-vm/src/interp.rs @@ -1,6 +1,1475 @@ -//! Interpreterschleife der TBVM. +//! TBVM-Interpreter: zustandsbehaftete, vom Einbetter getriebene +//! Ausführung (Design: docs/tbvm-design.md). //! -//! Läuft kooperativ: die Ausführung kann an Anweisungsgrenzen angehalten werden -//! (Debugger, Ereignisverarbeitung der Forms-Engine, Strg+Untbr). +//! - Enge `match`-Schleife über dekodierte Instruktionen; Tick-Prüfung +//! (Breakpoints, Einzelschritt, Abbruch) nur an Anweisungsgrenzen +//! (`Stmt`-Instruktion) über ein einzelnes Flag-Wort. +//! - Fehlersemantik: Handler modulweit + Frame-lokal, Unwinding bis zum +//! Handler-Frame, `RESUME` über gemerkten Anweisungs-Offset; Fehler +//! während aktiver Behandlung ist fatal (keine Kaskaden). +//! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3. -// Platzhalter — wird in Phase 2 ausgearbeitet (siehe PLAN.md) +use crate::bytecode::{CmpOp, CompiledModule, Instr}; +use std::collections::HashSet; +use std::rc::Rc; +use tb_runtime::builtins::{builtin_table, RtState}; +use tb_runtime::errors::RuntimeError; +use tb_runtime::host::Host; +use tb_runtime::value::{ + self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef, +}; + +/// Warum die VM die Kontrolle abgibt. +#[derive(Debug, Clone, PartialEq)] +pub enum RunEvent { + /// `END`, `SYSTEM` oder Programmende. + Ended, + /// `STOP` — VM-Zustand bleibt fortsetzbar (IDE: CONT). + Stopped { line: u32 }, + Breakpoint { line: u32 }, + Stepped { line: u32 }, + Interrupted { line: u32 }, + /// Unbehandelter Laufzeitfehler. + Error { code: u16, line: u32, message: String }, +} + +const F_STEP: u32 = 1; +const F_BREAK: u32 = 2; +const F_POLL: u32 = 4; + +#[derive(Clone, Copy, PartialEq)] +enum Handler { + None, + Goto(u32), + ResumeNext, +} + +struct Frame { + proc: usize, + pc: usize, + locals_base: usize, + stack_base: usize, + gosub: Vec, + local_handler: Handler, + /// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`). + last_stmt_pc: usize, + line: u32, +} + +pub struct Vm { + module: CompiledModule, + globals: Vec, + locals: Vec, + stack: Vec, + frames: Vec, + pub rt: RtState, + // Fehlerzustand + err: u16, + erl: u32, + module_handler: Handler, + in_handler: bool, + resume_pc: usize, + // Steuerung + flags: u32, + breakpoints: HashSet, + data_ptr: usize, +} + +enum Flow { + Normal, + Event(RunEvent), +} + +impl Vm { + pub fn new(module: CompiledModule) -> Vm { + let globals = module + .globals_init + .iter() + .map(|t| default_value(t, &module.udts)) + .collect(); + let mut vm = Vm { + globals, + locals: Vec::new(), + stack: Vec::new(), + frames: Vec::new(), + rt: RtState::default(), + err: 0, + erl: 0, + module_handler: Handler::None, + in_handler: false, + resume_pc: 0, + flags: 0, + breakpoints: HashSet::new(), + data_ptr: 0, + module, + }; + vm.push_frame(0, 0); + vm + } + + fn push_frame(&mut self, proc: usize, argc: usize) { + // Argumente liegen zuoberst auf dem Stack (links → rechts). + let locals_base = self.locals.len(); + let p = &self.module.procs[proc]; + for init in &p.locals_init { + self.locals.push(default_value(init, &self.module.udts)); + } + for i in (0..argc).rev() { + let v = self.stack.pop().unwrap_or(Value::Empty); + self.locals[locals_base + i] = v; + } + let stack_base = self.stack.len(); + self.frames.push(Frame { + proc, + pc: 0, + locals_base, + stack_base, + gosub: Vec::new(), + local_handler: Handler::None, + last_stmt_pc: 0, + line: 0, + }); + } + + fn pop_frame(&mut self) { + let f = self.frames.pop().expect("Frame-Unterlauf"); + self.locals.truncate(f.locals_base); + self.stack.truncate(f.stack_base); + } + + // ---- Steuerung (Debugger/Runner) ---------------------------------------- + + pub fn set_step(&mut self, on: bool) { + if on { + self.flags |= F_STEP; + } else { + self.flags &= !F_STEP; + } + } + + pub fn set_poll_interrupt(&mut self, on: bool) { + if on { + self.flags |= F_POLL; + } else { + self.flags &= !F_POLL; + } + } + + pub fn add_breakpoint(&mut self, line: u32) { + self.breakpoints.insert(line); + self.flags |= F_BREAK; + } + + pub fn remove_breakpoint(&mut self, line: u32) { + self.breakpoints.remove(&line); + if self.breakpoints.is_empty() { + self.flags &= !F_BREAK; + } + } + + pub fn current_line(&self) -> u32 { + self.frames.last().map(|f| f.line).unwrap_or(0) + } + + pub fn current_proc_name(&self) -> &str { + self.frames + .last() + .map(|f| self.module.procs[f.proc].name.as_str()) + .unwrap_or("") + } + + /// Variableninspektion (Debugger): erst Locals des obersten Frames + /// (Referenzen werden aufgelöst), dann Modulvariablen. Ein + /// Typ-Suffix (`n%`, `s$`) wird toleriert — Slots tragen Basisnamen. + pub fn inspect(&self, name: &str) -> Option { + let name = name.trim_end_matches(['%', '&', '!', '#', '$', '@']); + if let Some(f) = self.frames.last() { + let p = &self.module.procs[f.proc]; + for (i, n) in p.local_names.iter().enumerate() { + if n.eq_ignore_ascii_case(name) { + let v = self.locals[f.locals_base + i].clone(); + return Some(self.deref_for_inspect(v)); + } + } + } + for (i, n) in self.module.global_names.iter().enumerate() { + if n.eq_ignore_ascii_case(name) { + return Some(self.deref_for_inspect(self.globals[i].clone())); + } + } + None + } + + /// Arrayelement inspizieren. + pub fn inspect_element(&self, name: &str, idx: &[i32]) -> Option { + let v = self.inspect(name)?; + match v { + Value::Arr(a) => { + let a = a.borrow(); + let flat = a.flat_index(idx).ok()?; + Some(a.data[flat as usize].clone()) + } + _ => None, + } + } + + /// UDT-Feld inspizieren (Pfad über Feldindizes). + pub fn inspect_field(&self, name: &str, path: &[u16]) -> Option { + let mut v = self.inspect(name)?; + for f in path { + let Value::Rec(r) = v else { return None }; + let r = r.borrow(); + v = r.fields.get(*f as usize)?.clone(); + } + Some(v) + } + + fn deref_for_inspect(&self, v: Value) -> Value { + match v { + Value::Ref(r) => self.read_ref(&r).unwrap_or(Value::Empty), + other => other, + } + } + + // ---- Hauptschleife -------------------------------------------------------- + + pub fn run(&mut self, host: &mut dyn Host) -> RunEvent { + loop { + let frame = self.frames.last_mut().expect("kein Frame"); + let proc = frame.proc; + let pc = frame.pc; + let code = &self.module.procs[proc].code; + if pc >= code.len() { + // Sollte nicht passieren (Exit wird emittiert) — defensiv. + return RunEvent::Ended; + } + let instr = code[pc].clone(); + frame.pc = pc + 1; + match self.exec(instr, pc, host) { + Ok(Flow::Normal) => {} + Ok(Flow::Event(ev)) => return ev, + Err(e) => { + if let Some(ev) = self.handle_error(e.0, host) { + return ev; + } + } + } + } + } + + // ---- Fehlerbehandlung ------------------------------------------------------- + + /// Fehler auslösen: Handler suchen, Stack abwickeln, springen. + /// `Some(event)` = unbehandelt (Programmabbruch). + fn handle_error(&mut self, code: u16, _host: &mut dyn Host) -> Option { + let line = self.current_line(); + if self.in_handler { + // Fehler im Handler: fatal, keine Kaskade. + return Some(self.error_event(code, line)); + } + // Frame-Kette von innen nach außen nach LOCAL-Handlern absuchen. + let mut target: Option<(usize, Handler)> = None; + for (depth, f) in self.frames.iter().enumerate().rev() { + if f.local_handler != Handler::None { + target = Some((depth, f.local_handler)); + break; + } + } + if target.is_none() && self.module_handler != Handler::None { + target = Some((0, self.module_handler)); + } + let Some((depth, handler)) = target else { + return Some(self.error_event(code, line)); + }; + // Unwinding bis zum Handler-Frame. + while self.frames.len() > depth + 1 { + self.pop_frame(); + } + let (stack_base, last_stmt_pc, fproc) = { + let f = self.frames.last().unwrap(); + (f.stack_base, f.last_stmt_pc, f.proc) + }; + self.stack.truncate(stack_base); + self.err = code; + match handler { + Handler::Goto(pc) => { + self.resume_pc = last_stmt_pc; + self.in_handler = true; + self.frames.last_mut().unwrap().pc = pc as usize; + } + Handler::ResumeNext => { + // Weiter mit der Anweisung nach der zuletzt begonnenen. + let next = self.next_stmt_pc(fproc, last_stmt_pc); + self.frames.last_mut().unwrap().pc = next; + } + Handler::None => unreachable!(), + } + None + } + + fn error_event(&self, code: u16, line: u32) -> RunEvent { + RunEvent::Error { + code, + line, + message: RuntimeError(code).message().to_string(), + } + } + + /// Nächste Anweisungsgrenze nach `from` (Instruktionsindex). + fn next_stmt_pc(&self, proc: usize, from: usize) -> usize { + let code = &self.module.procs[proc].code; + let mut i = from + 1; + while i < code.len() { + if matches!(code[i], Instr::Stmt(_)) { + return i; + } + i += 1; + } + code.len().saturating_sub(1) // läuft auf das Rumpfende (End/Ret) + } + + // ---- Werte-Hilfen ------------------------------------------------------------- + + fn pop(&mut self) -> Result { + self.stack.pop().ok_or(RuntimeError(51)) // Internal error + } + + fn pop_i16(&mut self) -> Result { + match self.pop()? { + Value::Int(v) => Ok(v), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_i32(&mut self) -> Result { + match self.pop()? { + Value::Lng(v) => Ok(v), + Value::Int(v) => Ok(v as i32), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_f32(&mut self) -> Result { + match self.pop()? { + Value::Sng(v) => Ok(v), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_f64(&mut self) -> Result { + match self.pop()? { + Value::Dbl(v) => Ok(v), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_cur(&mut self) -> Result { + match self.pop()? { + Value::Cur(v) => Ok(v), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_str(&mut self) -> Result, RuntimeError> { + match self.pop()? { + Value::Str(s) => Ok(s), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_arr(&mut self) -> Result>, RuntimeError> { + match self.pop()? { + Value::Arr(a) => Ok(a), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_rec(&mut self) -> Result>, RuntimeError> { + match self.pop()? { + Value::Rec(r) => Ok(r), + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn pop_indices(&mut self, dims: u8) -> Result, RuntimeError> { + let mut idx = vec![0i32; dims as usize]; + for i in (0..dims as usize).rev() { + idx[i] = self.pop_i32()?; + } + Ok(idx) + } + + fn push(&mut self, v: Value) { + self.stack.push(v); + } + + fn truthy(v: &Value) -> bool { + match v { + Value::Int(v) => *v != 0, + Value::Lng(v) => *v != 0, + Value::Sng(v) => *v != 0.0, + Value::Dbl(v) => *v != 0.0, + Value::Cur(v) => *v != 0, + _ => false, + } + } + + fn read_ref(&self, r: &VarRef) -> Result { + Ok(match r { + VarRef::Global(i) => self.globals[*i as usize].clone(), + VarRef::Stack(i) => self.locals[*i as usize].clone(), + VarRef::Elem(a, flat) => a.borrow().data[*flat as usize].clone(), + VarRef::Field(rec, path) => { + let mut v = Value::Rec(rec.clone()); + for f in path { + let Value::Rec(r) = v else { + return Err(RuntimeError::TYPE_MISMATCH); + }; + let val = r.borrow().fields[*f as usize].clone(); + v = val; + } + v + } + }) + } + + fn write_ref(&mut self, r: &VarRef, val: Value) -> Result<(), RuntimeError> { + match r { + VarRef::Global(i) => self.globals[*i as usize] = val, + VarRef::Stack(i) => self.locals[*i as usize] = val, + VarRef::Elem(a, flat) => a.borrow_mut().data[*flat as usize] = val, + VarRef::Field(rec, path) => { + let mut cur = rec.clone(); + for f in &path[..path.len() - 1] { + let next = match &cur.borrow().fields[*f as usize] { + Value::Rec(r) => r.clone(), + _ => return Err(RuntimeError::TYPE_MISMATCH), + }; + cur = next; + } + cur.borrow_mut().fields[*path.last().unwrap() as usize] = val; + } + } + Ok(()) + } + + /// Array-Slot sicherstellen (Auto-DIM impliziter Arrays) und Handle liefern. + fn ensure_array( + &mut self, + global: bool, + slot: u16, + dims: u8, + elem: &TypeInit, + ) -> Result>, RuntimeError> { + let frame_base = self.frames.last().map(|f| f.locals_base).unwrap_or(0); + let cell = if global { + &mut self.globals[slot as usize] + } else { + &mut self.locals[frame_base + slot as usize] + }; + match cell { + Value::Arr(a) => Ok(a.clone()), + Value::Empty => { + let lo = self.module.option_base as i32; + let bounds = vec![(lo, 10); dims as usize]; + let arr = ArrayObj::new(elem.clone(), bounds, &self.module.udts)?; + let handle = Rc::new(std::cell::RefCell::new(arr)); + let cell = if global { + &mut self.globals[slot as usize] + } else { + &mut self.locals[frame_base + slot as usize] + }; + *cell = Value::Arr(handle.clone()); + Ok(handle) + } + _ => Err(RuntimeError::TYPE_MISMATCH), + } + } + + fn slot_value(&mut self, global: bool, slot: u16) -> &mut Value { + if global { + &mut self.globals[slot as usize] + } else { + let base = self.frames.last().map(|f| f.locals_base).unwrap_or(0); + &mut self.locals[base + slot as usize] + } + } + + fn jump(&mut self, pc: u32) { + self.frames.last_mut().unwrap().pc = pc as usize; + } + + // ---- Instruktionsausführung ------------------------------------------------ + + fn exec(&mut self, instr: Instr, pc: usize, host: &mut dyn Host) -> Result { + use Instr as I; + match instr { + I::Stmt(line) => { + let f = self.frames.last_mut().unwrap(); + f.line = line; + f.last_stmt_pc = pc; + if self.flags != 0 { + if self.flags & F_STEP != 0 { + return Ok(Flow::Event(RunEvent::Stepped { line })); + } + if self.flags & F_BREAK != 0 && self.breakpoints.contains(&line) { + return Ok(Flow::Event(RunEvent::Breakpoint { line })); + } + if self.flags & F_POLL != 0 && host.interrupted() { + return Ok(Flow::Event(RunEvent::Interrupted { line })); + } + } + Ok(Flow::Normal) + } + I::SetErl(n) => { + self.erl = n; + Ok(Flow::Normal) + } + I::End | I::SystemInstr => Ok(Flow::Event(RunEvent::Ended)), + I::StopInstr => Ok(Flow::Event(RunEvent::Stopped { line: self.current_line() })), + I::Unsupported(_) => Err(RuntimeError(73)), // Advanced feature + + I::PushInt(v) => { + self.push(Value::Int(v)); + Ok(Flow::Normal) + } + I::PushLng(v) => { + self.push(Value::Lng(v)); + Ok(Flow::Normal) + } + I::PushSng(v) => { + self.push(Value::Sng(v)); + Ok(Flow::Normal) + } + I::PushDbl(v) => { + self.push(Value::Dbl(v)); + Ok(Flow::Normal) + } + I::PushCur(v) => { + self.push(Value::Cur(v)); + Ok(Flow::Normal) + } + I::PushStr(i) => { + self.push(Value::Str(self.module.strings[i as usize].clone())); + Ok(Flow::Normal) + } + I::Dup => { + let v = self.stack.last().cloned().ok_or(RuntimeError(51))?; + self.push(v); + Ok(Flow::Normal) + } + I::Pop => { + self.pop()?; + Ok(Flow::Normal) + } + + I::LoadGlobal(i) => { + self.push(self.globals[i as usize].clone()); + Ok(Flow::Normal) + } + I::StoreGlobal(i) => { + let v = self.pop()?; + self.globals[i as usize] = v; + Ok(Flow::Normal) + } + I::LoadLocal(i) => { + let base = self.frames.last().unwrap().locals_base; + self.push(self.locals[base + i as usize].clone()); + Ok(Flow::Normal) + } + I::StoreLocal(i) => { + let v = self.pop()?; + let base = self.frames.last().unwrap().locals_base; + self.locals[base + i as usize] = v; + Ok(Flow::Normal) + } + I::LoadRef(i) => { + let base = self.frames.last().unwrap().locals_base; + match &self.locals[base + i as usize] { + Value::Ref(r) => { + let r = r.clone(); + let v = self.read_ref(&r)?; + self.push(v); + } + // BYVAL-übergebener Wert im selben Slot + other => { + let v = other.clone(); + self.push(v); + } + } + Ok(Flow::Normal) + } + I::StoreRef(i) => { + let v = self.pop()?; + let base = self.frames.last().unwrap().locals_base; + match self.locals[base + i as usize].clone() { + Value::Ref(r) => self.write_ref(&r, v)?, + _ => self.locals[base + i as usize] = v, + } + Ok(Flow::Normal) + } + I::MakeRefGlobal(i) => { + self.push(Value::Ref(VarRef::Global(i))); + Ok(Flow::Normal) + } + I::MakeRefLocal(i) => { + let base = self.frames.last().unwrap().locals_base; + self.push(Value::Ref(VarRef::Stack((base + i as usize) as u32))); + Ok(Flow::Normal) + } + I::MakeRefElem(dims) => { + let idx = self.pop_indices(dims)?; + let a = self.pop_arr()?; + let flat = a.borrow().flat_index(&idx)?; + self.push(Value::Ref(VarRef::Elem(a, flat))); + Ok(Flow::Normal) + } + I::MakeRefField(f) => { + match self.pop()? { + Value::Rec(r) => self.push(Value::Ref(VarRef::Field(r, vec![f]))), + Value::Ref(VarRef::Field(r, mut path)) => { + path.push(f); + self.push(Value::Ref(VarRef::Field(r, path))); + } + _ => return Err(RuntimeError::TYPE_MISMATCH), + } + Ok(Flow::Normal) + } + + I::LoadArr(global, slot, dims, elem) => { + let a = self.ensure_array(global, slot, dims, &elem)?; + self.push(Value::Arr(a)); + Ok(Flow::Normal) + } + I::LoadElem(dims) => { + let idx = self.pop_indices(dims)?; + let a = self.pop_arr()?; + let a = a.borrow(); + let flat = a.flat_index(&idx)?; + self.push(a.data[flat as usize].clone()); + Ok(Flow::Normal) + } + I::StoreElem(dims) => { + let v = self.pop()?; + let idx = self.pop_indices(dims)?; + let a = self.pop_arr()?; + let mut a = a.borrow_mut(); + let flat = a.flat_index(&idx)?; + a.data[flat as usize] = v; + Ok(Flow::Normal) + } + I::DimArr(global, slot, dims, elem) => { + let bounds = self.pop_bounds(dims)?; + let cell = self.slot_value(global, slot); + if !matches!(cell, Value::Empty) { + return Err(RuntimeError::DUPLICATE_DEFINITION); + } + let arr = ArrayObj::new(elem, bounds, &self.module.udts)?; + *self.slot_value(global, slot) = Value::Arr(Rc::new(std::cell::RefCell::new(arr))); + Ok(Flow::Normal) + } + I::RedimArr(global, slot, dims, elem) => { + let bounds = self.pop_bounds(dims)?; + let arr = ArrayObj::new(elem, bounds, &self.module.udts)?; + let cell = self.slot_value(global, slot); + match cell { + Value::Arr(handle) => { + // In-place ersetzen: geteilte Handles (Parameter) + // sehen die neue Dimensionierung. + *handle.borrow_mut() = arr; + } + _ => *cell = Value::Arr(Rc::new(std::cell::RefCell::new(arr))), + } + Ok(Flow::Normal) + } + I::EraseSlot(global, slot) => { + *self.slot_value(global, slot) = Value::Empty; + Ok(Flow::Normal) + } + I::LoadField(f) => { + let r = self.pop_rec()?; + let v = r.borrow().fields[f as usize].clone(); + self.push(v); + Ok(Flow::Normal) + } + I::StoreField(f) => { + let v = self.pop()?; + let r = self.pop_rec()?; + r.borrow_mut().fields[f as usize] = v; + Ok(Flow::Normal) + } + I::CopyRec => { + let dst = self.pop_rec()?; + let src = self.pop_rec()?; + if !Rc::ptr_eq(&dst, &src) { + let copied: Vec = + src.borrow().fields.iter().map(deep_copy).collect(); + dst.borrow_mut().fields = copied; + } + Ok(Flow::Normal) + } + I::ArrBound(lower) => { + let dim = self.pop_i32()?; + let a = self.pop_arr()?; + let a = a.borrow(); + if dim < 1 || dim as usize > a.dims.len() { + return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE); + } + let (lo, hi) = a.dims[(dim - 1) as usize]; + self.push(Value::Lng(if lower { lo } else { hi })); + Ok(Flow::Normal) + } + I::FixStr(n) => { + let s = self.pop_str()?; + let n = n as usize; + let len = s.chars().count(); + let fixed: String = if len >= n { + s.chars().take(n).collect() + } else { + let mut t = s.to_string(); + t.extend(std::iter::repeat(' ').take(n - len)); + t + }; + self.push(Value::Str(Rc::from(fixed.as_str()))); + Ok(Flow::Normal) + } + + // ---- Arithmetik ---- + I::AddI2 => self.bin_i16(|a, b| a.checked_add(b)), + I::AddI4 => self.bin_i32(|a, b| a.checked_add(b)), + I::AddR4 => self.bin_f32(|a, b| a + b), + I::AddR8 => self.bin_f64(|a, b| a + b), + I::AddCy => self.bin_cur(|a, b| a.checked_add(b)), + I::SubI2 => self.bin_i16(|a, b| a.checked_sub(b)), + I::SubI4 => self.bin_i32(|a, b| a.checked_sub(b)), + I::SubR4 => self.bin_f32(|a, b| a - b), + I::SubR8 => self.bin_f64(|a, b| a - b), + I::SubCy => self.bin_cur(|a, b| a.checked_sub(b)), + I::MulI2 => self.bin_i16(|a, b| a.checked_mul(b)), + I::MulI4 => self.bin_i32(|a, b| a.checked_mul(b)), + I::MulR4 => self.bin_f32(|a, b| a * b), + I::MulR8 => self.bin_f64(|a, b| a * b), + I::MulCy => { + let b = self.pop_cur()?; + let a = self.pop_cur()?; + let r = cur_mul(a, b)?; + self.push(Value::Cur(r)); + Ok(Flow::Normal) + } + I::NegI2 => { + let a = self.pop_i16()?; + self.push(Value::Int(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?)); + Ok(Flow::Normal) + } + I::NegI4 => { + let a = self.pop_i32()?; + self.push(Value::Lng(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?)); + Ok(Flow::Normal) + } + I::NegR4 => { + let a = self.pop_f32()?; + self.push(Value::Sng(-a)); + Ok(Flow::Normal) + } + I::NegR8 => { + let a = self.pop_f64()?; + self.push(Value::Dbl(-a)); + Ok(Flow::Normal) + } + I::NegCy => { + let a = self.pop_cur()?; + self.push(Value::Cur(a.checked_neg().ok_or(RuntimeError::OVERFLOW)?)); + Ok(Flow::Normal) + } + I::DivR4 => { + let b = self.pop_f32()?; + let a = self.pop_f32()?; + if b == 0.0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + let r = a / b; + if r.is_infinite() { + return Err(RuntimeError::OVERFLOW); + } + self.push(Value::Sng(r)); + Ok(Flow::Normal) + } + I::DivR8 => { + let b = self.pop_f64()?; + let a = self.pop_f64()?; + if b == 0.0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + let r = a / b; + if r.is_infinite() { + return Err(RuntimeError::OVERFLOW); + } + self.push(Value::Dbl(r)); + Ok(Flow::Normal) + } + I::IDivI2 => { + let b = self.pop_i16()?; + let a = self.pop_i16()?; + if b == 0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + let r = a.checked_div(b).ok_or(RuntimeError::OVERFLOW)?; + self.push(Value::Int(r)); + Ok(Flow::Normal) + } + I::IDivI4 => { + let b = self.pop_i32()?; + let a = self.pop_i32()?; + if b == 0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + let r = a.checked_div(b).ok_or(RuntimeError::OVERFLOW)?; + self.push(Value::Lng(r)); + Ok(Flow::Normal) + } + I::ModI2 => { + let b = self.pop_i16()?; + let a = self.pop_i16()?; + if b == 0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + // MIN MOD -1 ist mathematisch 0 (kein Überlauf). + let r = a.checked_rem(b).unwrap_or(0); + self.push(Value::Int(r)); + Ok(Flow::Normal) + } + I::ModI4 => { + let b = self.pop_i32()?; + let a = self.pop_i32()?; + if b == 0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + let r = a.checked_rem(b).unwrap_or(0); + self.push(Value::Lng(r)); + Ok(Flow::Normal) + } + I::PowR8 => { + let b = self.pop_f64()?; + let a = self.pop_f64()?; + if a == 0.0 && b < 0.0 { + return Err(RuntimeError::DIVISION_BY_ZERO); + } + if a < 0.0 && b.fract() != 0.0 { + return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); + } + let r = a.powf(b); + if r.is_infinite() || r.is_nan() { + return Err(RuntimeError::OVERFLOW); + } + self.push(Value::Dbl(r)); + Ok(Flow::Normal) + } + I::Concat => { + let b = self.pop_str()?; + let a = self.pop_str()?; + if a.chars().count() + b.chars().count() > 32_767 { + return Err(RuntimeError::OUT_OF_STRING_SPACE); + } + let mut s = a.to_string(); + s.push_str(&b); + self.push(Value::Str(Rc::from(s.as_str()))); + Ok(Flow::Normal) + } + + // ---- Konvertierungen (Matrix) ---- + I::ConvI2I4 => { + let v = self.pop_i16()?; + self.push(Value::Lng(v as i32)); + Ok(Flow::Normal) + } + I::ConvI2R4 => { + let v = self.pop_i16()?; + self.push(Value::Sng(v as f32)); + Ok(Flow::Normal) + } + I::ConvI2R8 => { + let v = self.pop_i16()?; + self.push(Value::Dbl(v as f64)); + Ok(Flow::Normal) + } + I::ConvI2Cy => { + let v = self.pop_i16()?; + self.push(Value::Cur(v as i64 * 10_000)); + Ok(Flow::Normal) + } + I::ConvI4I2 => { + let v = self.pop_i32()?; + self.push(Value::Int(value::i32_to_i16(v)?)); + Ok(Flow::Normal) + } + I::ConvI4R4 => { + let v = self.pop_i32()?; + self.push(Value::Sng(v as f32)); + Ok(Flow::Normal) + } + I::ConvI4R8 => { + let v = self.pop_i32()?; + self.push(Value::Dbl(v as f64)); + Ok(Flow::Normal) + } + I::ConvI4Cy => { + let v = self.pop_i32()?; + self.push(Value::Cur(v as i64 * 10_000)); + Ok(Flow::Normal) + } + I::ConvR4I2 => { + let v = self.pop_f32()?; + self.push(Value::Int(value::f64_to_i16(v as f64)?)); + Ok(Flow::Normal) + } + I::ConvR4I4 => { + let v = self.pop_f32()?; + self.push(Value::Lng(value::f64_to_i32(v as f64)?)); + Ok(Flow::Normal) + } + I::ConvR4R8 => { + let v = self.pop_f32()?; + self.push(Value::Dbl(v as f64)); + Ok(Flow::Normal) + } + I::ConvR4Cy => { + let v = self.pop_f32()?; + self.push(Value::Cur(value::f64_to_cur(v as f64)?)); + Ok(Flow::Normal) + } + I::ConvR8I2 => { + let v = self.pop_f64()?; + self.push(Value::Int(value::f64_to_i16(v)?)); + Ok(Flow::Normal) + } + I::ConvR8I4 => { + let v = self.pop_f64()?; + self.push(Value::Lng(value::f64_to_i32(v)?)); + Ok(Flow::Normal) + } + I::ConvR8R4 => { + let v = self.pop_f64()?; + self.push(Value::Sng(value::f64_to_f32(v)?)); + Ok(Flow::Normal) + } + I::ConvR8Cy => { + let v = self.pop_f64()?; + self.push(Value::Cur(value::f64_to_cur(v)?)); + Ok(Flow::Normal) + } + I::ConvCyI2 => { + let v = self.pop_cur()?; + self.push(Value::Int(value::cur_to_i16(v)?)); + Ok(Flow::Normal) + } + I::ConvCyI4 => { + let v = self.pop_cur()?; + self.push(Value::Lng(value::cur_to_i32(v)?)); + Ok(Flow::Normal) + } + I::ConvCyR4 => { + let v = self.pop_cur()?; + self.push(Value::Sng(value::cur_to_f64(v) as f32)); + Ok(Flow::Normal) + } + I::ConvCyR8 => { + let v = self.pop_cur()?; + self.push(Value::Dbl(value::cur_to_f64(v))); + Ok(Flow::Normal) + } + + // ---- Logik ---- + I::NotI2 => { + let a = self.pop_i16()?; + self.push(Value::Int(!a)); + Ok(Flow::Normal) + } + I::NotI4 => { + let a = self.pop_i32()?; + self.push(Value::Lng(!a)); + Ok(Flow::Normal) + } + I::AndI2 => self.logic_i16(|a, b| a & b), + I::AndI4 => self.logic_i32(|a, b| a & b), + I::OrI2 => self.logic_i16(|a, b| a | b), + I::OrI4 => self.logic_i32(|a, b| a | b), + I::XorI2 => self.logic_i16(|a, b| a ^ b), + I::XorI4 => self.logic_i32(|a, b| a ^ b), + I::EqvI2 => self.logic_i16(|a, b| !(a ^ b)), + I::EqvI4 => self.logic_i32(|a, b| !(a ^ b)), + I::ImpI2 => self.logic_i16(|a, b| !a | b), + I::ImpI4 => self.logic_i32(|a, b| !a | b), + + // ---- Vergleiche ---- + I::CmpI2(op) => { + let b = self.pop_i16()?; + let a = self.pop_i16()?; + self.push(cmp_result(cmp_ord(a.cmp(&b), op))); + Ok(Flow::Normal) + } + I::CmpI4(op) => { + let b = self.pop_i32()?; + let a = self.pop_i32()?; + self.push(cmp_result(cmp_ord(a.cmp(&b), op))); + Ok(Flow::Normal) + } + I::CmpR4(op) => { + let b = self.pop_f32()?; + let a = self.pop_f32()?; + self.push(cmp_result(cmp_float(a as f64, b as f64, op))); + Ok(Flow::Normal) + } + I::CmpR8(op) => { + let b = self.pop_f64()?; + let a = self.pop_f64()?; + self.push(cmp_result(cmp_float(a, b, op))); + Ok(Flow::Normal) + } + I::CmpCy(op) => { + let b = self.pop_cur()?; + let a = self.pop_cur()?; + self.push(cmp_result(cmp_ord(a.cmp(&b), op))); + Ok(Flow::Normal) + } + I::CmpStr(op) => { + let b = self.pop_str()?; + let a = self.pop_str()?; + self.push(cmp_result(cmp_ord(a.as_bytes().cmp(b.as_bytes()), op))); + Ok(Flow::Normal) + } + + // ---- Kontrollfluss ---- + I::Jump(t) => { + self.jump(t); + Ok(Flow::Normal) + } + I::JumpIfFalse(t) => { + let v = self.pop()?; + if !Self::truthy(&v) { + self.jump(t); + } + Ok(Flow::Normal) + } + I::JumpIfTrue(t) => { + let v = self.pop()?; + if Self::truthy(&v) { + self.jump(t); + } + Ok(Flow::Normal) + } + I::Gosub(t) => { + let f = self.frames.last_mut().unwrap(); + let ret = f.pc; + f.gosub.push(ret); + f.pc = t as usize; + Ok(Flow::Normal) + } + I::RetGosub => { + let f = self.frames.last_mut().unwrap(); + match f.gosub.pop() { + Some(ret) => { + f.pc = ret; + Ok(Flow::Normal) + } + None => Err(RuntimeError::RETURN_WITHOUT_GOSUB), + } + } + I::RetGosubTo(t) => { + let f = self.frames.last_mut().unwrap(); + match f.gosub.pop() { + Some(_) => { + f.pc = t as usize; + Ok(Flow::Normal) + } + None => Err(RuntimeError::RETURN_WITHOUT_GOSUB), + } + } + I::OnJump(table, gosub) => { + let n = self.pop_i16()?; + if !(0..=255).contains(&n) { + return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); + } + let t = &self.module.jump_tables[table as usize]; + if n >= 1 && (n as usize) <= t.len() { + let target = t[(n - 1) as usize]; + let f = self.frames.last_mut().unwrap(); + if gosub { + let ret = f.pc; + f.gosub.push(ret); + } + f.pc = target as usize; + } + Ok(Flow::Normal) + } + + // ---- Prozeduren ---- + I::Call(proc, argc) => { + self.push_frame(proc as usize, argc as usize); + Ok(Flow::Normal) + } + I::RetProc => { + self.pop_frame(); + Ok(Flow::Normal) + } + I::RetFn => { + let v = self.pop()?; + self.pop_frame(); + self.push(v); + Ok(Flow::Normal) + } + I::CallBuiltin(id, argc) => { + let mut args = Vec::with_capacity(argc as usize); + for _ in 0..argc { + args.push(self.pop()?); + } + args.reverse(); + let f = builtin_table()[id as usize]; + match f(&mut self.rt, host, &mut args) { + Ok(Some(v)) => { + self.push(v); + Ok(Flow::Normal) + } + Ok(None) => Ok(Flow::Normal), + Err(e) => Err(e), + } + } + + // ---- Fehlerbehandlung ---- + I::OnErrorGoto(t) => { + self.module_handler = Handler::Goto(t); + Ok(Flow::Normal) + } + I::OnErrorLocal(t) => { + self.frames.last_mut().unwrap().local_handler = Handler::Goto(t); + Ok(Flow::Normal) + } + I::OnErrorDisable => { + self.module_handler = Handler::None; + Ok(Flow::Normal) + } + I::OnErrorLocalDisable => { + self.frames.last_mut().unwrap().local_handler = Handler::None; + Ok(Flow::Normal) + } + I::OnErrorResumeNext(local) => { + if local { + self.frames.last_mut().unwrap().local_handler = Handler::ResumeNext; + } else { + self.module_handler = Handler::ResumeNext; + } + Ok(Flow::Normal) + } + I::Resume0 => self.do_resume(|vm| vm.resume_pc), + I::ResumeNext => self.do_resume(|vm| { + let f = vm.frames.last().unwrap(); + vm.next_stmt_pc(f.proc, vm.resume_pc) + }), + I::ResumeLabel(t) => self.do_resume(move |_| t as usize), + I::RaiseError => { + let code = self.pop_i16()?; + if code < 1 { + return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); + } + Err(RuntimeError(code as u16)) + } + I::LoadErr => { + self.push(Value::Lng(self.err as i32)); + Ok(Flow::Normal) + } + I::LoadErl => { + self.push(Value::Lng(self.erl as i32)); + Ok(Flow::Normal) + } + + // ---- DATA und Eingabe ---- + I::ReadData(kind) => { + if self.data_ptr >= self.module.data.len() { + return Err(RuntimeError::OUT_OF_DATA); + } + let item = self.module.data[self.data_ptr].text.clone(); + self.data_ptr += 1; + if kind == 0 { + self.push(Value::Str(Rc::from(item.as_str()))); + } else { + let v = parse_data_number(&item)?; + self.push(Value::Dbl(v)); + } + Ok(Flow::Normal) + } + I::Restore(idx) => { + self.data_ptr = idx as usize; + Ok(Flow::Normal) + } + I::Input(argc, line_mode, prompt, question) => { + self.do_input(argc, line_mode, prompt, question, host)?; + Ok(Flow::Normal) + } + } + } + + fn pop_bounds(&mut self, dims: u8) -> Result, RuntimeError> { + let mut flat = vec![0i32; dims as usize * 2]; + for i in (0..flat.len()).rev() { + flat[i] = self.pop_i32()?; + } + Ok(flat.chunks(2).map(|c| (c[0], c[1])).collect()) + } + + fn do_resume(&mut self, target: impl FnOnce(&Vm) -> usize) -> Result { + if !self.in_handler { + return Err(RuntimeError(20)); // RESUME without error + } + self.in_handler = false; + self.err = 0; + let t = target(self); + self.frames.last_mut().unwrap().pc = t; + Ok(Flow::Normal) + } + + fn do_input( + &mut self, + argc: u8, + line_mode: bool, + prompt: u16, + question: bool, + host: &mut dyn Host, + ) -> Result<(), RuntimeError> { + // Referenzen in Reihenfolge einsammeln. + let mut refs = Vec::with_capacity(argc as usize); + for _ in 0..argc { + match self.pop()? { + Value::Ref(r) => refs.push(r), + _ => return Err(RuntimeError::TYPE_MISMATCH), + } + } + refs.reverse(); + + loop { + if prompt != 0xFFFF { + let p = self.module.strings[prompt as usize].clone(); + self.rt.print.write(host, &p); + } + if question { + self.rt.print.write(host, "? "); + } + let Some(mut line) = host.read_line() else { + return Err(RuntimeError(62)); // Input past end of file + }; + self.rt.print.col = 0; + if line_mode { + let r = refs.first().ok_or(RuntimeError(51))?; + self.write_ref(r, Value::Str(Rc::from(line.as_str())))?; + return Ok(()); + } + // Felder trennen (Quotes respektieren). + let fields = split_input_fields(&line); + if fields.len() != refs.len() { + self.rt.print.write(host, "Redo from start\n"); + line.clear(); + continue; + } + // Konvertieren nach aktuellem Zieltyp; Fehler → Redo. + let mut vals = Vec::with_capacity(refs.len()); + let mut ok = true; + for (r, text) in refs.iter().zip(&fields) { + let target = self.read_ref(r)?; + match input_value(&target, text) { + Some(v) => vals.push(v), + None => { + ok = false; + break; + } + } + } + if !ok { + self.rt.print.write(host, "Redo from start\n"); + continue; + } + for (r, v) in refs.iter().zip(vals) { + self.write_ref(r, v)?; + } + return Ok(()); + } + } + + // Typisierte Binäroperationen mit Überlaufprüfung. + fn bin_i16(&mut self, f: impl Fn(i16, i16) -> Option) -> Result { + let b = self.pop_i16()?; + let a = self.pop_i16()?; + let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?; + self.push(Value::Int(r)); + Ok(Flow::Normal) + } + fn bin_i32(&mut self, f: impl Fn(i32, i32) -> Option) -> Result { + let b = self.pop_i32()?; + let a = self.pop_i32()?; + let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?; + self.push(Value::Lng(r)); + Ok(Flow::Normal) + } + fn bin_f32(&mut self, f: impl Fn(f32, f32) -> f32) -> Result { + let b = self.pop_f32()?; + let a = self.pop_f32()?; + let r = f(a, b); + if r.is_infinite() { + return Err(RuntimeError::OVERFLOW); + } + self.push(Value::Sng(r)); + Ok(Flow::Normal) + } + fn bin_f64(&mut self, f: impl Fn(f64, f64) -> f64) -> Result { + let b = self.pop_f64()?; + let a = self.pop_f64()?; + let r = f(a, b); + if r.is_infinite() { + return Err(RuntimeError::OVERFLOW); + } + self.push(Value::Dbl(r)); + Ok(Flow::Normal) + } + fn bin_cur(&mut self, f: impl Fn(i64, i64) -> Option) -> Result { + let b = self.pop_cur()?; + let a = self.pop_cur()?; + let r = f(a, b).ok_or(RuntimeError::OVERFLOW)?; + self.push(Value::Cur(r)); + Ok(Flow::Normal) + } + fn logic_i16(&mut self, f: impl Fn(i16, i16) -> i16) -> Result { + let b = self.pop_i16()?; + let a = self.pop_i16()?; + self.push(Value::Int(f(a, b))); + Ok(Flow::Normal) + } + fn logic_i32(&mut self, f: impl Fn(i32, i32) -> i32) -> Result { + let b = self.pop_i32()?; + let a = self.pop_i32()?; + self.push(Value::Lng(f(a, b))); + Ok(Flow::Normal) + } +} + +/// CURRENCY-Multiplikation: exakt in i128, Skalierung ÷10 000 mit +/// Banker's Rounding. +fn cur_mul(a: i64, b: i64) -> Result { + let p = a as i128 * b as i128; + let q = p.div_euclid(10_000); + let r = p.rem_euclid(10_000); + let rounded = if r > 5_000 { + q + 1 + } else if r < 5_000 { + q + } else if q % 2 == 0 { + q + } else { + q + 1 + }; + i64::try_from(rounded).map_err(|_| RuntimeError::OVERFLOW) +} + +fn deep_copy(v: &Value) -> Value { + match v { + Value::Rec(r) => { + let fields = r.borrow().fields.iter().map(deep_copy).collect(); + Value::Rec(Rc::new(std::cell::RefCell::new(RecordObj { fields }))) + } + other => other.clone(), + } +} + +fn cmp_ord(o: std::cmp::Ordering, op: CmpOp) -> bool { + use std::cmp::Ordering::*; + match op { + CmpOp::Eq => o == Equal, + CmpOp::Ne => o != Equal, + CmpOp::Lt => o == Less, + CmpOp::Le => o != Greater, + CmpOp::Gt => o == Greater, + CmpOp::Ge => o != Less, + } +} + +fn cmp_float(a: f64, b: f64, op: CmpOp) -> bool { + match op { + CmpOp::Eq => a == b, + CmpOp::Ne => a != b, + CmpOp::Lt => a < b, + CmpOp::Le => a <= b, + CmpOp::Gt => a > b, + CmpOp::Ge => a >= b, + } +} + +fn cmp_result(b: bool) -> Value { + Value::Int(if b { -1 } else { 0 }) +} + +/// DATA-Element als Zahl: der komplette Eintrag muss numerisch sein +/// (leer = 0), sonst Fehler 13. +fn parse_data_number(s: &str) -> Result { + let t = s.trim(); + if t.is_empty() { + return Ok(0.0); + } + let cleaned = t.replace(['d', 'D'], "E").replace('e', "E"); + cleaned + .parse::() + .map_err(|_| RuntimeError::TYPE_MISMATCH) +} + +/// INPUT-Zeile in Felder trennen: Kommas trennen, Anführungszeichen +/// schützen; Leerraum um unquotierte Felder wird entfernt. +fn split_input_fields(line: &str) -> Vec { + let mut fields = Vec::new(); + let mut cur = String::new(); + let mut in_quotes = false; + for c in line.chars() { + match c { + '"' => { + in_quotes = !in_quotes; + cur.push(c); + } + ',' if !in_quotes => { + fields.push(cur.clone()); + cur.clear(); + } + _ => cur.push(c), + } + } + fields.push(cur); + fields +} + +/// Eingabefeld in den Typ des Ziels konvertieren (Tag des aktuellen +/// Werts bestimmt den Typ); `None` = „Redo from start". +fn input_value(target: &Value, text: &str) -> Option { + let t = text.trim(); + match target { + Value::Str(_) => { + let s = t.strip_prefix('"').and_then(|s| s.strip_suffix('"')).unwrap_or(t); + Some(Value::Str(Rc::from(s))) + } + Value::Int(_) => { + let v = strict_number(t)?; + value::f64_to_i16(v).ok().map(Value::Int) + } + Value::Lng(_) => { + let v = strict_number(t)?; + value::f64_to_i32(v).ok().map(Value::Lng) + } + Value::Sng(_) => strict_number(t).map(|v| Value::Sng(v as f32)), + Value::Dbl(_) => strict_number(t).map(Value::Dbl), + Value::Cur(_) => { + let v = strict_number(t)?; + value::f64_to_cur(v).ok().map(Value::Cur) + } + _ => None, + } +} + +fn strict_number(t: &str) -> Option { + if t.is_empty() { + return Some(0.0); + } + let cleaned = t.replace(['d', 'D'], "E").replace('e', "E"); + cleaned.parse::().ok() +} diff --git a/crates/tb-vm/src/lib.rs b/crates/tb-vm/src/lib.rs index 48873fb..dafb029 100644 --- a/crates/tb-vm/src/lib.rs +++ b/crates/tb-vm/src/lib.rs @@ -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> { + 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)) +} diff --git a/crates/tb-vm/tests/vm.rs b/crates/tb-vm/tests/vm.rs new file mode 100644 index 0000000..f333d91 --- /dev/null +++ b/crates/tb-vm/tests/vm.rs @@ -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:?}"), + } +} diff --git a/docs/sprachreferenz.md b/docs/sprachreferenz.md index 9be7405..731284d 100644 --- a/docs/sprachreferenz.md +++ b/docs/sprachreferenz.md @@ -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) diff --git a/docs/tbvm-design.md b/docs/tbvm-design.md index 95fce2c..fdb7f84 100644 --- a/docs/tbvm-design.md +++ b/docs/tbvm-design.md @@ -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`, 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__` 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`, 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. diff --git a/openspec/changes/phase-2-bytecode-vm/.openspec.yaml b/openspec/changes/phase-2-bytecode-vm/.openspec.yaml new file mode 100644 index 0000000..032461f --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-02 diff --git a/openspec/changes/phase-2-bytecode-vm/design.md b/openspec/changes/phase-2-bytecode-vm/design.md new file mode 100644 index 0000000..613b070 --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/design.md @@ -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. diff --git a/openspec/changes/phase-2-bytecode-vm/proposal.md b/openspec/changes/phase-2-bytecode-vm/proposal.md new file mode 100644 index 0000000..1c52470 --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/proposal.md @@ -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. diff --git a/openspec/changes/phase-2-bytecode-vm/specs/bytecode-kompilat/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/bytecode-kompilat/spec.md new file mode 100644 index 0000000..2e2aa1e --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/bytecode-kompilat/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/specs/kompat-testkorpus/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/kompat-testkorpus/spec.md new file mode 100644 index 0000000..1f2acd0 --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/kompat-testkorpus/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/specs/konsolen-basisbibliothek/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/konsolen-basisbibliothek/spec.md new file mode 100644 index 0000000..23c25be --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/konsolen-basisbibliothek/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/specs/sprach-frontend/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/sprach-frontend/spec.md new file mode 100644 index 0000000..ec23f2c --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/sprach-frontend/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/specs/vm-ausfuehrung/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/vm-ausfuehrung/spec.md new file mode 100644 index 0000000..538b927 --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/vm-ausfuehrung/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/specs/vm-fehlerbehandlung/spec.md b/openspec/changes/phase-2-bytecode-vm/specs/vm-fehlerbehandlung/spec.md new file mode 100644 index 0000000..b7ee93b --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/specs/vm-fehlerbehandlung/spec.md @@ -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 diff --git a/openspec/changes/phase-2-bytecode-vm/tasks.md b/openspec/changes/phase-2-bytecode-vm/tasks.md new file mode 100644 index 0000000..5ef6d54 --- /dev/null +++ b/openspec/changes/phase-2-bytecode-vm/tasks.md @@ -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 `: 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 diff --git a/tests/compat/byref.bas b/tests/compat/byref.bas new file mode 100644 index 0000000..d66a237 --- /dev/null +++ b/tests/compat/byref.bas @@ -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 diff --git a/tests/compat/byref.out b/tests/compat/byref.out new file mode 100644 index 0000000..5df4c01 --- /dev/null +++ b/tests/compat/byref.out @@ -0,0 +1,5 @@ + 2 + 2 + 720 +zaehler 1 +zaehler 2 diff --git a/tests/compat/fehlerbehandlung.bas b/tests/compat/fehlerbehandlung.bas new file mode 100644 index 0000000..f3d66b4 --- /dev/null +++ b/tests/compat/fehlerbehandlung.bas @@ -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 diff --git a/tests/compat/fehlerbehandlung.out b/tests/compat/fehlerbehandlung.out new file mode 100644 index 0000000..77c1703 --- /dev/null +++ b/tests/compat/fehlerbehandlung.out @@ -0,0 +1,5 @@ +ERR= 5 ERL= 10 +nach RESUME NEXT +ERR= 11 ERL= 20 +weiter +fertig diff --git a/tests/compat/konvertierung.bas b/tests/compat/konvertierung.bas new file mode 100644 index 0000000..0ce452b --- /dev/null +++ b/tests/compat/konvertierung.bas @@ -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 diff --git a/tests/compat/konvertierung.out b/tests/compat/konvertierung.out new file mode 100644 index 0000000..0d9aedf --- /dev/null +++ b/tests/compat/konvertierung.out @@ -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