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

1592 lines
56 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 {
common_arrays: hir
.commons
.iter()
.filter(|c| c.dims.is_some())
.map(|c| c.slot)
.collect(),
strings: Vec::new(),
string_ids: HashMap::new(),
jump_tables: Vec::new(),
modul_label_pc: Vec::new(),
initialize_main: hir
.objects
.iter()
.any(|object| object.class == tb_frontend::forms::ObjectClass::Form),
};
let mut procs = Vec::new();
for proc in &hir.procs {
procs.push(cg.compile_proc(proc));
}
CompiledModule {
modules: vec![(hir.name.clone(), hir.option_base)],
sources: vec![tb_frontend::source::SourceFile {
module: 0,
path: hir.name.clone(),
}],
form_initial: vec![],
startup_form: None,
name: hir.name.clone(),
option_base: hir.option_base,
strings: cg.strings,
globals_init: hir.globals.iter().map(slot_init).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,
objects: hir.objects.clone(),
event_procs: hir.event_procs.clone(),
}
}
/// 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)
}
}
pub(crate) 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),
HTy::Form | HTy::Control => TypeInit::Empty,
}
}
struct Codegen {
common_arrays: std::collections::HashSet<u16>,
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>>,
/// Formulare müssen erst nach den globalen DIM-Anweisungen Ereignisse
/// zustellen; reine Textprogramme behalten ihre bisherigen Grenzen.
initialize_main: bool,
}
struct ProcCtx {
pos: tb_frontend::SourcePos,
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
}
/// Label auf die unmittelbar zuvor emittierte `Stmt`-Grenze binden.
///
/// Schleifen brauchen im Kreis eine Anweisungsgrenze — sonst wird dort
/// nie ein Ereignis zugestellt, kein Breakpoint erreicht und kein
/// Abbruch bemerkt (`FOR i = 1 TO 10000: NEXT` als Warteschleife).
/// Wo die Grenze ohnehin direkt vor dem Schleifenkopf liegt, zeigt der
/// Rücksprung einfach auf sie, statt eine zweite zu emittieren.
fn bind_auf_stmt(&mut self, label: u16) {
let pc = self.code.len().saturating_sub(1) as u32;
debug_assert!(
matches!(self.code.last(), Some(Instr::Stmt(_))),
"bind_auf_stmt ohne vorangehende Anweisungsgrenze"
);
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 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) {
if matches!(i, Instr::Stmt(_) | Instr::InitStmt(_)) {
self.code
.push(Instr::Source(self.pos.source, self.pos.column));
}
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 {
pos: tb_frontend::SourcePos::default(),
code: Vec::new(),
label_pc: vec![None; proc.label_count as usize],
fixups: Vec::new(),
table_fixups: Vec::new(),
};
if proc.kind == hir::HProcKind::Main {
let declarations = proc
.body
.iter()
.filter(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
.collect::<Vec<_>>();
for stmt in declarations {
self.stmt_inner(&mut ctx, proc, stmt, false);
}
if self.initialize_main
|| proc
.body
.iter()
.any(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
{
ctx.emit(Instr::Stmt(0));
}
}
for stmt in &proc.body {
if proc.kind == hir::HProcKind::Main
&& matches!(stmt.kind, HStmtKind::Dim { redim: false, .. })
{
continue;
}
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)
| Instr::TrapDefine(_, 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 {
module: 0,
kind: proc.kind,
params: proc.params.clone(),
ret_ty: proc.ret_ty.clone(),
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) {
self.stmt_inner(ctx, proc, stmt, true);
}
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
ctx.pos = stmt.pos;
match &stmt.kind {
HStmtKind::Label(l) => {
ctx.bind(*l);
return;
}
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
_ => ctx.emit(Instr::InitStmt(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::SetObjectProperty {
object,
index,
property,
value,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
self.expr(ctx, value);
ctx.emit(Instr::StoreObjectProperty(
*object,
*property,
index.is_some(),
));
}
HStmtKind::SetObjectIndexedProperty {
object,
object_index,
property,
index,
value,
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
self.expr(ctx, value);
ctx.emit(Instr::StoreObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HStmtKind::SetDynamicObjectProperty {
object,
property,
value,
} => {
self.expr(ctx, object);
self.expr(ctx, value);
let property = self.pool(property);
ctx.emit(Instr::StoreDynamicObjectProperty(property));
}
HStmtKind::ObjectMethod {
object,
index,
method,
args,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethod(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HStmtKind::ObjectLoad {
object,
index,
unload,
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::ObjectLoad(*object, *unload, index.is_some()));
}
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),
HTy::Form | HTy::Control => (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 {
end_pos,
pre,
post,
body,
exit_label,
} => {
let l_start = ctx.new_label();
ctx.bind_auf_stmt(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);
}
ctx.pos = *end_pos;
ctx.emit(Instr::Stmt(end_pos.line));
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 {
end_pos,
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,
*end_pos,
);
}
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::TrapDef { art, index, ziel } => {
self.expr(ctx, index);
match ziel {
Some(l) => ctx.emit_jump(Instr::TrapDefine(*art, 0), *l),
None => ctx.emit(Instr::TrapDisable(*art)),
}
}
HStmtKind::TrapSet {
art,
index,
zustand,
} => {
self.expr(ctx, index);
ctx.emit(Instr::TrapSet(*art, *zustand));
}
HStmtKind::EventSwitch(an) => ctx.emit(Instr::EventSwitch(*an)),
HStmtKind::ReturnGosub(target) => match target {
None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
},
HStmtKind::Run { target, string } => {
if let Some(target) = target {
self.expr(ctx, target);
}
ctx.emit(Instr::Run(if target.is_none() {
0
} else if *string {
2
} else {
1
}));
}
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);
}
// `DOEVENTS` und `SLEEP` sind Zustellpunkte und damit Sache
// der Ausführung, nicht der Bibliothek: ein Builtin kann
// keine Ereignisprozedur des Programms aufrufen
// (design.md, D1).
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
ctx.emit(i);
if matches!(b, Builtin::Doevents) {
ctx.emit(Instr::Pop);
}
return;
}
let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if matches!(b, Builtin::MsgBox) {
ctx.emit(Instr::Pop);
}
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 if global && self.common_arrays.contains(&s) {
ctx.emit(Instr::CommonArr(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::SystemInstr),
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,
end_pos: tb_frontend::SourcePos,
) {
// 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: eigene Quellgrenze für Fehler, RESUME und Debugger.
ctx.pos = end_pos;
ctx.emit(Instr::Stmt(end_pos.line));
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::UdtId(id) => ctx.emit(Instr::PushUdtId(*id)),
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::ObjectProperty {
object,
index,
property,
..
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::LoadObjectProperty(
*object,
*property,
index.is_some(),
));
}
HExpr::ObjectIndexedProperty {
object,
object_index,
property,
index,
..
} => {
if let Some(object_index) = object_index {
self.expr(ctx, object_index);
}
self.expr(ctx, index);
ctx.emit(Instr::LoadObjectIndexedProperty(
*object,
*property | if object_index.is_some() { 0x8000 } else { 0 },
));
}
HExpr::ObjectMethodCall {
object,
index,
method,
args,
..
} => {
if let Some(index) = index {
self.expr(ctx, index);
}
for arg in args {
self.expr(ctx, arg);
}
ctx.emit(Instr::ObjectMethodFn(
*object,
*method,
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
));
}
HExpr::DynamicObjectProperty { object, property } => {
self.expr(ctx, object);
let property = self.pool(property);
ctx.emit(Instr::LoadDynamicObjectProperty(property));
}
HExpr::ObjectRef { object, index, .. } => {
if let Some(index) = index {
self.expr(ctx, index);
}
ctx.emit(Instr::PushObject(*object, index.is_some()));
}
HExpr::TypeOf { value, class } => {
self.expr(ctx, value);
ctx.emit(Instr::TypeOf(class.id()));
}
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);
}
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
ctx.emit(i);
return;
}
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.
/// `DOEVENTS`/`SLEEP` → eigene Instruktion statt Builtin-Aufruf.
fn zustellpunkt_instr(b: Builtin, argc: usize) -> Option<Instr> {
match b {
Builtin::Doevents => Some(Instr::Doevents),
Builtin::Sleep => Some(Instr::Sleep(argc > 0)),
_ => None,
}
}
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::GraphicsLine => ids::GRAPHICS_LINE,
Builtin::GraphicsPaint => ids::GRAPHICS_PAINT,
Builtin::GraphicsView => ids::GRAPHICS_VIEW,
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::TimezoneKnown => ids::TIMEZONEKNOWN,
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::SetUEvent => ids::SETUEVENT,
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,
Builtin::MsgBox => ids::MSGBOX,
Builtin::InputBoxS => ids::INPUTBOX_S,
Builtin::ClipboardAdd => ids::CLIPBOARD_ADD,
Builtin::ClipboardGet => ids::CLIPBOARD_GET,
}
}
/// 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);
}
}