//! 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_frontend::forms::{FormObject, ObjectClass}; use tb_frontend::hir::{HEventProc, HParam, HProcKind, HTy, NumTy}; use tb_frontend::source::SourceFile; use tb_runtime::value::{TypeInit, UdtLayout}; use tb_ui::forms::PropertyValue; use tb_ui::frm::FormInitial; pub const TBC_MAGIC: &[u8; 4] = b"TBC\0"; pub const TBC_VERSION: u16 = 4; /// 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 finish(&self) -> Result<(), LoadError> { if self.pos != self.buf.len() { Err(LoadError::Corrupt("überzählige Abschnittsdaten")) } else { Ok(()) } } fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> { if n > self.buf.len().saturating_sub(self.pos) { 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 { match r.u8()? { 0 => Ok(false), 1 => Ok(true), _ => Err(LoadError::Corrupt("bool")), } } } 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()?; if (tag != 6 && tag != 7 && extra != 0) || (tag == 7 && extra > u16::MAX as u32) { return Err(LoadError::Corrupt("TypeInit-Zusatz")); } 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 0x06 Source(a: u32, b: u32); // Quelldatei-ID, physische Spalte der folgenden Stmt-Grenze 0x07 InitStmt(a: u32); // globale Initialisierung: Quellort/Debugger, noch keine Ereignisse // 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; 0x18 PushUdtId(a: u16); // TYPE-Index als LONG auf dem Stack (ISAM) // 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 0x3B CommonArr(a: bool, b: u16, c: u8, d: TypeInit); // gemeinsame Initialisierung/Layoutprüfung // 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? 0xA7 Run(a: u8); // 0 = ohne Ziel, 1 = Zeile, 2 = Datei // 0xB0 — Prozeduren und Builtins 0xB0 Call(a: u16, b: u8); 0xB1 RetProc; 0xB2 RetFn; 0xB3 CallBuiltin(a: u16, b: u8); 0xB4 LoadObjectProperty(a: u16, b: u16, c: bool); 0xB5 StoreObjectProperty(a: u16, b: u16, c: bool); 0xB6 PushObject(a: u16, b: bool); 0xB7 TypeOf(a: u8); 0xB8 ObjectMethod(a: u16, b: u16, c: u8); 0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack? 0xBA LoadDynamicObjectProperty(a: u16); 0xBB StoreDynamicObjectProperty(a: u16); 0xBC LoadObjectIndexedProperty(a: u16, b: u16); 0xBD ObjectMethodFn(a: u16, b: u16, c: u8); 0xBE StoreObjectIndexedProperty(a: u16, b: u16); // 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack 0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel 0xE1 TrapDisable(a: u8); // GOSUB 0 0xE2 TrapSet(a: u8, b: u8); // Quellenart, Zustand 0xE3 EventSwitch(a: bool); 0xE4 Doevents; // Zustellpunkt; legt 0 ab 0xE5 Sleep(a: bool); // hat Argument? Dauer vom Stack (Sekunden) // 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 } impl Enc for HTy { fn enc(&self, out: &mut Vec) { let tag = match self { HTy::Num(NumTy::Int) => 0, HTy::Num(NumTy::Lng) => 1, HTy::Num(NumTy::Sng) => 2, HTy::Num(NumTy::Dbl) => 3, HTy::Num(NumTy::Cur) => 4, HTy::Str => 5, HTy::FixedStr(_) => 6, HTy::Udt(_) => 7, HTy::Form => 8, HTy::Control => 9, }; out.push(tag); match self { HTy::FixedStr(n) => n.enc(out), HTy::Udt(n) => n.enc(out), _ => {} } } fn dec(r: &mut Reader) -> Result { Ok(match r.u8()? { 0 => HTy::Num(NumTy::Int), 1 => HTy::Num(NumTy::Lng), 2 => HTy::Num(NumTy::Sng), 3 => HTy::Num(NumTy::Dbl), 4 => HTy::Num(NumTy::Cur), 5 => HTy::Str, 6 => HTy::FixedStr(r.u32()?), 7 => HTy::Udt(r.u16()?), 8 => HTy::Form, 9 => HTy::Control, _ => return Err(LoadError::Corrupt("Signaturtyp")), }) } } impl Enc for PropertyValue { fn enc(&self, out: &mut Vec) { match self { Self::Integer(v) => { out.push(0); v.enc(out); } Self::Single(v) => { out.push(1); v.enc(out); } Self::String(v) => { out.push(2); w_string(out, v); } Self::Boolean(v) => { out.push(3); v.enc(out); } Self::Object(v) => { out.push(4); v.is_some().enc(out); if let Some((object, index)) = v { object.enc(out); index.is_some().enc(out); if let Some(index) = index { index.enc(out); } } } Self::IntegerArray(v) => { out.push(5); (v.len() as u32).enc(out); for v in v { v.enc(out); } } } } fn dec(r: &mut Reader) -> Result { Ok(match r.u8()? { 0 => Self::Integer(i32::dec(r)?), 1 => Self::Single(f32::dec(r)?), 2 => Self::String(r.string()?), 3 => Self::Boolean(bool::dec(r)?), 4 => Self::Object(if bool::dec(r)? { Some(( r.u16()?, if bool::dec(r)? { Some(i32::dec(r)?) } else { None }, )) } else { None }), 5 => { let n = r.u32()?; let mut values = Vec::new(); for _ in 0..n { values.push(i32::dec(r)?); } Self::IntegerArray(values) } _ => return Err(LoadError::Corrupt("Anfangswerttyp")), }) } } // ---- Modulstruktur ------------------------------------------------------------ #[derive(Debug, Clone)] pub struct ProcCode { pub module: u16, pub kind: HProcKind, pub params: Vec, pub ret_ty: Option, 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, Clone)] pub struct CompiledModule { pub modules: Vec<(String, u8)>, pub sources: Vec, pub form_initial: Vec, pub startup_form: Option, 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>, pub objects: Vec, pub event_procs: 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 { pub fn validate(&self) -> Result<(), LoadError> { let bad = || LoadError::Corrupt("ungültige Tabellenreferenz oder Anfangsdaten"); if self.procs.is_empty() || self.sources.is_empty() || self.modules.is_empty() || self.modules.len() > u16::MAX as usize || self.procs.len() > u16::MAX as usize || self.objects.len() >= u16::MAX as usize || self.strings.len() >= u16::MAX as usize || self.globals_init.len() > u16::MAX as usize || self.global_names.len() != self.globals_init.len() || self.modules.iter().any(|(_, base)| *base > 1) || self.option_base != self.modules[0].1 || self .sources .iter() .any(|s| s.module as usize >= self.modules.len()) { return Err(bad()); } let ty_ok = |ty: &TypeInit| !matches!(ty, TypeInit::Udt(id) if *id as usize >= self.udts.len()); let sig_ok = |ty: &HTy| !matches!(ty, HTy::Udt(id) if *id as usize >= self.udts.len()); if self.globals_init.iter().any(|t| !ty_ok(t)) { return Err(bad()); } for (id, udt) in self.udts.iter().enumerate() { if udt .fields .iter() .any(|t| matches!(t, TypeInit::Udt(n) if *n as usize >= id)) { return Err(bad()); } } for (id, o) in self.objects.iter().enumerate() { if o.parent.is_some_and(|parent| parent as usize >= id) { return Err(bad()); } if o.parent_form.as_deref() != o.parent .map(|parent| self.objects[parent as usize].name.as_str()) { return Err(bad()); } } if self.startup_form.is_some_and(|id| { self.objects .get(id as usize) .is_none_or(|o| o.class != ObjectClass::Form) }) { return Err(bad()); } for e in &self.event_procs { if e.object as usize >= self.objects.len() || e.proc as usize >= self.procs.len() { return Err(bad()); } } let mut initials = std::collections::HashSet::new(); for initial in &self.form_initial { let object = self.objects.get(initial.object as usize).ok_or_else(bad)?; if !initials.insert((initial.object, initial.index)) || (initial.index != 0 && !object.array) { return Err(bad()); } for (id, value) in &initial.properties { use tb_frontend::forms::PropertyType as T; let spec = tb_frontend::forms::properties(object.class) .get(*id as usize) .copied() .ok_or_else(bad)?; let valid = match (value, spec.ty) { (PropertyValue::Integer(v), T::Integer) => { !spec.min.is_some_and(|min| *v < min) && !spec.max.is_some_and(|max| *v > max) && (spec.name != "INDEX" || *v == initial.index) } (PropertyValue::Single(v), T::Single) => v.is_finite(), (PropertyValue::String(_), T::String) | (PropertyValue::Boolean(_), T::Boolean) | (PropertyValue::IntegerArray(_), T::IntegerArray) => true, (PropertyValue::Object(v), T::Object) => { v.is_none_or(|(id, _)| (id as usize) < self.objects.len()) } _ => false, }; if !valid { return Err(bad()); } } } // PARENT overrides carry design-array indices in the existing TBC4 property format. let mut parents: std::collections::HashMap<_, _> = self .objects .iter() .enumerate() .map(|(id, object)| ((id as u16, 0), object.parent.map(|id| (id, 0)))) .collect(); for initial in &self.form_initial { let object = &self.objects[initial.object as usize]; let parent = tb_frontend::forms::property(object.class, "PARENT") .and_then(|(id, _)| initial.properties.get(&id)); parents.insert( (initial.object, initial.index), match parent { Some(PropertyValue::Object(parent)) => { parent.map(|(id, index)| (id, index.unwrap_or(0))) } _ => parents[&(initial.object, 0)], }, ); } for key in parents.keys() { let mut seen = std::collections::HashSet::new(); let mut current = Some(*key); while let Some(key) = current { if !seen.insert(key) { return Err(bad()); } current = *parents.get(&key).ok_or_else(bad)?; } } for p in &self.procs { if p.module as usize >= self.modules.len() || p.n_params as usize != p.params.len() || p.params.len() > p.locals_init.len() || p.locals_init.len() != p.local_names.len() || p.locals_init.iter().any(|t| !ty_ok(t)) || p.params.iter().any(|p| !sig_ok(&p.ty)) || p.ret_ty.as_ref().is_some_and(|t| !sig_ok(t)) || matches!(p.kind, HProcKind::Function | HProcKind::DefFn) != p.ret_ty.is_some() || p.params.iter().zip(&p.locals_init).any(|(param, init)| { *init != if param.array { TypeInit::Empty } else { crate::codegen::type_init(¶m.ty) } }) { return Err(bad()); } for instruction in &p.code { use Instr::*; let valid = match instruction { Source(id, _) => (*id as usize) < self.sources.len(), PushUdtId(id) => (*id as usize) < self.udts.len(), PushStr(id) | Unsupported(id) => (*id as usize) < self.strings.len(), LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => { (*id as usize) < self.globals_init.len() } LoadLocal(id) | StoreLocal(id) | MakeRefLocal(id) | LoadRef(id) | StoreRef(id) => (*id as usize) < p.locals_init.len(), LoadArr(global, id, _, ty) | DimArr(global, id, _, ty) | CommonArr(global, id, _, ty) | RedimArr(global, id, _, ty) => { ty_ok(ty) && (*id as usize) < if *global { self.globals_init.len() } else { p.locals_init.len() } } EraseSlot(global, id) => { (*id as usize) < if *global { self.globals_init.len() } else { p.locals_init.len() } } Call(id, argc) => self .procs .get(*id as usize) .is_some_and(|p| p.n_params == *argc as u16), Jump(pc) | JumpIfFalse(pc) | JumpIfTrue(pc) | Gosub(pc) | RetGosubTo(pc) | OnErrorLocal(pc) | ResumeLabel(pc) => (*pc as usize) < p.code.len(), OnErrorGoto(pc) => self .procs .first() .is_some_and(|p| (*pc as usize) < p.code.len()), OnJump(id, _) => self .jump_tables .get(*id as usize) .is_some_and(|t| t.iter().all(|pc| (*pc as usize) < p.code.len())), Restore(id) => (*id as usize) <= self.data.len(), Input(_, _, id, _) => *id == u16::MAX || (*id as usize) < self.strings.len(), LoadObjectProperty(id, prop, _) | StoreObjectProperty(id, prop, _) | LoadObjectIndexedProperty(id, prop) | StoreObjectIndexedProperty(id, prop) => { self.objects.get(*id as usize).is_some_and(|o| { ((*prop & 0x7fff) as usize) < tb_frontend::forms::properties(o.class).len() }) } PushObject(id, _) | ObjectLoad(id, _, _) => (*id as usize) < self.objects.len(), ObjectMethod(id, method, _) | ObjectMethodFn(id, method, _) => { self.objects.get(*id as usize).is_some_and(|o| { (*method as usize) < tb_frontend::forms::methods(o.class).len() }) } LoadDynamicObjectProperty(name) | StoreDynamicObjectProperty(name) => { (*name as usize) < self.strings.len() } GetPut(_, _, 7, id) => (*id as usize) < self.udts.len(), GetPut(_, _, kind, _) => *kind <= 8, TypeOf(class) => ObjectClass::from_id(*class).is_some(), TrapDefine(kind, pc) => *kind <= 3 && (*pc as usize) < p.code.len(), TrapDisable(kind) => *kind <= 3, TrapSet(kind, state) => *kind <= 3 && *state <= 2, Run(kind) => *kind <= 2, ReadData(kind) => *kind <= 1, _ => true, }; if !valid { return Err(bad()); } } } Ok(()) } /// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle /// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC /// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS. 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 srcs = Vec::new(); (self.modules.len() as u32).enc(&mut srcs); for (name, base) in &self.modules { w_string(&mut srcs, name); base.enc(&mut srcs); } (self.sources.len() as u32).enc(&mut srcs); for source in &self.sources { source.module.enc(&mut srcs); w_string(&mut srcs, &source.path); } sections.push((*b"SRCS", srcs)); 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); p.module.enc(&mut proc); (p.kind as u8).enc(&mut proc); (p.params.len() as u32).enc(&mut proc); for param in &p.params { w_string(&mut proc, ¶m.name); param.ty.enc(&mut proc); param.array.enc(&mut proc); param.by_ref.enc(&mut proc); } p.ret_ty.is_some().enc(&mut proc); if let Some(ty) = &p.ret_ty { ty.enc(&mut proc); } 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)); let mut objs = Vec::new(); objs.extend_from_slice(&(self.objects.len() as u32).to_le_bytes()); for o in &self.objects { w_string(&mut objs, &o.name); objs.push(o.class.id()); w_string(&mut objs, o.parent_form.as_deref().unwrap_or("")); objs.push(o.array as u8); o.parent.unwrap_or(u16::MAX).enc(&mut objs); } objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes()); for e in &self.event_procs { objs.extend_from_slice(&e.object.to_le_bytes()); w_string(&mut objs, &e.event); objs.extend_from_slice(&e.proc.to_le_bytes()); } self.startup_form.unwrap_or(u16::MAX).enc(&mut objs); (self.form_initial.len() as u32).enc(&mut objs); for initial in &self.form_initial { initial.object.enc(&mut objs); initial.index.enc(&mut objs); (initial.properties.len() as u32).enc(&mut objs); for (id, value) in &initial.properties { id.enc(&mut objs); value.enc(&mut objs); } } sections.push((*b"OBJS", objs)); // 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)); } if r.u16()? != 0 { return Err(LoadError::Corrupt("Header-Flags")); } let n_sections = r.u32()? as usize; if n_sections != 9 { return Err(LoadError::Corrupt("Abschnittsanzahl")); } 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 expected = [ *b"MODN", *b"SRCS", *b"CONS", *b"TYPS", *b"GLOB", *b"PROC", *b"DATA", *b"JMPT", *b"OBJS", ]; let mut seen = std::collections::HashSet::new(); let mut ranges = Vec::new(); for (id, off, len) in &table { if !expected.contains(id) || !seen.insert(*id) || *off < r.pos || *off > buf.len() || *len > buf.len() - *off { return Err(LoadError::Corrupt("Abschnittstabelle")); } ranges.push((*off, off + len)); } ranges.sort_unstable(); if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) { return Err(LoadError::Corrupt("überlappende Abschnitte")); } 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()?; r.finish()?; let mut r = section(b"SRCS")?; let n = r.u32()?; let mut modules = Vec::new(); for _ in 0..n { modules.push((r.string()?, r.u8()?)); } let n = r.u32()?; let mut sources = Vec::new(); for _ in 0..n { sources.push(SourceFile { module: r.u16()?, path: r.string()?, }); } r.finish()?; let mut r = section(b"CONS")?; let n = r.u32()? as usize; let mut strings = Vec::new(); for _ in 0..n { strings.push(Rc::from(r.string()?.as_str())); } r.finish()?; let mut r = section(b"TYPS")?; let n = r.u32()? as usize; let mut udts = Vec::new(); for _ in 0..n { let name = r.string()?; let nf = r.u32()? as usize; let mut fields = Vec::new(); for _ in 0..nf { fields.push(TypeInit::dec(&mut r)?); } udts.push(UdtLayout { name, fields }); } r.finish()?; let mut r = section(b"GLOB")?; let n = r.u32()? as usize; let mut globals_init = Vec::new(); let mut global_names = Vec::new(); for _ in 0..n { globals_init.push(TypeInit::dec(&mut r)?); global_names.push(r.string()?); } r.finish()?; let mut r = section(b"PROC")?; let n = r.u32()? as usize; let mut procs = Vec::new(); for _ in 0..n { let name = r.string()?; let module = r.u16()?; let kind = match r.u8()? { 0 => HProcKind::Main, 1 => HProcKind::Sub, 2 => HProcKind::Function, 3 => HProcKind::DefFn, _ => return Err(LoadError::Corrupt("Prozedurart")), }; let np = r.u32()?; let mut params = Vec::new(); for _ in 0..np { params.push(HParam { name: r.string()?, ty: HTy::dec(&mut r)?, array: bool::dec(&mut r)?, by_ref: bool::dec(&mut r)?, }); } let ret_ty = if bool::dec(&mut r)? { Some(HTy::dec(&mut r)?) } else { None }; let n_params = r.u16()?; let nl = r.u32()? as usize; let mut locals_init = Vec::new(); let mut local_names = Vec::new(); 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::new(); for _ in 0..n_instr { code.push(Instr::decode(&mut cr)?); } cr.finish()?; procs.push(ProcCode { module, kind, params, ret_ty, name, n_params, locals_init, local_names, code, }); } r.finish()?; let mut r = section(b"DATA")?; let n = r.u32()? as usize; let mut data = Vec::new(); for _ in 0..n { let text = r.string()?; let line = r.u32()?; data.push(DataItem { text, line }); } r.finish()?; let mut r = section(b"JMPT")?; let n = r.u32()? as usize; let mut jump_tables = Vec::new(); for _ in 0..n { let m = r.u32()? as usize; let mut t = Vec::new(); for _ in 0..m { t.push(r.u32()?); } jump_tables.push(t); } r.finish()?; let mut r = section(b"OBJS")?; let n = r.u32()? as usize; let mut objects = Vec::new(); for _ in 0..n { let name = r.string()?; let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?; let parent = r.string()?; let array = bool::dec(&mut r)?; let parent_id = r.u16()?; objects.push(FormObject { name, class, parent_form: (!parent.is_empty()).then_some(parent), parent: (parent_id != u16::MAX).then_some(parent_id), array, }); } let n = r.u32()? as usize; let mut event_procs = Vec::new(); for _ in 0..n { event_procs.push(HEventProc { object: r.u16()?, event: r.string()?, proc: r.u16()?, }); } let startup = r.u16()?; let startup_form = (startup != u16::MAX).then_some(startup); let n = r.u32()?; let mut form_initial = Vec::new(); for _ in 0..n { let object = r.u16()?; let index = i32::dec(&mut r)?; let n = r.u32()?; let mut properties = std::collections::BTreeMap::new(); for _ in 0..n { if properties .insert(r.u16()?, PropertyValue::dec(&mut r)?) .is_some() { return Err(LoadError::Corrupt("doppelte Anfangseigenschaft")); } } form_initial.push(FormInitial { object, index, properties, }); } r.finish()?; let module = CompiledModule { modules, sources, form_initial, startup_form, name, option_base, strings, globals_init, global_names, udts, procs, data, jump_tables, objects, event_procs, }; module.validate()?; Ok(module) } } #[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::TrapDefine(1, 4242), Instr::TrapDisable(3), Instr::TrapSet(0, 2), Instr::EventSwitch(false), Instr::Doevents, Instr::Sleep(true), 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 { modules: vec![("TEST".into(), 1)], sources: vec![SourceFile { module: 0, path: "test.bas".into(), }], form_initial: vec![], startup_form: None, 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 { module: 0, kind: HProcKind::Main, params: vec![], ret_ty: None, 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]], objects: vec![], event_procs: vec![], }; 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 { modules: vec![("TEST".into(), 0)], sources: vec![SourceFile { module: 0, path: "test.bas".into(), }], form_initial: vec![], startup_form: None, name: "T".into(), option_base: 0, strings: vec![], globals_init: vec![], global_names: vec![], udts: vec![], procs: vec![], data: vec![], jump_tables: vec![], objects: vec![], event_procs: 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}"); } }