//! 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, strings: Vec>, string_ids: HashMap, jump_tables: Vec>, /// 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>, /// 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, /// LabelId → Instruktionsindex. label_pc: Vec>, /// (Instruktionsindex, LabelId) — nach dem Emit gepatcht. fixups: Vec<(usize, u16)>, /// (Tabellenindex, Labels) — Sprungtabellen nach dem Emit auflösen. table_fixups: Vec<(usize, Vec)>, } 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::>(); 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 = 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, 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 { 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); } }