//! 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);
    }
}
