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