Files
TerminalBasic/crates/tb-vm/src/bytecode.rs

1305 lines
45 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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(&param.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, &param.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 &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));
}
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}");
}
}