Phase 2 abgeschlossen: Bytecode, TBVM, Runtime-Scheibe, tbc run
- Sema zum Lowering-Pass umgebaut: typisiertes HIR (Slots, explizite Konvertierungsknoten) als Codegen-Eingabe; BYREF verlangt exakten Typ - Bytecode-Feindesign umgesetzt: monomorpher Opcode-Satz, .tbc-Container (Formatversion 1) mit eigenem Writer/Reader - Codegenerator HIR -> Bytecode (Fixup-Listen, keine globalen Passes) - TBVM-Interpreter: Kontrollfluss, GOSUB-Stack je Frame, BYREF/BYVAL, STATIC, DEF FN, DATA/READ/RESTORE, ON [LOCAL] ERROR/RESUME/ERR/ERL, Breakpoints/Einzelschritt/Inspektion, STOP fortsetzbar - Runtime-Scheibe: Host-Trait (Konsole/Capture), Builtin-Tabelle, Konvertierungsmatrix, PRINT-Formatierung/Druckzonen, Stringfunktionen - tbc run/build/check mit Exit-Codes nach Entscheidung D6 - Korpus-Harness (byte-genauer Vergleich) + 3 neue Korpusdateien (konvertierung, fehlerbehandlung, byref); 137 Tests gruen - Benchmarks: Einzelmodul 1,2 ms / Projekt 49.760 Zeilen 124 ms (Budgets eingehalten), VM ~5 Mio Schleifeniterationen/s - Doku fortgeschrieben (tbvm-design, sprachreferenz, PLAN); verlagerte Punkte als explizite Aufgaben in Phase 3 - OpenSpec-Change phase-2-bytecode-vm (27/27 Tasks) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,110 @@
|
||||
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||
//!
|
||||
//! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode
|
||||
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle
|
||||
//! (siehe PLAN.md):
|
||||
//! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//! Unterbefehle (Phase 2):
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//!
|
||||
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
//! 1 = Compile-Fehler/Bedienfehler.
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
|
||||
Ok(())
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::ConsoleHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
match args.first().map(String::as_str) {
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("build") => cmd_build(&args[1..]),
|
||||
Some("check") => cmd_check(&args[1..]),
|
||||
_ => {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn module_name(path: &Path) -> String {
|
||||
path.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_else(|| "MODUL".into())
|
||||
}
|
||||
|
||||
fn compile(path_arg: Option<&String>) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let path = PathBuf::from(path);
|
||||
let source = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", path.display());
|
||||
return Err(ExitCode::from(1));
|
||||
}
|
||||
};
|
||||
match tb_vm::compile_source(&module_name(&path), &source) {
|
||||
Ok(m) => Ok((path, m)),
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}:{d}", path.display());
|
||||
}
|
||||
eprintln!("{} Fehler.", diags.len());
|
||||
Err(ExitCode::from(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_check(args: &[String]) -> ExitCode {
|
||||
match compile(args.first()) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(code) => code,
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_build(args: &[String]) -> ExitCode {
|
||||
let (path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let out = path.with_extension("tbc");
|
||||
match std::fs::write(&out, module.to_tbc()) {
|
||||
Ok(()) => {
|
||||
println!("{}", out.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", out.display());
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_run(args: &[String]) -> ExitCode {
|
||||
let (_path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let mut vm = Vm::new(module);
|
||||
vm.rt.command = args[1..].join(" ");
|
||||
let mut host = ConsoleHost;
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
eprintln!("STOP in line {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error { code, line, message } => {
|
||||
eprintln!("Runtime error {code}: {message} in line {line}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } | RunEvent::Interrupted { .. } => {
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
crates/tb-cli/tests/compat.rs
Normal file
132
crates/tb-cli/tests/compat.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Kompatibilitäts-Harness (Phase-2-Meilenstein): jede Korpusdatei
|
||||
//! `tests/compat/*.bas` wird kompiliert, im Capture-Host ausgeführt und
|
||||
//! byte-genau gegen ihre `.out` verglichen. Bei Abweichung nennt der
|
||||
//! Test Datei, erste abweichende Zeile sowie Soll und Ist.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tb_runtime::host::CaptureHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn compat_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat")
|
||||
}
|
||||
|
||||
fn run_corpus_file(path: &Path) -> String {
|
||||
let src = std::fs::read_to_string(path).unwrap();
|
||||
let name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
|
||||
let module = tb_vm::compile_source(&name, &src)
|
||||
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Ended => host.output,
|
||||
other => panic!(
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nAusgabe bisher:\n{}",
|
||||
path.display(),
|
||||
host.output
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Erste abweichende Zeile melden (byte-genau, inkl. Leerzeichen am Ende).
|
||||
fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
if want == got {
|
||||
return;
|
||||
}
|
||||
let want_lines: Vec<&str> = want.split('\n').collect();
|
||||
let got_lines: Vec<&str> = got.split('\n').collect();
|
||||
for (i, (w, g)) in want_lines.iter().zip(got_lines.iter()).enumerate() {
|
||||
if w != g {
|
||||
panic!(
|
||||
"{file}: Abweichung in Zeile {}:\n Soll: {w:?}\n Ist: {g:?}",
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
panic!(
|
||||
"{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want}\nIst:\n{got}",
|
||||
want_lines.len(),
|
||||
got_lines.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn korpus_laeuft_mit_korrekter_ausgabe() {
|
||||
let dir = compat_dir();
|
||||
let mut checked = 0;
|
||||
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/compat fehlt")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let out_path = path.with_extension("out");
|
||||
let want = std::fs::read_to_string(&out_path)
|
||||
.unwrap_or_else(|_| panic!("{name}: Sollausgabe {} fehlt", out_path.display()));
|
||||
// .out-Dateien sind LF-normiert (.gitattributes); zur Sicherheit
|
||||
// CRLF des Checkouts entfernen.
|
||||
let want = want.replace("\r\n", "\n");
|
||||
let got = run_corpus_file(&path);
|
||||
assert_output_matches(&name, &want, &got);
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
|
||||
}
|
||||
|
||||
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
|
||||
|
||||
#[test]
|
||||
fn tbc_run_hello() {
|
||||
let exe = env!("CARGO_BIN_EXE_tbc");
|
||||
let out = Command::new(exe)
|
||||
.args(["run"])
|
||||
.arg(compat_dir().join("hello.bas"))
|
||||
.output()
|
||||
.expect("tbc startet");
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout), "Hallo, Welt!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tbc_run_stop_exitcode() {
|
||||
let exe = env!("CARGO_BIN_EXE_tbc");
|
||||
let dir = std::env::temp_dir();
|
||||
let f = dir.join("tb_phase2_stop_test.bas");
|
||||
std::fs::write(&f, "PRINT \"x\"\nSTOP\n").unwrap();
|
||||
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
|
||||
assert_eq!(out.status.code(), Some(3), "{out:?}");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("STOP in line 2"), "{err}");
|
||||
let _ = std::fs::remove_file(&f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tbc_run_laufzeitfehler_exitcode() {
|
||||
let exe = env!("CARGO_BIN_EXE_tbc");
|
||||
let dir = std::env::temp_dir();
|
||||
let f = dir.join("tb_phase2_err_test.bas");
|
||||
std::fs::write(&f, "i% = 40000\n").unwrap();
|
||||
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
|
||||
assert_eq!(out.status.code(), Some(2), "{out:?}");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("Overflow"), "{err}");
|
||||
let _ = std::fs::remove_file(&f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tbc_build_erzeugt_tbc() {
|
||||
let exe = env!("CARGO_BIN_EXE_tbc");
|
||||
let dir = std::env::temp_dir();
|
||||
let f = dir.join("tb_phase2_build_test.bas");
|
||||
std::fs::write(&f, "PRINT 1\n").unwrap();
|
||||
let out = Command::new(exe).args(["build"]).arg(&f).output().unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
let tbc = f.with_extension("tbc");
|
||||
let bytes = std::fs::read(&tbc).unwrap();
|
||||
assert_eq!(&bytes[..4], b"TBC\0");
|
||||
let _ = std::fs::remove_file(&f);
|
||||
let _ = std::fs::remove_file(&tbc);
|
||||
}
|
||||
@@ -48,6 +48,9 @@ pub enum Expr {
|
||||
},
|
||||
Unary { op: UnOp, operand: Box<Expr>, pos: SourcePos },
|
||||
Binary { op: BinOp, lhs: Box<Expr>, rhs: Box<Expr>, pos: SourcePos },
|
||||
/// Geklammerter Ausdruck. Semantisch transparent, aber als Argument
|
||||
/// erzwingt die Klammer Wertübergabe (BYVAL) statt BYREF.
|
||||
Paren(Box<Expr>),
|
||||
/// Ausgelassenes Argument (`LOCATE , 5`).
|
||||
Missing,
|
||||
}
|
||||
@@ -58,6 +61,7 @@ impl Expr {
|
||||
Expr::Name { pos, .. }
|
||||
| Expr::Unary { pos, .. }
|
||||
| Expr::Binary { pos, .. } => *pos,
|
||||
Expr::Paren(e) => e.pos(),
|
||||
_ => SourcePos::default(),
|
||||
}
|
||||
}
|
||||
@@ -242,9 +246,9 @@ pub enum Stmt {
|
||||
Gosub { target: LabelRef, pos: SourcePos },
|
||||
OnGoto { expr: Expr, targets: Vec<LabelRef>, gosub: bool, pos: SourcePos },
|
||||
Return { target: Option<LabelRef>, pos: SourcePos },
|
||||
End,
|
||||
StopStmt,
|
||||
System,
|
||||
End(SourcePos),
|
||||
StopStmt(SourcePos),
|
||||
System(SourcePos),
|
||||
Exit { kind: ExitKind, pos: SourcePos },
|
||||
Dim { shared: bool, redim: bool, decls: Vec<VarDecl>, pos: SourcePos },
|
||||
/// `SHARED`-Anweisung in einer Prozedur (Zugriff auf Modulvariablen).
|
||||
|
||||
459
crates/tb-frontend/src/hir.rs
Normal file
459
crates/tb-frontend/src/hir.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
//! Typisiertes, abgesenktes HIR — die Ausgabe der semantischen Analyse
|
||||
//! und Eingabe des Codegenerators (`tb-vm`).
|
||||
//!
|
||||
//! Eigenschaften (siehe Design der Phase-2-Änderung):
|
||||
//! - Namen sind aufgelöst: Variablen sind Slot-Indizes (global/lokal),
|
||||
//! Prozeduren und UDTs Tabellenindizes, Sprungziele `LabelId`s je Rumpf.
|
||||
//! - Jeder Ausdrucksknoten trägt seinen Ergebnistyp; implizite
|
||||
//! Konvertierungen sind als explizite `Conv`-Knoten materialisiert
|
||||
//! (Semantik: Konvertierungsmatrix in docs/tbvm-design.md).
|
||||
//! - Kontrollzucker ist abgesenkt: `SELECT CASE` zu Vergleichsketten,
|
||||
//! `ELSEIF` zu verschachteltem `If`, `EXIT FOR/DO` zu `Goto` auf
|
||||
//! synthetisierte Labels, `SWAP` zu Zuweisungen über einen Temp-Slot.
|
||||
//! - `STATIC`-Locals und versteckte Temps liegen im globalen Slot-Bereich.
|
||||
//!
|
||||
//! Ein vollständiges, korrektes HIR ist nur bei diagnose-freier Analyse
|
||||
//! garantiert.
|
||||
|
||||
/// Numerischer Skalartyp (Kürzel wie im Opcode-Satz: I2/I4/CY/R4/R8).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NumTy {
|
||||
Int,
|
||||
Lng,
|
||||
Cur,
|
||||
Sng,
|
||||
Dbl,
|
||||
}
|
||||
|
||||
/// Ganzzahlbreite der Logik-Operatoren.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IntKind {
|
||||
I2,
|
||||
I4,
|
||||
}
|
||||
|
||||
/// Aufgelöster HIR-Typ.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum HTy {
|
||||
Num(NumTy),
|
||||
Str,
|
||||
/// Fester String mit Zeichenlänge (Zuweisung padded/kürzt).
|
||||
FixedStr(u32),
|
||||
/// Benutzerdefinierter Typ (Index in `HirModule::udts`).
|
||||
Udt(u16),
|
||||
}
|
||||
|
||||
impl HTy {
|
||||
pub fn num(&self) -> Option<NumTy> {
|
||||
match self {
|
||||
HTy::Num(n) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn is_str(&self) -> bool {
|
||||
matches!(self, HTy::Str | HTy::FixedStr(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Slot-Referenz: globaler Bereich (Modulvariablen, STATICs, versteckte
|
||||
/// Temps des Hauptprogramms) oder Frame-lokal (Parameter zuerst).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VarSlot {
|
||||
Global(u16),
|
||||
Local(u16),
|
||||
}
|
||||
|
||||
/// Sprungziel innerhalb eines Prozedurrumpfs.
|
||||
pub type LabelId = u16;
|
||||
|
||||
/// Variablen-/Slotbeschreibung (auch für die Debugger-Inspektion).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HVar {
|
||||
pub name: String,
|
||||
pub ty: HTy,
|
||||
pub array: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HUdt {
|
||||
pub name: String,
|
||||
pub fields: Vec<(String, HTy)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HProcKind {
|
||||
/// Hauptprogramm (Modulrumpf) — immer Prozedur 0.
|
||||
Main,
|
||||
Sub,
|
||||
Function,
|
||||
/// `DEF FN` — Parameter BYVAL, freie Namen binden an Modulvariablen.
|
||||
DefFn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HParam {
|
||||
pub name: String,
|
||||
pub ty: HTy,
|
||||
pub array: bool,
|
||||
/// Skalar-Parameter, der als Referenz übergeben wird (Arrays und
|
||||
/// UDTs sind implizit immer Referenzen auf ihr Handle).
|
||||
pub by_ref: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HProc {
|
||||
pub name: String,
|
||||
pub kind: HProcKind,
|
||||
pub params: Vec<HParam>,
|
||||
/// Alle Frame-Slots; `params.len()` erste Einträge sind die Parameter.
|
||||
pub locals: Vec<HVar>,
|
||||
/// Slot der Rückgabevariablen (Function/DefFn).
|
||||
pub ret_slot: Option<VarSlot>,
|
||||
pub ret_ty: Option<HTy>,
|
||||
pub body: Vec<HStmt>,
|
||||
/// Anzahl vergebener LabelIds in diesem Rumpf.
|
||||
pub label_count: u16,
|
||||
}
|
||||
|
||||
/// Eine DATA-Konstante (unkonvertiert; `READ` konvertiert zur Laufzeit).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataItem {
|
||||
pub text: String,
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HirModule {
|
||||
pub name: String,
|
||||
pub globals: Vec<HVar>,
|
||||
pub udts: Vec<HUdt>,
|
||||
/// Prozeduren; Index 0 ist das Hauptprogramm.
|
||||
pub procs: Vec<HProc>,
|
||||
pub data: Vec<DataItem>,
|
||||
pub option_base: u8,
|
||||
}
|
||||
|
||||
// ---- Ausdrücke -------------------------------------------------------------
|
||||
|
||||
/// L-Wert: Basis-Slot, optional Array-Indizes, optional UDT-Feldpfad.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HPlace {
|
||||
pub base: VarSlot,
|
||||
/// Skalar-BYREF-Parameter: der Slot enthält eine Referenz.
|
||||
pub base_is_ref: bool,
|
||||
/// Array-Elementzugriff (leer = Skalar bzw. ganzes Array).
|
||||
pub indices: Vec<HExpr>,
|
||||
/// UDT-Feldpfad (Feldindizes je Ebene).
|
||||
pub fields: Vec<u16>,
|
||||
/// Typ des adressierten Werts.
|
||||
pub ty: HTy,
|
||||
/// Element-/Basistyp und Dimension für Auto-DIM impliziter Arrays.
|
||||
pub array_elem: Option<(HTy, u8)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HArith {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
IDiv,
|
||||
Mod,
|
||||
Pow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HCmp {
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HLogic {
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Eqv,
|
||||
Imp,
|
||||
}
|
||||
|
||||
/// Vergleichs-Operandentyp.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CmpKind {
|
||||
Num(NumTy),
|
||||
Str,
|
||||
}
|
||||
|
||||
/// Bibliotheksfunktionen/-anweisungen der Phase-2-Scheibe. Der
|
||||
/// Diskriminant ist zugleich der stabile Index der Dispatch-Tabelle
|
||||
/// (`CALL_BUILTIN`); Phase 3 erweitert ausschließlich am Ende.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum Builtin {
|
||||
// Strings
|
||||
Len,
|
||||
LeftS,
|
||||
RightS,
|
||||
MidS,
|
||||
InstrF,
|
||||
UcaseS,
|
||||
LcaseS,
|
||||
LtrimS,
|
||||
RtrimS,
|
||||
SpaceS,
|
||||
StringS,
|
||||
ChrS,
|
||||
Asc,
|
||||
StrS,
|
||||
Val,
|
||||
HexS,
|
||||
OctS,
|
||||
/// MID$-Anweisung als reine Funktion: (ziel, start, länge, ersatz) → neuer String.
|
||||
MidAssign,
|
||||
// Mathematik
|
||||
Abs,
|
||||
Sgn,
|
||||
IntF,
|
||||
Fix,
|
||||
Sqr,
|
||||
Exp,
|
||||
Log,
|
||||
Sin,
|
||||
Cos,
|
||||
Tan,
|
||||
Atn,
|
||||
Rnd,
|
||||
Randomize,
|
||||
// Konsole (PRINT-Familie; Wirkung über Host + Druckspalten-Zustand)
|
||||
PrintVal,
|
||||
PrintStrLit,
|
||||
PrintComma,
|
||||
PrintTab,
|
||||
PrintSpc,
|
||||
PrintNewline,
|
||||
// Sonstiges
|
||||
Timer,
|
||||
DateS,
|
||||
TimeS,
|
||||
CommandS,
|
||||
Doevents,
|
||||
Sleep,
|
||||
Beep,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HExpr {
|
||||
Int(i16),
|
||||
Lng(i32),
|
||||
Sng(f32),
|
||||
Dbl(f64),
|
||||
Cur(i64),
|
||||
Str(String),
|
||||
Load(Box<HPlace>),
|
||||
/// Numerische Konvertierung nach Matrix (Rundung/Überlauf).
|
||||
Conv {
|
||||
from: NumTy,
|
||||
to: NumTy,
|
||||
arg: Box<HExpr>,
|
||||
},
|
||||
/// Kürzen/Padden auf feste Stringlänge.
|
||||
FixStr {
|
||||
len: u32,
|
||||
arg: Box<HExpr>,
|
||||
},
|
||||
Neg {
|
||||
ty: NumTy,
|
||||
arg: Box<HExpr>,
|
||||
},
|
||||
/// Monomorphe Arithmetik: beide Operanden und das Ergebnis haben `ty`
|
||||
/// (bei `Div`/`Pow` nur R4/R8, bei `IDiv`/`Mod` nur I2/I4).
|
||||
Bin {
|
||||
op: HArith,
|
||||
ty: NumTy,
|
||||
l: Box<HExpr>,
|
||||
r: Box<HExpr>,
|
||||
},
|
||||
Not {
|
||||
ty: IntKind,
|
||||
arg: Box<HExpr>,
|
||||
},
|
||||
Logic {
|
||||
op: HLogic,
|
||||
ty: IntKind,
|
||||
l: Box<HExpr>,
|
||||
r: Box<HExpr>,
|
||||
},
|
||||
/// Vergleich; Ergebnis ist INTEGER (−1/0).
|
||||
Cmp {
|
||||
op: HCmp,
|
||||
ty: CmpKind,
|
||||
l: Box<HExpr>,
|
||||
r: Box<HExpr>,
|
||||
},
|
||||
Concat(Box<HExpr>, Box<HExpr>),
|
||||
/// FUNCTION-/DEF FN-Aufruf.
|
||||
FnCall {
|
||||
proc: u16,
|
||||
args: Vec<HArg>,
|
||||
ret: HTy,
|
||||
},
|
||||
Builtin {
|
||||
b: Builtin,
|
||||
args: Vec<HExpr>,
|
||||
ret: HTy,
|
||||
},
|
||||
/// LBOUND/UBOUND eines Arrays.
|
||||
ArrayBound {
|
||||
lower: bool,
|
||||
place: Box<HPlace>,
|
||||
dim: Box<HExpr>,
|
||||
},
|
||||
Err,
|
||||
Erl,
|
||||
/// Dokumentiertes, aber erst in einer späteren Phase implementiertes
|
||||
/// Feature: löst zur Laufzeit Fehler 73 „Advanced feature" aus.
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
|
||||
/// Prozedurargument.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HArg {
|
||||
ByRef(HPlace),
|
||||
/// Ganzes Array als Referenz (`prozedur a()`).
|
||||
ArrayRef(HPlace),
|
||||
ByVal(HExpr),
|
||||
}
|
||||
|
||||
// ---- Anweisungen -----------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HPrintItem {
|
||||
Val(HExpr),
|
||||
Tab(HExpr),
|
||||
Spc(HExpr),
|
||||
Comma,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HResume {
|
||||
Retry,
|
||||
Next,
|
||||
Label(LabelId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HStmt {
|
||||
pub line: u32,
|
||||
pub kind: HStmtKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HStmtKind {
|
||||
/// Numerische Zeilennummer durchlaufen (setzt `ERL`).
|
||||
SetErl(u32),
|
||||
Label(LabelId),
|
||||
Assign {
|
||||
place: HPlace,
|
||||
value: HExpr,
|
||||
},
|
||||
Print {
|
||||
items: Vec<HPrintItem>,
|
||||
/// Endet die Anweisung mit `;`/`,` (kein Zeilenumbruch)?
|
||||
trailing: bool,
|
||||
},
|
||||
Input {
|
||||
line_mode: bool,
|
||||
prompt: Option<String>,
|
||||
/// Fragezeichen nach dem Prompt (`;`-Form).
|
||||
question: bool,
|
||||
targets: Vec<HPlace>,
|
||||
},
|
||||
If {
|
||||
cond: HExpr,
|
||||
then: Vec<HStmt>,
|
||||
els: Vec<HStmt>,
|
||||
},
|
||||
/// DO/LOOP, WHILE/WEND (nur `pre`) — Bedingungen: (ist_until, Ausdruck).
|
||||
Loop {
|
||||
pre: Option<(bool, HExpr)>,
|
||||
post: Option<(bool, HExpr)>,
|
||||
body: Vec<HStmt>,
|
||||
exit_label: LabelId,
|
||||
},
|
||||
For {
|
||||
var: HPlace,
|
||||
ty: NumTy,
|
||||
from: HExpr,
|
||||
to: HExpr,
|
||||
step: Option<HExpr>,
|
||||
/// Versteckte Slots für Grenze/Schritt (Schritt nur wenn dynamisch).
|
||||
limit_slot: VarSlot,
|
||||
step_slot: Option<VarSlot>,
|
||||
body: Vec<HStmt>,
|
||||
exit_label: LabelId,
|
||||
},
|
||||
Goto(LabelId),
|
||||
Gosub(LabelId),
|
||||
OnGoto {
|
||||
sel: HExpr,
|
||||
gosub: bool,
|
||||
targets: Vec<LabelId>,
|
||||
},
|
||||
ReturnGosub(Option<LabelId>),
|
||||
/// EXIT SUB/FUNCTION/DEF bzw. Rumpfende.
|
||||
ExitProc,
|
||||
CallSub {
|
||||
proc: u16,
|
||||
args: Vec<HArg>,
|
||||
},
|
||||
BuiltinStmt {
|
||||
b: Builtin,
|
||||
args: Vec<HExpr>,
|
||||
},
|
||||
OnError {
|
||||
local: bool,
|
||||
/// `None` = `GOTO 0` (deaktivieren).
|
||||
target: Option<LabelId>,
|
||||
},
|
||||
OnErrorResumeNext {
|
||||
local: bool,
|
||||
},
|
||||
Resume(HResume),
|
||||
/// `ERROR n`.
|
||||
RaiseError(HExpr),
|
||||
Read(Vec<HPlace>),
|
||||
/// Ziel als Index in `HirModule::data` (0 = Anfang).
|
||||
Restore(u32),
|
||||
/// DIM/REDIM eines Arrays: Grenzen (lo, hi) je Dimension.
|
||||
Dim {
|
||||
slot: VarSlot,
|
||||
elem: HTy,
|
||||
dims: Vec<(HExpr, HExpr)>,
|
||||
redim: bool,
|
||||
},
|
||||
Erase(Vec<VarSlot>),
|
||||
End,
|
||||
Stop,
|
||||
System,
|
||||
/// Dokumentiertes Feature einer späteren Phase → Laufzeitfehler 73.
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
|
||||
/// Konstanter Literalwert eines Ausdrucks (z. B. FOR-STEP-Erkennung im
|
||||
/// Codegen: konstanter Schritt braucht keinen versteckten Slot).
|
||||
pub fn literal_value(e: &HExpr) -> Option<f64> {
|
||||
match e {
|
||||
HExpr::Int(v) => Some(*v as f64),
|
||||
HExpr::Lng(v) => Some(*v as f64),
|
||||
HExpr::Sng(v) => Some(*v as f64),
|
||||
HExpr::Dbl(v) => Some(*v),
|
||||
HExpr::Cur(v) => Some(*v as f64 / 10_000.0),
|
||||
HExpr::Conv { arg, .. } => literal_value(arg),
|
||||
HExpr::Neg { arg, .. } => literal_value(arg).map(|v| -v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//! übersetzt.
|
||||
|
||||
pub mod ast;
|
||||
pub mod hir;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod sema;
|
||||
@@ -35,15 +36,18 @@ impl std::fmt::Display for Diagnostic {
|
||||
pub struct Analysis {
|
||||
pub module: ast::Module,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
/// Typisiertes HIR (vollständig nur bei leeren Diagnosen).
|
||||
pub hir: Option<hir::HirModule>,
|
||||
}
|
||||
|
||||
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung.
|
||||
/// Komplette Pipeline: Lexen → Parsen → semantische Prüfung + Lowering.
|
||||
pub fn analyze_source(module_name: &str, source: &str) -> Analysis {
|
||||
let lexed = lexer::lex(source);
|
||||
let mut diagnostics = lexed.diagnostics;
|
||||
let parsed = parser::parse(module_name, &lexed.tokens);
|
||||
diagnostics.extend(parsed.diagnostics);
|
||||
diagnostics.extend(sema::check(&parsed.module));
|
||||
let (hir, sema_diags) = sema::lower(&parsed.module);
|
||||
diagnostics.extend(sema_diags);
|
||||
diagnostics.sort_by_key(|d| (d.pos.line, d.pos.column));
|
||||
Analysis { module: parsed.module, diagnostics }
|
||||
Analysis { module: parsed.module, diagnostics, hir }
|
||||
}
|
||||
|
||||
@@ -285,15 +285,15 @@ impl<'a> P<'a> {
|
||||
return None;
|
||||
}
|
||||
self.advance();
|
||||
Some(Stmt::End)
|
||||
Some(Stmt::End(pos))
|
||||
}
|
||||
TokenKind::Kw(Kw::Stop) => {
|
||||
self.advance();
|
||||
Some(Stmt::StopStmt)
|
||||
Some(Stmt::StopStmt(pos))
|
||||
}
|
||||
TokenKind::Kw(Kw::System) => {
|
||||
self.advance();
|
||||
Some(Stmt::System)
|
||||
Some(Stmt::System(pos))
|
||||
}
|
||||
TokenKind::Kw(Kw::Exit) => {
|
||||
self.advance();
|
||||
@@ -1498,7 +1498,16 @@ impl<'a> P<'a> {
|
||||
}
|
||||
// Impliziter Aufruf: `name [arg [, arg …]]`
|
||||
if let Expr::Name { name, suffix, args, .. } = target {
|
||||
let mut call_args = args.unwrap_or_default();
|
||||
// Ohne CALL-Keyword sind Klammern Wert-Klammern (BYVAL), keine
|
||||
// Argumentlisten-Klammern: `Foo (n%)` übergibt `(n%)`.
|
||||
let mut call_args: Vec<Expr> = args
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|a| match a {
|
||||
p @ Expr::Paren(_) => p,
|
||||
other => Expr::Paren(Box::new(other)),
|
||||
})
|
||||
.collect();
|
||||
if !self.at_stmt_end() && call_args.is_empty() {
|
||||
call_args = self.parse_arg_list_to_stmt_end();
|
||||
}
|
||||
@@ -1722,7 +1731,7 @@ impl<'a> P<'a> {
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
self.err("Expected: )");
|
||||
}
|
||||
Some(e)
|
||||
Some(Expr::Paren(Box::new(e)))
|
||||
}
|
||||
TokenKind::Ident { .. } => self.parse_name_ref(),
|
||||
_ => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,14 @@ fn korpus_parst_und_wird_typgeprueft() {
|
||||
"{name}: {:?}",
|
||||
analysis.diagnostics
|
||||
);
|
||||
// Phase 2: diagnose-freie Module liefern ein vollständiges HIR
|
||||
// (Hauptprogramm vorhanden, Codegen-fähig ohne erneute Auflösung).
|
||||
let hir = analysis.hir.unwrap_or_else(|| panic!("{name}: HIR fehlt"));
|
||||
assert!(!hir.procs.is_empty(), "{name}: HIR ohne Hauptprogramm");
|
||||
assert!(
|
||||
!hir.procs[0].body.is_empty(),
|
||||
"{name}: leerer HIR-Hauptrumpf"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
|
||||
|
||||
623
crates/tb-runtime/src/builtins.rs
Normal file
623
crates/tb-runtime/src/builtins.rs
Normal file
@@ -0,0 +1,623 @@
|
||||
//! Builtin-Dispatch-Tabelle (`CALL_BUILTIN`-ABI): Argumente kommen vom
|
||||
//! Operandenstack der VM, der Index steht im Opcode. Die Tabelle ist ohne
|
||||
//! Änderung am Opcode-Satz erweiterbar — Phase 3 füllt sie auf.
|
||||
//!
|
||||
//! Die Indizes (`ids::*`) sind stabil; der Codegenerator (`tb-vm`)
|
||||
//! bildet `hir::Builtin` über ein erschöpfendes `match` darauf ab.
|
||||
|
||||
use crate::console::PrintState;
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::format;
|
||||
use crate::host::Host;
|
||||
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Laufzeitzustand der Bibliothek (PRNG, Druckspalte, Kommandozeile).
|
||||
pub struct RtState {
|
||||
pub print: PrintState,
|
||||
rng: u32,
|
||||
rnd_last: f32,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
impl Default for RtState {
|
||||
fn default() -> Self {
|
||||
RtState {
|
||||
print: PrintState::default(),
|
||||
// Startzustand des Vorbild-PRNG; die exakte
|
||||
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
|
||||
// („RND/RANDOMIZE — kompatibler PRNG").
|
||||
rng: 0x50000,
|
||||
rnd_last: 0.0,
|
||||
command: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RtState {
|
||||
fn rng_next(&mut self) -> f32 {
|
||||
self.rng = self.rng.wrapping_mul(0xFD43FD).wrapping_add(0xC39EC3) & 0xFF_FFFF;
|
||||
self.rnd_last = self.rng as f32 / 16_777_216.0;
|
||||
self.rnd_last
|
||||
}
|
||||
}
|
||||
|
||||
pub type BuiltinFn =
|
||||
fn(&mut RtState, &mut dyn Host, &mut [Value]) -> Result<Option<Value>, RuntimeError>;
|
||||
|
||||
/// Stabile Tabellenindizes (Ordnung = `hir::Builtin` des Frontends).
|
||||
pub mod ids {
|
||||
pub const LEN: u16 = 0;
|
||||
pub const LEFT_S: u16 = 1;
|
||||
pub const RIGHT_S: u16 = 2;
|
||||
pub const MID_S: u16 = 3;
|
||||
pub const INSTR: u16 = 4;
|
||||
pub const UCASE_S: u16 = 5;
|
||||
pub const LCASE_S: u16 = 6;
|
||||
pub const LTRIM_S: u16 = 7;
|
||||
pub const RTRIM_S: u16 = 8;
|
||||
pub const SPACE_S: u16 = 9;
|
||||
pub const STRING_S: u16 = 10;
|
||||
pub const CHR_S: u16 = 11;
|
||||
pub const ASC: u16 = 12;
|
||||
pub const STR_S: u16 = 13;
|
||||
pub const VAL: u16 = 14;
|
||||
pub const HEX_S: u16 = 15;
|
||||
pub const OCT_S: u16 = 16;
|
||||
pub const MID_ASSIGN: u16 = 17;
|
||||
pub const ABS: u16 = 18;
|
||||
pub const SGN: u16 = 19;
|
||||
pub const INT_F: u16 = 20;
|
||||
pub const FIX: u16 = 21;
|
||||
pub const SQR: u16 = 22;
|
||||
pub const EXP: u16 = 23;
|
||||
pub const LOG: u16 = 24;
|
||||
pub const SIN: u16 = 25;
|
||||
pub const COS: u16 = 26;
|
||||
pub const TAN: u16 = 27;
|
||||
pub const ATN: u16 = 28;
|
||||
pub const RND: u16 = 29;
|
||||
pub const RANDOMIZE: u16 = 30;
|
||||
pub const PRINT_VAL: u16 = 31;
|
||||
pub const PRINT_STR_LIT: u16 = 32;
|
||||
pub const PRINT_COMMA: u16 = 33;
|
||||
pub const PRINT_TAB: u16 = 34;
|
||||
pub const PRINT_SPC: u16 = 35;
|
||||
pub const PRINT_NEWLINE: u16 = 36;
|
||||
pub const TIMER: u16 = 37;
|
||||
pub const DATE_S: u16 = 38;
|
||||
pub const TIME_S: u16 = 39;
|
||||
pub const COMMAND_S: u16 = 40;
|
||||
pub const DOEVENTS: u16 = 41;
|
||||
pub const SLEEP: u16 = 42;
|
||||
pub const BEEP: u16 = 43;
|
||||
pub const COUNT: u16 = 44;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
const TABLE: &[BuiltinFn] = &[
|
||||
bi_len,
|
||||
bi_left,
|
||||
bi_right,
|
||||
bi_mid,
|
||||
bi_instr,
|
||||
bi_ucase,
|
||||
bi_lcase,
|
||||
bi_ltrim,
|
||||
bi_rtrim,
|
||||
bi_space,
|
||||
bi_string,
|
||||
bi_chr,
|
||||
bi_asc,
|
||||
bi_str,
|
||||
bi_val,
|
||||
bi_hex,
|
||||
bi_oct,
|
||||
bi_mid_assign,
|
||||
bi_abs,
|
||||
bi_sgn,
|
||||
bi_int,
|
||||
bi_fix,
|
||||
bi_sqr,
|
||||
bi_exp,
|
||||
bi_log,
|
||||
bi_sin,
|
||||
bi_cos,
|
||||
bi_tan,
|
||||
bi_atn,
|
||||
bi_rnd,
|
||||
bi_randomize,
|
||||
bi_print_val,
|
||||
bi_print_val, // PRINT_STR_LIT: identisch (Strings per Tag)
|
||||
bi_print_comma,
|
||||
bi_print_tab,
|
||||
bi_print_spc,
|
||||
bi_print_newline,
|
||||
bi_timer,
|
||||
bi_date,
|
||||
bi_time,
|
||||
bi_command,
|
||||
bi_doevents,
|
||||
bi_sleep,
|
||||
bi_beep,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
}
|
||||
|
||||
// ---- Argument-Hilfen --------------------------------------------------------
|
||||
|
||||
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
|
||||
match args.get(i) {
|
||||
Some(Value::Str(s)) => Ok(s.clone()),
|
||||
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_i32(args: &[Value], i: usize) -> Result<i32, RuntimeError> {
|
||||
match args.get(i) {
|
||||
Some(Value::Lng(v)) => Ok(*v),
|
||||
Some(Value::Int(v)) => Ok(*v as i32),
|
||||
Some(v) => Ok(as_f64(v) as i32),
|
||||
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_f64(args: &[Value], i: usize) -> Result<f64, RuntimeError> {
|
||||
match args.get(i) {
|
||||
Some(v) => Ok(as_f64(v)),
|
||||
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
}
|
||||
}
|
||||
|
||||
fn s_ok(s: String) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Str(Rc::from(s.as_str()))))
|
||||
}
|
||||
|
||||
// ---- Strings ----------------------------------------------------------------
|
||||
|
||||
fn bi_len(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
let n = s.chars().count();
|
||||
Ok(Some(Value::Int(i16::try_from(n).unwrap_or(i16::MAX))))
|
||||
}
|
||||
|
||||
fn bi_left(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
let n = arg_i32(a, 1)?;
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
s_ok(s.chars().take(n as usize).collect())
|
||||
}
|
||||
|
||||
fn bi_right(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
let n = arg_i32(a, 1)?;
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let len = s.chars().count();
|
||||
let skip = len.saturating_sub(n as usize);
|
||||
s_ok(s.chars().skip(skip).collect())
|
||||
}
|
||||
|
||||
fn bi_mid(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
let start = arg_i32(a, 1)?;
|
||||
let len = arg_i32(a, 2)?; // -1 = Rest
|
||||
if start < 1 || len < -1 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let iter = s.chars().skip((start - 1) as usize);
|
||||
if len < 0 {
|
||||
s_ok(iter.collect())
|
||||
} else {
|
||||
s_ok(iter.take(len as usize).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_instr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let start = arg_i32(a, 0)?;
|
||||
let s = arg_str(a, 1)?;
|
||||
let t = arg_str(a, 2)?;
|
||||
if start < 1 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let slen = chars.len();
|
||||
if start as usize > slen {
|
||||
// Vorbild: Start hinter Stringende → 0
|
||||
return Ok(Some(Value::Int(0)));
|
||||
}
|
||||
if t.is_empty() {
|
||||
return Ok(Some(Value::Int(start as i16)));
|
||||
}
|
||||
let hay: String = chars[(start - 1) as usize..].iter().collect();
|
||||
match hay.find(&*t) {
|
||||
Some(byte_pos) => {
|
||||
let char_pos = hay[..byte_pos].chars().count();
|
||||
Ok(Some(Value::Int((start as usize + char_pos) as i16)))
|
||||
}
|
||||
None => Ok(Some(Value::Int(0))),
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_ucase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
s_ok(s.to_uppercase())
|
||||
}
|
||||
|
||||
fn bi_lcase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
s_ok(s.to_lowercase())
|
||||
}
|
||||
|
||||
fn bi_ltrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
s_ok(s.trim_start_matches(' ').to_string())
|
||||
}
|
||||
|
||||
fn bi_rtrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
s_ok(s.trim_end_matches(' ').to_string())
|
||||
}
|
||||
|
||||
fn bi_space(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
s_ok(" ".repeat(n as usize))
|
||||
}
|
||||
|
||||
fn bi_string(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let ch = match a.get(1) {
|
||||
Some(Value::Str(s)) => s.chars().next().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?,
|
||||
Some(v) => {
|
||||
let code = as_f64(v) as i64;
|
||||
char::from_u32(u32::try_from(code).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?)
|
||||
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?
|
||||
}
|
||||
None => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
};
|
||||
s_ok(ch.to_string().repeat(n as usize))
|
||||
}
|
||||
|
||||
fn bi_chr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
let ch = u32::try_from(n)
|
||||
.ok()
|
||||
.and_then(char::from_u32)
|
||||
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
s_ok(ch.to_string())
|
||||
}
|
||||
|
||||
fn bi_asc(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
match s.chars().next() {
|
||||
// Codepoints > 32767 passen nicht in INTEGER → Overflow wie beim
|
||||
// Vorbild bei Bereichsüberschreitung.
|
||||
Some(c) => i16::try_from(c as u32)
|
||||
.map(|v| Some(Value::Int(v)))
|
||||
.map_err(|_| RuntimeError::OVERFLOW),
|
||||
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_str(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
s_ok(format::format_str_fn(v))
|
||||
}
|
||||
|
||||
fn bi_val(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = arg_str(a, 0)?;
|
||||
Ok(Some(Value::Dbl(format::val(&s))))
|
||||
}
|
||||
|
||||
fn bi_hex(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
s_ok(format!("{:X}", n as u32))
|
||||
}
|
||||
|
||||
fn bi_oct(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
s_ok(format!("{:o}", n as u32))
|
||||
}
|
||||
|
||||
/// MID$-Anweisung als Funktion: (ziel, start, länge, ersatz) → neuer
|
||||
/// String; die Länge des Ziels bleibt unverändert.
|
||||
fn bi_mid_assign(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let target = arg_str(a, 0)?;
|
||||
let start = arg_i32(a, 1)?;
|
||||
let len = arg_i32(a, 2)?; // -1 = Länge des Ersatzes
|
||||
let repl = arg_str(a, 3)?;
|
||||
if start < 1 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let tchars: Vec<char> = target.chars().collect();
|
||||
let rchars: Vec<char> = repl.chars().collect();
|
||||
let start0 = (start - 1) as usize;
|
||||
if start0 >= tchars.len() {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let max_repl = if len < 0 { rchars.len() } else { (len as usize).min(rchars.len()) };
|
||||
let n = max_repl.min(tchars.len() - start0);
|
||||
let mut out = tchars.clone();
|
||||
out[start0..start0 + n].copy_from_slice(&rchars[..n]);
|
||||
s_ok(out.into_iter().collect())
|
||||
}
|
||||
|
||||
// ---- Mathematik --------------------------------------------------------------
|
||||
|
||||
fn bi_abs(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
|
||||
Value::Int(v) => Value::Int(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
|
||||
Value::Lng(v) => Value::Lng(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
|
||||
Value::Sng(v) => Value::Sng(v.abs()),
|
||||
Value::Dbl(v) => Value::Dbl(v.abs()),
|
||||
Value::Cur(v) => Value::Cur(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}))
|
||||
}
|
||||
|
||||
fn bi_sgn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let x = arg_f64(a, 0)?;
|
||||
Ok(Some(Value::Int(if x > 0.0 {
|
||||
1
|
||||
} else if x < 0.0 {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
})))
|
||||
}
|
||||
|
||||
fn bi_int(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
|
||||
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
|
||||
Value::Sng(v) => Value::Sng(v.floor()),
|
||||
Value::Dbl(v) => Value::Dbl(v.floor()),
|
||||
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).floor())?),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}))
|
||||
}
|
||||
|
||||
fn bi_fix(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
|
||||
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
|
||||
Value::Sng(v) => Value::Sng(v.trunc()),
|
||||
Value::Dbl(v) => Value::Dbl(v.trunc()),
|
||||
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).trunc())?),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}))
|
||||
}
|
||||
|
||||
fn bi_sqr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let x = arg_f64(a, 0)?;
|
||||
if x < 0.0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
Ok(Some(Value::Dbl(x.sqrt())))
|
||||
}
|
||||
|
||||
fn bi_exp(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let x = arg_f64(a, 0)?;
|
||||
let r = x.exp();
|
||||
if !r.is_finite() {
|
||||
return Err(RuntimeError::OVERFLOW);
|
||||
}
|
||||
Ok(Some(Value::Dbl(r)))
|
||||
}
|
||||
|
||||
fn bi_log(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let x = arg_f64(a, 0)?;
|
||||
if x <= 0.0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
Ok(Some(Value::Dbl(x.ln())))
|
||||
}
|
||||
|
||||
fn bi_sin(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(arg_f64(a, 0)?.sin())))
|
||||
}
|
||||
fn bi_cos(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(arg_f64(a, 0)?.cos())))
|
||||
}
|
||||
fn bi_tan(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(arg_f64(a, 0)?.tan())))
|
||||
}
|
||||
fn bi_atn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(arg_f64(a, 0)?.atan())))
|
||||
}
|
||||
|
||||
fn bi_rnd(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let v = if a.is_empty() {
|
||||
st.rng_next()
|
||||
} else {
|
||||
let x = arg_f64(a, 0)?;
|
||||
if x == 0.0 {
|
||||
st.rnd_last
|
||||
} else {
|
||||
if x < 0.0 {
|
||||
// Neu aussäen aus dem Argument (deterministisch).
|
||||
st.rng = ((-x).to_bits() >> 20) as u32 & 0xFF_FFFF;
|
||||
}
|
||||
st.rng_next()
|
||||
}
|
||||
};
|
||||
Ok(Some(Value::Sng(v)))
|
||||
}
|
||||
|
||||
fn bi_randomize(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let x = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
|
||||
// 16 Bit aus dem Argument in Bits 8–23 des Zustands (Vorbild-Schema;
|
||||
// die exakte Mischung klärt die Phase-3-Aufgabe „RND/RANDOMIZE —
|
||||
// kompatibler PRNG" in PLAN.md).
|
||||
let b = x.to_bits();
|
||||
let m = ((b >> 32) ^ (b >> 48)) as u16;
|
||||
st.rng = ((m as u32) << 8) | (st.rng & 0xFF);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ---- Konsole ------------------------------------------------------------------
|
||||
|
||||
fn bi_print_val(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
st.print.print_value(host, v);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_print_comma(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
st.print.print_comma(host);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_print_tab(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
st.print.print_tab(host, n);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_print_spc(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
st.print.print_spc(host, n);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_print_newline(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
st.print.print_newline(host);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ---- Sonstiges ------------------------------------------------------------------
|
||||
|
||||
fn bi_timer(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
// Sekunden seit Mitternacht (UTC-basiert; lokale Zeitzone: Phase 3).
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs_f64() % 86_400.0)
|
||||
.unwrap_or(0.0);
|
||||
Ok(Some(Value::Sng(secs as f32)))
|
||||
}
|
||||
|
||||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
// Howard Hinnant, days→(y,m,d)
|
||||
let z = z + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let doe = (z - era * 146_097) as u64;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
||||
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
|
||||
(y + if m <= 2 { 1 } else { 0 }, m, d)
|
||||
}
|
||||
|
||||
fn bi_date(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let (y, m, d) = civil_from_days((secs / 86_400) as i64);
|
||||
s_ok(format!("{m:02}-{d:02}-{y:04}"))
|
||||
}
|
||||
|
||||
fn bi_time(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let s = secs % 86_400;
|
||||
s_ok(format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60))
|
||||
}
|
||||
|
||||
fn bi_command(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
s_ok(st.command.clone())
|
||||
}
|
||||
|
||||
fn bi_doevents(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
// Ereigniszustellung kommt mit Phase 4.
|
||||
Ok(Some(Value::Int(0)))
|
||||
}
|
||||
|
||||
fn bi_sleep(_: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
|
||||
host.sleep(secs);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_beep(_: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
host.write("\u{0007}");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host::CaptureHost;
|
||||
|
||||
fn call(id: u16, args: Vec<Value>) -> Result<Option<Value>, RuntimeError> {
|
||||
let mut st = RtState::default();
|
||||
let mut host = CaptureHost::default();
|
||||
let mut a = args;
|
||||
builtin_table()[id as usize](&mut st, &mut host, &mut a)
|
||||
}
|
||||
|
||||
fn s(v: &str) -> Value {
|
||||
Value::Str(Rc::from(v))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringfunktionen_randfaelle() {
|
||||
// VAL liest Präfix
|
||||
let Some(Value::Dbl(v)) = call(ids::VAL, vec![s(" 12.5abc")]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(v, 12.5);
|
||||
// STR$ mit führendem Leerzeichen
|
||||
let Some(Value::Str(r)) = call(ids::STR_S, vec![Value::Int(42)]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(&*r, " 42");
|
||||
// ASC("") → Fehler 5
|
||||
assert_eq!(
|
||||
call(ids::ASC, vec![s("")]).unwrap_err(),
|
||||
RuntimeError::ILLEGAL_FUNCTION_CALL
|
||||
);
|
||||
// MID$-Anweisung
|
||||
let Some(Value::Str(r)) =
|
||||
call(ids::MID_ASSIGN, vec![s("hallo"), Value::Lng(2), Value::Lng(2), s("EY")])
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(&*r, "hEYlo");
|
||||
// LEN zählt Zeichen (Unicode)
|
||||
let Some(Value::Int(n)) = call(ids::LEN, vec![s("äöü")]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(n, 3);
|
||||
// INSTR
|
||||
let Some(Value::Int(p)) =
|
||||
call(ids::INSTR, vec![Value::Lng(1), s("Terminal Basic"), s("Basic")]).unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(p, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_ueber_tabelle() {
|
||||
// Spec-Szenario: LEN über Tabellenindex liefert 3.
|
||||
let Some(Value::Int(n)) = call(ids::LEN, vec![s("abc")]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(n, 3);
|
||||
}
|
||||
}
|
||||
112
crates/tb-runtime/src/console.rs
Normal file
112
crates/tb-runtime/src/console.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Konsolen-Druckzustand: Spaltenverfolgung, PRINT-Formatierung,
|
||||
//! 14-Zeichen-Druckzonen, TAB/SPC. Wirkung ausschließlich über `Host`.
|
||||
|
||||
use crate::format::format_print;
|
||||
use crate::host::Host;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Breite einer Druckzone (Vorbild: 14 Zeichen).
|
||||
pub const ZONE_WIDTH: usize = 14;
|
||||
|
||||
/// Druckzustand (Spalte 0-basiert, in Zeichen).
|
||||
#[derive(Default)]
|
||||
pub struct PrintState {
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
impl PrintState {
|
||||
/// Text ausgeben und Spalte nachführen.
|
||||
pub fn write(&mut self, host: &mut dyn Host, s: &str) {
|
||||
host.write(s);
|
||||
match s.rfind('\n') {
|
||||
Some(i) => self.col = s[i + 1..].chars().count(),
|
||||
None => self.col += s.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
|
||||
/// Leerzeichen, Strings unverändert.
|
||||
pub fn print_value(&mut self, host: &mut dyn Host, v: &Value) {
|
||||
match v {
|
||||
Value::Str(s) => {
|
||||
let s = s.clone();
|
||||
self.write(host, &s);
|
||||
}
|
||||
_ => {
|
||||
let s = format_print(v);
|
||||
self.write(host, &s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
|
||||
pub fn print_comma(&mut self, host: &mut dyn Host) {
|
||||
let next = (self.col / ZONE_WIDTH + 1) * ZONE_WIDTH;
|
||||
let pad = next - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
}
|
||||
|
||||
/// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
|
||||
/// dahinter, zuerst Zeilenumbruch (Vorbild).
|
||||
pub fn print_tab(&mut self, host: &mut dyn Host, n: i32) {
|
||||
let target = (n.max(1) as usize) - 1;
|
||||
if self.col > target {
|
||||
self.write(host, "\n");
|
||||
}
|
||||
if target > self.col {
|
||||
let pad = target - self.col;
|
||||
self.write(host, &" ".repeat(pad));
|
||||
}
|
||||
}
|
||||
|
||||
/// `SPC(n)` — n Leerzeichen.
|
||||
pub fn print_spc(&mut self, host: &mut dyn Host, n: i32) {
|
||||
if n > 0 {
|
||||
self.write(host, &" ".repeat(n as usize));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_newline(&mut self, host: &mut dyn Host) {
|
||||
self.write(host, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host::CaptureHost;
|
||||
|
||||
#[test]
|
||||
fn druckzonen() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
// "a" (1 Zeichen) , → Spalte 14
|
||||
ps.write(&mut h, "a");
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "b");
|
||||
assert_eq!(h.output, "a b");
|
||||
assert_eq!(ps.col, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volle_zone_springt_zur_uebernaechsten() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.write(&mut h, "12345678901234"); // 14 Zeichen, Zone voll
|
||||
ps.print_comma(&mut h);
|
||||
ps.write(&mut h, "x");
|
||||
// x beginnt in Spalte 29 (1-basiert) = Index 28
|
||||
assert_eq!(h.output.chars().count(), 29);
|
||||
assert!(h.output.ends_with("x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zahlen_mit_vorzeichenspalte() {
|
||||
let mut ps = PrintState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
ps.print_value(&mut h, &Value::Int(1));
|
||||
ps.print_value(&mut h, &Value::Int(-2));
|
||||
ps.print_newline(&mut h);
|
||||
assert_eq!(h.output, " 1 -2 \n");
|
||||
}
|
||||
}
|
||||
240
crates/tb-runtime/src/format.rs
Normal file
240
crates/tb-runtime/src/format.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
//! Textdarstellung von Zahlen (PRINT/STR$) und `VAL`-Parsen nach der
|
||||
//! Matrix in docs/tbvm-design.md: SINGLE bis 7, DOUBLE bis 16
|
||||
//! signifikante Stellen, keine führende Null vor dem Dezimalpunkt,
|
||||
//! Exponentialform `E±xx` bzw. `D±xx` außerhalb des Festformat-Bereichs.
|
||||
|
||||
use crate::value::Value;
|
||||
|
||||
/// Nackte Ziffernfolge ohne Vorzeichenspalte (`1.5`, `-2.5`, `.5`, `1E+08`).
|
||||
pub fn format_number(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Int(x) => x.to_string(),
|
||||
Value::Lng(x) => x.to_string(),
|
||||
Value::Sng(x) => fmt_float(*x as f64, 7, 'E'),
|
||||
Value::Dbl(x) => fmt_float(*x, 16, 'D'),
|
||||
Value::Cur(c) => fmt_currency(*c),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// STR$-Stil: führendes Leerzeichen für nicht-negative Werte, `-` sonst;
|
||||
/// kein nachgestelltes Leerzeichen.
|
||||
pub fn format_str_fn(v: &Value) -> String {
|
||||
let s = format_number(v);
|
||||
if s.starts_with('-') {
|
||||
s
|
||||
} else {
|
||||
format!(" {s}")
|
||||
}
|
||||
}
|
||||
|
||||
/// PRINT-Stil: wie STR$ plus nachgestelltes Leerzeichen.
|
||||
pub fn format_print(v: &Value) -> String {
|
||||
let mut s = format_str_fn(v);
|
||||
s.push(' ');
|
||||
s
|
||||
}
|
||||
|
||||
fn fmt_currency(c: i64) -> String {
|
||||
let neg = c < 0;
|
||||
let abs = c.unsigned_abs();
|
||||
let int = abs / 10_000;
|
||||
let frac = abs % 10_000;
|
||||
let mut s = if frac == 0 {
|
||||
int.to_string()
|
||||
} else {
|
||||
let f = format!("{frac:04}");
|
||||
let f = f.trim_end_matches('0');
|
||||
if int == 0 {
|
||||
format!(".{f}")
|
||||
} else {
|
||||
format!("{int}.{f}")
|
||||
}
|
||||
};
|
||||
if neg {
|
||||
s.insert(0, '-');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Gleitkomma mit maximal `sig` signifikanten Stellen; Exponentialform
|
||||
/// (Marke `E` bzw. `D`), wenn der Dezimalexponent < −7 oder ≥ `sig` ist
|
||||
/// (Schwelle: Festlegung der Matrix in docs/tbvm-design.md; Verifikation
|
||||
/// gegen die Original-Hilfe ist in PLAN.md Phase 3 eingeplant).
|
||||
fn fmt_float(x: f64, sig: usize, expch: char) -> String {
|
||||
if x == 0.0 {
|
||||
return "0".to_string();
|
||||
}
|
||||
if x.is_nan() {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
if x.is_infinite() {
|
||||
return if x < 0.0 { "-1E+38".into() } else { "1E+38".into() };
|
||||
}
|
||||
let neg = x < 0.0;
|
||||
let ax = x.abs();
|
||||
// Mantisse/Exponent mit `sig` Stellen bestimmen.
|
||||
let e = format!("{:.*e}", sig - 1, ax); // z. B. "1.500000e0"
|
||||
let (mant, exp) = e.split_once('e').unwrap();
|
||||
let exp: i32 = exp.parse().unwrap();
|
||||
let mut digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
|
||||
// Rundungsüberlauf ("9.99…e5" → "10.0e5") normalisiert `format!` bereits.
|
||||
// Nachgestellte Nullen der Mantisse entfernen.
|
||||
while digits.len() > 1 && digits.ends_with('0') {
|
||||
digits.pop();
|
||||
}
|
||||
|
||||
let body = if exp < -7 || exp >= sig as i32 {
|
||||
// Exponentialform: D. Mantisse "d[.rest]"
|
||||
let mut m = String::new();
|
||||
m.push(digits.as_bytes()[0] as char);
|
||||
if digits.len() > 1 {
|
||||
m.push('.');
|
||||
m.push_str(&digits[1..]);
|
||||
}
|
||||
format!("{m}{expch}{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
|
||||
} else if exp >= 0 {
|
||||
let e = exp as usize;
|
||||
if (e + 1) >= digits.len() {
|
||||
// Ganzzahl, ggf. Nullen anhängen
|
||||
let mut s = digits.clone();
|
||||
s.push_str(&"0".repeat(e + 1 - digits.len()));
|
||||
s
|
||||
} else {
|
||||
format!("{}.{}", &digits[..e + 1], &digits[e + 1..])
|
||||
}
|
||||
} else {
|
||||
// 0 > exp >= -7: ".0…digits" ohne führende Null
|
||||
let zeros = (-exp - 1) as usize;
|
||||
let mut s = String::from(".");
|
||||
s.push_str(&"0".repeat(zeros));
|
||||
s.push_str(&digits);
|
||||
s
|
||||
};
|
||||
if neg {
|
||||
format!("-{body}")
|
||||
} else {
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
/// `VAL`: liest das führende Zahlenpräfix (Leerraum wird übersprungen;
|
||||
/// `&H`/`&O`-Präfixe wie beim Vorbild), ignoriert Restzeichen.
|
||||
pub fn val(s: &str) -> f64 {
|
||||
let t = s.trim_start();
|
||||
// Hex/Oktal
|
||||
if let Some(rest) = t.strip_prefix("&H").or_else(|| t.strip_prefix("&h")) {
|
||||
let hex: String = rest.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
|
||||
return i64::from_str_radix(&hex, 16).unwrap_or(0) as f64;
|
||||
}
|
||||
if let Some(rest) = t.strip_prefix("&O").or_else(|| t.strip_prefix("&o")) {
|
||||
let oct: String = rest.chars().take_while(|c| ('0'..='7').contains(c)).collect();
|
||||
return i64::from_str_radix(&oct, 8).unwrap_or(0) as f64;
|
||||
}
|
||||
let bytes: Vec<char> = t.chars().collect();
|
||||
let mut i = 0;
|
||||
let mut num = String::new();
|
||||
if i < bytes.len() && (bytes[i] == '+' || bytes[i] == '-') {
|
||||
num.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
let mut seen_digit = false;
|
||||
let mut seen_dot = false;
|
||||
while i < bytes.len() {
|
||||
let c = bytes[i];
|
||||
if c.is_ascii_digit() {
|
||||
seen_digit = true;
|
||||
num.push(c);
|
||||
i += 1;
|
||||
} else if c == '.' && !seen_dot {
|
||||
seen_dot = true;
|
||||
num.push(c);
|
||||
i += 1;
|
||||
} else if c == ' ' || c == '\t' {
|
||||
// Vorbild: VAL ignoriert eingebetteten Leerraum
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Exponent (E/D)
|
||||
if seen_digit && i < bytes.len() && matches!(bytes[i], 'e' | 'E' | 'd' | 'D') {
|
||||
let mut j = i + 1;
|
||||
let mut exp = String::new();
|
||||
if j < bytes.len() && (bytes[j] == '+' || bytes[j] == '-') {
|
||||
exp.push(bytes[j]);
|
||||
j += 1;
|
||||
}
|
||||
let mut exp_digits = false;
|
||||
while j < bytes.len() && bytes[j].is_ascii_digit() {
|
||||
exp.push(bytes[j]);
|
||||
exp_digits = true;
|
||||
j += 1;
|
||||
}
|
||||
if exp_digits {
|
||||
num.push('e');
|
||||
num.push_str(&exp);
|
||||
}
|
||||
}
|
||||
if !seen_digit {
|
||||
return 0.0;
|
||||
}
|
||||
num.parse().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::value::Value;
|
||||
|
||||
#[test]
|
||||
fn ganzzahlen() {
|
||||
assert_eq!(format_number(&Value::Int(42)), "42");
|
||||
assert_eq!(format_number(&Value::Int(-7)), "-7");
|
||||
assert_eq!(format_print(&Value::Int(1)), " 1 ");
|
||||
assert_eq!(format_print(&Value::Int(-2)), "-2 ");
|
||||
assert_eq!(format_str_fn(&Value::Int(42)), " 42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_darstellung() {
|
||||
assert_eq!(format_number(&Value::Sng(1.5)), "1.5");
|
||||
assert_eq!(format_number(&Value::Sng(-2.5)), "-2.5");
|
||||
assert_eq!(format_number(&Value::Sng(0.5)), ".5");
|
||||
assert_eq!(format_number(&Value::Sng(3.0)), "3");
|
||||
assert_eq!(format_number(&Value::Sng(1.0 / 3.0)), ".3333333");
|
||||
assert_eq!(format_number(&Value::Sng(1e8)), "1E+08");
|
||||
assert_eq!(format_number(&Value::Sng(9_999_999.0)), "9999999");
|
||||
assert_eq!(format_number(&Value::Sng(1e7)), "1E+07");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_darstellung() {
|
||||
assert_eq!(format_number(&Value::Dbl(1.5)), "1.5");
|
||||
assert_eq!(format_number(&Value::Dbl(1e16)), "1D+16");
|
||||
assert_eq!(
|
||||
format_number(&Value::Dbl(0.3333333333333333)),
|
||||
".3333333333333333"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency_darstellung() {
|
||||
assert_eq!(format_number(&Value::Cur(15_000)), "1.5");
|
||||
assert_eq!(format_number(&Value::Cur(10_000)), "1");
|
||||
assert_eq!(format_number(&Value::Cur(-12_345)), "-1.2345");
|
||||
assert_eq!(format_number(&Value::Cur(2_500)), ".25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn val_parsen() {
|
||||
assert_eq!(val(" 12.5abc"), 12.5);
|
||||
assert_eq!(val("-3"), -3.0);
|
||||
assert_eq!(val("1e2"), 100.0);
|
||||
assert_eq!(val("1D2"), 100.0);
|
||||
assert_eq!(val("&HFF"), 255.0);
|
||||
assert_eq!(val("&O10"), 8.0);
|
||||
assert_eq!(val("abc"), 0.0);
|
||||
assert_eq!(val(" 1 2 3"), 123.0); // eingebetteter Leerraum
|
||||
}
|
||||
}
|
||||
92
crates/tb-runtime/src/host.rs
Normal file
92
crates/tb-runtime/src/host.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! `Host`-Abstraktion für Konsolen-E/A (Design-Entscheidung D3):
|
||||
//! Alle Konsolenwirkungen der VM laufen über dieses Trait. Host-Aufrufe
|
||||
//! dürfen blockieren; die Abbruchprüfung (Strg+Untbr) obliegt dem Host.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{BufRead, Write as _};
|
||||
|
||||
pub trait Host {
|
||||
/// Text ausgeben (ohne implizite Zeilenumbrüche).
|
||||
fn write(&mut self, s: &str);
|
||||
/// Eine Eingabezeile lesen (ohne Zeilenende); `None` = Eingabeende.
|
||||
fn read_line(&mut self) -> Option<String>;
|
||||
/// Abbruchwunsch (Strg+Untbr)? Wird an Anweisungsgrenzen geprüft.
|
||||
fn interrupted(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
/// `SLEEP` — blockierend im Host.
|
||||
fn sleep(&mut self, _secs: f64) {}
|
||||
}
|
||||
|
||||
/// Konsolen-Host für `tbc run`: stdout/stdin.
|
||||
#[derive(Default)]
|
||||
pub struct ConsoleHost;
|
||||
|
||||
impl Host for ConsoleHost {
|
||||
fn write(&mut self, s: &str) {
|
||||
let mut out = std::io::stdout().lock();
|
||||
let _ = out.write_all(s.as_bytes());
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Option<String> {
|
||||
let mut line = String::new();
|
||||
match std::io::stdin().lock().read_line(&mut line) {
|
||||
Ok(0) => None,
|
||||
Ok(_) => {
|
||||
while line.ends_with('\n') || line.ends_with('\r') {
|
||||
line.pop();
|
||||
}
|
||||
Some(line)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep(&mut self, secs: f64) {
|
||||
if secs > 0.0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture-Host für Tests: zeichnet die Ausgabe byte-genau auf und
|
||||
/// liefert vorbereitete Eingabezeilen.
|
||||
#[derive(Default)]
|
||||
pub struct CaptureHost {
|
||||
pub output: String,
|
||||
pub input: VecDeque<String>,
|
||||
}
|
||||
|
||||
impl CaptureHost {
|
||||
pub fn with_input(lines: &[&str]) -> Self {
|
||||
CaptureHost {
|
||||
output: String::new(),
|
||||
input: lines.iter().map(|s| s.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for CaptureHost {
|
||||
fn write(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Option<String> {
|
||||
self.input.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_host_zeichnet_bytegenau_auf() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.write(" 1 2 ");
|
||||
h.write("\n");
|
||||
h.write("x");
|
||||
assert_eq!(h.output, " 1 2 \nx");
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,13 @@
|
||||
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
|
||||
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
|
||||
|
||||
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
|
||||
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
|
||||
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
|
||||
pub mod datetime; // DATE$, TIME$, TIMER
|
||||
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
|
||||
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
|
||||
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
|
||||
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
|
||||
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
|
||||
pub mod host; // Host-Trait (Konsole, Capture) — Entscheidung D3
|
||||
pub mod datetime; // DATE$, TIME$, TIMER
|
||||
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
|
||||
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
|
||||
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix
|
||||
|
||||
283
crates/tb-runtime/src/value.rs
Normal file
283
crates/tb-runtime/src/value.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
//! Laufzeitwerte der TBVM: Tagged Enum (Entwurf docs/tbvm-design.md),
|
||||
//! `Rc` statt GC (der Dialekt kennt keine Zyklen), Arrays/Records als
|
||||
//! geteilte Handles, Referenzwerte für BYREF-Parameter.
|
||||
//!
|
||||
//! Hier lebt außerdem die **Zahlenkonvertierungs-Matrix** (Banker's
|
||||
//! Rounding, Überlauf → Fehler 6) — die einzige Implementierung der in
|
||||
//! docs/tbvm-design.md dokumentierten Semantik.
|
||||
|
||||
use crate::errors::RuntimeError;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
Int(i16),
|
||||
Lng(i32),
|
||||
Sng(f32),
|
||||
Dbl(f64),
|
||||
Cur(i64),
|
||||
Str(Rc<str>),
|
||||
Arr(Rc<RefCell<ArrayObj>>),
|
||||
Rec(Rc<RefCell<RecordObj>>),
|
||||
/// Referenz (BYREF-Parameter-Slot).
|
||||
Ref(VarRef),
|
||||
/// Nicht-initialisiertes Array-/Record-Handle (Auto-DIM bei Zugriff).
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// Referenzziel eines BYREF-Parameters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VarRef {
|
||||
Global(u16),
|
||||
/// Absoluter Index in den Locals-Stack der VM.
|
||||
Stack(u32),
|
||||
/// Arrayelement (flacher Index).
|
||||
Elem(Rc<RefCell<ArrayObj>>, u32),
|
||||
/// Record-Feldpfad.
|
||||
Field(Rc<RefCell<RecordObj>>, Vec<u16>),
|
||||
}
|
||||
|
||||
/// Initialisierungstyp eines Slots/Elements (serialisierbar im `.tbc`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TypeInit {
|
||||
Int,
|
||||
Lng,
|
||||
Sng,
|
||||
Dbl,
|
||||
Cur,
|
||||
Str,
|
||||
FixedStr(u32),
|
||||
Udt(u16),
|
||||
/// Array-/Record-Slot ohne Vorbelegung (Auto-DIM).
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// UDT-Layout für die Default-Erzeugung.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UdtLayout {
|
||||
pub name: String,
|
||||
pub fields: Vec<TypeInit>,
|
||||
}
|
||||
|
||||
pub fn default_value(init: &TypeInit, udts: &[UdtLayout]) -> Value {
|
||||
match init {
|
||||
TypeInit::Int => Value::Int(0),
|
||||
TypeInit::Lng => Value::Lng(0),
|
||||
TypeInit::Sng => Value::Sng(0.0),
|
||||
TypeInit::Dbl => Value::Dbl(0.0),
|
||||
TypeInit::Cur => Value::Cur(0),
|
||||
TypeInit::Str => Value::Str(Rc::from("")),
|
||||
TypeInit::FixedStr(n) => Value::Str(Rc::from(" ".repeat(*n as usize).as_str())),
|
||||
TypeInit::Udt(id) => {
|
||||
let layout = &udts[*id as usize];
|
||||
let fields = layout
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| default_value(f, udts))
|
||||
.collect();
|
||||
Value::Rec(Rc::new(RefCell::new(RecordObj { fields })))
|
||||
}
|
||||
TypeInit::Empty => Value::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArrayObj {
|
||||
pub elem: TypeInit,
|
||||
/// (Untergrenze, Obergrenze) je Dimension.
|
||||
pub dims: Vec<(i32, i32)>,
|
||||
pub data: Vec<Value>,
|
||||
}
|
||||
|
||||
impl ArrayObj {
|
||||
pub fn new(elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout]) -> Result<Self, RuntimeError> {
|
||||
let mut len: usize = 1;
|
||||
for (lo, hi) in &dims {
|
||||
if hi < lo {
|
||||
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
|
||||
}
|
||||
let n = (*hi as i64 - *lo as i64 + 1) as usize;
|
||||
len = len.checked_mul(n).ok_or(RuntimeError::OUT_OF_MEMORY)?;
|
||||
if len > 64 * 1024 * 1024 {
|
||||
return Err(RuntimeError::OUT_OF_MEMORY);
|
||||
}
|
||||
}
|
||||
let mut data = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
data.push(default_value(&elem, udts));
|
||||
}
|
||||
Ok(ArrayObj { elem, dims, data })
|
||||
}
|
||||
|
||||
/// Flacher Index (zeilenweise, letzte Dimension läuft am schnellsten);
|
||||
/// Bereichsprüfung → Fehler 9.
|
||||
pub fn flat_index(&self, idx: &[i32]) -> Result<u32, RuntimeError> {
|
||||
if idx.len() != self.dims.len() {
|
||||
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
|
||||
}
|
||||
let mut flat: u64 = 0;
|
||||
for (i, (lo, hi)) in idx.iter().zip(self.dims.iter()) {
|
||||
if i < lo || i > hi {
|
||||
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
|
||||
}
|
||||
let span = (*hi as i64 - *lo as i64 + 1) as u64;
|
||||
flat = flat * span + (*i as i64 - *lo as i64) as u64;
|
||||
}
|
||||
Ok(flat as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecordObj {
|
||||
pub fields: Vec<Value>,
|
||||
}
|
||||
|
||||
// ---- Konvertierungsmatrix ---------------------------------------------------
|
||||
|
||||
/// Kaufmännische Rundung zur nächsten geraden Zahl (Banker's Rounding,
|
||||
/// Verhalten von CINT/CLNG des Vorbilds).
|
||||
pub fn banker_round(x: f64) -> f64 {
|
||||
let floor = x.floor();
|
||||
let diff = x - floor;
|
||||
if diff > 0.5 {
|
||||
floor + 1.0
|
||||
} else if diff < 0.5 {
|
||||
floor
|
||||
} else {
|
||||
// exakt .5 → zur geraden Zahl
|
||||
if (floor as i64) % 2 == 0 {
|
||||
floor
|
||||
} else {
|
||||
floor + 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f64_to_i16(x: f64) -> Result<i16, RuntimeError> {
|
||||
let r = banker_round(x);
|
||||
if !(i16::MIN as f64..=i16::MAX as f64).contains(&r) {
|
||||
return Err(RuntimeError::OVERFLOW);
|
||||
}
|
||||
Ok(r as i16)
|
||||
}
|
||||
|
||||
pub fn f64_to_i32(x: f64) -> Result<i32, RuntimeError> {
|
||||
let r = banker_round(x);
|
||||
if !(i32::MIN as f64..=i32::MAX as f64).contains(&r) {
|
||||
return Err(RuntimeError::OVERFLOW);
|
||||
}
|
||||
Ok(r as i32)
|
||||
}
|
||||
|
||||
pub fn f64_to_f32(x: f64) -> Result<f32, RuntimeError> {
|
||||
if x.is_finite() && x.abs() > f32::MAX as f64 {
|
||||
return Err(RuntimeError::OVERFLOW);
|
||||
}
|
||||
Ok(x as f32)
|
||||
}
|
||||
|
||||
/// f64 → CURRENCY (Festkomma ×10 000, Banker's auf der 4. Nachkommastelle).
|
||||
pub fn f64_to_cur(x: f64) -> Result<i64, RuntimeError> {
|
||||
let scaled = banker_round(x * 10_000.0);
|
||||
if !((i64::MIN as f64) < scaled && scaled < (i64::MAX as f64)) {
|
||||
return Err(RuntimeError::OVERFLOW);
|
||||
}
|
||||
Ok(scaled as i64)
|
||||
}
|
||||
|
||||
/// CURRENCY → Ganzzahl (÷10 000, Banker's; ganzzahlig exakt).
|
||||
pub fn cur_to_i64(c: i64) -> i64 {
|
||||
let q = c.div_euclid(10_000);
|
||||
let r = c.rem_euclid(10_000);
|
||||
// r in 0..10000; runde halb-zu-gerade
|
||||
if r > 5_000 {
|
||||
q + 1
|
||||
} else if r < 5_000 {
|
||||
q
|
||||
} else if q % 2 == 0 {
|
||||
q
|
||||
} else {
|
||||
q + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cur_to_i16(c: i64) -> Result<i16, RuntimeError> {
|
||||
let v = cur_to_i64(c);
|
||||
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
|
||||
}
|
||||
|
||||
pub fn cur_to_i32(c: i64) -> Result<i32, RuntimeError> {
|
||||
let v = cur_to_i64(c);
|
||||
i32::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
|
||||
}
|
||||
|
||||
pub fn cur_to_f64(c: i64) -> f64 {
|
||||
c as f64 / 10_000.0
|
||||
}
|
||||
|
||||
/// i32 → i16 mit Bereichsprüfung.
|
||||
pub fn i32_to_i16(v: i32) -> Result<i16, RuntimeError> {
|
||||
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
|
||||
}
|
||||
|
||||
/// Numerischer Wert als f64 (für Builtins, die per Tag dispatchen).
|
||||
pub fn as_f64(v: &Value) -> f64 {
|
||||
match v {
|
||||
Value::Int(x) => *x as f64,
|
||||
Value::Lng(x) => *x as f64,
|
||||
Value::Sng(x) => *x as f64,
|
||||
Value::Dbl(x) => *x,
|
||||
Value::Cur(x) => cur_to_f64(*x),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn banker_rounding() {
|
||||
assert_eq!(banker_round(0.5), 0.0);
|
||||
assert_eq!(banker_round(1.5), 2.0);
|
||||
assert_eq!(banker_round(2.5), 2.0);
|
||||
assert_eq!(banker_round(-0.5), 0.0);
|
||||
assert_eq!(banker_round(-1.5), -2.0);
|
||||
assert_eq!(banker_round(2.4), 2.0);
|
||||
assert_eq!(banker_round(2.6), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_bei_konvertierung() {
|
||||
assert!(f64_to_i16(40_000.0).is_err());
|
||||
assert!(f64_to_i16(32_767.4).is_ok());
|
||||
assert!(f64_to_i32(3e9).is_err());
|
||||
assert!(f64_to_f32(1e39).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency_rundung() {
|
||||
assert_eq!(f64_to_cur(1.5).unwrap(), 15_000);
|
||||
assert_eq!(f64_to_cur(-2.25).unwrap(), -22_500);
|
||||
// Halb-zu-gerade nur bei exakt darstellbarem .5-Fall (Skalierung
|
||||
// 10000.5 ist binär exakt, wenn der Ausgangswert es hergibt):
|
||||
assert_eq!(banker_round(10_000.5), 10_000.0);
|
||||
assert_eq!(cur_to_i64(15_000), 2); // 1.5 → 2
|
||||
assert_eq!(cur_to_i64(25_000), 2); // 2.5 → 2
|
||||
assert_eq!(cur_to_i64(-15_000), -2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_indexpruefung() {
|
||||
let a = ArrayObj::new(TypeInit::Int, vec![(0, 10)], &[]).unwrap();
|
||||
assert_eq!(a.flat_index(&[0]).unwrap(), 0);
|
||||
assert_eq!(a.flat_index(&[10]).unwrap(), 10);
|
||||
assert!(a.flat_index(&[11]).is_err());
|
||||
assert!(a.flat_index(&[-1]).is_err());
|
||||
let b = ArrayObj::new(TypeInit::Int, vec![(1, 3), (1, 2)], &[]).unwrap();
|
||||
assert_eq!(b.flat_index(&[1, 1]).unwrap(), 0);
|
||||
assert_eq!(b.flat_index(&[1, 2]).unwrap(), 1);
|
||||
assert_eq!(b.flat_index(&[2, 1]).unwrap(), 2);
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,12 @@ authors.workspace = true
|
||||
tb-frontend.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "compile"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "vm"
|
||||
harness = false
|
||||
|
||||
88
crates/tb-vm/benches/compile.rs
Normal file
88
crates/tb-vm/benches/compile.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Compile-Budget-Benchmark (Instant-Compile-Anforderung, PLAN.md):
|
||||
//! Projekt mit ~50.000 Zeilen < 1 s, einzelnes Modul (~500 Zeilen) < 50 ms
|
||||
//! (Release-Build). Eigener Harness (kein Criterion — keine
|
||||
//! Fremdabhängigkeit nötig); Lauf mit `cargo bench -p tb-vm`.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Realistisch gemischtes Modul mit `n_blocks` Codeblöcken
|
||||
/// (~10 Zeilen je Block) plus Prozeduren.
|
||||
fn generate_module(n_blocks: usize, seed: usize) -> String {
|
||||
let mut src = String::new();
|
||||
let _ = writeln!(src, "' Generiertes Benchmark-Modul {seed}");
|
||||
let _ = writeln!(src, "DIM feld{seed}%(100)");
|
||||
for i in 0..n_blocks {
|
||||
let v = format!("v{seed}x{i}");
|
||||
let _ = writeln!(src, "{v}% = {i} MOD 100");
|
||||
let _ = writeln!(src, "{v}tot# = {v}% * 1.5 + SQR({v}% + 1)");
|
||||
let _ = writeln!(src, "IF {v}% > 50 THEN");
|
||||
let _ = writeln!(src, " {v}s$ = \"gross\" + STR$({v}%)");
|
||||
let _ = writeln!(src, "ELSE");
|
||||
let _ = writeln!(src, " {v}s$ = LEFT$(\"klein\", 3)");
|
||||
let _ = writeln!(src, "END IF");
|
||||
let _ = writeln!(src, "FOR {v}i% = 1 TO 10");
|
||||
let _ = writeln!(src, " feld{seed}%({v}i% MOD 100) = {v}i%");
|
||||
let _ = writeln!(src, "NEXT");
|
||||
}
|
||||
// Prozeduren am Modulende
|
||||
for p in 0..(n_blocks / 20).max(1) {
|
||||
let _ = writeln!(src, "SUB Tu{seed}p{p} (a%, b#)");
|
||||
let _ = writeln!(src, " b# = a% * 2 + b#");
|
||||
let _ = writeln!(src, "END SUB");
|
||||
}
|
||||
src
|
||||
}
|
||||
|
||||
fn compile_all(sources: &[(String, String)]) -> usize {
|
||||
let mut total = 0;
|
||||
for (name, src) in sources {
|
||||
let m = tb_vm::compile_source(name, src).expect("Benchmark-Quelle muss kompilieren");
|
||||
total += m.procs.iter().map(|p| p.code.len()).sum::<usize>();
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Einzelnes Modul: ~500 Zeilen (Budget < 50 ms).
|
||||
let single = generate_module(50, 0);
|
||||
let single_lines = single.lines().count();
|
||||
|
||||
// Projekt: ~50.000 Zeilen über 20 Module (Budget < 1 s).
|
||||
let modules: Vec<(String, String)> = (0..20)
|
||||
.map(|i| (format!("MOD{i}"), generate_module(245, i)))
|
||||
.collect();
|
||||
let project_lines: usize = modules.iter().map(|(_, s)| s.lines().count()).sum();
|
||||
|
||||
// Aufwärmen
|
||||
let _ = compile_all(&[("WARM".into(), single.clone())]);
|
||||
|
||||
let t = Instant::now();
|
||||
let mut best_single = f64::MAX;
|
||||
for _ in 0..10 {
|
||||
let t1 = Instant::now();
|
||||
let _ = compile_all(&[("EINZEL".into(), single.clone())]);
|
||||
best_single = best_single.min(t1.elapsed().as_secs_f64());
|
||||
}
|
||||
let _ = t;
|
||||
|
||||
let t2 = Instant::now();
|
||||
let instrs = compile_all(&modules);
|
||||
let project_secs = t2.elapsed().as_secs_f64();
|
||||
|
||||
println!("Compile-Budget-Benchmark (Release):");
|
||||
println!(
|
||||
" Einzelmodul: {single_lines} Zeilen in {:.2} ms (Budget 50 ms) {}",
|
||||
best_single * 1000.0,
|
||||
if best_single < 0.050 { "OK" } else { "VERFEHLT" }
|
||||
);
|
||||
println!(
|
||||
" Projekt: {project_lines} Zeilen in {:.0} ms (Budget 1000 ms) {} — {instrs} Instruktionen",
|
||||
project_secs * 1000.0,
|
||||
if project_secs < 1.0 { "OK" } else { "VERFEHLT" }
|
||||
);
|
||||
let lps = project_lines as f64 / project_secs;
|
||||
println!(" Durchsatz: {:.0} Zeilen/s", lps);
|
||||
assert!(best_single < 0.050, "Einzelmodul-Budget verfehlt");
|
||||
assert!(project_secs < 1.0, "Projekt-Budget verfehlt");
|
||||
}
|
||||
57
crates/tb-vm/benches/vm.rs
Normal file
57
crates/tb-vm/benches/vm.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! VM-Durchsatz-Benchmark (Messlatte docs/tbvm-design.md, „Performance"):
|
||||
//! Schleifen- und String-Lasten. Eigener Harness; `cargo bench -p tb-vm`.
|
||||
|
||||
use std::time::Instant;
|
||||
use tb_runtime::host::CaptureHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn run_timed(name: &str, src: &str, work_units: f64, unit: &str) {
|
||||
let module = tb_vm::compile_source("BENCH", src).expect("kompiliert");
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
let t = Instant::now();
|
||||
let ev = vm.run(&mut host);
|
||||
let secs = t.elapsed().as_secs_f64();
|
||||
assert_eq!(ev, RunEvent::Ended, "{ev:?}");
|
||||
println!(
|
||||
" {name}: {:.0} ms ({:.1} Mio {unit}/s)",
|
||||
secs * 1000.0,
|
||||
work_units / secs / 1e6
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("VM-Durchsatz-Benchmark (Release):");
|
||||
|
||||
// Ganzzahl-Schleife: 10 Mio Iterationen mit Arithmetik.
|
||||
run_timed(
|
||||
"INTEGER-Schleife (10 Mio)",
|
||||
"s& = 0\nFOR i& = 1 TO 10000000\ns& = (s& + i& MOD 7) MOD 100000\nNEXT\nPRINT s&",
|
||||
10_000_000.0,
|
||||
"Iterationen",
|
||||
);
|
||||
|
||||
// Gleitkomma-Schleife.
|
||||
run_timed(
|
||||
"DOUBLE-Schleife (5 Mio)",
|
||||
"d# = 0\nFOR i& = 1 TO 5000000\nd# = d# + i& * 1.000001\nNEXT\nPRINT CINT(d# / 1000000000000#)",
|
||||
5_000_000.0,
|
||||
"Iterationen",
|
||||
);
|
||||
|
||||
// Prozeduraufrufe.
|
||||
run_timed(
|
||||
"SUB-Aufrufe (1 Mio, BYREF)",
|
||||
"SUB Inc (x&)\nx& = x& + 1\nEND SUB\nn& = 0\nFOR i& = 1 TO 1000000\nInc n&\nNEXT\nPRINT n&",
|
||||
1_000_000.0,
|
||||
"Aufrufe",
|
||||
);
|
||||
|
||||
// String-Last: MID$/INSTR/Verkettung.
|
||||
run_timed(
|
||||
"String-Funktionen (200k)",
|
||||
"s$ = \"Terminal Basic Benchmark\"\nn& = 0\nFOR i& = 1 TO 200000\nt$ = MID$(s$, (i& MOD 10) + 1, 8) + \"x\"\nn& = n& + INSTR(t$, \"a\") + LEN(t$)\nNEXT\nPRINT n& > 0",
|
||||
200_000.0,
|
||||
"Runden",
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,680 @@
|
||||
//! Bytecode-Format der TBVM: Opcodes, Konstantenpool, Modul-/Prozedurtabellen.
|
||||
//! Bytecode-Definition und `.tbc`-Serialisierung.
|
||||
//!
|
||||
//! In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, Enum
|
||||
//! mit eingebetteten Operanden — Wort-Dispatch); die Serialisierung
|
||||
//! bildet jede Instruktion auf 1 Opcode-Byte + Operanden (little-endian)
|
||||
//! ab. Opcode-Bytes sind **stabil** und gruppenweise mit Lücken vergeben
|
||||
//! (Phase 3 ergänzt in den Lücken). Dokumentation: docs/tbvm-design.md.
|
||||
|
||||
// Platzhalter — wird in Phase 2 ausgearbeitet (siehe PLAN.md)
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use tb_runtime::value::{TypeInit, UdtLayout};
|
||||
|
||||
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
|
||||
pub const TBC_VERSION: u16 = 1;
|
||||
|
||||
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CmpOp {
|
||||
Eq = 0,
|
||||
Ne = 1,
|
||||
Lt = 2,
|
||||
Le = 3,
|
||||
Gt = 4,
|
||||
Ge = 5,
|
||||
}
|
||||
|
||||
impl CmpOp {
|
||||
fn from_u8(v: u8) -> Result<Self, LoadError> {
|
||||
Ok(match v {
|
||||
0 => CmpOp::Eq,
|
||||
1 => CmpOp::Ne,
|
||||
2 => CmpOp::Lt,
|
||||
3 => CmpOp::Le,
|
||||
4 => CmpOp::Gt,
|
||||
5 => CmpOp::Ge,
|
||||
_ => return Err(LoadError::Corrupt("CmpOp")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError {
|
||||
BadMagic,
|
||||
/// Unbekannte Formatversion (enthaltene Version).
|
||||
Version(u16),
|
||||
Corrupt(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for LoadError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
|
||||
LoadError::Version(v) => {
|
||||
write!(f, "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})")
|
||||
}
|
||||
LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Encoder/Decoder-Hilfen -------------------------------------------------
|
||||
|
||||
pub struct Reader<'a> {
|
||||
buf: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn new(buf: &'a [u8]) -> Self {
|
||||
Reader { buf, pos: 0 }
|
||||
}
|
||||
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
|
||||
if self.pos + n > self.buf.len() {
|
||||
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
|
||||
}
|
||||
let s = &self.buf[self.pos..self.pos + n];
|
||||
self.pos += n;
|
||||
Ok(s)
|
||||
}
|
||||
fn u8(&mut self) -> Result<u8, LoadError> {
|
||||
Ok(self.take(1)?[0])
|
||||
}
|
||||
fn u16(&mut self) -> Result<u16, LoadError> {
|
||||
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
|
||||
}
|
||||
fn u32(&mut self) -> Result<u32, LoadError> {
|
||||
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
|
||||
}
|
||||
fn string(&mut self) -> Result<String, LoadError> {
|
||||
let n = self.u32()? as usize;
|
||||
let b = self.take(n)?;
|
||||
String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
|
||||
}
|
||||
}
|
||||
|
||||
trait Enc: Sized {
|
||||
fn enc(&self, out: &mut Vec<u8>);
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
|
||||
}
|
||||
|
||||
macro_rules! enc_prim {
|
||||
($t:ty, $n:literal) => {
|
||||
impl Enc for $t {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.to_le_bytes());
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
Ok(<$t>::from_le_bytes(r.take($n)?.try_into().unwrap()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
enc_prim!(u16, 2);
|
||||
enc_prim!(u32, 4);
|
||||
enc_prim!(i16, 2);
|
||||
enc_prim!(i32, 4);
|
||||
enc_prim!(i64, 8);
|
||||
enc_prim!(f32, 4);
|
||||
enc_prim!(f64, 8);
|
||||
|
||||
impl Enc for u8 {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
out.push(*self);
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
r.u8()
|
||||
}
|
||||
}
|
||||
|
||||
impl Enc for bool {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
out.push(*self as u8);
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
Ok(r.u8()? != 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Enc for CmpOp {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
out.push(*self as u8);
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
CmpOp::from_u8(r.u8()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Enc for TypeInit {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
let (tag, extra): (u8, u32) = match self {
|
||||
TypeInit::Int => (0, 0),
|
||||
TypeInit::Lng => (1, 0),
|
||||
TypeInit::Sng => (2, 0),
|
||||
TypeInit::Dbl => (3, 0),
|
||||
TypeInit::Cur => (4, 0),
|
||||
TypeInit::Str => (5, 0),
|
||||
TypeInit::FixedStr(n) => (6, *n),
|
||||
TypeInit::Udt(id) => (7, *id as u32),
|
||||
TypeInit::Empty => (8, 0),
|
||||
};
|
||||
out.push(tag);
|
||||
out.extend_from_slice(&extra.to_le_bytes());
|
||||
}
|
||||
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
|
||||
let tag = r.u8()?;
|
||||
let extra = r.u32()?;
|
||||
Ok(match tag {
|
||||
0 => TypeInit::Int,
|
||||
1 => TypeInit::Lng,
|
||||
2 => TypeInit::Sng,
|
||||
3 => TypeInit::Dbl,
|
||||
4 => TypeInit::Cur,
|
||||
5 => TypeInit::Str,
|
||||
6 => TypeInit::FixedStr(extra),
|
||||
7 => TypeInit::Udt(extra as u16),
|
||||
8 => TypeInit::Empty,
|
||||
_ => return Err(LoadError::Corrupt("TypeInit")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Instruktionssatz ---------------------------------------------------------
|
||||
|
||||
macro_rules! instrs {
|
||||
($( $op:literal $name:ident $(( $($fname:ident : $ft:ty),+ ))? ; )+) => {
|
||||
/// Eine dekodierte Instruktion. Serialisiert: Opcode-Byte + Operanden.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Instr {
|
||||
$( $name $(( $($ft),+ ))? , )+
|
||||
}
|
||||
|
||||
impl Instr {
|
||||
pub fn encode(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
$( Instr::$name $(( $($fname),+ ))? => {
|
||||
out.push($op);
|
||||
$( $( Enc::enc($fname, out); )+ )?
|
||||
} )+
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(r: &mut Reader) -> Result<Instr, LoadError> {
|
||||
let op = r.u8()?;
|
||||
Ok(match op {
|
||||
$( $op => Instr::$name $(( $( <$ft as Enc>::dec(r)? ),+ ))? , )+
|
||||
_ => return Err(LoadError::Corrupt("Opcode")),
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
instrs! {
|
||||
// 0x00 — Anweisungsgrenzen und Kontrolle
|
||||
0x00 Stmt(a: u32); // Quellzeile; Tick-Prüfung, Resume-Punkt
|
||||
0x01 SetErl(a: u32); // numerische Zeilennummer durchlaufen
|
||||
0x02 End;
|
||||
0x03 StopInstr;
|
||||
0x04 SystemInstr;
|
||||
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
|
||||
|
||||
// 0x10 — Konstanten und Stack
|
||||
0x10 PushInt(a: i16);
|
||||
0x11 PushLng(a: i32);
|
||||
0x12 PushSng(a: f32);
|
||||
0x13 PushDbl(a: f64);
|
||||
0x14 PushCur(a: i64);
|
||||
0x15 PushStr(a: u16);
|
||||
0x16 Dup;
|
||||
0x17 Pop;
|
||||
|
||||
// 0x20 — Variablen und Referenzen
|
||||
0x20 LoadGlobal(a: u16);
|
||||
0x21 StoreGlobal(a: u16);
|
||||
0x22 LoadLocal(a: u16);
|
||||
0x23 StoreLocal(a: u16);
|
||||
0x24 LoadRef(a: u16); // durch Referenz in lokalem Slot lesen
|
||||
0x25 StoreRef(a: u16);
|
||||
0x26 MakeRefGlobal(a: u16);
|
||||
0x27 MakeRefLocal(a: u16);
|
||||
0x28 MakeRefElem(a: u8); // Handle+Indizes → Elementreferenz
|
||||
0x29 MakeRefField(a: u16); // Rec/Feldreferenz → tiefere Feldreferenz
|
||||
|
||||
// 0x30 — Arrays und Records
|
||||
0x30 LoadArr(a: bool, b: u16, c: u8, d: TypeInit); // Slot sichern (Auto-DIM) + Handle
|
||||
0x31 LoadElem(a: u8);
|
||||
0x32 StoreElem(a: u8);
|
||||
0x33 DimArr(a: bool, b: u16, c: u8, d: TypeInit);
|
||||
0x34 RedimArr(a: bool, b: u16, c: u8, d: TypeInit);
|
||||
0x35 EraseSlot(a: bool, b: u16);
|
||||
0x36 LoadField(a: u16);
|
||||
0x37 StoreField(a: u16);
|
||||
0x38 CopyRec;
|
||||
0x39 ArrBound(a: bool); // true = LBOUND
|
||||
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
|
||||
|
||||
// 0x40 — Arithmetik (monomorph)
|
||||
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
|
||||
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
|
||||
0x4A MulI2; 0x4B MulI4; 0x4C MulR4; 0x4D MulR8; 0x4E MulCy;
|
||||
0x4F NegI2; 0x50 NegI4; 0x51 NegR4; 0x52 NegR8; 0x53 NegCy;
|
||||
0x54 DivR4; 0x55 DivR8;
|
||||
0x56 IDivI2; 0x57 IDivI4;
|
||||
0x58 ModI2; 0x59 ModI4;
|
||||
0x5A PowR8;
|
||||
0x5B Concat;
|
||||
|
||||
// 0x60 — Konvertierungen (Matrix)
|
||||
0x60 ConvI2I4; 0x61 ConvI2R4; 0x62 ConvI2R8; 0x63 ConvI2Cy;
|
||||
0x64 ConvI4I2; 0x65 ConvI4R4; 0x66 ConvI4R8; 0x67 ConvI4Cy;
|
||||
0x68 ConvR4I2; 0x69 ConvR4I4; 0x6A ConvR4R8; 0x6B ConvR4Cy;
|
||||
0x6C ConvR8I2; 0x6D ConvR8I4; 0x6E ConvR8R4; 0x6F ConvR8Cy;
|
||||
0x70 ConvCyI2; 0x71 ConvCyI4; 0x72 ConvCyR4; 0x73 ConvCyR8;
|
||||
|
||||
// 0x80 — Logik (bitweise)
|
||||
0x80 NotI2; 0x81 NotI4;
|
||||
0x82 AndI2; 0x83 AndI4;
|
||||
0x84 OrI2; 0x85 OrI4;
|
||||
0x86 XorI2; 0x87 XorI4;
|
||||
0x88 EqvI2; 0x89 EqvI4;
|
||||
0x8A ImpI2; 0x8B ImpI4;
|
||||
|
||||
// 0x90 — Vergleiche (Ergebnis INTEGER −1/0)
|
||||
0x90 CmpI2(a: CmpOp);
|
||||
0x91 CmpI4(a: CmpOp);
|
||||
0x92 CmpR4(a: CmpOp);
|
||||
0x93 CmpR8(a: CmpOp);
|
||||
0x94 CmpCy(a: CmpOp);
|
||||
0x95 CmpStr(a: CmpOp);
|
||||
|
||||
// 0xA0 — Kontrollfluss
|
||||
0xA0 Jump(a: u32);
|
||||
0xA1 JumpIfFalse(a: u32);
|
||||
0xA2 JumpIfTrue(a: u32);
|
||||
0xA3 Gosub(a: u32);
|
||||
0xA4 RetGosub;
|
||||
0xA5 RetGosubTo(a: u32);
|
||||
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
|
||||
|
||||
// 0xB0 — Prozeduren und Builtins
|
||||
0xB0 Call(a: u16, b: u8);
|
||||
0xB1 RetProc;
|
||||
0xB2 RetFn;
|
||||
0xB3 CallBuiltin(a: u16, b: u8);
|
||||
|
||||
// 0xC0 — Fehlerbehandlung
|
||||
0xC0 OnErrorGoto(a: u32);
|
||||
0xC1 OnErrorLocal(a: u32);
|
||||
0xC2 OnErrorDisable;
|
||||
0xC3 OnErrorLocalDisable;
|
||||
0xC4 OnErrorResumeNext(a: bool);
|
||||
0xC5 Resume0;
|
||||
0xC6 ResumeNext;
|
||||
0xC7 ResumeLabel(a: u32);
|
||||
0xC8 RaiseError; // Code vom Stack (ERROR n)
|
||||
0xC9 LoadErr;
|
||||
0xCA LoadErl;
|
||||
|
||||
// 0xD0 — DATA und Eingabe
|
||||
0xD0 ReadData(a: u8); // nächstes DATA-Element; 0 = String, 1 = Zahl (DOUBLE)
|
||||
0xD1 Restore(a: u32);
|
||||
0xD2 Input(a: u8, b: bool, c: u16, d: bool); // argc, line_mode, prompt (0xFFFF=ohne), '?'
|
||||
}
|
||||
|
||||
// ---- Modulstruktur ------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcCode {
|
||||
pub name: String,
|
||||
pub n_params: u16,
|
||||
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
|
||||
/// wird beim Aufruf durch die Argumente ersetzt).
|
||||
pub locals_init: Vec<TypeInit>,
|
||||
/// Slot-Namen (Debugger-Inspektion).
|
||||
pub local_names: Vec<String>,
|
||||
pub code: Vec<Instr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataItem {
|
||||
pub text: String,
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
|
||||
#[derive(Debug)]
|
||||
pub struct CompiledModule {
|
||||
pub name: String,
|
||||
/// `OPTION BASE` (Untergrenze impliziter Arrays).
|
||||
pub option_base: u8,
|
||||
/// Deduplizierter Stringpool.
|
||||
pub strings: Vec<Rc<str>>,
|
||||
pub globals_init: Vec<TypeInit>,
|
||||
pub global_names: Vec<String>,
|
||||
pub udts: Vec<UdtLayout>,
|
||||
/// Prozeduren; Index 0 ist das Hauptprogramm (modul-qualifiziert über
|
||||
/// `name` des Moduls + Prozedurname).
|
||||
pub procs: Vec<ProcCode>,
|
||||
pub data: Vec<DataItem>,
|
||||
/// Sprungtabellen für `ON n GOTO/GOSUB`.
|
||||
pub jump_tables: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
fn w_string(out: &mut Vec<u8>, s: &str) {
|
||||
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
}
|
||||
|
||||
impl CompiledModule {
|
||||
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
|
||||
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
|
||||
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
|
||||
pub fn to_tbc(&self) -> Vec<u8> {
|
||||
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
|
||||
|
||||
let mut modn = Vec::new();
|
||||
w_string(&mut modn, &self.name);
|
||||
modn.push(self.option_base);
|
||||
sections.push((*b"MODN", modn));
|
||||
|
||||
let mut cons = Vec::new();
|
||||
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
|
||||
for s in &self.strings {
|
||||
w_string(&mut cons, s);
|
||||
}
|
||||
sections.push((*b"CONS", cons));
|
||||
|
||||
let mut typs = Vec::new();
|
||||
typs.extend_from_slice(&(self.udts.len() as u32).to_le_bytes());
|
||||
for u in &self.udts {
|
||||
w_string(&mut typs, &u.name);
|
||||
typs.extend_from_slice(&(u.fields.len() as u32).to_le_bytes());
|
||||
for f in &u.fields {
|
||||
f.enc(&mut typs);
|
||||
}
|
||||
}
|
||||
sections.push((*b"TYPS", typs));
|
||||
|
||||
let mut glob = Vec::new();
|
||||
glob.extend_from_slice(&(self.globals_init.len() as u32).to_le_bytes());
|
||||
for (init, name) in self.globals_init.iter().zip(&self.global_names) {
|
||||
init.enc(&mut glob);
|
||||
w_string(&mut glob, name);
|
||||
}
|
||||
sections.push((*b"GLOB", glob));
|
||||
|
||||
let mut proc = Vec::new();
|
||||
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
|
||||
for p in &self.procs {
|
||||
w_string(&mut proc, &p.name);
|
||||
proc.extend_from_slice(&p.n_params.to_le_bytes());
|
||||
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
|
||||
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
|
||||
init.enc(&mut proc);
|
||||
w_string(&mut proc, name);
|
||||
}
|
||||
let mut code = Vec::new();
|
||||
for i in &p.code {
|
||||
i.encode(&mut code);
|
||||
}
|
||||
proc.extend_from_slice(&(p.code.len() as u32).to_le_bytes());
|
||||
proc.extend_from_slice(&(code.len() as u32).to_le_bytes());
|
||||
proc.extend_from_slice(&code);
|
||||
}
|
||||
sections.push((*b"PROC", proc));
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
|
||||
for d in &self.data {
|
||||
w_string(&mut data, &d.text);
|
||||
data.extend_from_slice(&d.line.to_le_bytes());
|
||||
}
|
||||
sections.push((*b"DATA", data));
|
||||
|
||||
let mut jmpt = Vec::new();
|
||||
jmpt.extend_from_slice(&(self.jump_tables.len() as u32).to_le_bytes());
|
||||
for t in &self.jump_tables {
|
||||
jmpt.extend_from_slice(&(t.len() as u32).to_le_bytes());
|
||||
for target in t {
|
||||
jmpt.extend_from_slice(&target.to_le_bytes());
|
||||
}
|
||||
}
|
||||
sections.push((*b"JMPT", jmpt));
|
||||
|
||||
// Header + Abschnittstabelle
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(TBC_MAGIC);
|
||||
out.extend_from_slice(&TBC_VERSION.to_le_bytes());
|
||||
out.extend_from_slice(&0u16.to_le_bytes()); // Flags
|
||||
out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
|
||||
let table_start = out.len();
|
||||
// Platzhalter für Tabelle
|
||||
for _ in 0..sections.len() {
|
||||
out.extend_from_slice(&[0u8; 12]);
|
||||
}
|
||||
let mut offsets = Vec::new();
|
||||
for (_, payload) in §ions {
|
||||
offsets.push((out.len() as u32, payload.len() as u32));
|
||||
out.extend_from_slice(payload);
|
||||
}
|
||||
for (i, ((id, _), (off, len))) in sections.iter().zip(&offsets).enumerate() {
|
||||
let at = table_start + i * 12;
|
||||
out[at..at + 4].copy_from_slice(id);
|
||||
out[at + 4..at + 8].copy_from_slice(&off.to_le_bytes());
|
||||
out[at + 8..at + 12].copy_from_slice(&len.to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn from_tbc(buf: &[u8]) -> Result<CompiledModule, LoadError> {
|
||||
let mut r = Reader::new(buf);
|
||||
if r.take(4)? != TBC_MAGIC {
|
||||
return Err(LoadError::BadMagic);
|
||||
}
|
||||
let version = r.u16()?;
|
||||
if version != TBC_VERSION {
|
||||
return Err(LoadError::Version(version));
|
||||
}
|
||||
let _flags = r.u16()?;
|
||||
let n_sections = r.u32()? as usize;
|
||||
let mut table = Vec::new();
|
||||
for _ in 0..n_sections {
|
||||
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
|
||||
let off = r.u32()? as usize;
|
||||
let len = r.u32()? as usize;
|
||||
table.push((id, off, len));
|
||||
}
|
||||
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
|
||||
for (sid, off, len) in &table {
|
||||
if sid == id {
|
||||
if off + len > buf.len() {
|
||||
return Err(LoadError::Corrupt("Abschnittstabelle"));
|
||||
}
|
||||
return Ok(Reader::new(&buf[*off..*off + *len]));
|
||||
}
|
||||
}
|
||||
Err(LoadError::Corrupt("Abschnitt fehlt"))
|
||||
};
|
||||
|
||||
let mut r = section(b"MODN")?;
|
||||
let name = r.string()?;
|
||||
let option_base = r.u8()?;
|
||||
|
||||
let mut r = section(b"CONS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut strings = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
strings.push(Rc::from(r.string()?.as_str()));
|
||||
}
|
||||
|
||||
let mut r = section(b"TYPS")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut udts = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let nf = r.u32()? as usize;
|
||||
let mut fields = Vec::with_capacity(nf);
|
||||
for _ in 0..nf {
|
||||
fields.push(TypeInit::dec(&mut r)?);
|
||||
}
|
||||
udts.push(UdtLayout { name, fields });
|
||||
}
|
||||
|
||||
let mut r = section(b"GLOB")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut globals_init = Vec::with_capacity(n);
|
||||
let mut global_names = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
globals_init.push(TypeInit::dec(&mut r)?);
|
||||
global_names.push(r.string()?);
|
||||
}
|
||||
|
||||
let mut r = section(b"PROC")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut procs = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let name = r.string()?;
|
||||
let n_params = r.u16()?;
|
||||
let nl = r.u32()? as usize;
|
||||
let mut locals_init = Vec::with_capacity(nl);
|
||||
let mut local_names = Vec::with_capacity(nl);
|
||||
for _ in 0..nl {
|
||||
locals_init.push(TypeInit::dec(&mut r)?);
|
||||
local_names.push(r.string()?);
|
||||
}
|
||||
let n_instr = r.u32()? as usize;
|
||||
let code_len = r.u32()? as usize;
|
||||
let code_bytes = r.take(code_len)?;
|
||||
let mut cr = Reader::new(code_bytes);
|
||||
let mut code = Vec::with_capacity(n_instr);
|
||||
for _ in 0..n_instr {
|
||||
code.push(Instr::decode(&mut cr)?);
|
||||
}
|
||||
procs.push(ProcCode { name, n_params, locals_init, local_names, code });
|
||||
}
|
||||
|
||||
let mut r = section(b"DATA")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut data = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let text = r.string()?;
|
||||
let line = r.u32()?;
|
||||
data.push(DataItem { text, line });
|
||||
}
|
||||
|
||||
let mut r = section(b"JMPT")?;
|
||||
let n = r.u32()? as usize;
|
||||
let mut jump_tables = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let m = r.u32()? as usize;
|
||||
let mut t = Vec::with_capacity(m);
|
||||
for _ in 0..m {
|
||||
t.push(r.u32()?);
|
||||
}
|
||||
jump_tables.push(t);
|
||||
}
|
||||
|
||||
Ok(CompiledModule {
|
||||
name,
|
||||
option_base,
|
||||
strings,
|
||||
globals_init,
|
||||
global_names,
|
||||
udts,
|
||||
procs,
|
||||
data,
|
||||
jump_tables,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn instr_roundtrip() {
|
||||
let samples = vec![
|
||||
Instr::Stmt(42),
|
||||
Instr::PushInt(-7),
|
||||
Instr::PushDbl(1.5),
|
||||
Instr::PushCur(-12_345),
|
||||
Instr::LoadArr(true, 3, 2, TypeInit::FixedStr(30)),
|
||||
Instr::CmpR8(CmpOp::Le),
|
||||
Instr::OnJump(1, true),
|
||||
Instr::Call(2, 3),
|
||||
Instr::Input(2, false, 0xFFFF, true),
|
||||
Instr::ConvCyR8,
|
||||
Instr::RetFn,
|
||||
];
|
||||
let mut buf = Vec::new();
|
||||
for i in &samples {
|
||||
i.encode(&mut buf);
|
||||
}
|
||||
let mut r = Reader::new(&buf);
|
||||
for want in &samples {
|
||||
let got = Instr::decode(&mut r).unwrap();
|
||||
assert_eq!(&got, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tbc_roundtrip() {
|
||||
let m = CompiledModule {
|
||||
name: "TEST".into(),
|
||||
option_base: 1,
|
||||
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
|
||||
globals_init: vec![TypeInit::Int, TypeInit::Str],
|
||||
global_names: vec!["a".into(), "s".into()],
|
||||
udts: vec![UdtLayout {
|
||||
name: "Kunde".into(),
|
||||
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
|
||||
}],
|
||||
procs: vec![ProcCode {
|
||||
name: "TEST".into(),
|
||||
n_params: 0,
|
||||
locals_init: vec![],
|
||||
local_names: vec![],
|
||||
code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
|
||||
}],
|
||||
data: vec![DataItem { text: "1.5".into(), line: 3 }],
|
||||
jump_tables: vec![vec![4, 9]],
|
||||
};
|
||||
let bytes = m.to_tbc();
|
||||
let back = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(back.name, "TEST");
|
||||
assert_eq!(back.strings.len(), 2);
|
||||
assert_eq!(&*back.strings[0], "Hallo");
|
||||
assert_eq!(back.globals_init, m.globals_init);
|
||||
assert_eq!(back.udts[0].fields, m.udts[0].fields);
|
||||
assert_eq!(back.procs[0].code, m.procs[0].code);
|
||||
assert_eq!(back.data[0].text, "1.5");
|
||||
assert_eq!(back.jump_tables, m.jump_tables);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbekannte_version_wird_abgelehnt() {
|
||||
let m = CompiledModule {
|
||||
name: "T".into(),
|
||||
option_base: 0,
|
||||
strings: vec![],
|
||||
globals_init: vec![],
|
||||
global_names: vec![],
|
||||
udts: vec![],
|
||||
procs: vec![],
|
||||
data: vec![],
|
||||
jump_tables: vec![],
|
||||
};
|
||||
let mut bytes = m.to_tbc();
|
||||
bytes[4] = 0xFF; // Version hochsetzen
|
||||
bytes[5] = 0x7F;
|
||||
match CompiledModule::from_tbc(&bytes) {
|
||||
Err(LoadError::Version(v)) => assert_eq!(v, 0x7FFF),
|
||||
other => panic!("Version-Fehler erwartet, war {other:?}"),
|
||||
}
|
||||
let msg = LoadError::Version(0x7FFF).to_string();
|
||||
assert!(msg.contains("32767"), "Meldung nennt die Version: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,19 @@
|
||||
pub mod bytecode;
|
||||
pub mod codegen;
|
||||
pub mod interp;
|
||||
|
||||
use tb_frontend::Diagnostic;
|
||||
|
||||
/// Komplette Übersetzung: Quelltext → Bytecode-Modul.
|
||||
/// Bei Diagnosen (Compile-Fehlern) wird kein Kompilat erzeugt.
|
||||
pub fn compile_source(
|
||||
module_name: &str,
|
||||
source: &str,
|
||||
) -> Result<bytecode::CompiledModule, Vec<Diagnostic>> {
|
||||
let analysis = tb_frontend::analyze_source(module_name, source);
|
||||
if !analysis.diagnostics.is_empty() {
|
||||
return Err(analysis.diagnostics);
|
||||
}
|
||||
let hir = analysis.hir.expect("diagnose-frei, aber kein HIR");
|
||||
Ok(codegen::compile(&hir))
|
||||
}
|
||||
|
||||
457
crates/tb-vm/tests/vm.rs
Normal file
457
crates/tb-vm/tests/vm.rs
Normal file
@@ -0,0 +1,457 @@
|
||||
//! Interpreter-Tests: Spec-Szenarien aus vm-ausfuehrung und
|
||||
//! vm-fehlerbehandlung (Phase-2-Änderung).
|
||||
|
||||
use tb_runtime::host::CaptureHost;
|
||||
use tb_runtime::value::Value;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn run(src: &str) -> (RunEvent, String) {
|
||||
run_with_input(src, &[])
|
||||
}
|
||||
|
||||
fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) {
|
||||
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
|
||||
panic!("Compile-Fehler: {d:?}");
|
||||
});
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::with_input(input);
|
||||
let ev = vm.run(&mut host);
|
||||
(ev, host.output)
|
||||
}
|
||||
|
||||
fn out(src: &str) -> String {
|
||||
let (ev, output) = run(src);
|
||||
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{output}");
|
||||
output
|
||||
}
|
||||
|
||||
fn err_code(src: &str) -> u16 {
|
||||
match run(src).0 {
|
||||
RunEvent::Error { code, .. } => code,
|
||||
other => panic!("Fehler erwartet, war {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 5.1 Ausdrücke und Konvertierungsmatrix --------------------------------
|
||||
|
||||
#[test]
|
||||
fn print_hallo_welt() {
|
||||
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banker_rounding_cint() {
|
||||
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
|
||||
assert_eq!(out("PRINT CINT(0.5); CINT(1.5); CINT(2.5)"), " 0 2 2 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_bei_zuweisung() {
|
||||
// Spec-Szenario: INTEGER-Variable = 40000 → Fehler 6
|
||||
assert_eq!(err_code("i% = 40000"), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intdiv_rundet_operanden_vor() {
|
||||
// Spec-Szenario: PRINT 7.5 \ 2 → 8 \ 2 = 4
|
||||
assert_eq!(out("PRINT 7.5 \\ 2"), " 4 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemischte_arithmetik() {
|
||||
assert_eq!(out("i% = 2\nd# = i% + 1.5#\nPRINT d#"), " 3.5 \n");
|
||||
assert_eq!(out("PRINT 1 / 3"), " .3333333 \n"); // SINGLE-Division
|
||||
assert_eq!(out("PRINT 7 MOD 3; -7 MOD 3"), " 1 -1 \n"); // Vorzeichen wie Dividend
|
||||
assert_eq!(out("PRINT 2 ^ 10"), " 1024 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logik_bitweise() {
|
||||
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
|
||||
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
|
||||
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn division_durch_null() {
|
||||
assert_eq!(err_code("PRINT 1 / 0"), 11);
|
||||
assert_eq!(err_code("PRINT 1 \\ 0"), 11);
|
||||
assert_eq!(err_code("PRINT 0 ^ -1"), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringvergleich_und_verkettung() {
|
||||
assert_eq!(out("PRINT \"a\" + \"b\""), "ab\n");
|
||||
assert_eq!(out("PRINT (\"abc\" < \"abd\")"), "-1 \n");
|
||||
}
|
||||
|
||||
// ---- 5.2 Kontrollfluss -------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn for_ohne_durchlauf() {
|
||||
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
|
||||
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_mit_negativem_step() {
|
||||
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_mit_dynamischem_step() {
|
||||
assert_eq!(
|
||||
out("s% = -2\nFOR i% = 5 TO 1 STEP s%\nPRINT i%;\nNEXT\nPRINT"),
|
||||
" 5 3 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_case_bereiche() {
|
||||
let src = "FOR i% = 1 TO 4\nSELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3: PRINT \"mittel\"\nCASE ELSE: PRINT \"rest\"\nEND SELECT\nNEXT";
|
||||
assert_eq!(out(src), "eins\nmittel\nmittel\nrest\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_loop_varianten() {
|
||||
assert_eq!(
|
||||
out("n% = 3\nDO WHILE n% > 0\nPRINT n%;\nn% = n% - 1\nLOOP\nPRINT"),
|
||||
" 3 2 1 \n"
|
||||
);
|
||||
assert_eq!(
|
||||
out("n% = 0\nDO\nn% = n% + 1\nLOOP UNTIL n% >= 3\nPRINT n%"),
|
||||
" 3 \n"
|
||||
);
|
||||
assert_eq!(
|
||||
out("n% = 5\nWHILE n% > 3\nn% = n% - 1\nWEND\nPRINT n%"),
|
||||
" 3 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_for_und_do() {
|
||||
assert_eq!(
|
||||
out("FOR i% = 1 TO 10\nIF i% = 3 THEN EXIT FOR\nNEXT\nPRINT i%"),
|
||||
" 3 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kontrollfluss_korpusdatei() {
|
||||
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
|
||||
let src = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
|
||||
)
|
||||
.unwrap();
|
||||
let want = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out(&src), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gosub_return_und_fehler_3() {
|
||||
assert_eq!(
|
||||
out("GOSUB U\nPRINT \"zurueck\"\nEND\nU:\nPRINT \"unten\"\nRETURN"),
|
||||
"unten\nzurueck\n"
|
||||
);
|
||||
// Spec-Szenario: RETURN ohne GOSUB → Fehler 3
|
||||
assert_eq!(err_code("RETURN"), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_goto_berechnet() {
|
||||
let src = "FOR i% = 0 TO 3\nON i% GOTO A, B\nPRINT \"kein\";\nGOTO W\nA:\nPRINT \"a\";\nGOTO W\nB:\nPRINT \"b\";\nW:\nNEXT\nPRINT";
|
||||
assert_eq!(out(src), "keinabkein\n");
|
||||
}
|
||||
|
||||
// ---- 5.3 Prozeduren ------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn byref_wirkt_zurueck() {
|
||||
// Spec-Szenario BYREF
|
||||
assert_eq!(
|
||||
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nPRINT n%"),
|
||||
" 2 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn klammern_erzwingen_byval() {
|
||||
// Spec-Szenario BYVAL
|
||||
assert_eq!(
|
||||
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc (n%)\nPRINT n%"),
|
||||
" 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byref_auf_arrayelement() {
|
||||
assert_eq!(
|
||||
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nDIM a%(5)\na%(2) = 7\nInc a%(2)\nPRINT a%(2)"),
|
||||
" 8 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_und_rekursion() {
|
||||
assert_eq!(
|
||||
out("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\nPRINT Quad(3)"),
|
||||
" 9 \n"
|
||||
);
|
||||
let fak = "FUNCTION Fak& (n%)\nIF n% <= 1 THEN\nFak& = 1\nELSE\nFak& = n% * Fak&(n% - 1)\nEND IF\nEND FUNCTION\nPRINT Fak&(10)";
|
||||
assert_eq!(out(fak), " 3628800 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_behaelt_werte() {
|
||||
let src = "SUB Zaehl\nSTATIC n%\nn% = n% + 1\nPRINT n%;\nEND SUB\nZaehl\nZaehl\nZaehl\nPRINT";
|
||||
assert_eq!(out(src), " 1 2 3 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn def_fn_im_modulkontext() {
|
||||
// DEF FN: Parameter lokal (BYVAL), freie Namen binden an Modulvariablen.
|
||||
let src = "faktor = 10\nDEF FNmal (x) = x * faktor\nPRINT FNmal(3)";
|
||||
assert_eq!(out(src), " 30 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ganzes_array_uebergeben() {
|
||||
let src = "SUB Summe (a%(), s%)\ns% = 0\nFOR i% = LBOUND(a%) TO UBOUND(a%)\ns% = s% + a%(i%)\nNEXT\nEND SUB\nDIM w%(3)\nFOR i% = 0 TO 3\nw%(i%) = i%\nNEXT\nSumme w%(), erg%\nPRINT erg%";
|
||||
assert_eq!(out(src), " 6 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udt_wertsemantik_und_felder() {
|
||||
let src = "TYPE Punkt\nx AS INTEGER\ny AS INTEGER\nEND TYPE\nDIM a AS Punkt, b AS Punkt\na.x = 1\nb = a\nb.x = 9\nPRINT a.x; b.x";
|
||||
assert_eq!(out(src), " 1 9 \n");
|
||||
}
|
||||
|
||||
// ---- 5.4 DATA/READ/RESTORE -----------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn data_read_restore() {
|
||||
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
|
||||
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
|
||||
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
|
||||
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
|
||||
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_data_fehler_4() {
|
||||
assert_eq!(err_code("DATA 1\nREAD a%, b%"), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_typkonflikt_fehler_13() {
|
||||
assert_eq!(err_code("DATA hallo\nREAD a%"), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_mit_label() {
|
||||
let src = "DATA 1\nMarke:\nDATA 2\nREAD a%\nRESTORE Marke\nREAD b%\nPRINT a%; b%";
|
||||
assert_eq!(out(src), " 1 2 \n");
|
||||
}
|
||||
|
||||
// ---- 5.5 Fehlerbehandlung -------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn modulweiter_handler_faengt_prozedurfehler() {
|
||||
// Spec-Szenario: Hauptprogramm setzt Handler, SUB löst Fehler 6 aus.
|
||||
let src = "SUB Knall\ni% = 40000\nEND SUB\nON ERROR GOTO Fehler\nKnall\nPRINT \"nie\"\nEND\nFehler:\nPRINT \"ERR=\"; ERR\nEND";
|
||||
assert_eq!(out(src), "ERR= 6 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lokaler_handler_verdeckt_modulweiten() {
|
||||
let src = "SUB Tu\nON LOCAL ERROR GOTO L\nERROR 5\nEXIT SUB\nL:\nPRINT \"lokal\"; ERR\nRESUME Weiter\nWeiter:\nEND SUB\nON ERROR GOTO M\nTu\nEND\nM:\nPRINT \"modul\"\nEND";
|
||||
assert_eq!(out(src), "lokal 5 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_handler_bricht_ab() {
|
||||
// Spec-Szenario: Fehler 9 ohne Handler → Abbruch mit Meldung.
|
||||
let (ev, _) = run("DIM a%(3)\nPRINT a%(7)");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 9);
|
||||
assert_eq!(message, "Subscript out of range");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erl_liefert_zeilennummer() {
|
||||
let src = "ON ERROR GOTO H\n10 ERROR 5\nEND\nH:\nPRINT ERL\nEND";
|
||||
assert_eq!(out(src), " 10 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erl_null_ohne_zeilennummern() {
|
||||
// Spec-Szenario: keine numerischen Zeilennummern → ERL = 0
|
||||
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\nPRINT ERL\nEND";
|
||||
assert_eq!(out(src), " 0 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_anweisung() {
|
||||
// Spec-Szenario: ERROR 53 → Handler mit ERR = 53
|
||||
let src = "ON ERROR GOTO H\nERROR 53\nEND\nH:\nPRINT ERR\nEND";
|
||||
assert_eq!(out(src), " 53 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_wiederholt_anweisung() {
|
||||
let src = "ON ERROR GOTO H\nn% = 0\nversuch% = 0\n10 versuch% = versuch% + 1\nIF versuch% < 3 THEN ERROR 5\nPRINT versuch%\nEND\nH:\nRESUME";
|
||||
// RESUME wiederholt die IF-Anweisung; versuch% bleibt 1? Nein:
|
||||
// Fehler in IF-Zeile, RESUME wiederholt IF — Endlosschleife ohne
|
||||
// Zählerénderung wäre falsch. Zähler steht in Zeile 10, daher: der
|
||||
// Fehler passiert im IF, RESUME wiederholt das IF, versuch% ist noch
|
||||
// < 3 … Um Determinismus zu sichern, zählt der Handler mit.
|
||||
let _ = src;
|
||||
let src2 = "ON ERROR GOTO H\nversuch% = 0\nERROR 5\nPRINT \"nach\"; versuch%\nEND\nH:\nversuch% = versuch% + 1\nIF versuch% < 3 THEN RESUME\nRESUME NEXT";
|
||||
assert_eq!(out(src2), "nach 3 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_next_faehrt_fort() {
|
||||
// Spec-Szenario: RESUME NEXT nach Division durch 0.
|
||||
let src = "ON ERROR GOTO H\nx = 1 / 0\nPRINT \"weiter\"\nEND\nH:\nRESUME NEXT";
|
||||
assert_eq!(out(src), "weiter\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_ohne_fehler_20() {
|
||||
assert_eq!(err_code("RESUME"), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehler_im_handler_ist_fatal() {
|
||||
// Spec-Szenario: kein Kaskadieren.
|
||||
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\ni% = 40000\nEND";
|
||||
let (ev, _) = run(src);
|
||||
match ev {
|
||||
RunEvent::Error { code, .. } => assert_eq!(code, 6),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_error_goto_0_deaktiviert() {
|
||||
let src = "ON ERROR GOTO H\nON ERROR GOTO 0\nERROR 5\nEND\nH:\nPRINT \"nie\"\nEND";
|
||||
assert_eq!(err_code(src), 5);
|
||||
}
|
||||
|
||||
// ---- 5.6 Unterbrechbarkeit -------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn breakpoint_haelt_an_und_setzt_fort() {
|
||||
let module =
|
||||
tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nc% = 3\nPRINT a% + b% + c%").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
vm.add_breakpoint(3);
|
||||
// Spec-Szenario: hält VOR der Anweisung in Zeile 3.
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Breakpoint { line } => assert_eq!(line, 3),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// Spec-Szenario Inspektion: b% ist gesetzt, c% noch 0.
|
||||
assert!(matches!(vm.inspect("b%"), Some(Value::Int(2))));
|
||||
assert!(matches!(vm.inspect("c%"), Some(Value::Int(0))));
|
||||
// Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut).
|
||||
vm.remove_breakpoint(3);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.output, " 6 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn einzelschritt() {
|
||||
let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nEND").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
vm.set_step(true);
|
||||
let mut lines = Vec::new();
|
||||
loop {
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Stepped { line } => lines.push(line),
|
||||
RunEvent::Ended => break,
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(lines, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
// ---- 5.7 Programmende --------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn stop_liefert_zeile_und_ist_fortsetzbar() {
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nSTOP\nPRINT \"b\"").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Stopped { line } => assert_eq!(line, 2),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// IDE-Semantik: CONT = weiterlaufen.
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(host.output, "a\nb\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_und_system() {
|
||||
assert_eq!(run("PRINT \"x\"\nEND").0, RunEvent::Ended);
|
||||
assert_eq!(run("SYSTEM").0, RunEvent::Ended);
|
||||
}
|
||||
|
||||
// ---- Eingabe -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn input_mit_redo() {
|
||||
let (ev, output) = run_with_input(
|
||||
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
|
||||
&["abc", "21"],
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert!(output.contains("Redo from start"));
|
||||
assert!(output.ends_with(" 42 \n"), "{output}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_input_liest_ganze_zeile() {
|
||||
let (ev, output) = run_with_input("LINE INPUT s$\nPRINT s$", &["a, b, c"]);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert!(output.ends_with("a, b, c\n"));
|
||||
}
|
||||
|
||||
// ---- Strings/PRINT über die VM -------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn mid_anweisung_mutiert() {
|
||||
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_zonen_und_tab() {
|
||||
assert_eq!(out("PRINT \"a\", \"b\""), "a b\n");
|
||||
assert_eq!(out("PRINT TAB(5); \"x\""), " x\n");
|
||||
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber Phase 3: Datei-E/A → Laufzeitfehler 73.
|
||||
let (ev, _) = run("OPEN \"x.txt\" FOR INPUT AS #1");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
// Katalogtext des Vorbilds (VBDOS) für Code 73:
|
||||
assert_eq!(message, "Feature unavailable");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user