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

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

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

View File

@@ -1,3 +1,680 @@
//! Bytecode-Format der TBVM: Opcodes, Konstantenpool, Modul-/Prozedurtabellen.
//! Bytecode-Definition und `.tbc`-Serialisierung.
//!
//! In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, Enum
//! mit eingebetteten Operanden — Wort-Dispatch); die Serialisierung
//! bildet jede Instruktion auf 1 Opcode-Byte + Operanden (little-endian)
//! ab. Opcode-Bytes sind **stabil** und gruppenweise mit Lücken vergeben
//! (Phase 3 ergänzt in den Lücken). Dokumentation: docs/tbvm-design.md.
// Platzhalter — wird in Phase 2 ausgearbeitet (siehe PLAN.md)
use std::fmt;
use std::rc::Rc;
use tb_runtime::value::{TypeInit, UdtLayout};
pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
pub const TBC_VERSION: u16 = 1;
/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CmpOp {
Eq = 0,
Ne = 1,
Lt = 2,
Le = 3,
Gt = 4,
Ge = 5,
}
impl CmpOp {
fn from_u8(v: u8) -> Result<Self, LoadError> {
Ok(match v {
0 => CmpOp::Eq,
1 => CmpOp::Ne,
2 => CmpOp::Lt,
3 => CmpOp::Le,
4 => CmpOp::Gt,
5 => CmpOp::Ge,
_ => return Err(LoadError::Corrupt("CmpOp")),
})
}
}
#[derive(Debug)]
pub enum LoadError {
BadMagic,
/// Unbekannte Formatversion (enthaltene Version).
Version(u16),
Corrupt(&'static str),
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
LoadError::Version(v) => {
write!(f, "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})")
}
LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
}
}
}
// ---- Encoder/Decoder-Hilfen -------------------------------------------------
pub struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Reader { buf, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
if self.pos + n > self.buf.len() {
return Err(LoadError::Corrupt("unerwartetes Dateiende"));
}
let s = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, LoadError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, LoadError> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Result<u32, LoadError> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn string(&mut self) -> Result<String, LoadError> {
let n = self.u32()? as usize;
let b = self.take(n)?;
String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
}
}
trait Enc: Sized {
fn enc(&self, out: &mut Vec<u8>);
fn dec(r: &mut Reader) -> Result<Self, LoadError>;
}
macro_rules! enc_prim {
($t:ty, $n:literal) => {
impl Enc for $t {
fn enc(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.to_le_bytes());
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(<$t>::from_le_bytes(r.take($n)?.try_into().unwrap()))
}
}
};
}
enc_prim!(u16, 2);
enc_prim!(u32, 4);
enc_prim!(i16, 2);
enc_prim!(i32, 4);
enc_prim!(i64, 8);
enc_prim!(f32, 4);
enc_prim!(f64, 8);
impl Enc for u8 {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
r.u8()
}
}
impl Enc for bool {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self as u8);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
Ok(r.u8()? != 0)
}
}
impl Enc for CmpOp {
fn enc(&self, out: &mut Vec<u8>) {
out.push(*self as u8);
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
CmpOp::from_u8(r.u8()?)
}
}
impl Enc for TypeInit {
fn enc(&self, out: &mut Vec<u8>) {
let (tag, extra): (u8, u32) = match self {
TypeInit::Int => (0, 0),
TypeInit::Lng => (1, 0),
TypeInit::Sng => (2, 0),
TypeInit::Dbl => (3, 0),
TypeInit::Cur => (4, 0),
TypeInit::Str => (5, 0),
TypeInit::FixedStr(n) => (6, *n),
TypeInit::Udt(id) => (7, *id as u32),
TypeInit::Empty => (8, 0),
};
out.push(tag);
out.extend_from_slice(&extra.to_le_bytes());
}
fn dec(r: &mut Reader) -> Result<Self, LoadError> {
let tag = r.u8()?;
let extra = r.u32()?;
Ok(match tag {
0 => TypeInit::Int,
1 => TypeInit::Lng,
2 => TypeInit::Sng,
3 => TypeInit::Dbl,
4 => TypeInit::Cur,
5 => TypeInit::Str,
6 => TypeInit::FixedStr(extra),
7 => TypeInit::Udt(extra as u16),
8 => TypeInit::Empty,
_ => return Err(LoadError::Corrupt("TypeInit")),
})
}
}
// ---- Instruktionssatz ---------------------------------------------------------
macro_rules! instrs {
($( $op:literal $name:ident $(( $($fname:ident : $ft:ty),+ ))? ; )+) => {
/// Eine dekodierte Instruktion. Serialisiert: Opcode-Byte + Operanden.
#[derive(Debug, Clone, PartialEq)]
pub enum Instr {
$( $name $(( $($ft),+ ))? , )+
}
impl Instr {
pub fn encode(&self, out: &mut Vec<u8>) {
match self {
$( Instr::$name $(( $($fname),+ ))? => {
out.push($op);
$( $( Enc::enc($fname, out); )+ )?
} )+
}
}
pub fn decode(r: &mut Reader) -> Result<Instr, LoadError> {
let op = r.u8()?;
Ok(match op {
$( $op => Instr::$name $(( $( <$ft as Enc>::dec(r)? ),+ ))? , )+
_ => return Err(LoadError::Corrupt("Opcode")),
})
}
}
};
}
instrs! {
// 0x00 — Anweisungsgrenzen und Kontrolle
0x00 Stmt(a: u32); // Quellzeile; Tick-Prüfung, Resume-Punkt
0x01 SetErl(a: u32); // numerische Zeilennummer durchlaufen
0x02 End;
0x03 StopInstr;
0x04 SystemInstr;
0x05 Unsupported(a: u16); // Name im Stringpool → Fehler 73
// 0x10 — Konstanten und Stack
0x10 PushInt(a: i16);
0x11 PushLng(a: i32);
0x12 PushSng(a: f32);
0x13 PushDbl(a: f64);
0x14 PushCur(a: i64);
0x15 PushStr(a: u16);
0x16 Dup;
0x17 Pop;
// 0x20 — Variablen und Referenzen
0x20 LoadGlobal(a: u16);
0x21 StoreGlobal(a: u16);
0x22 LoadLocal(a: u16);
0x23 StoreLocal(a: u16);
0x24 LoadRef(a: u16); // durch Referenz in lokalem Slot lesen
0x25 StoreRef(a: u16);
0x26 MakeRefGlobal(a: u16);
0x27 MakeRefLocal(a: u16);
0x28 MakeRefElem(a: u8); // Handle+Indizes → Elementreferenz
0x29 MakeRefField(a: u16); // Rec/Feldreferenz → tiefere Feldreferenz
// 0x30 — Arrays und Records
0x30 LoadArr(a: bool, b: u16, c: u8, d: TypeInit); // Slot sichern (Auto-DIM) + Handle
0x31 LoadElem(a: u8);
0x32 StoreElem(a: u8);
0x33 DimArr(a: bool, b: u16, c: u8, d: TypeInit);
0x34 RedimArr(a: bool, b: u16, c: u8, d: TypeInit);
0x35 EraseSlot(a: bool, b: u16);
0x36 LoadField(a: u16);
0x37 StoreField(a: u16);
0x38 CopyRec;
0x39 ArrBound(a: bool); // true = LBOUND
0x3A FixStr(a: u32); // auf feste Länge kürzen/padden
// 0x40 — Arithmetik (monomorph)
0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
0x4A MulI2; 0x4B MulI4; 0x4C MulR4; 0x4D MulR8; 0x4E MulCy;
0x4F NegI2; 0x50 NegI4; 0x51 NegR4; 0x52 NegR8; 0x53 NegCy;
0x54 DivR4; 0x55 DivR8;
0x56 IDivI2; 0x57 IDivI4;
0x58 ModI2; 0x59 ModI4;
0x5A PowR8;
0x5B Concat;
// 0x60 — Konvertierungen (Matrix)
0x60 ConvI2I4; 0x61 ConvI2R4; 0x62 ConvI2R8; 0x63 ConvI2Cy;
0x64 ConvI4I2; 0x65 ConvI4R4; 0x66 ConvI4R8; 0x67 ConvI4Cy;
0x68 ConvR4I2; 0x69 ConvR4I4; 0x6A ConvR4R8; 0x6B ConvR4Cy;
0x6C ConvR8I2; 0x6D ConvR8I4; 0x6E ConvR8R4; 0x6F ConvR8Cy;
0x70 ConvCyI2; 0x71 ConvCyI4; 0x72 ConvCyR4; 0x73 ConvCyR8;
// 0x80 — Logik (bitweise)
0x80 NotI2; 0x81 NotI4;
0x82 AndI2; 0x83 AndI4;
0x84 OrI2; 0x85 OrI4;
0x86 XorI2; 0x87 XorI4;
0x88 EqvI2; 0x89 EqvI4;
0x8A ImpI2; 0x8B ImpI4;
// 0x90 — Vergleiche (Ergebnis INTEGER 1/0)
0x90 CmpI2(a: CmpOp);
0x91 CmpI4(a: CmpOp);
0x92 CmpR4(a: CmpOp);
0x93 CmpR8(a: CmpOp);
0x94 CmpCy(a: CmpOp);
0x95 CmpStr(a: CmpOp);
// 0xA0 — Kontrollfluss
0xA0 Jump(a: u32);
0xA1 JumpIfFalse(a: u32);
0xA2 JumpIfTrue(a: u32);
0xA3 Gosub(a: u32);
0xA4 RetGosub;
0xA5 RetGosubTo(a: u32);
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
// 0xB0 — Prozeduren und Builtins
0xB0 Call(a: u16, b: u8);
0xB1 RetProc;
0xB2 RetFn;
0xB3 CallBuiltin(a: u16, b: u8);
// 0xC0 — Fehlerbehandlung
0xC0 OnErrorGoto(a: u32);
0xC1 OnErrorLocal(a: u32);
0xC2 OnErrorDisable;
0xC3 OnErrorLocalDisable;
0xC4 OnErrorResumeNext(a: bool);
0xC5 Resume0;
0xC6 ResumeNext;
0xC7 ResumeLabel(a: u32);
0xC8 RaiseError; // Code vom Stack (ERROR n)
0xC9 LoadErr;
0xCA LoadErl;
// 0xD0 — DATA und Eingabe
0xD0 ReadData(a: u8); // nächstes DATA-Element; 0 = String, 1 = Zahl (DOUBLE)
0xD1 Restore(a: u32);
0xD2 Input(a: u8, b: bool, c: u16, d: bool); // argc, line_mode, prompt (0xFFFF=ohne), '?'
}
// ---- Modulstruktur ------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct ProcCode {
pub name: String,
pub n_params: u16,
/// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
/// wird beim Aufruf durch die Argumente ersetzt).
pub locals_init: Vec<TypeInit>,
/// Slot-Namen (Debugger-Inspektion).
pub local_names: Vec<String>,
pub code: Vec<Instr>,
}
#[derive(Debug, Clone)]
pub struct DataItem {
pub text: String,
pub line: u32,
}
/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
#[derive(Debug)]
pub struct CompiledModule {
pub name: String,
/// `OPTION BASE` (Untergrenze impliziter Arrays).
pub option_base: u8,
/// Deduplizierter Stringpool.
pub strings: Vec<Rc<str>>,
pub globals_init: Vec<TypeInit>,
pub global_names: Vec<String>,
pub udts: Vec<UdtLayout>,
/// Prozeduren; Index 0 ist das Hauptprogramm (modul-qualifiziert über
/// `name` des Moduls + Prozedurname).
pub procs: Vec<ProcCode>,
pub data: Vec<DataItem>,
/// Sprungtabellen für `ON n GOTO/GOSUB`.
pub jump_tables: Vec<Vec<u32>>,
}
fn w_string(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}
impl CompiledModule {
/// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
/// (Kennung/Offset/Länge), Abschnitte MODN, CONS, TYPS, GLOB, PROC
/// (mit eingebettetem Code und Zeileninfo), DATA, JMPT.
pub fn to_tbc(&self) -> Vec<u8> {
let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();
let mut modn = Vec::new();
w_string(&mut modn, &self.name);
modn.push(self.option_base);
sections.push((*b"MODN", modn));
let mut cons = Vec::new();
cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
for s in &self.strings {
w_string(&mut cons, s);
}
sections.push((*b"CONS", cons));
let mut typs = Vec::new();
typs.extend_from_slice(&(self.udts.len() as u32).to_le_bytes());
for u in &self.udts {
w_string(&mut typs, &u.name);
typs.extend_from_slice(&(u.fields.len() as u32).to_le_bytes());
for f in &u.fields {
f.enc(&mut typs);
}
}
sections.push((*b"TYPS", typs));
let mut glob = Vec::new();
glob.extend_from_slice(&(self.globals_init.len() as u32).to_le_bytes());
for (init, name) in self.globals_init.iter().zip(&self.global_names) {
init.enc(&mut glob);
w_string(&mut glob, name);
}
sections.push((*b"GLOB", glob));
let mut proc = Vec::new();
proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
for p in &self.procs {
w_string(&mut proc, &p.name);
proc.extend_from_slice(&p.n_params.to_le_bytes());
proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
for (init, name) in p.locals_init.iter().zip(&p.local_names) {
init.enc(&mut proc);
w_string(&mut proc, name);
}
let mut code = Vec::new();
for i in &p.code {
i.encode(&mut code);
}
proc.extend_from_slice(&(p.code.len() as u32).to_le_bytes());
proc.extend_from_slice(&(code.len() as u32).to_le_bytes());
proc.extend_from_slice(&code);
}
sections.push((*b"PROC", proc));
let mut data = Vec::new();
data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
for d in &self.data {
w_string(&mut data, &d.text);
data.extend_from_slice(&d.line.to_le_bytes());
}
sections.push((*b"DATA", data));
let mut jmpt = Vec::new();
jmpt.extend_from_slice(&(self.jump_tables.len() as u32).to_le_bytes());
for t in &self.jump_tables {
jmpt.extend_from_slice(&(t.len() as u32).to_le_bytes());
for target in t {
jmpt.extend_from_slice(&target.to_le_bytes());
}
}
sections.push((*b"JMPT", jmpt));
// Header + Abschnittstabelle
let mut out = Vec::new();
out.extend_from_slice(TBC_MAGIC);
out.extend_from_slice(&TBC_VERSION.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); // Flags
out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
let table_start = out.len();
// Platzhalter für Tabelle
for _ in 0..sections.len() {
out.extend_from_slice(&[0u8; 12]);
}
let mut offsets = Vec::new();
for (_, payload) in &sections {
offsets.push((out.len() as u32, payload.len() as u32));
out.extend_from_slice(payload);
}
for (i, ((id, _), (off, len))) in sections.iter().zip(&offsets).enumerate() {
let at = table_start + i * 12;
out[at..at + 4].copy_from_slice(id);
out[at + 4..at + 8].copy_from_slice(&off.to_le_bytes());
out[at + 8..at + 12].copy_from_slice(&len.to_le_bytes());
}
out
}
pub fn from_tbc(buf: &[u8]) -> Result<CompiledModule, LoadError> {
let mut r = Reader::new(buf);
if r.take(4)? != TBC_MAGIC {
return Err(LoadError::BadMagic);
}
let version = r.u16()?;
if version != TBC_VERSION {
return Err(LoadError::Version(version));
}
let _flags = r.u16()?;
let n_sections = r.u32()? as usize;
let mut table = Vec::new();
for _ in 0..n_sections {
let id: [u8; 4] = r.take(4)?.try_into().unwrap();
let off = r.u32()? as usize;
let len = r.u32()? as usize;
table.push((id, off, len));
}
let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
for (sid, off, len) in &table {
if sid == id {
if off + len > buf.len() {
return Err(LoadError::Corrupt("Abschnittstabelle"));
}
return Ok(Reader::new(&buf[*off..*off + *len]));
}
}
Err(LoadError::Corrupt("Abschnitt fehlt"))
};
let mut r = section(b"MODN")?;
let name = r.string()?;
let option_base = r.u8()?;
let mut r = section(b"CONS")?;
let n = r.u32()? as usize;
let mut strings = Vec::with_capacity(n);
for _ in 0..n {
strings.push(Rc::from(r.string()?.as_str()));
}
let mut r = section(b"TYPS")?;
let n = r.u32()? as usize;
let mut udts = Vec::with_capacity(n);
for _ in 0..n {
let name = r.string()?;
let nf = r.u32()? as usize;
let mut fields = Vec::with_capacity(nf);
for _ in 0..nf {
fields.push(TypeInit::dec(&mut r)?);
}
udts.push(UdtLayout { name, fields });
}
let mut r = section(b"GLOB")?;
let n = r.u32()? as usize;
let mut globals_init = Vec::with_capacity(n);
let mut global_names = Vec::with_capacity(n);
for _ in 0..n {
globals_init.push(TypeInit::dec(&mut r)?);
global_names.push(r.string()?);
}
let mut r = section(b"PROC")?;
let n = r.u32()? as usize;
let mut procs = Vec::with_capacity(n);
for _ in 0..n {
let name = r.string()?;
let n_params = r.u16()?;
let nl = r.u32()? as usize;
let mut locals_init = Vec::with_capacity(nl);
let mut local_names = Vec::with_capacity(nl);
for _ in 0..nl {
locals_init.push(TypeInit::dec(&mut r)?);
local_names.push(r.string()?);
}
let n_instr = r.u32()? as usize;
let code_len = r.u32()? as usize;
let code_bytes = r.take(code_len)?;
let mut cr = Reader::new(code_bytes);
let mut code = Vec::with_capacity(n_instr);
for _ in 0..n_instr {
code.push(Instr::decode(&mut cr)?);
}
procs.push(ProcCode { name, n_params, locals_init, local_names, code });
}
let mut r = section(b"DATA")?;
let n = r.u32()? as usize;
let mut data = Vec::with_capacity(n);
for _ in 0..n {
let text = r.string()?;
let line = r.u32()?;
data.push(DataItem { text, line });
}
let mut r = section(b"JMPT")?;
let n = r.u32()? as usize;
let mut jump_tables = Vec::with_capacity(n);
for _ in 0..n {
let m = r.u32()? as usize;
let mut t = Vec::with_capacity(m);
for _ in 0..m {
t.push(r.u32()?);
}
jump_tables.push(t);
}
Ok(CompiledModule {
name,
option_base,
strings,
globals_init,
global_names,
udts,
procs,
data,
jump_tables,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instr_roundtrip() {
let samples = vec![
Instr::Stmt(42),
Instr::PushInt(-7),
Instr::PushDbl(1.5),
Instr::PushCur(-12_345),
Instr::LoadArr(true, 3, 2, TypeInit::FixedStr(30)),
Instr::CmpR8(CmpOp::Le),
Instr::OnJump(1, true),
Instr::Call(2, 3),
Instr::Input(2, false, 0xFFFF, true),
Instr::ConvCyR8,
Instr::RetFn,
];
let mut buf = Vec::new();
for i in &samples {
i.encode(&mut buf);
}
let mut r = Reader::new(&buf);
for want in &samples {
let got = Instr::decode(&mut r).unwrap();
assert_eq!(&got, want);
}
}
#[test]
fn tbc_roundtrip() {
let m = CompiledModule {
name: "TEST".into(),
option_base: 1,
strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
globals_init: vec![TypeInit::Int, TypeInit::Str],
global_names: vec!["a".into(), "s".into()],
udts: vec![UdtLayout {
name: "Kunde".into(),
fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
}],
procs: vec![ProcCode {
name: "TEST".into(),
n_params: 0,
locals_init: vec![],
local_names: vec![],
code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
}],
data: vec![DataItem { text: "1.5".into(), line: 3 }],
jump_tables: vec![vec![4, 9]],
};
let bytes = m.to_tbc();
let back = CompiledModule::from_tbc(&bytes).unwrap();
assert_eq!(back.name, "TEST");
assert_eq!(back.strings.len(), 2);
assert_eq!(&*back.strings[0], "Hallo");
assert_eq!(back.globals_init, m.globals_init);
assert_eq!(back.udts[0].fields, m.udts[0].fields);
assert_eq!(back.procs[0].code, m.procs[0].code);
assert_eq!(back.data[0].text, "1.5");
assert_eq!(back.jump_tables, m.jump_tables);
}
#[test]
fn unbekannte_version_wird_abgelehnt() {
let m = CompiledModule {
name: "T".into(),
option_base: 0,
strings: vec![],
globals_init: vec![],
global_names: vec![],
udts: vec![],
procs: vec![],
data: vec![],
jump_tables: vec![],
};
let mut bytes = m.to_tbc();
bytes[4] = 0xFF; // Version hochsetzen
bytes[5] = 0x7F;
match CompiledModule::from_tbc(&bytes) {
Err(LoadError::Version(v)) => assert_eq!(v, 0x7FFF),
other => panic!("Version-Fehler erwartet, war {other:?}"),
}
let msg = LoadError::Version(0x7FFF).to_string();
assert!(msg.contains("32767"), "Meldung nennt die Version: {msg}");
}
}