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:
@@ -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