1305 lines
45 KiB
Rust
1305 lines
45 KiB
Rust
//! 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.
|
||
|
||
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<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 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<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> {
|
||
match r.u8()? {
|
||
0 => Ok(false),
|
||
1 => Ok(true),
|
||
_ => Err(LoadError::Corrupt("bool")),
|
||
}
|
||
}
|
||
}
|
||
|
||
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()?;
|
||
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<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
|
||
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<u8>) {
|
||
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<Self, LoadError> {
|
||
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<u8>) {
|
||
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<Self, LoadError> {
|
||
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<HParam>,
|
||
pub ret_ty: Option<HTy>,
|
||
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, Clone)]
|
||
pub struct CompiledModule {
|
||
pub modules: Vec<(String, u8)>,
|
||
pub sources: Vec<SourceFile>,
|
||
pub form_initial: Vec<FormInitial>,
|
||
pub startup_form: Option<u16>,
|
||
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>>,
|
||
pub objects: Vec<FormObject>,
|
||
pub event_procs: Vec<HEventProc>,
|
||
}
|
||
|
||
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 {
|
||
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<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 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<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));
|
||
}
|
||
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<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()?;
|
||
|
||
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}");
|
||
}
|
||
}
|