Setzt den OpenSpec-Change phase-3-isam um (49/49 Aufgaben) und schliesst damit Phase 3 ab. Alle 22 ISAM-Elemente des Inventars sind implementiert; der Abdeckungsstand steigt auf 217 implementiert / 15 offen / 53 Non-Feature. Frontend - ISAM-Anweisungen als eigener Zweig im Parser: NAME [#]n [, arg ...], Sonderform ROLLBACK ALL ueber eine Sentinel-Kennung - 22 Signaturen in builtin_stmt/builtin_fn, neue Argumentart R (Satzvariable eines benutzerdefinierten Typs) - Satzargumente von INSERT/RETRIEVE/UPDATE werden gegen den Typ der Dateinummer geprueft, sofern beide literal bekannt sind - Die Unsupported-Absenkung von OPEN ... FOR ISAM entfaellt Speicherschicht (tb-runtime::isam, einziger Ort mit redb) - Je Tabelle eine Satztabelle satz-id -> Satzbytes, je Index eine Tabelle Schluesselbytes -> satz-id. Satz-IDs sind monoton und werden nie wiederverwendet: der Cursor merkt sich eine ID, eine neu vergebene koennte still auf einen fremden Satz zeigen - Satzbytes entstehen mit fileio::wert_schreiben/wert_lesen, also mit den Recordpuffern und der UTF-32-Festtextkodierung aus datei-eio - Ordnungserhaltende Schluesselkodierung je Spaltentyp; Text als UTF-8, dessen Bytereihenfolge die Codepoint-Reihenfolge ist und damit dieselbe Ordnung wie CmpStr der VM. Ein Eigenschaftstest ueber 5000 zufaellige Wertepaare je Typ haelt fest, dass der Bytevergleich dem fachlichen Vergleich entspricht -- eine Ordnungsverletzung faellt sonst erst bei bestimmten Datenwerten auf - Cursor als Wert (aktiver Index, letzte Satz-ID, unpositioniert), nicht als gehaltener Iterator: er ueberlebt Satzaenderungen und Transaktionsgrenzen - Sicherungspunkte ueber ein eigenes Ruecknahmeprotokoll; ROLLBACK ALL bricht die Bibliothekstransaktion direkt ab Semantik durchweg aus der Original-Hilfe - Die Argumentformen stammen erstmals aus den Einzelseiten, nicht nur aus der Themenliste. Das korrigierte eine Annahme des Entwurfs: die Spaltenliste von CREATEINDEX ist keine Zeichenkette mit Trennzeichen, sondern eine Folge einzelner Stringargumente - SEEKEQ mit unvollstaendigem Schluessel schlaegt immer fehl, SEEKGT mit Teilschluessel positioniert wie SEEKGE - Nach SETINDEX ist der erste Satz der neuen Ordnung aktuell, nach DELETE der folgende - Suche ueber den NULL-Index meldet 87, nicht 83; damit hat jeder Code von 81 bis 89 einen Ausloeser - Einzige Erweiterung: ein - vor dem Spaltennamen ordnet absteigend. Die Original-Hilfe kennt bei CREATEINDEX keine Sortierrichtung, die Anforderung verlangt sie. Kollisionsfrei, weil ein TYPE-Feldname nie mit - beginnen kann CLOSE beendet keine Transaktion - Erst schrieb es sie fest, womit das Schliessen irgendeiner Dateinummer die Transaktion aller anderen mit beendete und ein folgendes ROLLBACK ALL ins Leere lief. Ausstehende Aenderungen sind ohnehin festgeschrieben, weil jede Operation ausserhalb einer Transaktion fuer sich eine ist - Ueber das Ende entscheiden allein COMMITTRANS und ROLLBACK ALL; eine beim Programmende offene Transaktion verfaellt, in beiden Wegen gleich SETMEM und Fehler 89 - Ohne DOS-Speichermodell ist SETMEM die Obergrenze des ISAM-Puffers (Vorgabe 65536 Bytes); ohne echte Grenze waere Code 89 nie erreichbar und die Anweisung eine Attrappe Tests - 18 Einheitentests (Kodierung, Formatversion, Satz-IDs, Roundtrip ueber alle Feldtypen, Indexordnung, Cursor, Protokoll) - 6 Korpusprogramme: Tabellen, Indizes, Cursor, Saetze, Transaktionen, Puffer und Vergleichsordnung -- je in einem temporaeren Arbeits- verzeichnis, der Projektbaum bleibt sauber - Der VM-Test zu Fehler 73 prueft ISAM nicht mehr, sondern SETUEVENT; dafuer belegt ein neuer Test, dass OPEN ... FOR ISAM arbeitet Dokumentation: Datenbankdateiformat in dateiformate.md, Abschnitt 10a samt Sortierordnung und SETMEM-Abweichung in sprachreferenz.md, ISAM-Abschnitt in bibliothek.md, Inventar auf implementiert, PLAN.md schaerft die Leitplanke Referenzverhalten -- sie gilt auch gegenueber den eigenen Spezifikationen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1245 lines
45 KiB
Rust
1245 lines
45 KiB
Rust
//! Codegenerator: typisiertes HIR → Bytecode.
|
|
//!
|
|
//! Dummer Tree-Walk (Design D1): Typen und Konvertierungen sind im HIR
|
|
//! bereits explizit, hier passiert nur noch Instruktionsauswahl und
|
|
//! Label-Fixup (Vorwärtsziele über Fixup-Listen, kein zweiter Pass).
|
|
|
|
use crate::bytecode::{CmpOp, CompiledModule, DataItem, Instr, ProcCode};
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
use tb_frontend::hir::{
|
|
self, Builtin, CmpKind, HArg, HArith, HCmp, HExpr, HLogic, HPlace, HPrintItem, HProcKind,
|
|
HResume, HStmt, HStmtKind, HTy, HirModule, IntKind, NumTy, VarSlot,
|
|
};
|
|
use tb_runtime::builtins::ids;
|
|
use tb_runtime::value::TypeInit;
|
|
|
|
pub fn compile(hir: &HirModule) -> CompiledModule {
|
|
let mut cg = Codegen {
|
|
strings: Vec::new(),
|
|
string_ids: HashMap::new(),
|
|
jump_tables: Vec::new(),
|
|
modul_label_pc: Vec::new(),
|
|
};
|
|
let mut procs = Vec::new();
|
|
for proc in &hir.procs {
|
|
procs.push(cg.compile_proc(proc));
|
|
}
|
|
CompiledModule {
|
|
name: hir.name.clone(),
|
|
option_base: hir.option_base,
|
|
strings: cg.strings,
|
|
globals_init: hir.globals.iter().map(|g| slot_init(g)).collect(),
|
|
global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
|
|
udts: hir
|
|
.udts
|
|
.iter()
|
|
.map(|u| tb_runtime::value::UdtLayout {
|
|
name: u.name.clone(),
|
|
fields: u.fields.iter().map(|(_, t)| type_init(t)).collect(),
|
|
})
|
|
.collect(),
|
|
procs,
|
|
data: hir
|
|
.data
|
|
.iter()
|
|
.map(|d| DataItem { text: d.text.clone(), line: d.line })
|
|
.collect(),
|
|
jump_tables: cg.jump_tables,
|
|
}
|
|
}
|
|
|
|
/// Slot-Vorbelegung: Arrays und UDT-Handles starten leer (Auto-Init).
|
|
fn slot_init(v: &hir::HVar) -> TypeInit {
|
|
if v.array {
|
|
TypeInit::Empty
|
|
} else {
|
|
type_init(&v.ty)
|
|
}
|
|
}
|
|
|
|
fn type_init(t: &HTy) -> TypeInit {
|
|
match t {
|
|
HTy::Num(NumTy::Int) => TypeInit::Int,
|
|
HTy::Num(NumTy::Lng) => TypeInit::Lng,
|
|
HTy::Num(NumTy::Sng) => TypeInit::Sng,
|
|
HTy::Num(NumTy::Dbl) => TypeInit::Dbl,
|
|
HTy::Num(NumTy::Cur) => TypeInit::Cur,
|
|
HTy::Str => TypeInit::Str,
|
|
HTy::FixedStr(n) => TypeInit::FixedStr(*n),
|
|
HTy::Udt(id) => TypeInit::Udt(*id),
|
|
}
|
|
}
|
|
|
|
struct Codegen {
|
|
strings: Vec<Rc<str>>,
|
|
string_ids: HashMap<String, u16>,
|
|
jump_tables: Vec<Vec<u32>>,
|
|
/// Sprungziele des Modulrumpfs (Prozedur 0). Ein modulweites
|
|
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
|
|
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
|
|
modul_label_pc: Vec<Option<u32>>,
|
|
}
|
|
|
|
struct ProcCtx {
|
|
code: Vec<Instr>,
|
|
/// LabelId → Instruktionsindex.
|
|
label_pc: Vec<Option<u32>>,
|
|
/// (Instruktionsindex, LabelId) — nach dem Emit gepatcht.
|
|
fixups: Vec<(usize, u16)>,
|
|
/// (Tabellenindex, Labels) — Sprungtabellen nach dem Emit auflösen.
|
|
table_fixups: Vec<(usize, Vec<u16>)>,
|
|
}
|
|
|
|
impl ProcCtx {
|
|
fn here(&self) -> u32 {
|
|
self.code.len() as u32
|
|
}
|
|
fn bind(&mut self, label: u16) {
|
|
let pc = self.here();
|
|
if (label as usize) >= self.label_pc.len() {
|
|
self.label_pc.resize(label as usize + 1, None);
|
|
}
|
|
self.label_pc[label as usize] = Some(pc);
|
|
}
|
|
fn emit(&mut self, i: Instr) {
|
|
self.code.push(i);
|
|
}
|
|
/// Sprunginstruktion mit noch unbekanntem Ziel emittieren.
|
|
fn emit_jump(&mut self, i: Instr, label: u16) {
|
|
self.fixups.push((self.code.len(), label));
|
|
self.code.push(i);
|
|
}
|
|
/// Frisches internes Label (zusätzlich zu den HIR-Labels).
|
|
fn new_label(&mut self) -> u16 {
|
|
let id = self.label_pc.len() as u16;
|
|
self.label_pc.push(None);
|
|
id
|
|
}
|
|
}
|
|
|
|
impl Codegen {
|
|
fn pool(&mut self, s: &str) -> u16 {
|
|
if let Some(id) = self.string_ids.get(s) {
|
|
return *id;
|
|
}
|
|
let id = self.strings.len() as u16;
|
|
self.strings.push(Rc::from(s));
|
|
self.string_ids.insert(s.to_string(), id);
|
|
id
|
|
}
|
|
|
|
fn compile_proc(&mut self, proc: &hir::HProc) -> ProcCode {
|
|
let mut ctx = ProcCtx {
|
|
code: Vec::new(),
|
|
label_pc: vec![None; proc.label_count as usize],
|
|
fixups: Vec::new(),
|
|
table_fixups: Vec::new(),
|
|
};
|
|
for stmt in &proc.body {
|
|
self.stmt(&mut ctx, proc, stmt);
|
|
}
|
|
// Rumpfende
|
|
self.emit_proc_exit(&mut ctx, proc);
|
|
|
|
// Prozedur 0 (Modulrumpf) gibt ihre Sprungziele weiter.
|
|
if proc.kind == hir::HProcKind::Main {
|
|
self.modul_label_pc = ctx.label_pc.clone();
|
|
}
|
|
|
|
// Fixups patchen
|
|
for (idx, label) in std::mem::take(&mut ctx.fixups) {
|
|
// Modulweites `ON ERROR GOTO` in einer Prozedur: das Label lebt
|
|
// im Modulrumpf, nicht im eigenen.
|
|
let modulweit = proc.kind != hir::HProcKind::Main
|
|
&& matches!(ctx.code[idx], Instr::OnErrorGoto(_));
|
|
let pc = if modulweit {
|
|
self.modul_label_pc
|
|
.get(label as usize)
|
|
.copied()
|
|
.flatten()
|
|
.expect("Modul-Label ohne Position")
|
|
} else {
|
|
ctx.label_pc[label as usize].expect("Label ohne Position")
|
|
};
|
|
match &mut ctx.code[idx] {
|
|
Instr::Jump(t)
|
|
| Instr::JumpIfFalse(t)
|
|
| Instr::JumpIfTrue(t)
|
|
| Instr::Gosub(t)
|
|
| Instr::RetGosubTo(t)
|
|
| Instr::ResumeLabel(t)
|
|
| Instr::OnErrorGoto(t)
|
|
| Instr::OnErrorLocal(t) => *t = pc,
|
|
other => unreachable!("Fixup auf {other:?}"),
|
|
}
|
|
}
|
|
for (table, labels) in std::mem::take(&mut ctx.table_fixups) {
|
|
let pcs: Vec<u32> = labels
|
|
.iter()
|
|
.map(|l| ctx.label_pc[*l as usize].expect("Label ohne Position"))
|
|
.collect();
|
|
self.jump_tables[table] = pcs;
|
|
}
|
|
|
|
ProcCode {
|
|
name: proc.name.clone(),
|
|
n_params: proc.params.len() as u16,
|
|
locals_init: proc.locals.iter().map(slot_init).collect(),
|
|
local_names: proc.locals.iter().map(|l| l.name.clone()).collect(),
|
|
code: ctx.code,
|
|
}
|
|
}
|
|
|
|
fn emit_proc_exit(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc) {
|
|
match proc.kind {
|
|
HProcKind::Main => ctx.emit(Instr::End),
|
|
HProcKind::Sub => ctx.emit(Instr::RetProc),
|
|
HProcKind::Function | HProcKind::DefFn => {
|
|
if let Some(VarSlot::Local(slot)) = proc.ret_slot {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
} else {
|
|
ctx.emit(Instr::PushInt(0));
|
|
}
|
|
ctx.emit(Instr::RetFn);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Anweisungen -------------------------------------------------------
|
|
|
|
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) {
|
|
match &stmt.kind {
|
|
HStmtKind::Label(l) => {
|
|
ctx.bind(*l);
|
|
return;
|
|
}
|
|
_ => ctx.emit(Instr::Stmt(stmt.line)),
|
|
}
|
|
match &stmt.kind {
|
|
HStmtKind::Label(_) => unreachable!(),
|
|
HStmtKind::SetErl(n) => ctx.emit(Instr::SetErl(*n)),
|
|
HStmtKind::Assign { place, value } => {
|
|
self.store_place(ctx, place, |cg, ctx| cg.expr(ctx, value));
|
|
}
|
|
HStmtKind::Print { items, trailing } => {
|
|
for item in items {
|
|
match item {
|
|
HPrintItem::Val(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_VAL, 1));
|
|
}
|
|
HPrintItem::Tab(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_TAB, 1));
|
|
}
|
|
HPrintItem::Spc(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_SPC, 1));
|
|
}
|
|
HPrintItem::Comma => {
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_COMMA, 0));
|
|
}
|
|
}
|
|
}
|
|
if !trailing {
|
|
ctx.emit(Instr::CallBuiltin(ids::PRINT_NEWLINE, 0));
|
|
}
|
|
}
|
|
HStmtKind::SetErr(e) => {
|
|
self.expr(ctx, e);
|
|
ctx.emit(Instr::SetErr);
|
|
}
|
|
HStmtKind::Field { file, fields } => {
|
|
self.expr(ctx, file);
|
|
for (len, place) in fields {
|
|
self.expr(ctx, len);
|
|
self.make_ref(ctx, place);
|
|
}
|
|
ctx.emit(Instr::Field(fields.len() as u8));
|
|
}
|
|
HStmtKind::LsetRset { rset, target, value } => {
|
|
self.make_ref(ctx, target);
|
|
self.expr(ctx, value);
|
|
ctx.emit(Instr::LsetRset(*rset));
|
|
}
|
|
HStmtKind::GetPut { put, file, recnum, var } => {
|
|
self.expr(ctx, file);
|
|
if let Some(r) = recnum {
|
|
self.expr(ctx, r);
|
|
}
|
|
let (art, zusatz) = match var {
|
|
None => (0u8, 0u16),
|
|
Some(v) => {
|
|
self.make_ref(ctx, v);
|
|
match &v.ty {
|
|
HTy::Num(NumTy::Int) => (1, 0),
|
|
HTy::Num(NumTy::Lng) => (2, 0),
|
|
HTy::Num(NumTy::Sng) => (3, 0),
|
|
HTy::Num(NumTy::Dbl) => (4, 0),
|
|
HTy::Num(NumTy::Cur) => (5, 0),
|
|
HTy::FixedStr(n) => (6, *n as u16),
|
|
HTy::Udt(i) => (7, *i),
|
|
// Variable Strings: Länge erst zur Laufzeit.
|
|
HTy::Str => (8, 0),
|
|
}
|
|
}
|
|
};
|
|
ctx.emit(Instr::GetPut(*put, recnum.is_some(), art, zusatz));
|
|
}
|
|
HStmtKind::Input { file, line_mode, prompt, question, targets } => {
|
|
if let Some(f) = file {
|
|
// Dateinummer zuerst, dann die Referenzen darüber.
|
|
self.expr(ctx, f);
|
|
for t in targets {
|
|
self.make_ref(ctx, t);
|
|
}
|
|
ctx.emit(Instr::InputFile(targets.len() as u8, *line_mode));
|
|
return;
|
|
}
|
|
for t in targets {
|
|
self.make_ref(ctx, t);
|
|
}
|
|
let prompt_idx = match prompt {
|
|
Some(p) => self.pool(p),
|
|
None => 0xFFFF,
|
|
};
|
|
ctx.emit(Instr::Input(
|
|
targets.len() as u8,
|
|
*line_mode,
|
|
prompt_idx,
|
|
*question,
|
|
));
|
|
}
|
|
HStmtKind::If { cond, then, els } => {
|
|
self.expr(ctx, cond);
|
|
let l_else = ctx.new_label();
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_else);
|
|
for s in then {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
if els.is_empty() {
|
|
ctx.bind(l_else);
|
|
} else {
|
|
let l_end = ctx.new_label();
|
|
ctx.emit_jump(Instr::Jump(0), l_end);
|
|
ctx.bind(l_else);
|
|
for s in els {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
ctx.bind(l_end);
|
|
}
|
|
}
|
|
HStmtKind::Loop { pre, post, body, exit_label } => {
|
|
let l_start = ctx.new_label();
|
|
ctx.bind(l_start);
|
|
if let Some((is_until, cond)) = pre {
|
|
self.expr(ctx, cond);
|
|
if *is_until {
|
|
ctx.emit_jump(Instr::JumpIfTrue(0), *exit_label);
|
|
} else {
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), *exit_label);
|
|
}
|
|
}
|
|
for s in body {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
match post {
|
|
Some((is_until, cond)) => {
|
|
self.expr(ctx, cond);
|
|
if *is_until {
|
|
// LOOP UNTIL: weiter, solange falsch
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_start);
|
|
} else {
|
|
ctx.emit_jump(Instr::JumpIfTrue(0), l_start);
|
|
}
|
|
}
|
|
None => ctx.emit_jump(Instr::Jump(0), l_start),
|
|
}
|
|
ctx.bind(*exit_label);
|
|
}
|
|
HStmtKind::For {
|
|
var,
|
|
ty,
|
|
from,
|
|
to,
|
|
step,
|
|
limit_slot,
|
|
step_slot,
|
|
body,
|
|
exit_label,
|
|
} => {
|
|
self.gen_for(
|
|
ctx, proc, var, *ty, from, to, step.as_ref(), *limit_slot, *step_slot, body,
|
|
*exit_label,
|
|
);
|
|
}
|
|
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
|
|
HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(0), *l),
|
|
HStmtKind::OnGoto { sel, gosub, targets } => {
|
|
self.expr(ctx, sel);
|
|
let table = self.jump_tables.len();
|
|
self.jump_tables.push(Vec::new());
|
|
ctx.table_fixups.push((table, targets.clone()));
|
|
ctx.emit(Instr::OnJump(table as u16, *gosub));
|
|
}
|
|
HStmtKind::ReturnGosub(target) => match target {
|
|
None => ctx.emit(Instr::RetGosub),
|
|
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
|
|
},
|
|
HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc),
|
|
HStmtKind::CallSub { proc: id, args } => {
|
|
for a in args {
|
|
self.arg(ctx, a);
|
|
}
|
|
ctx.emit(Instr::Call(*id, args.len() as u8));
|
|
}
|
|
HStmtKind::BuiltinStmt { b, args } => {
|
|
for a in args {
|
|
self.expr(ctx, a);
|
|
}
|
|
let id = builtin_id(*b);
|
|
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
|
|
if builtin_returns_value(*b) {
|
|
ctx.emit(Instr::Pop);
|
|
}
|
|
}
|
|
HStmtKind::OnError { local, target } => match (local, target) {
|
|
(false, Some(l)) => ctx.emit_jump(Instr::OnErrorGoto(0), *l),
|
|
(true, Some(l)) => ctx.emit_jump(Instr::OnErrorLocal(0), *l),
|
|
(false, None) => ctx.emit(Instr::OnErrorDisable),
|
|
(true, None) => ctx.emit(Instr::OnErrorLocalDisable),
|
|
},
|
|
HStmtKind::OnErrorResumeNext { local } => {
|
|
ctx.emit(Instr::OnErrorResumeNext(*local));
|
|
}
|
|
HStmtKind::Resume(kind) => match kind {
|
|
HResume::Retry => ctx.emit(Instr::Resume0),
|
|
HResume::Next => ctx.emit(Instr::ResumeNext),
|
|
HResume::Label(l) => ctx.emit_jump(Instr::ResumeLabel(0), *l),
|
|
},
|
|
HStmtKind::RaiseError(code) => {
|
|
self.expr(ctx, code);
|
|
ctx.emit(Instr::RaiseError);
|
|
}
|
|
HStmtKind::Read(places) => {
|
|
for p in places {
|
|
let numeric = matches!(p.ty, HTy::Num(_));
|
|
if numeric {
|
|
self.store_place(ctx, p, |_cg, ctx| {
|
|
ctx.emit(Instr::ReadData(1));
|
|
emit_conv(ctx, NumTy::Dbl, p.ty.num().unwrap_or(NumTy::Dbl));
|
|
});
|
|
} else {
|
|
self.store_place(ctx, p, |_cg, ctx| {
|
|
ctx.emit(Instr::ReadData(0));
|
|
if let HTy::FixedStr(n) = &p.ty {
|
|
ctx.emit(Instr::FixStr(*n));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
HStmtKind::Restore(idx) => ctx.emit(Instr::Restore(*idx)),
|
|
HStmtKind::Dim { slot, elem, dims, redim } => {
|
|
for (lo, hi) in dims {
|
|
self.expr(ctx, lo);
|
|
self.expr(ctx, hi);
|
|
}
|
|
let (global, s) = slot_parts(*slot);
|
|
let init = type_init(elem);
|
|
if *redim {
|
|
ctx.emit(Instr::RedimArr(global, s, dims.len() as u8, init));
|
|
} else {
|
|
ctx.emit(Instr::DimArr(global, s, dims.len() as u8, init));
|
|
}
|
|
}
|
|
HStmtKind::Erase(slots) => {
|
|
for slot in slots {
|
|
let (global, s) = slot_parts(*slot);
|
|
ctx.emit(Instr::EraseSlot(global, s));
|
|
}
|
|
}
|
|
HStmtKind::End => ctx.emit(Instr::End),
|
|
HStmtKind::Stop => ctx.emit(Instr::StopInstr),
|
|
HStmtKind::System => ctx.emit(Instr::SystemInstr),
|
|
HStmtKind::Unsupported(name) => {
|
|
let idx = self.pool(name);
|
|
ctx.emit(Instr::Unsupported(idx));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn gen_for(
|
|
&mut self,
|
|
ctx: &mut ProcCtx,
|
|
proc: &hir::HProc,
|
|
var: &HPlace,
|
|
ty: NumTy,
|
|
from: &HExpr,
|
|
to: &HExpr,
|
|
step: Option<&HExpr>,
|
|
limit_slot: VarSlot,
|
|
step_slot: Option<VarSlot>,
|
|
body: &[HStmt],
|
|
exit_label: u16,
|
|
) {
|
|
// Startwert, Grenze, ggf. Schritt einmal auswerten.
|
|
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
|
|
self.expr(ctx, to);
|
|
emit_store_slot(ctx, limit_slot);
|
|
if let (Some(step_e), Some(sslot)) = (step, step_slot) {
|
|
self.expr(ctx, step_e);
|
|
emit_store_slot(ctx, sslot);
|
|
}
|
|
let const_step = match step {
|
|
None => Some(1.0),
|
|
Some(e) => hir::literal_value(e),
|
|
};
|
|
|
|
let l_test = ctx.new_label();
|
|
let l_body = ctx.new_label();
|
|
ctx.bind(l_test);
|
|
match const_step {
|
|
Some(s) => {
|
|
// Vergleichsrichtung zur Compilezeit.
|
|
let op = if s >= 0.0 { CmpOp::Le } else { CmpOp::Ge };
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, op));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
}
|
|
None => {
|
|
// Vorzeichen des Schritts zur Laufzeit prüfen.
|
|
let sslot = step_slot.expect("dynamischer STEP ohne Slot");
|
|
let l_neg = ctx.new_label();
|
|
emit_load_slot(ctx, sslot);
|
|
push_zero(ctx, ty);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Ge));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), l_neg);
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Le));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
ctx.emit_jump(Instr::Jump(0), l_body);
|
|
ctx.bind(l_neg);
|
|
self.load_place(ctx, var);
|
|
emit_load_slot(ctx, limit_slot);
|
|
ctx.emit(cmp_instr(ty, CmpOp::Ge));
|
|
ctx.emit_jump(Instr::JumpIfFalse(0), exit_label);
|
|
}
|
|
}
|
|
ctx.bind(l_body);
|
|
for s in body {
|
|
self.stmt(ctx, proc, s);
|
|
}
|
|
// NEXT: inkrementieren, zurück zum Test.
|
|
self.store_place(ctx, var, |cg, ctx| {
|
|
cg.load_place(ctx, var);
|
|
match (step, step_slot) {
|
|
(Some(e), None) => cg.expr(ctx, e), // konstanter STEP
|
|
(Some(_), Some(sslot)) => emit_load_slot(ctx, sslot),
|
|
(None, _) => push_one(ctx, ty),
|
|
}
|
|
ctx.emit(add_instr(ty));
|
|
});
|
|
ctx.emit_jump(Instr::Jump(0), l_test);
|
|
ctx.bind(exit_label);
|
|
}
|
|
|
|
// ---- Ausdrücke ---------------------------------------------------------
|
|
|
|
fn expr(&mut self, ctx: &mut ProcCtx, e: &HExpr) {
|
|
match e {
|
|
HExpr::Int(v) => ctx.emit(Instr::PushInt(*v)),
|
|
HExpr::Lng(v) => ctx.emit(Instr::PushLng(*v)),
|
|
HExpr::Sng(v) => ctx.emit(Instr::PushSng(*v)),
|
|
HExpr::Dbl(v) => ctx.emit(Instr::PushDbl(*v)),
|
|
HExpr::Cur(v) => ctx.emit(Instr::PushCur(*v)),
|
|
HExpr::Str(s) => {
|
|
let idx = self.pool(s);
|
|
ctx.emit(Instr::PushStr(idx));
|
|
}
|
|
HExpr::Load(p) => self.load_place(ctx, p),
|
|
HExpr::Conv { from, to, arg } => {
|
|
self.expr(ctx, arg);
|
|
emit_conv(ctx, *from, *to);
|
|
}
|
|
HExpr::FixStr { len, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(Instr::FixStr(*len));
|
|
}
|
|
HExpr::Neg { ty, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::NegI2,
|
|
NumTy::Lng => Instr::NegI4,
|
|
NumTy::Sng => Instr::NegR4,
|
|
NumTy::Dbl => Instr::NegR8,
|
|
NumTy::Cur => Instr::NegCy,
|
|
});
|
|
}
|
|
HExpr::Bin { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(arith_instr(*op, *ty));
|
|
}
|
|
HExpr::Not { ty, arg } => {
|
|
self.expr(ctx, arg);
|
|
ctx.emit(match ty {
|
|
IntKind::I2 => Instr::NotI2,
|
|
IntKind::I4 => Instr::NotI4,
|
|
});
|
|
}
|
|
HExpr::Logic { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(logic_instr(*op, *ty));
|
|
}
|
|
HExpr::Cmp { op, ty, l, r } => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
let cop = cmp_op(*op);
|
|
ctx.emit(match ty {
|
|
CmpKind::Num(NumTy::Int) => Instr::CmpI2(cop),
|
|
CmpKind::Num(NumTy::Lng) => Instr::CmpI4(cop),
|
|
CmpKind::Num(NumTy::Sng) => Instr::CmpR4(cop),
|
|
CmpKind::Num(NumTy::Dbl) => Instr::CmpR8(cop),
|
|
CmpKind::Num(NumTy::Cur) => Instr::CmpCy(cop),
|
|
CmpKind::Str => Instr::CmpStr(cop),
|
|
});
|
|
}
|
|
HExpr::Concat(l, r) => {
|
|
self.expr(ctx, l);
|
|
self.expr(ctx, r);
|
|
ctx.emit(Instr::Concat);
|
|
}
|
|
HExpr::FnCall { proc, args, .. } => {
|
|
for a in args {
|
|
self.arg(ctx, a);
|
|
}
|
|
ctx.emit(Instr::Call(*proc, args.len() as u8));
|
|
}
|
|
HExpr::Builtin { b, args, .. } => {
|
|
for a in args {
|
|
self.expr(ctx, a);
|
|
}
|
|
ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8));
|
|
}
|
|
HExpr::ArrayBound { lower, place, dim } => {
|
|
self.load_array_handle(ctx, place);
|
|
self.expr(ctx, dim);
|
|
ctx.emit(Instr::ArrBound(*lower));
|
|
}
|
|
HExpr::Err => ctx.emit(Instr::LoadErr),
|
|
HExpr::Erl => ctx.emit(Instr::LoadErl),
|
|
HExpr::Unsupported(name) => {
|
|
let idx = self.pool(name);
|
|
ctx.emit(Instr::Unsupported(idx));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn arg(&mut self, ctx: &mut ProcCtx, a: &HArg) {
|
|
match a {
|
|
HArg::ByVal(e) => self.expr(ctx, e),
|
|
HArg::ByRef(p) => self.make_ref(ctx, p),
|
|
HArg::ArrayRef(p) => self.load_array_handle(ctx, p),
|
|
}
|
|
}
|
|
|
|
// ---- Plätze (L-Werte) ----------------------------------------------------
|
|
|
|
/// Array-Handle eines Platzes laden (mit Auto-DIM-Information).
|
|
fn load_array_handle(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
let (elem, dims) = match &p.array_elem {
|
|
Some((t, d)) => (type_init(t), *d),
|
|
None => (type_init(&p.ty), 1),
|
|
};
|
|
ctx.emit(Instr::LoadArr(global, slot, dims, elem));
|
|
}
|
|
|
|
fn load_place(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields {
|
|
ctx.emit(Instr::LoadField(*f));
|
|
}
|
|
}
|
|
|
|
/// Platz speichern; `value` emittiert den Wert auf den Stack.
|
|
fn store_place(
|
|
&mut self,
|
|
ctx: &mut ProcCtx,
|
|
p: &HPlace,
|
|
value: impl FnOnce(&mut Self, &mut ProcCtx),
|
|
) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.fields.is_empty() {
|
|
// Basis-Handle (ggf. Element) laden, Feldpfad bis vorletzte Ebene.
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields[..p.fields.len() - 1] {
|
|
ctx.emit(Instr::LoadField(*f));
|
|
}
|
|
value(self, ctx);
|
|
ctx.emit(Instr::StoreField(*p.fields.last().unwrap()));
|
|
return;
|
|
}
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
value(self, ctx);
|
|
ctx.emit(Instr::StoreElem(p.indices.len() as u8));
|
|
return;
|
|
}
|
|
value(self, ctx);
|
|
if matches!(p.ty, HTy::Udt(_)) {
|
|
// UDT-Zuweisung kopiert Inhalte (Wertsemantik): Ziel-Handle
|
|
// laden und Quellfelder hineinkopieren.
|
|
if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
ctx.emit(Instr::CopyRec);
|
|
return;
|
|
}
|
|
if p.base_is_ref {
|
|
ctx.emit(Instr::StoreRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::StoreGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::StoreLocal(slot));
|
|
}
|
|
}
|
|
|
|
/// Referenz auf einen Platz erzeugen (BYREF-Argumente, INPUT-Ziele).
|
|
fn make_ref(&mut self, ctx: &mut ProcCtx, p: &HPlace) {
|
|
let (global, slot) = slot_parts(p.base);
|
|
if !p.indices.is_empty() && p.fields.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::MakeRefElem(p.indices.len() as u8));
|
|
return;
|
|
}
|
|
if !p.fields.is_empty() {
|
|
if !p.indices.is_empty() {
|
|
self.load_array_handle(ctx, p);
|
|
for i in &p.indices {
|
|
self.expr(ctx, i);
|
|
}
|
|
ctx.emit(Instr::LoadElem(p.indices.len() as u8));
|
|
} else if p.base_is_ref {
|
|
ctx.emit(Instr::LoadRef(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::LoadGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
}
|
|
for f in &p.fields {
|
|
ctx.emit(Instr::MakeRefField(*f));
|
|
}
|
|
return;
|
|
}
|
|
if p.base_is_ref {
|
|
// Referenz weiterreichen: der Slot enthält bereits eine Referenz.
|
|
ctx.emit(Instr::LoadLocal(slot));
|
|
} else if global {
|
|
ctx.emit(Instr::MakeRefGlobal(slot));
|
|
} else {
|
|
ctx.emit(Instr::MakeRefLocal(slot));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Instruktionsauswahl-Hilfen ----------------------------------------------
|
|
|
|
fn slot_parts(s: VarSlot) -> (bool, u16) {
|
|
match s {
|
|
VarSlot::Global(i) => (true, i),
|
|
VarSlot::Local(i) => (false, i),
|
|
}
|
|
}
|
|
|
|
fn emit_store_slot(ctx: &mut ProcCtx, s: VarSlot) {
|
|
match s {
|
|
VarSlot::Global(i) => ctx.emit(Instr::StoreGlobal(i)),
|
|
VarSlot::Local(i) => ctx.emit(Instr::StoreLocal(i)),
|
|
}
|
|
}
|
|
|
|
fn emit_load_slot(ctx: &mut ProcCtx, s: VarSlot) {
|
|
match s {
|
|
VarSlot::Global(i) => ctx.emit(Instr::LoadGlobal(i)),
|
|
VarSlot::Local(i) => ctx.emit(Instr::LoadLocal(i)),
|
|
}
|
|
}
|
|
|
|
fn push_zero(ctx: &mut ProcCtx, ty: NumTy) {
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::PushInt(0),
|
|
NumTy::Lng => Instr::PushLng(0),
|
|
NumTy::Sng => Instr::PushSng(0.0),
|
|
NumTy::Dbl => Instr::PushDbl(0.0),
|
|
NumTy::Cur => Instr::PushCur(0),
|
|
});
|
|
}
|
|
|
|
fn push_one(ctx: &mut ProcCtx, ty: NumTy) {
|
|
ctx.emit(match ty {
|
|
NumTy::Int => Instr::PushInt(1),
|
|
NumTy::Lng => Instr::PushLng(1),
|
|
NumTy::Sng => Instr::PushSng(1.0),
|
|
NumTy::Dbl => Instr::PushDbl(1.0),
|
|
NumTy::Cur => Instr::PushCur(10_000),
|
|
});
|
|
}
|
|
|
|
fn add_instr(ty: NumTy) -> Instr {
|
|
match ty {
|
|
NumTy::Int => Instr::AddI2,
|
|
NumTy::Lng => Instr::AddI4,
|
|
NumTy::Sng => Instr::AddR4,
|
|
NumTy::Dbl => Instr::AddR8,
|
|
NumTy::Cur => Instr::AddCy,
|
|
}
|
|
}
|
|
|
|
fn arith_instr(op: HArith, ty: NumTy) -> Instr {
|
|
use NumTy::*;
|
|
match (op, ty) {
|
|
(HArith::Add, Int) => Instr::AddI2,
|
|
(HArith::Add, Lng) => Instr::AddI4,
|
|
(HArith::Add, Sng) => Instr::AddR4,
|
|
(HArith::Add, Dbl) => Instr::AddR8,
|
|
(HArith::Add, Cur) => Instr::AddCy,
|
|
(HArith::Sub, Int) => Instr::SubI2,
|
|
(HArith::Sub, Lng) => Instr::SubI4,
|
|
(HArith::Sub, Sng) => Instr::SubR4,
|
|
(HArith::Sub, Dbl) => Instr::SubR8,
|
|
(HArith::Sub, Cur) => Instr::SubCy,
|
|
(HArith::Mul, Int) => Instr::MulI2,
|
|
(HArith::Mul, Lng) => Instr::MulI4,
|
|
(HArith::Mul, Sng) => Instr::MulR4,
|
|
(HArith::Mul, Dbl) => Instr::MulR8,
|
|
(HArith::Mul, Cur) => Instr::MulCy,
|
|
(HArith::Div, Sng) => Instr::DivR4,
|
|
(HArith::Div, Dbl) => Instr::DivR8,
|
|
(HArith::IDiv, Int) => Instr::IDivI2,
|
|
(HArith::IDiv, Lng) => Instr::IDivI4,
|
|
(HArith::Mod, Int) => Instr::ModI2,
|
|
(HArith::Mod, Lng) => Instr::ModI4,
|
|
(HArith::Pow, Dbl) => Instr::PowR8,
|
|
(op, ty) => unreachable!("arith {op:?} auf {ty:?}"),
|
|
}
|
|
}
|
|
|
|
fn logic_instr(op: HLogic, ty: IntKind) -> Instr {
|
|
match (op, ty) {
|
|
(HLogic::And, IntKind::I2) => Instr::AndI2,
|
|
(HLogic::And, IntKind::I4) => Instr::AndI4,
|
|
(HLogic::Or, IntKind::I2) => Instr::OrI2,
|
|
(HLogic::Or, IntKind::I4) => Instr::OrI4,
|
|
(HLogic::Xor, IntKind::I2) => Instr::XorI2,
|
|
(HLogic::Xor, IntKind::I4) => Instr::XorI4,
|
|
(HLogic::Eqv, IntKind::I2) => Instr::EqvI2,
|
|
(HLogic::Eqv, IntKind::I4) => Instr::EqvI4,
|
|
(HLogic::Imp, IntKind::I2) => Instr::ImpI2,
|
|
(HLogic::Imp, IntKind::I4) => Instr::ImpI4,
|
|
}
|
|
}
|
|
|
|
fn cmp_op(op: HCmp) -> CmpOp {
|
|
match op {
|
|
HCmp::Eq => CmpOp::Eq,
|
|
HCmp::Ne => CmpOp::Ne,
|
|
HCmp::Lt => CmpOp::Lt,
|
|
HCmp::Le => CmpOp::Le,
|
|
HCmp::Gt => CmpOp::Gt,
|
|
HCmp::Ge => CmpOp::Ge,
|
|
}
|
|
}
|
|
|
|
fn cmp_instr(ty: NumTy, op: CmpOp) -> Instr {
|
|
match ty {
|
|
NumTy::Int => Instr::CmpI2(op),
|
|
NumTy::Lng => Instr::CmpI4(op),
|
|
NumTy::Sng => Instr::CmpR4(op),
|
|
NumTy::Dbl => Instr::CmpR8(op),
|
|
NumTy::Cur => Instr::CmpCy(op),
|
|
}
|
|
}
|
|
|
|
fn emit_conv(ctx: &mut ProcCtx, from: NumTy, to: NumTy) {
|
|
use NumTy::*;
|
|
if from == to {
|
|
return;
|
|
}
|
|
ctx.emit(match (from, to) {
|
|
(Int, Lng) => Instr::ConvI2I4,
|
|
(Int, Sng) => Instr::ConvI2R4,
|
|
(Int, Dbl) => Instr::ConvI2R8,
|
|
(Int, Cur) => Instr::ConvI2Cy,
|
|
(Lng, Int) => Instr::ConvI4I2,
|
|
(Lng, Sng) => Instr::ConvI4R4,
|
|
(Lng, Dbl) => Instr::ConvI4R8,
|
|
(Lng, Cur) => Instr::ConvI4Cy,
|
|
(Sng, Int) => Instr::ConvR4I2,
|
|
(Sng, Lng) => Instr::ConvR4I4,
|
|
(Sng, Dbl) => Instr::ConvR4R8,
|
|
(Sng, Cur) => Instr::ConvR4Cy,
|
|
(Dbl, Int) => Instr::ConvR8I2,
|
|
(Dbl, Lng) => Instr::ConvR8I4,
|
|
(Dbl, Sng) => Instr::ConvR8R4,
|
|
(Dbl, Cur) => Instr::ConvR8Cy,
|
|
(Cur, Int) => Instr::ConvCyI2,
|
|
(Cur, Lng) => Instr::ConvCyI4,
|
|
(Cur, Sng) => Instr::ConvCyR4,
|
|
(Cur, Dbl) => Instr::ConvCyR8,
|
|
_ => unreachable!(),
|
|
});
|
|
}
|
|
|
|
/// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit.
|
|
/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege.
|
|
fn builtin_id(b: Builtin) -> u16 {
|
|
match b {
|
|
Builtin::Len => ids::LEN,
|
|
Builtin::LeftS => ids::LEFT_S,
|
|
Builtin::RightS => ids::RIGHT_S,
|
|
Builtin::MidS => ids::MID_S,
|
|
Builtin::InstrF => ids::INSTR,
|
|
Builtin::UcaseS => ids::UCASE_S,
|
|
Builtin::LcaseS => ids::LCASE_S,
|
|
Builtin::LtrimS => ids::LTRIM_S,
|
|
Builtin::RtrimS => ids::RTRIM_S,
|
|
Builtin::SpaceS => ids::SPACE_S,
|
|
Builtin::StringS => ids::STRING_S,
|
|
Builtin::ChrS => ids::CHR_S,
|
|
Builtin::Asc => ids::ASC,
|
|
Builtin::StrS => ids::STR_S,
|
|
Builtin::Val => ids::VAL,
|
|
Builtin::HexS => ids::HEX_S,
|
|
Builtin::OctS => ids::OCT_S,
|
|
Builtin::MidAssign => ids::MID_ASSIGN,
|
|
Builtin::Abs => ids::ABS,
|
|
Builtin::Sgn => ids::SGN,
|
|
Builtin::IntF => ids::INT_F,
|
|
Builtin::Fix => ids::FIX,
|
|
Builtin::Sqr => ids::SQR,
|
|
Builtin::Exp => ids::EXP,
|
|
Builtin::Log => ids::LOG,
|
|
Builtin::Sin => ids::SIN,
|
|
Builtin::Cos => ids::COS,
|
|
Builtin::Tan => ids::TAN,
|
|
Builtin::Atn => ids::ATN,
|
|
Builtin::Rnd => ids::RND,
|
|
Builtin::Randomize => ids::RANDOMIZE,
|
|
Builtin::PrintVal => ids::PRINT_VAL,
|
|
Builtin::PrintStrLit => ids::PRINT_STR_LIT,
|
|
Builtin::PrintComma => ids::PRINT_COMMA,
|
|
Builtin::PrintTab => ids::PRINT_TAB,
|
|
Builtin::PrintSpc => ids::PRINT_SPC,
|
|
Builtin::PrintNewline => ids::PRINT_NEWLINE,
|
|
Builtin::PrintUsing => ids::PRINT_USING,
|
|
Builtin::FormatS => ids::FORMAT_S,
|
|
Builtin::SetFormatCc => ids::SET_FORMAT_CC,
|
|
Builtin::Cls => ids::CLS,
|
|
Builtin::Color => ids::COLOR,
|
|
Builtin::Locate => ids::LOCATE,
|
|
Builtin::Width => ids::WIDTH,
|
|
Builtin::ViewPrint => ids::VIEW_PRINT,
|
|
Builtin::ScreenStmt => ids::SCREEN_STMT,
|
|
Builtin::KeyAssign => ids::KEY_ASSIGN,
|
|
Builtin::KeyList => ids::KEY_LIST,
|
|
Builtin::KeyDisplay => ids::KEY_DISPLAY,
|
|
Builtin::Csrlin => ids::CSRLIN,
|
|
Builtin::PosFn => ids::POS_FN,
|
|
Builtin::ScreenFn => ids::SCREEN_FN,
|
|
Builtin::InkeyS => ids::INKEY_S,
|
|
Builtin::InputS => ids::INPUT_S,
|
|
Builtin::EnvironS => ids::ENVIRON_S,
|
|
Builtin::EnvironSet => ids::ENVIRON_SET,
|
|
Builtin::Fre => ids::FRE,
|
|
Builtin::Clear => ids::CLEAR,
|
|
Builtin::Tron => ids::TRON,
|
|
Builtin::Troff => ids::TROFF,
|
|
Builtin::StackFn => ids::STACK_FN,
|
|
Builtin::StackStmt => ids::STACK_STMT,
|
|
Builtin::Erdev => ids::ERDEV,
|
|
Builtin::ErdevS => ids::ERDEV_S,
|
|
Builtin::Open => ids::OPEN,
|
|
Builtin::Close => ids::CLOSE,
|
|
Builtin::CloseAll => ids::CLOSE_ALL,
|
|
Builtin::PrintZiel => ids::PRINT_ZIEL,
|
|
Builtin::WriteFile => ids::WRITE_FILE,
|
|
Builtin::EofF => ids::EOF_F,
|
|
Builtin::LofF => ids::LOF_F,
|
|
Builtin::LocF => ids::LOC_F,
|
|
Builtin::SeekF => ids::SEEK_F,
|
|
Builtin::SeekStmt => ids::SEEK_STMT,
|
|
Builtin::Freefile => ids::FREEFILE,
|
|
Builtin::Fileattr => ids::FILEATTR,
|
|
Builtin::LockStmt => ids::LOCK_STMT,
|
|
Builtin::Kill => ids::KILL,
|
|
Builtin::NameStmt => ids::NAME_STMT,
|
|
Builtin::Files => ids::FILES,
|
|
Builtin::Chdir => ids::CHDIR,
|
|
Builtin::Chdrive => ids::CHDRIVE,
|
|
Builtin::Mkdir => ids::MKDIR,
|
|
Builtin::Rmdir => ids::RMDIR,
|
|
Builtin::CurdirS => ids::CURDIR_S,
|
|
Builtin::DirS => ids::DIR_S,
|
|
Builtin::Lpos => ids::LPOS,
|
|
Builtin::ShellStmt => ids::SHELL_STMT,
|
|
Builtin::ShellFn => ids::SHELL_FN,
|
|
Builtin::MkS => ids::MK_S,
|
|
Builtin::CvF => ids::CV_F,
|
|
Builtin::Fv => ids::FV,
|
|
Builtin::Pv => ids::PV,
|
|
Builtin::Pmt => ids::PMT,
|
|
Builtin::NPer => ids::NPER,
|
|
Builtin::IPmt => ids::IPMT,
|
|
Builtin::PPmt => ids::PPMT,
|
|
Builtin::Rate => ids::RATE,
|
|
Builtin::Npv => ids::NPV,
|
|
Builtin::Irr => ids::IRR,
|
|
Builtin::Mirr => ids::MIRR,
|
|
Builtin::Sln => ids::SLN,
|
|
Builtin::Syd => ids::SYD,
|
|
Builtin::Ddb => ids::DDB,
|
|
Builtin::Timer => ids::TIMER,
|
|
Builtin::DateS => ids::DATE_S,
|
|
Builtin::TimeS => ids::TIME_S,
|
|
Builtin::DateSet => ids::DATE_SET,
|
|
Builtin::TimeSet => ids::TIME_SET,
|
|
Builtin::Now => ids::NOW,
|
|
Builtin::DateSerial => ids::DATE_SERIAL,
|
|
Builtin::TimeSerial => ids::TIME_SERIAL,
|
|
Builtin::DateValue => ids::DATE_VALUE,
|
|
Builtin::TimeValue => ids::TIME_VALUE,
|
|
Builtin::DayF => ids::DAY_F,
|
|
Builtin::MonthF => ids::MONTH_F,
|
|
Builtin::YearF => ids::YEAR_F,
|
|
Builtin::WeekdayF => ids::WEEKDAY_F,
|
|
Builtin::HourF => ids::HOUR_F,
|
|
Builtin::MinuteF => ids::MINUTE_F,
|
|
Builtin::SecondF => ids::SECOND_F,
|
|
Builtin::CommandS => ids::COMMAND_S,
|
|
Builtin::Doevents => ids::DOEVENTS,
|
|
Builtin::Sleep => ids::SLEEP,
|
|
Builtin::Beep => ids::BEEP,
|
|
// ISAM
|
|
Builtin::IsamOpen => ids::ISAM_OPEN,
|
|
Builtin::IsamCreateIndex => ids::ISAM_CREATE_INDEX,
|
|
Builtin::IsamDeleteIndex => ids::ISAM_DELETE_INDEX,
|
|
Builtin::IsamSetIndex => ids::ISAM_SET_INDEX,
|
|
Builtin::IsamGetIndexS => ids::ISAM_GET_INDEX_S,
|
|
Builtin::IsamInsert => ids::ISAM_INSERT,
|
|
Builtin::IsamRetrieve => ids::ISAM_RETRIEVE,
|
|
Builtin::IsamUpdate => ids::ISAM_UPDATE,
|
|
Builtin::IsamDelete => ids::ISAM_DELETE,
|
|
Builtin::IsamDeleteTable => ids::ISAM_DELETE_TABLE,
|
|
Builtin::IsamMoveFirst => ids::ISAM_MOVE_FIRST,
|
|
Builtin::IsamMoveLast => ids::ISAM_MOVE_LAST,
|
|
Builtin::IsamMoveNext => ids::ISAM_MOVE_NEXT,
|
|
Builtin::IsamMovePrevious => ids::ISAM_MOVE_PREVIOUS,
|
|
Builtin::IsamSeekEq => ids::ISAM_SEEK_EQ,
|
|
Builtin::IsamSeekGt => ids::ISAM_SEEK_GT,
|
|
Builtin::IsamSeekGe => ids::ISAM_SEEK_GE,
|
|
Builtin::IsamBeginTrans => ids::ISAM_BEGIN_TRANS,
|
|
Builtin::IsamCommitTrans => ids::ISAM_COMMIT_TRANS,
|
|
Builtin::IsamRollback => ids::ISAM_ROLLBACK,
|
|
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
|
|
Builtin::IsamSetmem => ids::ISAM_SETMEM,
|
|
Builtin::IsamBof => ids::ISAM_BOF,
|
|
}
|
|
}
|
|
|
|
/// Liefert der Builtin in Anweisungsposition einen Wert (→ `Pop`)?
|
|
fn builtin_returns_value(b: Builtin) -> bool {
|
|
matches!(b, Builtin::Doevents)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn compile_src(src: &str) -> CompiledModule {
|
|
let a = tb_frontend::analyze_source("TEST", src);
|
|
assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
|
|
compile(&a.hir.unwrap())
|
|
}
|
|
|
|
fn main_code(m: &CompiledModule) -> &[Instr] {
|
|
&m.procs[0].code
|
|
}
|
|
|
|
#[test]
|
|
fn ausdruck_monomorph_mit_conv() {
|
|
// d# = i% + 1.5# → LoadGlobal, ConvI2R8, PushDbl, AddR8, StoreGlobal
|
|
let m = compile_src("i% = 2\nd# = i% + 1.5#");
|
|
let code = main_code(&m);
|
|
let want = [
|
|
Instr::LoadGlobal(0),
|
|
Instr::ConvI2R8,
|
|
Instr::PushDbl(1.5),
|
|
Instr::AddR8,
|
|
Instr::StoreGlobal(1),
|
|
];
|
|
assert!(
|
|
code.windows(want.len()).any(|w| w == want),
|
|
"erwartete Sequenz nicht gefunden: {code:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vergleich_und_logik() {
|
|
let m = compile_src("a% = 1\nb% = a% > 0 AND a% < 5");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Gt)));
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Lt)));
|
|
assert!(code.contains(&Instr::AndI2));
|
|
}
|
|
|
|
#[test]
|
|
fn string_konkatenation() {
|
|
let m = compile_src("s$ = \"a\" + \"b\"");
|
|
assert!(main_code(&m).contains(&Instr::Concat));
|
|
}
|
|
|
|
#[test]
|
|
fn if_mit_fixup() {
|
|
let m = compile_src("IF 1 THEN\nPRINT \"a\"\nELSE\nPRINT \"b\"\nEND IF");
|
|
let code = main_code(&m);
|
|
// Es gibt einen bedingten Sprung und einen unbedingten, beide gepatcht (≠ 0).
|
|
let jf = code.iter().find_map(|i| match i {
|
|
Instr::JumpIfFalse(t) => Some(*t),
|
|
_ => None,
|
|
});
|
|
assert!(jf.is_some() && jf.unwrap() > 0, "{code:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn for_mit_konstantem_step() {
|
|
let m = compile_src("FOR i% = 1 TO 3\nPRINT i%\nNEXT");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Le)), "{code:?}");
|
|
assert!(code.contains(&Instr::AddI2));
|
|
// Kein Laufzeit-Vorzeichentest bei konstantem Schritt:
|
|
assert!(!code.contains(&Instr::CmpI2(CmpOp::Ge)));
|
|
}
|
|
|
|
#[test]
|
|
fn for_mit_dynamischem_step() {
|
|
let m = compile_src("s% = -1\nFOR i% = 3 TO 1 STEP s%\nNEXT");
|
|
let code = main_code(&m);
|
|
// Vorzeichentest → beide Vergleichsrichtungen vorhanden
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Le)));
|
|
assert!(code.contains(&Instr::CmpI2(CmpOp::Ge)));
|
|
}
|
|
|
|
#[test]
|
|
fn gosub_und_on_goto() {
|
|
let m = compile_src("GOSUB U\nON 2 GOTO A, B\nA:\nB:\nU:\nRETURN");
|
|
let code = main_code(&m);
|
|
assert!(code.iter().any(|i| matches!(i, Instr::Gosub(_))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::OnJump(0, false))));
|
|
assert_eq!(m.jump_tables.len(), 1);
|
|
assert_eq!(m.jump_tables[0].len(), 2);
|
|
assert!(code.contains(&Instr::RetGosub));
|
|
}
|
|
|
|
#[test]
|
|
fn prozedur_und_byref() {
|
|
let m = compile_src("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
|
|
let code = main_code(&m);
|
|
assert!(code.contains(&Instr::MakeRefGlobal(0)));
|
|
assert!(code.iter().filter(|i| matches!(i, Instr::Call(1, 1))).count() == 2);
|
|
// Prozedurrumpf liest/schreibt über Referenz
|
|
let sub = &m.procs[1].code;
|
|
assert!(sub.contains(&Instr::LoadRef(0)));
|
|
assert!(sub.contains(&Instr::StoreRef(0)));
|
|
assert!(sub.last() == Some(&Instr::RetProc));
|
|
}
|
|
|
|
#[test]
|
|
fn function_liefert_wert() {
|
|
let m = compile_src("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
|
|
let f = &m.procs[1].code;
|
|
assert!(f.contains(&Instr::RetFn));
|
|
let code = main_code(&m);
|
|
assert!(code.iter().any(|i| matches!(i, Instr::Call(1, 1))));
|
|
}
|
|
|
|
#[test]
|
|
fn arrays_dim_und_zugriff() {
|
|
let m = compile_src("DIM a%(10)\na%(3) = 7\nPRINT a%(3)");
|
|
let code = main_code(&m);
|
|
assert!(code
|
|
.iter()
|
|
.any(|i| matches!(i, Instr::DimArr(true, 0, 1, TypeInit::Int))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::StoreElem(1))));
|
|
assert!(code.iter().any(|i| matches!(i, Instr::LoadElem(1))));
|
|
}
|
|
|
|
#[test]
|
|
fn data_read_restore() {
|
|
let m = compile_src("DATA 1, 2\nREAD a%, b%\nRESTORE\nREAD c%");
|
|
let code = main_code(&m);
|
|
assert_eq!(m.data.len(), 2);
|
|
assert!(code.iter().filter(|i| matches!(i, Instr::ReadData(1))).count() == 3);
|
|
assert!(code.contains(&Instr::Restore(0)));
|
|
assert!(code.contains(&Instr::ConvR8I2));
|
|
}
|
|
|
|
#[test]
|
|
fn fehlerbehandlung_emit() {
|
|
let m = compile_src("ON ERROR GOTO H\nERROR 5\nEND\nH:\nRESUME NEXT");
|
|
let code = main_code(&m);
|
|
assert!(code.iter().any(|i| matches!(i, Instr::OnErrorGoto(t) if *t > 0)));
|
|
assert!(code.contains(&Instr::RaiseError));
|
|
assert!(code.contains(&Instr::ResumeNext));
|
|
}
|
|
|
|
#[test]
|
|
fn tbc_roundtrip_ueber_codegen() {
|
|
let m = compile_src("PRINT \"Hallo\"");
|
|
let bytes = m.to_tbc();
|
|
let back = CompiledModule::from_tbc(&bytes).unwrap();
|
|
assert_eq!(back.procs[0].code, m.procs[0].code);
|
|
}
|
|
}
|