Files
TerminalBasic/crates/tb-vm/src/bytecode.rs
Chili Palmer 52ccbb5848 Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A
Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben).
Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature.

Vollstaendigkeits-Inventar
- docs/inventar.md mit 285 Eintraegen aus den Themenlisten von
  bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle
- crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide
  Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten

Bruchschritt (Puffer, Host, Korpus)
- Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui
  behaelt Farbabbildung, Widget und den neuen Terminal-Host
- Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus
  Ereignisse (Taste, Groessenaenderung, Abbruch)
- Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht
  bestehenden Sollausgaben blieben dabei unveraendert

Groessenunabhaengigkeit
- 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend
  der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor
  und VIEW PRINT geklemmt)
- Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener
  Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf

Bibliothek
- Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch)
- Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS
- Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON)
- PRINT USING, LPRINT USING, FORMAT$, SetFormatCC
- Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge)
- Datum und Zeit mit Serienwerten ab 1899
- Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#,
  IRR#, MIRR#, SLN#, SYD#, DDB#
- Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen),
  BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV
- System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung

Altlasten aus Phase 2
- ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar
  (prozeduruebergreifender Fixup im Codegenerator)
- DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum)
- Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest

Vom Inventar aufgedeckte Fehler
- Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht
  ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die
  String*-Routinen, LINE und OPEN "COMn:") -- geschlossen
- Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik,
  Record-Konvertierung, Rest der Dateisystemfunktionen
- bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar
  bildet die Vereinigung mit qb45advr.hlp

Neue Changes
- phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb)
- phase-3-ortszeit: zieht die UTC-Abweichung zurueck

Neue Abhaengigkeit: unicode-width.
Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei,
docs/bibliothek.md neu, PLAN.md fortgeschrieben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 06:54:19 +02:00

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