//! Semantische Analyse **und** HIR-Lowering (Design-Entscheidung D1 der
//! Phase-2-Änderung): Symboltabellen, implizite Deklaration,
//! `DEFtype`-Regeln, `OPTION EXPLICIT`/`BASE`, Typprüfung inkl.
//! UDT-Feldtypen, Konstantenfaltung, Builtin-Signaturen, Label-Prüfung,
//! Compile-Zeit-Abweisung deklarierter Non-Features — und in demselben
//! Durchlauf die Erzeugung des typisierten HIR (Slots statt Namen,
//! explizite Konvertierungsknoten nach der Matrix in docs/tbvm-design.md).
//!
//! Ein vollständiges HIR ist nur bei diagnose-freier Analyse garantiert;
//! bei Fehlern werden betroffene Konstrukte bestmöglich übersprungen.

use crate::ast::*;
use crate::forms::{self, FormCatalog, ObjectClass, PropertyType};
use crate::hir::{
    self, Builtin, HArg, HExpr, HPlace, HPrintItem, HStmt, HStmtKind, HTy, IntKind, LabelId, NumTy,
    VarSlot,
};
use crate::lexer::Suffix;
use crate::{Diagnostic, SourcePos};
use std::collections::{hash_map::Entry, HashMap, HashSet};

#[derive(Debug, Clone, PartialEq)]
pub enum Ty {
    Int,
    Lng,
    Cur,
    Sng,
    Dbl,
    Str,
    FixedStr(u32),
    Udt(String),
    Form,
    Control,
    Unknown,
}

fn is_num(t: &Ty) -> bool {
    matches!(
        t,
        Ty::Int | Ty::Lng | Ty::Cur | Ty::Sng | Ty::Dbl | Ty::Unknown
    )
}
fn is_str(t: &Ty) -> bool {
    matches!(t, Ty::Str | Ty::FixedStr(_) | Ty::Unknown)
}

/// Numerischer HIR-Typ eines `Ty` (Fallback SINGLE bei `Unknown`).
fn num_ty(t: &Ty) -> NumTy {
    match t {
        Ty::Int => NumTy::Int,
        Ty::Lng => NumTy::Lng,
        Ty::Cur => NumTy::Cur,
        Ty::Dbl => NumTy::Dbl,
        _ => NumTy::Sng,
    }
}

fn ty_of_num(n: NumTy) -> Ty {
    match n {
        NumTy::Int => Ty::Int,
        NumTy::Lng => Ty::Lng,
        NumTy::Cur => Ty::Cur,
        NumTy::Sng => Ty::Sng,
        NumTy::Dbl => Ty::Dbl,
    }
}

fn rank(n: NumTy) -> u8 {
    match n {
        NumTy::Int => 1,
        NumTy::Lng => 2,
        NumTy::Cur => 3,
        NumTy::Sng => 4,
        NumTy::Dbl => 5,
    }
}

/// Promotion nach Matrix: Rangfolge INT < LNG < CUR < SNG < DBL,
/// CURRENCY gemischt mit Gleitkomma ergibt DOUBLE.
fn promote_num(a: NumTy, b: NumTy) -> NumTy {
    if a == b {
        return a;
    }
    let hi = if rank(a) >= rank(b) { a } else { b };
    let has_cur = a == NumTy::Cur || b == NumTy::Cur;
    if has_cur && matches!(hi, NumTy::Sng | NumTy::Dbl) {
        NumTy::Dbl
    } else {
        hi
    }
}

fn suffix_ty(s: Suffix) -> Ty {
    match s {
        Suffix::Integer => Ty::Int,
        Suffix::Long => Ty::Lng,
        Suffix::Single => Ty::Sng,
        Suffix::Double => Ty::Dbl,
        Suffix::Str => Ty::Str,
        Suffix::Currency => Ty::Cur,
    }
}

fn type_name_ty(t: &TypeName) -> Ty {
    match t {
        TypeName::Integer => Ty::Int,
        TypeName::Long => Ty::Lng,
        TypeName::Single => Ty::Sng,
        TypeName::Double => Ty::Dbl,
        TypeName::Currency => Ty::Cur,
        TypeName::Str => Ty::Str,
        TypeName::FixedStr(n) => Ty::FixedStr((*n).max(0) as u32),
        TypeName::Form => Ty::Form,
        TypeName::Control => Ty::Control,
        TypeName::Udt(n) => Ty::Udt(n.clone()),
    }
}

/// Gefalteter Konstantenwert (`CONST`).
#[derive(Debug, Clone, PartialEq)]
pub enum ConstVal {
    Num(f64),
    Str(String),
}

#[derive(Debug, Clone)]
struct VarInfo {
    ty: Ty,
    array: bool,
    explicit: bool,
    slot: u16,
    global: bool,
    /// Skalar-BYREF-Parameter (Slot enthält eine Referenz).
    by_ref: bool,
}

#[derive(Default)]
struct Scope {
    vars: HashMap<String, VarInfo>,
    labels: HashMap<String, LabelId>,
    line_labels: HashMap<u32, LabelId>,
    next_label: u16,
    /// Frame-Slots (leer im Modul-Scope: dort liegen Variablen global).
    locals: Vec<hir::HVar>,
    is_module: bool,
    /// `DEF FN`: freie Namen binden an Modulvariablen.
    is_def_fn: bool,
    /// `STATIC`-Prozedur: Locals wandern in globale Slots.
    is_static: bool,
    /// Stack der Schleifen-Exit-Labels für `EXIT FOR`/`EXIT DO`.
    loop_exits: Vec<(LoopKind, LabelId)>,
    current_line: u32,
    current_pos: SourcePos,
}

#[derive(PartialEq, Clone, Copy)]
enum LoopKind {
    For,
    Do,
}

impl Scope {
    fn new_label(&mut self) -> LabelId {
        let id = self.next_label;
        self.next_label += 1;
        id
    }
}

#[derive(Debug, Clone)]
struct ProcInfo {
    kind: ProcKind,
    ret: Ty,
    params: Vec<(Ty, bool)>, // (Typ, ist Array)
    id: u16,
    def_fn: bool,
}

// ---- Builtin-Signaturen ----------------------------------------------------

#[derive(Clone, Copy)]
enum ArgK {
    N, // numerisch
    S, // String
    A, // beliebig
    R, // Satzvariable eines benutzerdefinierten Typs (ISAM)
}

#[derive(Clone, Copy)]
enum RetK {
    I,
    L,
    Cu,
    Sg,
    Db,
    St,
}

fn ret_ty(r: RetK) -> Ty {
    match r {
        RetK::I => Ty::Int,
        RetK::L => Ty::Lng,
        RetK::Cu => Ty::Cur,
        RetK::Sg => Ty::Sng,
        RetK::Db => Ty::Dbl,
        RetK::St => Ty::Str,
    }
}

/// Builtin-Funktionen: Name (inkl. Suffix) → (min, max, Argtypen, Rückgabe).
fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
    use ArgK::*;
    use RetK::*;
    Some(match name {
        "ABS" | "FIX" | "INT" => (1, 1, &[N], Db),
        "SGN" => (1, 1, &[N], I),
        "SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => (1, 1, &[N], Db),
        "CINT" => (1, 1, &[N], I),
        "CLNG" => (1, 1, &[N], L),
        "CSNG" => (1, 1, &[N], Sg),
        "CDBL" => (1, 1, &[N], Db),
        "CCUR" => (1, 1, &[N], Cu),
        "RND" => (0, 1, &[N], Sg),
        "ASC" => (1, 1, &[S], I),
        "CHR$" => (1, 1, &[N], St),
        "LEN" => (1, 1, &[A], I),
        "LEFT$" | "RIGHT$" => (2, 2, &[S, N], St),
        "MID$" => (2, 3, &[S, N, N], St),
        "INSTR" => (2, 3, &[A, A, A], I), // Sonderfall, s. lower_builtin_fn
        "UCASE$" | "LCASE$" | "LTRIM$" | "RTRIM$" => (1, 1, &[S], St),
        "SPACE$" => (1, 1, &[N], St),
        "STRING$" => (2, 2, &[N, A], St),
        "STR$" => (1, 1, &[N], St),
        "VAL" => (1, 1, &[S], Db),
        "HEX$" | "OCT$" => (1, 1, &[N], St),
        "INKEY$" => (0, 0, &[], St),
        "INPUT$" => (1, 2, &[N, N], St),
        "LBOUND" | "UBOUND" => (1, 2, &[A, N], L),
        "CSRLIN" => (0, 0, &[], I),
        "POS" => (1, 1, &[N], I),
        "SCREEN" => (2, 3, &[N, N, N], I),
        "TAB" | "SPC" => (1, 1, &[N], St),
        "DATE$" | "TIME$" => (0, 0, &[], St),
        "TIMER" => (0, 0, &[], Sg),
        "NOW" => (0, 0, &[], Db),
        "TIMEZONEKNOWN" => (0, 0, &[], I),
        "DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
        "DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
        "DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
        "FORMAT$" => (1, 2, &[A, S], St),
        "ERR" | "ERL" => (0, 0, &[], L),
        "FRE" => (1, 1, &[A], L),
        "EOF" => (1, 1, &[N], I),
        "LOF" | "LOC" | "SEEK" => (1, 1, &[N], L),
        "FREEFILE" => (0, 0, &[], I),
        "FILEATTR" => (2, 2, &[N, N], L),
        "ENVIRON$" => (1, 1, &[A], St),
        "COMMAND$" => (0, 0, &[], St),
        "DOEVENTS" => (0, 0, &[], I),
        "MSGBOX" => (1, 3, &[S, N, S], I),
        "INPUTBOX$" => (1, 5, &[S, S, S, N, N], St),
        // Finanzmathematik (Original-Hilfe schreibt sie mit `#`; beide
        // Schreibweisen werden angenommen, gerechnet wird in DOUBLE).
        "FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
            (5, 5, &[N, N, N, N, N], Db)
        }
        "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db),
        "NPV" | "NPV#" => (2, 2, &[N, A], Db),
        "IRR" | "IRR#" => (2, 2, &[A, N], Db),
        "MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
        "SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
        "SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, N, N, N], Db),
        // Record-Konvertierung (Zahl ↔ Bytedarstellung im Feldpuffer)
        "MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" => (1, 1, &[N], St),
        "MKSMBF$" | "MKDMBF$" => (1, 1, &[N], St),
        "CVI" => (1, 1, &[S], I),
        "CVL" => (1, 1, &[S], L),
        "CVS" | "CVSMBF" => (1, 1, &[S], Sg),
        "CVD" | "CVDMBF" => (1, 1, &[S], Db),
        "CVC" => (1, 1, &[S], Cu),
        // Dateisystem und System
        "SHELL" => (1, 1, &[S], L),
        "CURDIR$" => (0, 1, &[S], St),
        "DIR$" => (0, 1, &[S], St),
        "LPOS" => (1, 1, &[N], I),
        "STACK" => (0, 0, &[], L),
        "ERDEV" => (0, 0, &[], I),
        "ERDEV$" => (0, 0, &[], St),
        // ISAM-Funktionen (Formen siehe umfang-und-signaturen.md des Changes)
        "GETINDEX$" => (1, 1, &[N], St),
        "BOF" => (1, 1, &[N], I),
        "SAVEPOINT" => (0, 0, &[], I),
        "SETMEM" => (1, 1, &[N], L),
        _ => return None,
    })
}

/// Deklarierte Non-Features (siehe Sprachreferenz „Abweichungen"):
/// Hardware-Nähe, CHAIN/Overlays, Grafik, PLAY/SOUND. Ablehnung erfolgt
/// zur Compile-Zeit mit der Meldung „Feature unavailable".
fn banned_feature(name: &str) -> bool {
    matches!(
        name,
        // Hardware-Nähe
        "PEEK" | "POKE" | "INP" | "OUT" | "WAIT" | "BLOAD" | "BSAVE"
            | "VARPTR" | "VARSEG" | "SADD" | "VARPTR$" | "ABSOLUTE"
            | "INTERRUPT" | "INTERRUPTX" | "IOCTL" | "IOCTL$"
            // Interlanguage-Schnittstelle
            | "CALLS" | "SSEG" | "SSEGADD"
            | "STRINGADDRESS" | "STRINGASSIGN" | "STRINGLENGTH" | "STRINGRELEASE"
            // Overlay-Mechanismus
            | "CHAIN"
            // Grafik
            | "PSET" | "PRESET" | "CIRCLE" | "DRAW" | "PALETTE"
            | "PCOPY" | "PMAP" | "WINDOW" | "POINT"
            // Event-Geräte (auch als Funktion in `ON COM(1) GOSUB …`)
            | "COM" | "PEN" | "STRIG" | "STICK"
            // Klang (außer BEEP)
            | "SOUND" | "PLAY"
    )
}

/// Non-Features, die nicht über ihren Namen abgewiesen werden können, weil
/// sie ihr Schlüsselwort mit einer unterstützten Form teilen. Sie werden an
/// der Syntax erkannt: `GET`/`PUT` mit `(` (Grafik) im Parser und `OPEN` mit
/// einem `COMn:`-Gerätenamen hier in der Semantik.
/// Der Inventar-Abgleich liest diese Liste.
pub const SYNTAKTISCH_ABGEWIESEN: &[&str] =
    &["GET (Grafik)", "PUT (Grafik)", "OPEN COM", "DEF SEG"];

/// Ist `s` ein serieller Gerätename (`COM1:` …)? Nur für Stringliterale.
fn ist_com_geraet(s: &str) -> bool {
    let up = s.to_ascii_uppercase();
    up.strip_prefix("COM")
        .and_then(|r| r.chars().next())
        .is_some_and(|c| c.is_ascii_digit())
}

/// Builtin-Anweisungen (Bibliothek, keine Keywords).
fn builtin_stmt(name: &str) -> Option<(u8, u8, &'static [ArgK])> {
    use ArgK::*;
    Some(match name {
        "CLS" => (0, 1, &[N]),
        "BEEP" | "DOEVENTS" | "TRON" | "TROFF" | "RESET" => (0, 0, &[]),
        "COLOR" => (0, 3, &[N, N, N]),
        "LOCATE" => (0, 5, &[N, N, N, N, N]),
        "RANDOMIZE" => (0, 1, &[N]),
        "SLEEP" => (0, 1, &[N]),
        "WIDTH" => (0, 2, &[N, N]),
        "SCREEN" => (1, 4, &[N, N, N, N]),
        "SWAP" => (2, 2, &[A, A]),
        "KILL" | "CHDIR" | "MKDIR" | "RMDIR" => (1, 1, &[S]),
        "FILES" => (0, 1, &[S]),
        "SHELL" => (0, 1, &[S]),
        "RUN" => (0, 1, &[A]),
        "CLEAR" => (0, 3, &[N, N, N]),
        "KEY" => (1, 2, &[A, S]),
        "ENVIRON" => (1, 1, &[S]),
        "MSGBOX" => (1, 3, &[S, N, S]),
        "SETUEVENT" => (0, 0, &[]),
        "CHDRIVE" => (1, 1, &[S]),
        "STACK" => (0, 1, &[N]),
        // `CALL SetFormatCC(49)` — Währungsformat nach Ländercode.
        "SETFORMATCC" => (1, 1, &[N]),
        // ---- ISAM-Anweisungen -------------------------------------------
        // Argumentformen wortgetreu aus der Original-Hilfe; belegt in
        // openspec/changes/phase-3-isam/umfang-und-signaturen.md.
        //
        // CREATEINDEX ist variadisch: Dateinummer, Indexname,
        // Eindeutigkeitskennzeichen, dann ein Stringargument je Spalte.
        // Die Original-Hilfe nennt keine Obergrenze für die Spaltenzahl, also
        // steht hier auch keine; die Argumenttypen ab dem vierten prüft
        // `check_and_lower_builtin_args` gesondert.
        "CREATEINDEX" => (4, u8::MAX, &[N, S, N, S]),
        "DELETEINDEX" => (2, 2, &[N, S]),
        // Ohne Indexnamen wird der NULL-Index (Einfügereihenfolge) gesetzt.
        "SETINDEX" => (1, 2, &[N, S]),
        "INSERT" | "RETRIEVE" | "UPDATE" => (2, 2, &[N, R]),
        "DELETE" => (1, 1, &[N]),
        "DELETETABLE" => (2, 2, &[S, S]),
        "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS" => (1, 1, &[N]),
        // Dateinummer, dann ein Schlüsselwert je Indexspalte.
        "SEEKEQ" | "SEEKGT" | "SEEKGE" => (2, 10, &[N, A, A, A, A, A, A, A, A, A]),
        "BEGINTRANS" | "COMMITTRANS" => (0, 0, &[]),
        "ROLLBACK" => (0, 1, &[N]),
        _ => return None,
    })
}

// ---- Einstiegspunkte -------------------------------------------------------

/// Nur Diagnosen (Kompatibilitäts-API; nutzt intern das Lowering).
pub fn check(module: &Module) -> Vec<Diagnostic> {
    lower(module).1
}

/// Analyse + Lowering: liefert das HIR (vollständig nur bei leeren
/// Diagnosen) und alle Diagnosen.
pub fn lower(module: &Module) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
    lower_with_forms(module, &FormCatalog::default())
}

pub fn lower_with_forms(
    module: &Module,
    catalog: &FormCatalog,
) -> (Option<hir::HirModule>, Vec<Diagnostic>) {
    let mut catalog = catalog.clone();
    if catalog.find("SCREEN").is_none() {
        catalog.add("SCREEN", ObjectClass::Screen, None, false);
    }
    if module
        .body
        .iter()
        .any(|s| matches!(s, Stmt::MetaForm { .. }))
        && catalog.find(&module.name).is_none()
    {
        catalog.add(&module.name, ObjectClass::Form, None, false);
    }
    let mut s = new_sema(module, &catalog);
    let hir = s.run(module);
    (hir, s.diags)
}

/// Semantisch gebundene Objekt-/Ereignisnamen für transaktionale IDE-Umbenennungen.
/// Aufrufer validieren die gesamte Übersetzungseinheit einschließlich ihrer Imports.
#[derive(Default)]
pub struct BoundFormReferences {
    pub objects: Vec<(SourcePos, String, u16)>,
    pub procedures: Vec<(SourcePos, String)>,
    pub diagnostics: Vec<Diagnostic>,
}
pub fn bound_form_references(module: &Module, catalog: &FormCatalog) -> BoundFormReferences {
    let mut s = new_sema(module, catalog);
    s.references = Some(BoundFormReferences::default());
    s.run(module);
    let mut references = s.references.unwrap();
    references.diagnostics = s.diags;
    references
}

fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
    Sema {
        diags: Vec::new(),
        deftypes: [const { None }; 26],
        explicit: false,
        option_base: 0,
        procs: HashMap::new(),
        next_proc_id: 1,
        udt_ids: HashMap::new(),
        udt_defs: Vec::new(),
        consts: HashMap::new(),
        module_vars: HashMap::new(),
        shared_vars: HashSet::new(),
        module_labels: HashMap::new(),
        module_line_labels: HashMap::new(),
        globals: Vec::new(),
        commons: Vec::new(),
        data: Vec::new(),
        data_marks_name: HashMap::new(),
        data_marks_line: HashMap::new(),
        hir_procs: Vec::new(),
        isam_typ: HashMap::new(),
        forms: catalog.clone(),
        module_name: module.name.clone(),
        event_procs: Vec::new(),
        references: None,
    }
}

/// Exportdeklarationen mit den Typen und Konstanten ihres Ursprungsmoduls.
/// Rümpfe werden hier nicht übersetzt; dafür bleibt der reguläre Sema-Durchlauf zuständig.
pub fn export_declarations(module: &Module, constants: &[Stmt]) -> Vec<Stmt> {
    let mut s = new_sema(module, &FormCatalog::default());
    let mut scope = Scope::default();
    let mut declarations = Vec::new();
    let local: HashSet<_> = module
        .body
        .iter()
        .flat_map(|stmt| match stmt {
            Stmt::ConstDecl { items, .. } => items.iter().map(|i| i.0.as_str()).collect(),
            _ => Vec::new(),
        })
        .collect();
    for stmt in constants {
        if let Stmt::ConstDecl { items, .. } = stmt {
            if !local.contains(items[0].0.as_str()) {
                s.lower_stmt(stmt, &mut scope, &mut Vec::new());
            }
        }
    }
    for stmt in &module.body {
        match stmt {
            Stmt::DefType { .. } => s.lower_stmt(stmt, &mut scope, &mut Vec::new()),
            Stmt::ConstDecl { items, pos } => {
                for item in items {
                    let declaration = Stmt::ConstDecl {
                        items: vec![item.clone()],
                        pos: *pos,
                    };
                    s.lower_stmt(&declaration, &mut scope, &mut Vec::new());
                    let value = match s.consts.get(&item.0).and_then(|(_, value)| value.as_ref()) {
                        Some(ConstVal::Num(n)) => Expr::DoubleLit(*n, item.2.pos()),
                        Some(ConstVal::Str(value)) => Expr::StrLit(value.clone(), item.2.pos()),
                        None => item.2.clone(),
                    };
                    declarations.push(Stmt::ConstDecl {
                        items: vec![(item.0.clone(), item.1, value)],
                        pos: *pos,
                    });
                }
            }
            Stmt::TypeDecl { .. } => declarations.push(stmt.clone()),
            _ => {}
        }
    }
    for proc in &module.procs {
        let mut sig = proc.sig.clone();
        if sig.suffix.is_none() && sig.kind == ProcKind::Function {
            sig.suffix =
                Suffix::from_char(s.var_key(&sig.name, &None, false).chars().last().unwrap());
        }
        for param in &mut sig.params {
            if param.suffix.is_none() && param.as_type.is_none() {
                param.suffix =
                    Suffix::from_char(s.var_key(&param.name, &None, false).chars().last().unwrap());
            }
        }
        declarations.push(Stmt::Declare { sig, pos: proc.pos });
    }
    declarations
}

struct Sema {
    diags: Vec<Diagnostic>,
    /// `DEFtype`-Zuordnung je Anfangsbuchstabe; None = Standard (SINGLE).
    deftypes: [Option<Ty>; 26],
    explicit: bool,
    /// `OPTION BASE` (0 oder 1).
    option_base: u8,
    procs: HashMap<String, ProcInfo>,
    next_proc_id: u16,
    /// Benutzerdefinierte Typen: Name → Index, Definitionen geordnet.
    udt_ids: HashMap<String, u16>,
    udt_defs: Vec<hir::HUdt>,
    /// Satztyp je ISAM-Dateinummer aus `OPEN … FOR ISAM typ tabelle AS #n`
    /// mit literaler Nummer — Grundlage der Satztypprüfung von `INSERT`,
    /// `RETRIEVE` und `UPDATE`.
    isam_typ: HashMap<i32, String>,
    /// Konstanten: Typ und gefalteter Wert.
    consts: HashMap<String, (Ty, Option<ConstVal>)>,
    /// Modulvariablen (Schlüssel wie `var_key`).
    module_vars: HashMap<String, VarInfo>,
    /// Mit `DIM SHARED`/`COMMON SHARED` für Prozeduren sichtbare Modulvariablen.
    shared_vars: HashSet<String>,
    /// Sprungziele des Modulrumpfs — ein `ON ERROR GOTO` ohne `LOCAL` in
    /// einer Prozedur verweist auf sie (Scoping-Regel des Vorbilds).
    module_labels: HashMap<String, LabelId>,
    module_line_labels: HashMap<u32, LabelId>,
    /// Globale Slots (Modulvariablen, STATICs, versteckte Temps).
    globals: Vec<hir::HVar>,
    commons: Vec<hir::HCommon>,
    /// DATA-Konstanten des Moduls (aus dem Prescan, statisch).
    data: Vec<hir::DataItem>,
    data_marks_name: HashMap<String, u32>,
    data_marks_line: HashMap<u32, u32>,
    /// Fertige HIR-Prozeduren nach Id (0 = Hauptprogramm).
    hir_procs: Vec<Option<hir::HProc>>,
    forms: FormCatalog,
    module_name: String,
    event_procs: Vec<hir::HEventProc>,
    references: Option<BoundFormReferences>,
}

impl Sema {
    fn err(&mut self, pos: SourcePos, msg: impl Into<String>) {
        self.diags.push(Diagnostic {
            file: None,
            pos,
            message: msg.into(),
        });
    }

    fn run(&mut self, module: &Module) -> Option<hir::HirModule> {
        // Pass 1: Prozeduren, DECLAREs und TYPEs registrieren.
        for stmt in &module.body {
            match stmt {
                Stmt::Declare { sig, pos } => self.register_proc(sig, *pos),
                Stmt::DefType { .. } => {
                    self.lower_stmt(stmt, &mut Scope::default(), &mut Vec::new())
                }
                Stmt::TypeDecl { name, fields, pos } => {
                    let mut hfields = Vec::new();
                    for (fname, ftype) in fields {
                        if let TypeName::Udt(n) = ftype {
                            if !self.udt_ids.contains_key(n) {
                                self.err(*pos, "Type not defined");
                            }
                        }
                        hfields.push((fname.clone(), self.h_ty(&type_name_ty(ftype))));
                    }
                    if self.udt_ids.contains_key(name) {
                        self.err(*pos, "Duplicate definition");
                    } else {
                        self.udt_ids
                            .insert(name.clone(), self.udt_defs.len() as u16);
                        self.udt_defs.push(hir::HUdt {
                            name: name.clone(),
                            fields: hfields,
                        });
                    }
                }
                _ => {}
            }
        }
        let mut defined = HashSet::new();
        for proc in &module.procs {
            if !defined.insert(proc.sig.name.clone()) {
                self.err(proc.pos, "Duplicate definition");
            }
            self.register_proc(&proc.sig, proc.pos);
        }
        for proc in &module.procs {
            self.register_event_proc(proc);
        }
        self.hir_procs = Vec::new();
        self.hir_procs
            .resize_with(self.next_proc_id as usize, || None);

        self.deftypes = [const { None }; 26];

        // Pass 2: Modulrumpf. Labels, Zeilennummern und DATA-Positionen
        // werden vorab eingesammelt (RESTORE nach vorn, statisches DATA).
        let mut scope = Scope {
            is_module: true,
            ..Scope::default()
        };
        self.prescan(&module.body, &mut scope, true);
        let body = self.lower_body(&module.body, &mut scope);
        let main = hir::HProc {
            name: module.name.clone(),
            kind: hir::HProcKind::Main,
            params: Vec::new(),
            locals: Vec::new(),
            ret_slot: None,
            ret_ty: None,
            body,
            label_count: scope.next_label,
        };
        self.hir_procs
            .resize_with(self.next_proc_id as usize, || None);
        self.hir_procs[0] = Some(main);
        self.module_labels = scope.labels.clone();
        self.module_line_labels = scope.line_labels.clone();

        // Pass 3: Prozedurrümpfe.
        for proc in &module.procs {
            let deftypes = self.deftypes.clone();
            self.lower_proc(proc);
            self.deftypes = deftypes;
        }

        // Zusammensetzen: fehlende Rümpfe (nur DECLARE) behalten ihre
        // Signatur, damit externe Bibliotheksaufrufe dieselben BYREF-Slots
        // und Funktionsrückgaben wie lokale Implementierungen besitzen.
        let mut declared = vec![None; self.next_proc_id as usize];
        for (name, info) in self.procs.iter() {
            declared[info.id as usize] = Some((name.clone(), info.clone()));
        }
        let procs: Vec<hir::HProc> = std::mem::take(&mut self.hir_procs)
            .into_iter()
            .enumerate()
            .map(|(i, p)| {
                p.unwrap_or_else(|| {
                    let (name, info) = declared[i].clone().unwrap();
                    let params = info
                        .params
                        .iter()
                        .enumerate()
                        .map(|(index, (ty, array))| hir::HParam {
                            name: format!("ARG{index}"),
                            ty: self.h_ty(ty),
                            array: *array,
                            by_ref: !array,
                        })
                        .collect::<Vec<_>>();
                    let mut locals = params
                        .iter()
                        .map(|param| hir::HVar {
                            name: param.name.clone(),
                            ty: param.ty.clone(),
                            array: param.array,
                        })
                        .collect::<Vec<_>>();
                    let (kind, ret_slot, ret_ty) = if info.kind == ProcKind::Function {
                        let ty = self.h_ty(&info.ret);
                        let slot = VarSlot::Local(locals.len() as u16);
                        locals.push(hir::HVar {
                            name: name.clone(),
                            ty: ty.clone(),
                            array: false,
                        });
                        (hir::HProcKind::Function, Some(slot), Some(ty))
                    } else {
                        (hir::HProcKind::Sub, None, None)
                    };
                    hir::HProc {
                        name,
                        kind,
                        params,
                        locals,
                        ret_slot,
                        ret_ty,
                        body: Vec::new(),
                        label_count: 0,
                    }
                })
            })
            .collect();

        Some(hir::HirModule {
            name: module.name.clone(),
            globals: std::mem::take(&mut self.globals),
            commons: std::mem::take(&mut self.commons),
            udts: std::mem::take(&mut self.udt_defs),
            procs,
            data: std::mem::take(&mut self.data),
            option_base: self.option_base,
            objects: self.forms.objects.clone(),
            event_procs: std::mem::take(&mut self.event_procs),
        })
    }

    fn lower_proc(&mut self, proc: &Proc) {
        let info = self.procs.get(&proc.sig.name).cloned();
        let Some(info) = info else { return };
        let mut scope = Scope {
            is_static: proc.is_static,
            ..Scope::default()
        };
        self.prescan(&proc.body, &mut scope, false);
        let mut params = Vec::new();
        for p in &proc.sig.params {
            let ty = self.param_ty(p);
            let key = self.var_key(&p.name, &p.suffix, p.as_type.is_some());
            let hty = self.h_ty(&ty);
            let slot = scope.locals.len() as u16;
            scope.locals.push(hir::HVar {
                name: key.trim_end_matches("\u{1}AS").to_string(),
                ty: hty.clone(),
                array: p.array,
            });
            let by_ref = !p.array;
            scope.vars.insert(
                key,
                VarInfo {
                    ty: ty.clone(),
                    array: p.array,
                    explicit: true,
                    slot,
                    global: false,
                    by_ref,
                },
            );
            params.push(hir::HParam {
                name: p.name.clone(),
                ty: hty,
                array: p.array,
                by_ref,
            });
        }
        // Funktionsname als Rückgabe-Variable (immer frame-lokal).
        let mut ret_slot = None;
        let mut ret_ty = None;
        if proc.sig.kind == ProcKind::Function {
            let ret = self.name_ty(&proc.sig.name, &proc.sig.suffix);
            let key = self.var_key(&proc.sig.name, &proc.sig.suffix, false);
            let slot = scope.locals.len() as u16;
            scope.locals.push(hir::HVar {
                name: key.clone(),
                ty: self.h_ty(&ret),
                array: false,
            });
            scope.vars.insert(
                key,
                VarInfo {
                    ty: ret.clone(),
                    array: false,
                    explicit: true,
                    slot,
                    global: false,
                    by_ref: false,
                },
            );
            ret_slot = Some(VarSlot::Local(slot));
            ret_ty = Some(self.h_ty(&ret));
        }
        let body = self.lower_body(&proc.body, &mut scope);
        let hproc = hir::HProc {
            name: proc.sig.name.clone(),
            kind: match proc.sig.kind {
                ProcKind::Sub => hir::HProcKind::Sub,
                ProcKind::Function => hir::HProcKind::Function,
            },
            params,
            locals: std::mem::take(&mut scope.locals),
            ret_slot,
            ret_ty,
            body,
            label_count: scope.next_label,
        };
        let idx = info.id as usize;
        if idx < self.hir_procs.len() {
            self.hir_procs[idx] = Some(hproc);
        }
    }

    /// Prescan eines Rumpfs: Labels/Zeilennummern erhalten `LabelId`s;
    /// im Modulrumpf werden zusätzlich DATA-Konstanten (statisch, in
    /// Quellreihenfolge) und RESTORE-Marken eingesammelt.
    fn prescan(&mut self, stmts: &[Stmt], scope: &mut Scope, module_data: bool) {
        for s in stmts {
            match s {
                Stmt::Label(n) => {
                    let id = scope.new_label();
                    scope.labels.insert(n.clone(), id);
                    if module_data {
                        self.data_marks_name
                            .insert(n.clone(), self.data.len() as u32);
                    }
                }
                Stmt::LineNumber(n, _) => {
                    let id = scope.new_label();
                    scope.line_labels.insert(*n, id);
                    if module_data {
                        self.data_marks_line.insert(*n, self.data.len() as u32);
                    }
                }
                Stmt::Data { items, pos } => {
                    if module_data {
                        for it in items {
                            self.data.push(hir::DataItem {
                                text: it.clone(),
                                line: pos.line,
                            });
                        }
                    }
                }
                Stmt::If {
                    then_body,
                    elseifs,
                    else_body,
                    ..
                } => {
                    self.prescan(then_body, scope, module_data);
                    for (_, b, _) in elseifs {
                        self.prescan(b, scope, module_data);
                    }
                    if let Some(b) = else_body {
                        self.prescan(b, scope, module_data);
                    }
                }
                Stmt::Select { arms, .. } => {
                    for a in arms {
                        self.prescan(&a.body, scope, module_data);
                    }
                }
                Stmt::For { body, .. } | Stmt::DoLoop { body, .. } | Stmt::While { body, .. } => {
                    self.prescan(body, scope, module_data)
                }
                _ => {}
            }
        }
    }

    fn param_ty(&self, p: &Param) -> Ty {
        if let Some(t) = &p.as_type {
            type_name_ty(t)
        } else {
            self.name_ty(&p.name, &p.suffix)
        }
    }

    fn register_proc(&mut self, sig: &ProcSig, pos: SourcePos) {
        let ret = match sig.kind {
            ProcKind::Function => self.name_ty(&sig.name, &sig.suffix),
            ProcKind::Sub => Ty::Unknown,
        };
        let params: Vec<_> = sig
            .params
            .iter()
            .map(|p| (self.param_ty(p), p.array))
            .collect();
        let id = match self.procs.get(&sig.name) {
            Some(p) => {
                let id = p.id;
                if p.kind != sig.kind || p.ret != ret || p.params != params {
                    self.err(pos, "Parameter type mismatch");
                }
                id
            }
            None => {
                let id = self.next_proc_id;
                self.next_proc_id += 1;
                id
            }
        };
        self.procs.insert(
            sig.name.clone(),
            ProcInfo {
                kind: sig.kind,
                ret,
                params,
                id,
                def_fn: false,
            },
        );
    }

    fn h_ty(&self, t: &Ty) -> HTy {
        match t {
            Ty::Int => HTy::Num(NumTy::Int),
            Ty::Lng => HTy::Num(NumTy::Lng),
            Ty::Cur => HTy::Num(NumTy::Cur),
            Ty::Sng => HTy::Num(NumTy::Sng),
            Ty::Dbl => HTy::Num(NumTy::Dbl),
            Ty::Str => HTy::Str,
            Ty::FixedStr(n) => HTy::FixedStr(*n),
            Ty::Udt(name) => HTy::Udt(*self.udt_ids.get(name).unwrap_or(&0)),
            Ty::Form => HTy::Form,
            Ty::Control => HTy::Control,
            Ty::Unknown => HTy::Num(NumTy::Sng),
        }
    }

    fn fixed_width(&self, ty: &HTy) -> Option<i16> {
        let bytes = match ty {
            HTy::Num(NumTy::Int) => 2,
            HTy::Num(NumTy::Lng | NumTy::Sng) => 4,
            HTy::Num(NumTy::Cur | NumTy::Dbl) => 8,
            HTy::FixedStr(length) => usize::try_from(*length).ok()?.checked_mul(4)?,
            HTy::Udt(id) => self
                .udt_defs
                .get(*id as usize)?
                .fields
                .iter()
                .try_fold(0usize, |sum, (_, field)| {
                    sum.checked_add(self.fixed_width(field)? as usize)
                })?,
            _ => return None,
        };
        i16::try_from(bytes).ok()
    }

    fn register_event_proc(&mut self, proc: &Proc) {
        let Some((object, class, event)) = self.event_binding(&proc.sig.name) else {
            return;
        };
        if !forms::events(class)
            .iter()
            .any(|e| e.eq_ignore_ascii_case(&event))
        {
            return;
        }
        if let Some(r) = &mut self.references {
            r.objects.push((proc.pos, proc.sig.name.clone(), object));
        }
        let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new();
        if self.forms.objects[object as usize].array {
            expected.push(("INDEX", forms::EventParamType::Integer));
        }
        expected.extend(forms::event_params(&event).unwrap_or(&[]).iter().copied());
        let valid = proc.sig.kind == ProcKind::Sub
            && proc.sig.params.len() == expected.len()
            && proc.sig.params.iter().zip(&expected).all(|(p, (name, t))| {
                let actual = self.param_ty(p);
                p.name.eq_ignore_ascii_case(name)
                    && matches!(
                        (actual, t),
                        (Ty::Int, forms::EventParamType::Integer)
                            | (Ty::Sng, forms::EventParamType::Single)
                            | (Ty::Control, forms::EventParamType::Control)
                    )
            });
        if !valid {
            let args = expected
                .iter()
                .map(|(n, t)| {
                    let ty = match t {
                        forms::EventParamType::Integer => "INTEGER",
                        forms::EventParamType::Single => "SINGLE",
                        forms::EventParamType::Control => "CONTROL",
                    };
                    format!("{n} AS {ty}")
                })
                .collect::<Vec<_>>()
                .join(", ");
            self.err(
                proc.pos,
                format!("Event procedure {} expects ({args})", proc.sig.name),
            );
        }
        if let Some(info) = self.procs.get(&proc.sig.name) {
            self.event_procs.push(hir::HEventProc {
                object,
                event,
                proc: info.id,
            });
        }
    }

    fn event_binding(&self, name: &str) -> Option<(u16, ObjectClass, String)> {
        let (base, event) = name.rsplit_once('_')?;
        let found = if base == "FORM" {
            self.current_form()
        } else {
            self.find_object(base)
        }?;
        Some((found.0, found.1.class, event.to_string()))
    }

    fn property_ty(spec: forms::PropertySpec) -> Ty {
        match spec.ty {
            PropertyType::String => Ty::Str,
            PropertyType::Single => Ty::Sng,
            PropertyType::Object => Ty::Control,
            PropertyType::Integer | PropertyType::Boolean | PropertyType::IntegerArray => Ty::Int,
        }
    }

    fn current_form(&self) -> Option<(u16, &forms::FormObject)> {
        self.forms
            .find(&self.module_name)
            .filter(|(_, o)| o.class == ObjectClass::Form)
            .or_else(|| {
                let mut forms = self
                    .forms
                    .objects
                    .iter()
                    .enumerate()
                    .filter(|(_, o)| o.class == ObjectClass::Form);
                let (id, object) = forms.next()?;
                forms.next().is_none().then_some((id as u16, object))
            })
    }

    fn find_object(&self, name: &str) -> Option<(u16, &forms::FormObject)> {
        self.forms
            .find_in(name, &self.module_name)
            .or_else(|| {
                self.forms
                    .find(name)
                    .filter(|(_, o)| o.class == ObjectClass::Form || o.class == ObjectClass::Screen)
            })
            .or_else(|| {
                let mut matches = self
                    .forms
                    .objects
                    .iter()
                    .enumerate()
                    .filter(|(_, o)| o.name.eq_ignore_ascii_case(name));
                let (id, object) = matches.next()?;
                matches.next().is_none().then_some((id as u16, object))
            })
    }

    fn scoped_object(
        &mut self,
        name: &str,
        parent: Option<&str>,
        pos: SourcePos,
    ) -> Option<(u16, forms::FormObject)> {
        if let Some(parent) = parent {
            if let Some((id, object)) = self.forms.find_in(name, parent) {
                if let Some(r) = &mut self.references {
                    r.objects.push((pos, name.into(), id));
                }
                if let Some((form, _)) = self.forms.find(parent) {
                    if let Some(r) = &mut self.references {
                        r.objects.push((pos, parent.into(), form));
                    }
                }
                return Some((id, object.clone()));
            }
            self.err(
                pos,
                format!("Control '{name}' not found in form '{parent}'"),
            );
            None
        } else {
            self.object_by_name(name, pos)
        }
    }

    fn object_by_name(
        &mut self,
        name: &str,
        pos: SourcePos,
    ) -> Option<(u16, crate::forms::FormObject)> {
        if let Some((id, object)) = self.find_object(name) {
            let object = object.clone();
            if let Some(r) = &mut self.references {
                r.objects.push((pos, name.into(), id));
            }
            return Some((id, object));
        }
        self.err(pos, format!("Unknown object '{name}'"));
        None
    }

    fn implicit_form_property(
        &self,
        scope: &Scope,
        name: &str,
    ) -> Option<(u16, u16, forms::PropertySpec)> {
        if self.declared_var(scope, name).is_some() || self.consts.contains_key(name) {
            return None;
        }
        let (object, form) = self.current_form()?;
        let (property, spec) = forms::property(form.class, name)?;
        Some((object, property, spec))
    }

    fn object_property(
        &mut self,
        path: &str,
        pos: SourcePos,
    ) -> Option<(u16, u16, forms::PropertySpec, ObjectClass)> {
        let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') {
            let (object, member) = tail.split_once('.').unwrap_or((tail, ""));
            (object, member, Some(form))
        } else {
            let (object, member) = path.split_once('.')?;
            (object, member, None)
        };
        let (id, object) = self.scoped_object(object_name, parent, pos)?;
        let Some((property, spec)) = forms::property(object.class, member) else {
            self.err(
                pos,
                format!("Unknown property '{member}' of {}", object.class.name()),
            );
            return None;
        };
        Some((id, property, spec, object.class))
    }

    fn object_member_target(
        &mut self,
        path: &str,
        pos: SourcePos,
    ) -> Option<(u16, crate::forms::FormObject, String)> {
        let (object_name, member, parent) = if let Some((form, tail)) = path.split_once('!') {
            let (object, member) = tail.split_once('.').unwrap_or((tail, ""));
            (object, member, Some(form))
        } else {
            let (object, member) = path.split_once('.')?;
            (object, member, None)
        };
        let (id, object) = self.scoped_object(object_name, parent, pos)?;
        Some((id, object, member.to_string()))
    }

    fn is_udt_path(&self, scope: &Scope, path: &str) -> bool {
        let base = path.split('.').next().unwrap_or(path);
        let as_key = format!("{base}\u{1}AS");
        let plain = self.var_key(base, &None, false);
        self.visible_var(scope, &as_key)
            .or_else(|| self.visible_var(scope, &plain))
            .is_some_and(|v| matches!(v.ty, Ty::Udt(_)))
    }

    fn declared_var(&self, scope: &Scope, name: &str) -> Option<VarInfo> {
        let as_key = format!("{name}\u{1}AS");
        let plain = self.var_key(name, &None, false);
        self.visible_var(scope, &as_key)
            .or_else(|| self.visible_var(scope, &plain))
            .cloned()
    }

    fn dynamic_property(
        &mut self,
        scope: &mut Scope,
        path: &str,
        pos: SourcePos,
    ) -> Result<Option<(HExpr, forms::PropertySpec)>, ()> {
        let Some((base, member)) = path.split_once('.') else {
            return Ok(None);
        };
        let Some(info) = self.declared_var(scope, base) else {
            return Ok(None);
        };
        let spec = match info.ty {
            Ty::Form => forms::property(ObjectClass::Form, member).map(|(_, spec)| spec),
            Ty::Control => ObjectClass::ALL
                .into_iter()
                .filter(|class| !matches!(class, ObjectClass::Form | ObjectClass::Screen))
                .find_map(|class| forms::property(class, member).map(|(_, spec)| spec)),
            _ => return Ok(None),
        };
        let Some(spec) = spec else {
            self.err(pos, format!("Unknown property '{member}'"));
            return Err(());
        };
        let base = Expr::Name {
            name: base.to_string(),
            suffix: None,
            args: None,
            pos,
        };
        let (place, _) = self.lower_place(&base, scope);
        Ok(place.map(|place| (HExpr::Load(Box::new(place)), spec)))
    }

    fn object_index(
        &mut self,
        object: u16,
        args: &Option<Vec<Expr>>,
        scope: &mut Scope,
        pos: SourcePos,
    ) -> Result<Option<HExpr>, ()> {
        let Some(args) = args else { return Ok(None) };
        let info = &self.forms.objects[object as usize];
        if !info.array {
            self.err(pos, format!("Object '{}' is not an array", info.name));
            return Err(());
        }
        if args.len() != 1 {
            self.err(pos, "Argument-count mismatch");
            return Err(());
        }
        Ok(Some(self.lower_num_as(&args[0], scope, NumTy::Lng)))
    }

    /// Standardtyp eines Namens ohne Suffix (DEFtype bzw. SINGLE).
    fn default_ty(&self, name: &str) -> Ty {
        let first = name.chars().next().unwrap_or('A');
        if first.is_ascii_alphabetic() {
            let idx = (first.to_ascii_uppercase() as u8 - b'A') as usize;
            if let Some(t) = &self.deftypes[idx] {
                return t.clone();
            }
        }
        Ty::Sng
    }

    fn name_ty(&self, name: &str, suffix: &Option<Suffix>) -> Ty {
        match suffix {
            Some(s) => suffix_ty(*s),
            None => self.default_ty(name),
        }
    }

    fn var_key(&self, name: &str, suffix: &Option<Suffix>, as_decl: bool) -> String {
        if as_decl {
            format!("{name}\u{1}AS")
        } else {
            let c = match suffix {
                Some(s) => s.as_char(),
                None => match self.default_ty(name) {
                    Ty::Int => '%',
                    Ty::Lng => '&',
                    Ty::Cur => '@',
                    Ty::Dbl => '#',
                    Ty::Str => '$',
                    _ => '!',
                },
            };
            format!("{name}{c}")
        }
    }

    // ---- Slot-Verwaltung ---------------------------------------------------

    fn alloc_global(&mut self, name: &str, ty: &Ty, array: bool) -> u16 {
        let slot = self.globals.len() as u16;
        self.globals.push(hir::HVar {
            name: name.trim_end_matches("\u{1}AS").to_string(),
            ty: self.h_ty(ty),
            array,
        });
        slot
    }

    /// Variable im gegebenen Scope anlegen (Modul → global; Prozedur →
    /// lokal, außer STATIC-Prozedur → globaler Slot mit Mangel-Namen).
    fn alloc_var(&mut self, scope: &mut Scope, name: &str, ty: &Ty, array: bool) -> (u16, bool) {
        if scope.is_module {
            (self.alloc_global(name, ty, array), true)
        } else if scope.is_static {
            (
                self.alloc_global(&format!("STATIC.{name}"), ty, array),
                true,
            )
        } else {
            let slot = scope.locals.len() as u16;
            scope.locals.push(hir::HVar {
                name: name.trim_end_matches("\u{1}AS").to_string(),
                ty: self.h_ty(ty),
                array,
            });
            (slot, false)
        }
    }

    /// Versteckter Temp-Slot (SELECT-Selektor, FOR-Grenzen, SWAP).
    fn alloc_temp(&mut self, scope: &mut Scope, ty: &Ty) -> VarSlot {
        let (slot, global) = self.alloc_var(scope, "<temp>", ty, false);
        if global {
            VarSlot::Global(slot)
        } else {
            VarSlot::Local(slot)
        }
    }

    fn vars_of<'a>(&'a self, scope: &'a Scope) -> &'a HashMap<String, VarInfo> {
        if scope.is_module {
            &self.module_vars
        } else {
            &scope.vars
        }
    }

    fn visible_var<'a>(&'a self, scope: &'a Scope, key: &str) -> Option<&'a VarInfo> {
        self.vars_of(scope).get(key).or_else(|| {
            (!scope.is_module && (scope.is_def_fn || self.shared_vars.contains(key)))
                .then(|| self.module_vars.get(key))
                .flatten()
        })
    }

    fn insert_var(&mut self, scope: &mut Scope, key: String, info: VarInfo) {
        if scope.is_module {
            self.module_vars.insert(key, info);
        } else {
            scope.vars.insert(key, info);
        }
    }

    /// Feldpfad eines UDT-Zugriffs auflösen: liefert Typ und Feldindizes.
    fn member_path(&mut self, base_ty: &Ty, path: &[&str], pos: SourcePos) -> (Ty, Vec<u16>) {
        let mut cur = base_ty.clone();
        let mut idxs = Vec::new();
        for seg in path {
            match cur.clone() {
                Ty::Udt(udt_name) => {
                    let found = self.udt_ids.get(&udt_name).and_then(|id| {
                        self.udt_defs[*id as usize]
                            .fields
                            .iter()
                            .position(|(n, _)| n == seg)
                            .map(|i| (i as u16, self.udt_defs[*id as usize].fields[i].1.clone()))
                    });
                    match found {
                        Some((i, hty)) => {
                            idxs.push(i);
                            cur = match hty {
                                HTy::Num(n) => ty_of_num(n),
                                HTy::Str => Ty::Str,
                                HTy::FixedStr(n) => Ty::FixedStr(n),
                                HTy::Udt(id) => Ty::Udt(self.udt_defs[id as usize].name.clone()),
                                HTy::Form => Ty::Form,
                                HTy::Control => Ty::Control,
                            };
                        }
                        None => {
                            self.err(pos, "Element not defined");
                            return (Ty::Unknown, idxs);
                        }
                    }
                }
                Ty::Unknown => return (Ty::Unknown, idxs),
                _ => {
                    self.err(pos, "Type mismatch");
                    return (Ty::Unknown, idxs);
                }
            }
        }
        (cur, idxs)
    }

    /// Variable nachschlagen bzw. implizit deklarieren.
    fn resolve_var(
        &mut self,
        scope: &mut Scope,
        name: &str,
        suffix: &Option<Suffix>,
        array: bool,
        pos: SourcePos,
    ) -> Option<VarInfo> {
        // AS-deklarierte Variable hat Vorrang, wenn kein Suffix angegeben
        let as_key = format!("{name}\u{1}AS");
        if suffix.is_none() {
            if let Some(v) = self.visible_var(scope, &as_key) {
                return Some(v.clone());
            }
        } else if let Some(v) = self.visible_var(scope, &as_key).cloned() {
            if v.ty == suffix_ty(suffix.unwrap()) {
                return Some(v);
            }
            self.err(pos, "Duplicate definition");
            return None;
        }
        let key = self.var_key(name, suffix, false);
        if let Some(v) = self.visible_var(scope, &key) {
            return Some(v.clone());
        }
        if self.explicit {
            self.err(pos, "Variable not defined");
            return None;
        }
        let ty = self.name_ty(name, suffix);
        // DEF FN: freie Namen binden an Modulvariablen (Vorbild-Semantik).
        let info = if scope.is_def_fn {
            let slot = self.alloc_global(&key, &ty, array);
            let info = VarInfo {
                ty,
                array,
                explicit: false,
                slot,
                global: true,
                by_ref: false,
            };
            self.module_vars.insert(key, info.clone());
            info
        } else {
            let (slot, global) = self.alloc_var(scope, &key, &ty, array);
            let info = VarInfo {
                ty,
                array,
                explicit: false,
                slot,
                global,
                by_ref: false,
            };
            self.insert_var(scope, key, info.clone());
            info
        };
        Some(info)
    }

    /// Konstantenfaltung für `CONST`-Ausdrücke.
    fn fold_const(&self, e: &Expr) -> Option<ConstVal> {
        match e {
            Expr::IntLit(v, _) => Some(ConstVal::Num(*v as f64)),
            Expr::LongLit(v, _) => Some(ConstVal::Num(*v as f64)),
            Expr::SingleLit(v, _) => Some(ConstVal::Num(*v as f64)),
            Expr::DoubleLit(v, _) => Some(ConstVal::Num(*v)),
            Expr::CurrencyLit(v, _) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
            Expr::StrLit(s, _) => Some(ConstVal::Str(s.clone())),
            Expr::Paren(e) => self.fold_const(e),
            Expr::Name {
                name, args: None, ..
            } => self.consts.get(name).and_then(|(_, v)| v.clone()),
            Expr::Unary {
                op: UnOp::Neg,
                operand,
                ..
            } => match self.fold_const(operand)? {
                ConstVal::Num(n) => Some(ConstVal::Num(-n)),
                ConstVal::Str(_) => None,
            },
            Expr::Unary {
                op: UnOp::Not,
                operand,
                ..
            } => match self.fold_const(operand)? {
                ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
                ConstVal::Str(_) => None,
            },
            Expr::Binary { op, lhs, rhs, .. } => {
                let l = self.fold_const(lhs)?;
                let r = self.fold_const(rhs)?;
                match (l, r) {
                    (ConstVal::Num(a), ConstVal::Num(b)) => {
                        let v = match op {
                            BinOp::Add => a + b,
                            BinOp::Sub => a - b,
                            BinOp::Mul => a * b,
                            BinOp::Div => a / b,
                            BinOp::Pow => a.powf(b),
                            BinOp::IntDiv => {
                                let bi = b as i64;
                                if bi == 0 {
                                    return None;
                                }
                                ((a as i64) / bi) as f64
                            }
                            BinOp::Mod => {
                                let bi = b as i64;
                                if bi == 0 {
                                    return None;
                                }
                                ((a as i64) % bi) as f64
                            }
                            _ => return None,
                        };
                        Some(ConstVal::Num(v))
                    }
                    (ConstVal::Str(a), ConstVal::Str(b)) if *op == BinOp::Add => {
                        Some(ConstVal::Str(format!("{a}{b}")))
                    }
                    _ => None,
                }
            }
            _ => None,
        }
    }

    // ---- Konvertierungen ---------------------------------------------------

    /// Numerische Konvertierung als expliziter HIR-Knoten (Matrix).
    fn conv_num(&mut self, e: HExpr, from: &Ty, to: NumTy) -> HExpr {
        let f = num_ty(from);
        if f == to {
            return e;
        }
        HExpr::Conv {
            from: f,
            to,
            arg: Box::new(e),
        }
    }

    /// Wert an Zieltyp anpassen (Zuweisung, BYVAL-Argument).
    fn coerce(&mut self, e: HExpr, from: &Ty, to: &Ty, pos: SourcePos) -> HExpr {
        if matches!(
            (from, to),
            (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
        ) {
            return e;
        }
        match (is_num(to), is_str(to)) {
            (true, _) if is_num(from) => {
                if matches!(to, Ty::Unknown) || matches!(from, Ty::Unknown) {
                    e
                } else {
                    self.conv_num(e, from, num_ty(to))
                }
            }
            (_, true) if is_str(from) => match to {
                Ty::FixedStr(n) => HExpr::FixStr {
                    len: *n,
                    arg: Box::new(e),
                },
                _ => e,
            },
            _ => {
                if !matches!(to, Ty::Udt(_)) && !matches!(from, Ty::Udt(_)) {
                    self.err(pos, "Type mismatch");
                }
                e
            }
        }
    }

    // ---- Anweisungen -------------------------------------------------------

    fn lower_body(&mut self, stmts: &[Stmt], scope: &mut Scope) -> Vec<HStmt> {
        let enclosing = (scope.current_line, scope.current_pos);
        let mut out = Vec::new();
        for stmt in stmts {
            self.lower_stmt(stmt, scope, &mut out);
        }
        (scope.current_line, scope.current_pos) = enclosing;
        out
    }

    fn push(&mut self, out: &mut Vec<HStmt>, scope: &Scope, kind: HStmtKind) {
        out.push(HStmt {
            pos: scope.current_pos,
            line: scope.current_line,
            kind,
        });
    }

    fn lower_stmt(&mut self, stmt: &Stmt, scope: &mut Scope, out: &mut Vec<HStmt>) {
        // Quellzeile für Anweisungsgrenzen/Fehlerortung mitführen.
        let pos = stmt_pos(stmt);
        if pos.line > 0 {
            scope.current_line = pos.line;
            scope.current_pos = pos;
        }
        match stmt {
            Stmt::Data { .. }
            | Stmt::Include { .. }
            | Stmt::MetaArrays { .. }
            | Stmt::MetaForm { .. } => {}
            Stmt::Label(n) => {
                if let Some(id) = scope.labels.get(n).copied() {
                    self.push(out, scope, HStmtKind::Label(id));
                }
            }
            Stmt::LineNumber(n, _) => {
                if let Some(id) = scope.line_labels.get(n).copied() {
                    self.push(out, scope, HStmtKind::Label(id));
                }
                self.push(out, scope, HStmtKind::SetErl(*n));
            }
            Stmt::End(_) => self.push(out, scope, HStmtKind::End),
            Stmt::StopStmt(_) => self.push(out, scope, HStmtKind::Stop),
            Stmt::System(_) => self.push(out, scope, HStmtKind::System),
            Stmt::Exit { kind, pos } => match kind {
                ExitKind::For => {
                    match scope
                        .loop_exits
                        .iter()
                        .rev()
                        .find(|(k, _)| *k == LoopKind::For)
                    {
                        Some((_, id)) => {
                            let id = *id;
                            self.push(out, scope, HStmtKind::Goto(id));
                        }
                        None => self.err(*pos, "EXIT not within FOR...NEXT"),
                    }
                }
                ExitKind::Do => {
                    match scope
                        .loop_exits
                        .iter()
                        .rev()
                        .find(|(k, _)| *k == LoopKind::Do)
                    {
                        Some((_, id)) => {
                            let id = *id;
                            self.push(out, scope, HStmtKind::Goto(id));
                        }
                        None => self.err(*pos, "EXIT DO not within DO...LOOP"),
                    }
                }
                ExitKind::Sub | ExitKind::Function | ExitKind::Def => {
                    self.push(out, scope, HStmtKind::ExitProc);
                }
            },

            Stmt::Assign { target, value, pos } => {
                if let Expr::Name {
                    name,
                    suffix: None,
                    args,
                    ..
                } = target
                {
                    if args.is_none() && !name.contains('.') && !name.contains('!') {
                        if let Some((object, property, spec)) =
                            self.implicit_form_property(scope, name)
                        {
                            if !spec.writable {
                                self.err(
                                    *pos,
                                    format!("Property '{}' is read-only at run time", spec.name),
                                );
                                self.lower_expr(value, scope);
                                return;
                            }
                            let (value, actual) = self.lower_expr(value, scope);
                            let expected = Self::property_ty(spec);
                            self.check_assign(&expected, &actual, *pos);
                            let value = self.coerce_silent(value, &actual, &expected);
                            self.push(
                                out,
                                scope,
                                HStmtKind::SetObjectProperty {
                                    object,
                                    index: None,
                                    property,
                                    value,
                                },
                            );
                            return;
                        }
                    }
                    if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name)
                    {
                        if name.contains('!') && !name.contains('.') {
                            let object_name = name.split_once('!').unwrap().1;
                            self.err(
                                *pos,
                                format!("Object '{object_name}' has no default property"),
                            );
                            self.lower_expr(value, scope);
                            return;
                        }
                        if args.is_none() {
                            match self.dynamic_property(scope, name, *pos) {
                                Ok(Some((object_expr, spec))) => {
                                    if !spec.writable {
                                        self.err(
                                            *pos,
                                            format!(
                                                "Property '{}' is read-only at run time",
                                                spec.name
                                            ),
                                        );
                                        self.lower_expr(value, scope);
                                        return;
                                    }
                                    let (value, actual) = self.lower_expr(value, scope);
                                    let expected = Self::property_ty(spec);
                                    if !((is_num(&expected) && is_num(&actual))
                                        || (is_str(&expected) && is_str(&actual)))
                                    {
                                        self.err(
                                            *pos,
                                            format!("Type mismatch for property '{}'", spec.name),
                                        );
                                    }
                                    let value = self.coerce_silent(value, &actual, &expected);
                                    self.push(
                                        out,
                                        scope,
                                        HStmtKind::SetDynamicObjectProperty {
                                            object: object_expr,
                                            property: spec.name.to_string(),
                                            value,
                                        },
                                    );
                                    return;
                                }
                                Err(()) => {
                                    self.lower_expr(value, scope);
                                    return;
                                }
                                Ok(None) => {}
                            }
                        }
                        if let Some((object, property, spec, _)) = self.object_property(name, *pos)
                        {
                            if spec.ty == PropertyType::IntegerArray {
                                let Some(indices) = args else {
                                    self.err(*pos, "Argument-count mismatch");
                                    self.lower_expr(value, scope);
                                    return;
                                };
                                let expected = if self.forms.objects[object as usize].array {
                                    2
                                } else {
                                    1
                                };
                                if indices.len() != expected {
                                    self.err(*pos, "Argument-count mismatch");
                                    self.lower_expr(value, scope);
                                    return;
                                }
                                let object_index = self.forms.objects[object as usize]
                                    .array
                                    .then(|| self.lower_num_as(&indices[0], scope, NumTy::Lng));
                                let index =
                                    self.lower_num_as(&indices[expected - 1], scope, NumTy::Lng);
                                let value = self.lower_num_as(value, scope, NumTy::Lng);
                                self.push(
                                    out,
                                    scope,
                                    HStmtKind::SetObjectIndexedProperty {
                                        object,
                                        object_index,
                                        property,
                                        index,
                                        value,
                                    },
                                );
                                return;
                            }
                            let Ok(index) = self.object_index(object, args, scope, *pos) else {
                                self.lower_expr(value, scope);
                                return;
                            };
                            if !spec.writable {
                                self.err(
                                    *pos,
                                    format!("Property '{}' is read-only at run time", spec.name),
                                );
                                self.lower_expr(value, scope);
                                return;
                            }
                            let (value, actual) = self.lower_expr(value, scope);
                            let expected = Self::property_ty(spec);
                            if !((is_num(&expected) && is_num(&actual))
                                || (is_str(&expected) && is_str(&actual)))
                            {
                                self.err(
                                    *pos,
                                    format!("Type mismatch for property '{}'", spec.name),
                                );
                            }
                            let value = self.coerce_silent(value, &actual, &expected);
                            self.push(
                                out,
                                scope,
                                HStmtKind::SetObjectProperty {
                                    object,
                                    index,
                                    property,
                                    value,
                                },
                            );
                        }
                        return;
                    }
                    if self.find_object(name).is_some() {
                        self.err(*pos, format!("Object '{name}' has no default property"));
                        self.lower_expr(value, scope);
                        return;
                    }
                }
                // MID$-Anweisung: MID$(s$, start [, laenge]) = ausdruck
                if let Expr::Name {
                    name,
                    suffix: Some(Suffix::Str),
                    args: Some(args),
                    ..
                } = target
                {
                    if name == "MID" {
                        if args.is_empty() || args.len() > 3 {
                            self.err(*pos, "Argument-count mismatch");
                        }
                        let mut place = None;
                        if let Some(sv) = args.first() {
                            let (p, t) = self.lower_place(sv, scope);
                            if !is_str(&t) {
                                self.err(sv.pos(), "Type mismatch");
                            }
                            place = p;
                        }
                        let start = args.get(1).map(|a| self.lower_num_as(a, scope, NumTy::Lng));
                        let len = args.get(2).map(|a| self.lower_num_as(a, scope, NumTy::Lng));
                        let (ve, vt) = self.lower_expr(value, scope);
                        if !is_str(&vt) {
                            self.err(value.pos(), "Type mismatch");
                        }
                        if let Some(place) = place {
                            let cur = HExpr::Load(Box::new(place.clone()));
                            let call = HExpr::Builtin {
                                b: Builtin::MidAssign,
                                args: vec![
                                    cur,
                                    start.unwrap_or(HExpr::Lng(1)),
                                    len.unwrap_or(HExpr::Lng(-1)),
                                    ve,
                                ],
                                ret: HTy::Str,
                            };
                            self.push(out, scope, HStmtKind::Assign { place, value: call });
                        }
                        return;
                    }
                }
                // `DATE$ = "..."` / `TIME$ = "..."` sind Anweisungen, keine
                // Zuweisungen an eine Variable.
                // `ERR = n` setzt den Fehlercode, ohne einen Fehler
                // auszulösen (Anweisungsform von ERR).
                if let Expr::Name {
                    name,
                    suffix: None,
                    args: None,
                    ..
                } = target
                {
                    if name == "ERR" {
                        let (e, t) = self.want_num(value, scope);
                        let e = self.conv_num(e, &t, NumTy::Lng);
                        self.push(out, scope, HStmtKind::SetErr(e));
                        return;
                    }
                }
                if let Expr::Name {
                    name,
                    suffix: Some(Suffix::Str),
                    args: None,
                    ..
                } = target
                {
                    if name == "DATE" || name == "TIME" {
                        let (ve, _) = self.want_str(value, scope);
                        let bt = if name == "DATE" {
                            Builtin::DateSet
                        } else {
                            Builtin::TimeSet
                        };
                        self.push(
                            out,
                            scope,
                            HStmtKind::BuiltinStmt {
                                b: bt,
                                args: vec![ve],
                            },
                        );
                        return;
                    }
                }
                let (place, tt) = self.lower_place(target, scope);
                let (ve, vt) = self.lower_expr(value, scope);
                self.check_assign(&tt, &vt, *pos);
                if let Some(place) = place {
                    let value = self.coerce_silent(ve, &vt, &tt);
                    self.push(out, scope, HStmtKind::Assign { place, value });
                }
            }
            Stmt::Print {
                printer,
                file,
                using,
                items,
                ..
            } => {
                if let Some(f) = file {
                    self.want_num(f, scope);
                }
                if let Some(u) = using {
                    self.want_str(u, scope);
                }
                let mut hitems = Vec::new();
                let mut trailing = false;
                for (i, item) in items.iter().enumerate() {
                    let last = i + 1 == items.len();
                    match item {
                        PrintItem::Expr(e) => {
                            trailing = false;
                            // TAB(n)/SPC(n) sind Positionssteuerungen.
                            if let Expr::Name {
                                name,
                                suffix: None,
                                args: Some(a),
                                ..
                            } = e
                            {
                                if (name == "TAB" || name == "SPC") && a.len() == 1 {
                                    let n = self.lower_num_as(&a[0], scope, NumTy::Lng);
                                    hitems.push(if name == "TAB" {
                                        HPrintItem::Tab(n)
                                    } else {
                                        HPrintItem::Spc(n)
                                    });
                                    // TAB/SPC wirken wie `;` danach
                                    trailing = last;
                                    continue;
                                }
                            }
                            let (he, _t) = self.lower_expr(e, scope);
                            hitems.push(HPrintItem::Val(he));
                        }
                        PrintItem::Comma => {
                            hitems.push(HPrintItem::Comma);
                            trailing = last;
                        }
                        PrintItem::Semicolon => {
                            trailing = last;
                        }
                    }
                }
                // `PRINT #n` und `LPRINT` schreiben dieselben Elemente an ein
                // anderes Ziel; danach steht der Bildschirm wieder.
                let ziel = if *printer {
                    Some(HExpr::Lng(-2))
                } else if let Some(f) = file {
                    let (e, t) = self.want_num(f, scope);
                    Some(self.conv_num(e, &t, NumTy::Lng))
                } else {
                    None
                };
                if let Some(z) = ziel.clone() {
                    self.push_ziel(out, scope, z);
                }
                if let Some(u) = using {
                    // `PRINT USING fmt$; a; b` — die Semikolons trennen nur
                    // die Werte; Druckzonen gibt es hier nicht.
                    let (fe, _) = self.want_str(u, scope);
                    let mut args = vec![fe];
                    for it in &hitems {
                        match it {
                            HPrintItem::Val(e) => args.push(e.clone()),
                            _ => self.err(pos, "Illegal function call: PRINT USING"),
                        }
                    }
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::PrintUsing,
                            args,
                        },
                    );
                    if !trailing {
                        self.push(
                            out,
                            scope,
                            HStmtKind::BuiltinStmt {
                                b: Builtin::PrintNewline,
                                args: vec![],
                            },
                        );
                    }
                } else {
                    self.push(
                        out,
                        scope,
                        HStmtKind::Print {
                            items: hitems,
                            trailing,
                        },
                    );
                }
                if ziel.is_some() {
                    self.push_ziel(out, scope, HExpr::Lng(-1));
                }
            }
            Stmt::Input {
                line,
                file,
                keep_cursor: _,
                prompt,
                vars,
                ..
            } => {
                let datei = file.as_ref().map(|f| {
                    let (e, t) = self.want_num(f, scope);
                    self.conv_num(e, &t, NumTy::Lng)
                });
                let mut targets = Vec::new();
                for v in vars {
                    if let (Some(p), _) = self.lower_place(v, scope) {
                        targets.push(p);
                    }
                }
                // `INPUT #n` kennt weder Eingabeaufforderung noch Fragezeichen.
                let (ptext, question) = match (&datei, prompt) {
                    (Some(_), _) => (None, false),
                    (None, Some((t, q))) => (Some(t.clone()), *q),
                    (None, None) => (None, true),
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::Input {
                        file: datei,
                        line_mode: *line,
                        prompt: ptext,
                        question,
                        targets,
                    },
                );
            }
            Stmt::If {
                cond,
                then_body,
                elseifs,
                else_body,
                ..
            } => {
                let c = self.lower_cond(cond, scope);
                let then = self.lower_body(then_body, scope);
                // ELSEIF-Kette zu verschachteltem If absenken (von hinten).
                let mut els = match else_body {
                    Some(b) => self.lower_body(b, scope),
                    None => Vec::new(),
                };
                for (ec, eb, elseif_pos) in elseifs.iter().rev() {
                    let c2 = self.lower_cond(ec, scope);
                    let body2 = self.lower_body(eb, scope);
                    let stmt = HStmt {
                        pos: *elseif_pos,
                        line: elseif_pos.line,
                        kind: HStmtKind::If {
                            cond: c2,
                            then: body2,
                            els,
                        },
                    };
                    els = vec![stmt];
                }
                self.push(out, scope, HStmtKind::If { cond: c, then, els });
            }
            Stmt::Select { expr, arms, .. } => {
                self.lower_select(expr, arms, scope, out);
            }
            Stmt::For {
                var,
                from,
                to,
                step,
                body,
                pos,
                end_pos,
            } => {
                let (place, vt) = self.lower_place(var, scope);
                if !is_num(&vt) {
                    self.err(*pos, "Type mismatch");
                }
                let ty = num_ty(&vt);
                let (fe, ft) = self.lower_expr_num(from, scope);
                let (te, tt2) = self.lower_expr_num(to, scope);
                let from_e = self.conv_num(fe, &ft, ty);
                let to_e = self.conv_num(te, &tt2, ty);
                let step_e = step.as_ref().map(|s| {
                    let (se, st) = self.lower_expr_num(s, scope);
                    self.conv_num(se, &st, ty)
                });
                let limit_slot = self.alloc_temp(scope, &ty_of_num(ty));
                // Dynamischer STEP braucht einen Slot; konstante Literale
                // bettet der Codegen direkt ein.
                let step_slot = match &step_e {
                    Some(e) if literal_value(e).is_none() => {
                        Some(self.alloc_temp(scope, &ty_of_num(ty)))
                    }
                    _ => None,
                };
                let exit_label = scope.new_label();
                scope.loop_exits.push((LoopKind::For, exit_label));
                let hbody = self.lower_body(body, scope);
                scope.loop_exits.pop();
                if let Some(place) = place {
                    self.push(
                        out,
                        scope,
                        HStmtKind::For {
                            var: place,
                            ty,
                            from: from_e,
                            to: to_e,
                            end_pos: *end_pos,
                            step: step_e,
                            limit_slot,
                            step_slot,
                            body: hbody,
                            exit_label,
                        },
                    );
                }
            }
            Stmt::DoLoop {
                pre,
                post,
                body,
                end_pos,
                ..
            } => {
                let pre_c = pre.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
                let exit_label = scope.new_label();
                scope.loop_exits.push((LoopKind::Do, exit_label));
                let hbody = self.lower_body(body, scope);
                scope.loop_exits.pop();
                let post_c = post.as_ref().map(|(u, c)| (*u, self.lower_cond(c, scope)));
                self.push(
                    out,
                    scope,
                    HStmtKind::Loop {
                        end_pos: *end_pos,
                        pre: pre_c,
                        post: post_c,
                        body: hbody,
                        exit_label,
                    },
                );
            }
            Stmt::While {
                cond,
                body,
                end_pos,
                ..
            } => {
                let c = self.lower_cond(cond, scope);
                let exit_label = scope.new_label();
                let hbody = self.lower_body(body, scope);
                self.push(
                    out,
                    scope,
                    HStmtKind::Loop {
                        end_pos: *end_pos,
                        pre: Some((false, c)),
                        post: None,
                        body: hbody,
                        exit_label,
                    },
                );
            }
            Stmt::Goto { target, pos } => {
                if let Some(id) = self.label_id(target, scope, *pos) {
                    self.push(out, scope, HStmtKind::Goto(id));
                }
            }
            Stmt::Gosub { target, pos } => {
                if let Some(id) = self.label_id(target, scope, *pos) {
                    self.push(out, scope, HStmtKind::Gosub(id));
                }
            }
            Stmt::OnGoto {
                expr,
                targets,
                gosub,
                pos,
            } => {
                let sel = self.lower_num_as(expr, scope, NumTy::Int);
                let mut ids = Vec::new();
                for t in targets {
                    if let Some(id) = self.label_id(t, scope, *pos) {
                        ids.push(id);
                    }
                }
                self.push(
                    out,
                    scope,
                    HStmtKind::OnGoto {
                        sel,
                        gosub: *gosub,
                        targets: ids,
                    },
                );
            }
            Stmt::Return { target, pos } => {
                let id = match target {
                    Some(t) => match self.label_id(t, scope, *pos) {
                        Some(id) => Some(id),
                        None => return,
                    },
                    None => None,
                };
                self.push(out, scope, HStmtKind::ReturnGosub(id));
            }
            Stmt::OnError { local, action, pos } => {
                // `ON ERROR GOTO label` ohne `LOCAL` setzt den modulweiten
                // Handler; sein Sprungziel liegt im Modulrumpf, auch wenn die
                // Anweisung in einer Prozedur steht. Nur `ON LOCAL ERROR`
                // verlangt ein Label im eigenen Rumpf.
                let local = *local;
                match action {
                    OnErrorAction::Goto(t) => {
                        let id = if local || scope.is_module {
                            self.label_id(t, scope, *pos)
                        } else {
                            self.module_label_id(t, *pos)
                        };
                        if let Some(id) = id {
                            self.push(
                                out,
                                scope,
                                HStmtKind::OnError {
                                    local,
                                    target: Some(id),
                                },
                            );
                        }
                    }
                    OnErrorAction::Disable => {
                        self.push(
                            out,
                            scope,
                            HStmtKind::OnError {
                                local,
                                target: None,
                            },
                        );
                    }
                    OnErrorAction::ResumeNext => {
                        self.push(out, scope, HStmtKind::OnErrorResumeNext { local });
                    }
                }
            }
            Stmt::Resume { kind, pos } => {
                let k = match kind {
                    ResumeKind::Retry => hir::HResume::Retry,
                    ResumeKind::Next => hir::HResume::Next,
                    ResumeKind::Label(t) => match self.label_id(t, scope, *pos) {
                        Some(id) => hir::HResume::Label(id),
                        None => return,
                    },
                };
                self.push(out, scope, HStmtKind::Resume(k));
            }
            Stmt::ErrorStmt { code, .. } => {
                let c = self.lower_num_as(code, scope, NumTy::Int);
                self.push(out, scope, HStmtKind::RaiseError(c));
            }
            Stmt::Dim {
                shared,
                redim,
                decls,
                ..
            } => {
                for d in decls {
                    if *shared && scope.is_module {
                        self.shared_vars.insert(self.var_key(
                            &d.name,
                            &d.suffix,
                            d.as_type.is_some(),
                        ));
                    }
                    self.declare(d, *redim, scope, out);
                }
            }
            Stmt::StaticDecl { decls, .. } => {
                // STATIC-Variablen leben in globalen Slots (eine Instanz).
                for d in decls {
                    let ty = self.decl_ty(d);
                    let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
                    if let Entry::Vacant(entry) = scope.vars.entry(key) {
                        let slot = self.alloc_global(
                            &format!("STATIC.{}", entry.key()),
                            &ty,
                            d.dims.is_some(),
                        );
                        entry.insert(VarInfo {
                            ty,
                            array: d.dims.is_some(),
                            explicit: true,
                            slot,
                            global: true,
                            by_ref: false,
                        });
                    }
                }
            }
            Stmt::CommonDecl {
                shared,
                decls,
                block,
                ..
            } => {
                for d in decls {
                    if *shared && scope.is_module {
                        self.shared_vars.insert(self.var_key(
                            &d.name,
                            &d.suffix,
                            d.as_type.is_some(),
                        ));
                    }
                    self.declare(d, false, scope, out);
                    if scope.is_module {
                        let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
                        let slot = self.module_vars[&key].slot;
                        let dims = d.dims.as_ref().map(|dims| {
                            dims.iter()
                                .map(|(lo, hi)| {
                                    let lower =
                                        lo.as_ref().map_or(Some(self.option_base as i32), |e| {
                                            self.common_bound(e)
                                        });
                                    let upper = self.common_bound(hi);
                                    (lower, upper)
                                })
                                .collect()
                        });
                        self.commons.push(hir::HCommon {
                            slot,
                            block: block.clone(),
                            key,
                            ty: self.globals[slot as usize].ty.clone(),
                            dims,
                            pos: d.pos,
                        });
                    }
                }
            }
            Stmt::SharedDecl { decls, .. } => {
                // Zugriff auf Modulvariablen aus einer Prozedur heraus.
                for d in decls {
                    let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
                    let info = match self.module_vars.get(&key) {
                        Some(v) => v.clone(),
                        None => {
                            let ty = self.decl_ty(d);
                            let slot = self.alloc_global(&key, &ty, d.dims.is_some());
                            let v = VarInfo {
                                ty,
                                array: d.dims.is_some(),
                                explicit: true,
                                slot,
                                global: true,
                                by_ref: false,
                            };
                            self.module_vars.insert(key.clone(), v.clone());
                            v
                        }
                    };
                    scope.vars.insert(key, info);
                }
            }
            Stmt::Erase { names, .. } => {
                let mut slots = Vec::new();
                for n in names {
                    if let Expr::Name {
                        name, suffix, pos, ..
                    } = n
                    {
                        if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) {
                            slots.push(if v.global {
                                VarSlot::Global(v.slot)
                            } else {
                                VarSlot::Local(v.slot)
                            });
                        }
                    }
                }
                self.push(out, scope, HStmtKind::Erase(slots));
            }
            Stmt::ConstDecl { items, pos } => {
                for (name, suffix, value) in items {
                    let ty = match suffix {
                        Some(s) => suffix_ty(*s),
                        None => match self.fold_const(value) {
                            Some(ConstVal::Str(_)) => Ty::Str,
                            _ => Ty::Dbl,
                        },
                    };
                    let folded = self.fold_const(value);
                    if folded.is_none() {
                        self.err(*pos, "Invalid constant");
                    }
                    if self.consts.insert(name.clone(), (ty, folded)).is_some() {
                        self.err(*pos, "Duplicate definition");
                    }
                }
            }
            Stmt::DefType { ty, ranges, .. } => {
                for (a, b) in ranges {
                    let (a, b) = (a.to_ascii_uppercase(), b.to_ascii_uppercase());
                    for c in a..=b {
                        if c.is_ascii_uppercase() {
                            self.deftypes[(c as u8 - b'A') as usize] = Some(type_name_ty(ty));
                        }
                    }
                }
            }
            Stmt::OptionStmt { kind, .. } => match kind {
                OptionKind::Explicit => self.explicit = true,
                OptionKind::Base(b) => self.option_base = *b,
            },
            Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
            Stmt::Declare { sig, pos } => {
                if let Some(r) = &mut self.references {
                    r.procedures.push((*pos, sig.name.clone()));
                }
            } // in Pass 1 registriert
            Stmt::Call {
                name, args, pos, ..
            } => {
                self.lower_call_stmt(name, args, scope, *pos, out);
            }
            Stmt::ReadStmt { vars, .. } => {
                let mut places = Vec::new();
                for v in vars {
                    if let (Some(p), _) = self.lower_place(v, scope) {
                        places.push(p);
                    }
                }
                self.push(out, scope, HStmtKind::Read(places));
            }
            Stmt::Restore { target, pos } => {
                let idx = match target {
                    None => 0,
                    Some(t) => {
                        self.check_label_exists(t, scope, *pos);
                        match t {
                            LabelRef::Name(n) => self.data_marks_name.get(n).copied().unwrap_or(0),
                            LabelRef::Line(n) => self.data_marks_line.get(n).copied().unwrap_or(0),
                        }
                    }
                };
                self.push(out, scope, HStmtKind::Restore(idx));
            }
            Stmt::DefFn {
                name,
                suffix,
                params,
                body,
                pos,
            } => {
                self.lower_def_fn(name, suffix, params, None, Some(body), *pos);
            }
            Stmt::DefFnBlock {
                name,
                suffix,
                params,
                body,
                pos,
            } => {
                self.lower_def_fn(name, suffix, params, Some(body), None, *pos);
            }
            // ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
            Stmt::Open {
                file,
                mode,
                isam,
                number,
                len,
                pos,
                ..
            } => {
                let (fe, _) = self.want_str(file, scope);
                self.reject_com_device(file, *pos);
                let (ne, nt) = self.want_num(number, scope);
                let ne = self.conv_num(ne, &nt, NumTy::Lng);
                let le = match len {
                    Some(l) => {
                        let (e, t) = self.want_num(l, scope);
                        self.conv_num(e, &t, NumTy::Lng)
                    }
                    // Vorgabe-Recordlänge des Vorbilds.
                    None => HExpr::Lng(128),
                };
                if let Some((ty_name, table)) = isam {
                    let Some(&udt) = self.udt_ids.get(ty_name) else {
                        self.err(*pos, "Type not defined");
                        return;
                    };
                    // Die Laufzeit braucht die Spaltennamen; `UdtLayout`
                    // führt nur die Feldtypen. Sie reisen deshalb als
                    // Stringargument mit — ohne Änderung am `.tbc`-Format.
                    let spalten = self.udt_defs[udt as usize]
                        .fields
                        .iter()
                        .map(|(n, _)| n.clone())
                        .collect::<Vec<_>>()
                        .join(",");
                    // Für die Satztypprüfung (Aufgabe 2.3) merken, welcher
                    // Typ an dieser Dateinummer hängt — nur bei literaler
                    // Nummer, sonst ist sie erst zur Laufzeit bekannt.
                    if let Some(ConstVal::Num(n)) = self.fold_const(number) {
                        self.isam_typ.insert(n as i32, ty_name.clone());
                    }
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::IsamOpen,
                            args: vec![
                                fe,
                                ne,
                                HExpr::Str(table.clone()),
                                HExpr::Str(spalten),
                                HExpr::UdtId(udt),
                            ],
                        },
                    );
                    return;
                }
                // Ohne `FOR`-Klausel gilt RANDOM (Vorbild).
                let m = match mode {
                    Some(OpenMode::Input) => "I",
                    Some(OpenMode::Output) => "O",
                    Some(OpenMode::Append) => "A",
                    Some(OpenMode::Binary) => "B",
                    _ => "R",
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::Open,
                        args: vec![fe, ne, HExpr::Str(m.to_string()), le],
                    },
                );
            }
            Stmt::OpenLegacy {
                mode,
                number,
                file,
                len,
                pos,
                ..
            } => {
                let (me, _) = self.want_str(mode, scope);
                let (ne, nt) = self.want_num(number, scope);
                let ne = self.conv_num(ne, &nt, NumTy::Lng);
                let (fe, _) = self.want_str(file, scope);
                self.reject_com_device(file, *pos);
                let le = match len {
                    Some(l) => {
                        let (e, t) = self.want_num(l, scope);
                        self.conv_num(e, &t, NumTy::Lng)
                    }
                    None => HExpr::Lng(128),
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::Open,
                        args: vec![fe, ne, me, le],
                    },
                );
            }
            Stmt::CloseStmt { files, .. } => {
                if files.is_empty() {
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::CloseAll,
                            args: vec![],
                        },
                    );
                }
                for f in files {
                    let (e, t) = self.want_num(f, scope);
                    let e = self.conv_num(e, &t, NumTy::Lng);
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::Close,
                            args: vec![e],
                        },
                    );
                }
            }
            Stmt::FieldStmt { file, fields, .. } => {
                let (fe, ft) = self.want_num(file, scope);
                let fe = self.conv_num(fe, &ft, NumTy::Lng);
                let mut hf = Vec::new();
                for (width, var) in fields {
                    let (we, wt) = self.want_num(width, scope);
                    let we = self.conv_num(we, &wt, NumTy::Lng);
                    let (place, t) = self.lower_place(var, scope);
                    if !is_str(&t) && t != Ty::Unknown {
                        self.err(var.pos(), "Type mismatch");
                    }
                    if let Some(p) = place {
                        hf.push((we, p));
                    }
                }
                self.push(
                    out,
                    scope,
                    HStmtKind::Field {
                        file: fe,
                        fields: hf,
                    },
                );
            }
            Stmt::GetPut {
                put,
                file,
                recnum,
                var,
                ..
            } => {
                let (fe, ft) = self.want_num(file, scope);
                let fe = self.conv_num(fe, &ft, NumTy::Lng);
                let re = recnum.as_ref().map(|r| {
                    let (e, t) = self.want_num(r, scope);
                    self.conv_num(e, &t, NumTy::Lng)
                });
                let ve = var.as_ref().and_then(|v| self.lower_place(v, scope).0);
                self.push(
                    out,
                    scope,
                    HStmtKind::GetPut {
                        put: *put,
                        file: fe,
                        recnum: re,
                        var: ve,
                    },
                );
            }
            Stmt::LsetRset {
                rset,
                target,
                value,
                pos,
            } => {
                let (_, tt) = self.lower_place(target, scope);
                let (_, vt) = self.lower_expr(value, scope);
                let ok = (is_str(&tt) && is_str(&vt))
                    || matches!((&tt, &vt), (Ty::Udt(a), Ty::Udt(b)) if a == b)
                    || tt == Ty::Unknown
                    || vt == Ty::Unknown;
                if !ok {
                    self.err(*pos, "Type mismatch");
                }
                let (place, _) = self.lower_place(target, scope);
                let (ve, _) = self.lower_expr(value, scope);
                if let Some(place) = place {
                    self.push(
                        out,
                        scope,
                        HStmtKind::LsetRset {
                            rset: *rset,
                            target: place,
                            value: ve,
                        },
                    );
                }
            }
            Stmt::WriteStmt { file, items, .. } => {
                if let Some(f) = file {
                    self.want_num(f, scope);
                }
                for e in items {
                    self.lower_expr(e, scope);
                }
                let mut args = Vec::new();
                for it in items {
                    args.push(self.lower_expr(it, scope).0);
                }
                if let Some(f) = file {
                    let (e, t) = self.want_num(f, scope);
                    let e = self.conv_num(e, &t, NumTy::Lng);
                    self.push_ziel(out, scope, e);
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::WriteFile,
                            args,
                        },
                    );
                    self.push_ziel(out, scope, HExpr::Lng(-1));
                } else {
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::WriteFile,
                            args,
                        },
                    );
                }
            }
            Stmt::SeekStmt { file, position, .. } => {
                self.want_num(file, scope);
                self.want_num(position, scope);
                let (fe, ft) = self.want_num(file, scope);
                let fe = self.conv_num(fe, &ft, NumTy::Lng);
                let (pe, pt) = self.want_num(position, scope);
                let pe = self.conv_num(pe, &pt, NumTy::Lng);
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::SeekStmt,
                        args: vec![fe, pe],
                    },
                );
            }
            Stmt::LockStmt { file, from, to, .. } => {
                self.want_num(file, scope);
                if let Some(f) = from {
                    self.want_num(f, scope);
                }
                if let Some(t) = to {
                    self.want_num(t, scope);
                }
                let (fe, ft) = self.want_num(file, scope);
                let fe = self.conv_num(fe, &ft, NumTy::Lng);
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::LockStmt,
                        args: vec![fe],
                    },
                );
            }
            Stmt::NameStmt { old, new, .. } => {
                self.want_str(old, scope);
                self.want_str(new, scope);
                let (a, _) = self.want_str(old, scope);
                let (b2, _) = self.want_str(new, scope);
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::NameStmt,
                        args: vec![a, b2],
                    },
                );
            }
            // ---- Bildschirm/Ereignisse ----
            Stmt::ViewPrint { top, bottom, .. } => {
                if let Some(t) = top {
                    self.want_num(t, scope);
                }
                if let Some(b) = bottom {
                    self.want_num(b, scope);
                }
                let a = match (top, bottom) {
                    (Some(t), Some(b)) => {
                        let t = self.want_num(t, scope);
                        let b = self.want_num(b, scope);
                        vec![
                            self.conv_num(t.0, &t.1, NumTy::Lng),
                            self.conv_num(b.0, &b.1, NumTy::Lng),
                        ]
                    }
                    _ => vec![HExpr::Lng(-1), HExpr::Lng(-1)],
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::ViewPrint,
                        args: a,
                    },
                );
            }
            Stmt::GraphicsLine {
                from,
                to,
                relative,
                color,
                fill,
                ..
            } => {
                let mut args = Vec::new();
                if let Some((x, y)) = from {
                    for expr in [x, y] {
                        let (value, ty) = self.want_num(expr, scope);
                        args.push(self.conv_num(value, &ty, NumTy::Lng));
                    }
                } else {
                    args.extend([HExpr::Lng(i32::MIN), HExpr::Lng(i32::MIN)]);
                }
                for expr in [&to.0, &to.1] {
                    let (value, ty) = self.want_num(expr, scope);
                    args.push(self.conv_num(value, &ty, NumTy::Lng));
                }
                let color = color.as_ref().map_or(HExpr::Lng(-1), |expr| {
                    let (value, ty) = self.want_num(expr, scope);
                    self.conv_num(value, &ty, NumTy::Lng)
                });
                args.extend([
                    color,
                    HExpr::Lng(i32::from(*relative)),
                    HExpr::Lng(i32::from(*fill)),
                ]);
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::GraphicsLine,
                        args,
                    },
                );
            }
            Stmt::GraphicsPaint {
                point,
                paint,
                border,
                ..
            } => {
                let mut args = Vec::new();
                for expr in [&point.0, &point.1] {
                    let (value, ty) = self.want_num(expr, scope);
                    args.push(self.conv_num(value, &ty, NumTy::Lng));
                }
                args.push(
                    paint
                        .as_ref()
                        .map_or(HExpr::Lng(-1), |expr| self.lower_expr(expr, scope).0),
                );
                args.push(border.as_ref().map_or(HExpr::Lng(-1), |expr| {
                    let (value, ty) = self.want_num(expr, scope);
                    self.conv_num(value, &ty, NumTy::Lng)
                }));
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::GraphicsPaint,
                        args,
                    },
                );
            }
            Stmt::GraphicsView {
                rect, fill, border, ..
            } => {
                let mut args = Vec::new();
                if let Some((x1, y1, x2, y2)) = rect {
                    for expr in [x1, y1, x2, y2] {
                        let (value, ty) = self.want_num(expr, scope);
                        args.push(self.conv_num(value, &ty, NumTy::Lng));
                    }
                } else {
                    args.extend([
                        HExpr::Lng(-1),
                        HExpr::Lng(-1),
                        HExpr::Lng(-1),
                        HExpr::Lng(-1),
                    ]);
                }
                for expr in [fill.as_ref(), border.as_ref()] {
                    args.push(expr.map_or(HExpr::Lng(-1), |expr| {
                        let (value, ty) = self.want_num(expr, scope);
                        self.conv_num(value, &ty, NumTy::Lng)
                    }));
                }
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::GraphicsView,
                        args,
                    },
                );
            }
            Stmt::TrapDef {
                device,
                index,
                target,
                pos,
            } => {
                let Some(art) = trap_art(device) else {
                    // COM/PEN/PLAY/STRIG sind Non-Feature — namentlich.
                    self.err(*pos, format!("Feature unavailable: {device}"));
                    return;
                };
                // Wertebereich prüfen, soweit die Kennung konstant ist.
                if let Some(i) = index {
                    if let Some(n) = const_zahl(i) {
                        if !trap_bereich_ok(art, n) {
                            self.err(*pos, trap_bereich_text(art));
                        }
                    }
                } else if art != TRAP_UEVENT {
                    self.err(*pos, trap_bereich_text(art));
                }
                if index.is_some() && art == TRAP_UEVENT {
                    self.err(*pos, "ON UEVENT nimmt keine Kennung");
                }
                let hindex = match index {
                    Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
                    None => HExpr::Lng(0),
                };
                // `GOSUB 0` schaltet den Trap ab und meint nicht Zeile 0.
                let ziel = if matches!(target, LabelRef::Line(0)) {
                    None
                } else {
                    match self.label_id(target, scope, *pos) {
                        Some(id) => Some(id),
                        None => return,
                    }
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::TrapDef {
                        art,
                        index: hindex,
                        ziel,
                    },
                );
            }
            Stmt::EventControl {
                device,
                index,
                action,
                pos,
            } => {
                match device.as_str() {
                    "TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
                    _ => self.err(*pos, format!("Feature unavailable: {device}")),
                }
                if let Some(i) = index {
                    self.want_num(i, scope);
                }
                // `KEY ON`/`KEY OFF` ohne Index blendet die Softkey-Zeile
                // ein bzw. aus — das ist keine Ereignissteuerung
                // (die hieße `KEY(n) ON`).
                if device == "KEY" && index.is_none() && *action != EventAction::Stop {
                    let ein = i32::from(*action == EventAction::On);
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::KeyDisplay,
                            args: vec![HExpr::Lng(ein)],
                        },
                    );
                    return;
                }
                if device == "EVENT" {
                    // Das Vorbild kennt nur ON und OFF — kein STOP.
                    if *action == EventAction::Stop {
                        self.err(*pos, "EVENT kennt nur ON und OFF");
                        return;
                    }
                    let an = *action == EventAction::On;
                    self.push(out, scope, HStmtKind::EventSwitch(an));
                    return;
                }
                let Some(art) = trap_art(device) else {
                    self.err(*pos, format!("Feature unavailable: {device}"));
                    return;
                };
                if let Some(i) = index {
                    if let Some(n) = const_zahl(i) {
                        if !trap_bereich_ok(art, n) {
                            self.err(*pos, trap_bereich_text(art));
                        }
                    }
                }
                let hindex = match index {
                    Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
                    None => HExpr::Lng(0),
                };
                let zustand = match action {
                    EventAction::On => 0,
                    EventAction::Off => 1,
                    EventAction::Stop => 2,
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::TrapSet {
                        art,
                        index: hindex,
                        zustand,
                    },
                );
            }
        }
    }

    fn decl_ty(&mut self, d: &VarDecl) -> Ty {
        if let Some(t) = &d.as_type {
            if let TypeName::Udt(n) = t {
                if !self.udt_ids.contains_key(n) {
                    self.err(d.pos, "Type not defined");
                }
            }
            type_name_ty(t)
        } else {
            self.name_ty(&d.name, &d.suffix)
        }
    }

    fn lower_select(
        &mut self,
        expr: &Expr,
        arms: &[CaseArm],
        scope: &mut Scope,
        out: &mut Vec<HStmt>,
    ) {
        let (se, st) = self.lower_expr(expr, scope);
        // Selektor einmal auswerten (versteckter Temp-Slot).
        let sel_ty = if st == Ty::Unknown {
            Ty::Sng
        } else {
            st.clone()
        };
        let temp = self.alloc_temp(scope, &sel_ty);
        let temp_place = HPlace {
            base: temp,
            base_is_ref: false,
            indices: Vec::new(),
            fields: Vec::new(),
            ty: self.h_ty(&sel_ty),
            array_elem: None,
        };
        self.push(
            out,
            scope,
            HStmtKind::Assign {
                place: temp_place.clone(),
                value: se,
            },
        );

        // Arme in Bedingungs-/Rumpf-Paare übersetzen; CASE ELSE gesondert.
        let mut cases: Vec<(HExpr, Vec<HStmt>, SourcePos)> = Vec::new();
        let mut else_body: Vec<HStmt> = Vec::new();
        for arm in arms {
            let body = self.lower_body(&arm.body, scope);
            if arm.specs.is_empty() {
                else_body = body;
                continue;
            }
            let mut cond: Option<HExpr> = None;
            for spec in &arm.specs {
                let c = self.lower_case_spec(spec, &temp_place, &st, scope);
                cond = Some(match cond {
                    None => c,
                    Some(prev) => HExpr::Logic {
                        op: hir::HLogic::Or,
                        ty: IntKind::I2,
                        l: Box::new(prev),
                        r: Box::new(c),
                    },
                });
            }
            cases.push((cond.unwrap(), body, arm.pos));
        }
        // Von hinten zu verschachteltem If zusammensetzen.
        let mut els = else_body;
        for (cond, body, pos) in cases.into_iter().rev() {
            let stmt = HStmt {
                pos,
                line: pos.line,
                kind: HStmtKind::If {
                    cond,
                    then: body,
                    els,
                },
            };
            els = vec![stmt];
        }
        out.extend(els);
    }

    fn lower_case_spec(
        &mut self,
        spec: &CaseSpec,
        temp: &HPlace,
        sel_ty: &Ty,
        scope: &mut Scope,
    ) -> HExpr {
        let load = || HExpr::Load(Box::new(temp.clone()));
        let cmp = |s: &mut Self, op: hir::HCmp, e: &Expr, scope: &mut Scope| -> HExpr {
            let (he, et) = s.lower_expr(e, scope);
            let ok = (is_num(sel_ty) && is_num(&et)) || (is_str(sel_ty) && is_str(&et));
            if !ok {
                s.err(e.pos(), "Type mismatch");
            }
            if is_str(sel_ty) && is_str(&et) {
                HExpr::Cmp {
                    op,
                    ty: hir::CmpKind::Str,
                    l: Box::new(load()),
                    r: Box::new(he),
                }
            } else {
                let common = promote_num(num_ty(sel_ty), num_ty(&et));
                let l = s.conv_num(load(), sel_ty, common);
                let r = s.conv_num(he, &et, common);
                HExpr::Cmp {
                    op,
                    ty: hir::CmpKind::Num(common),
                    l: Box::new(l),
                    r: Box::new(r),
                }
            }
        };
        match spec {
            CaseSpec::Expr(e) => cmp(self, hir::HCmp::Eq, e, scope),
            CaseSpec::Is(op, e) => {
                let hop = match op {
                    BinOp::Eq => hir::HCmp::Eq,
                    BinOp::Ne => hir::HCmp::Ne,
                    BinOp::Lt => hir::HCmp::Lt,
                    BinOp::Le => hir::HCmp::Le,
                    BinOp::Gt => hir::HCmp::Gt,
                    BinOp::Ge => hir::HCmp::Ge,
                    _ => hir::HCmp::Eq,
                };
                cmp(self, hop, e, scope)
            }
            CaseSpec::Range(a, b) => {
                let lo = cmp(self, hir::HCmp::Ge, a, scope);
                let hi = cmp(self, hir::HCmp::Le, b, scope);
                HExpr::Logic {
                    op: hir::HLogic::And,
                    ty: IntKind::I2,
                    l: Box::new(lo),
                    r: Box::new(hi),
                }
            }
        }
    }

    fn lower_def_fn(
        &mut self,
        name: &str,
        suffix: &Option<Suffix>,
        params: &[Param],
        block: Option<&[Stmt]>,
        single: Option<&Expr>,
        pos: SourcePos,
    ) {
        // Registrierung (mit Prozedur-Id).
        let ret = self.name_ty(name, suffix);
        let param_infos: Vec<(Ty, bool)> =
            params.iter().map(|p| (self.param_ty(p), p.array)).collect();
        let id = match self.procs.get(name) {
            Some(p) => p.id,
            None => {
                let id = self.next_proc_id;
                self.next_proc_id += 1;
                self.hir_procs.push(None);
                id
            }
        };
        self.procs.insert(
            name.to_string(),
            ProcInfo {
                kind: ProcKind::Function,
                ret: ret.clone(),
                params: param_infos,
                id,
                def_fn: true,
            },
        );
        // Rumpf-Scope: Parameter (BYVAL) und Rückgabevariable lokal,
        // freie Namen binden an Modulvariablen.
        let mut fscope = Scope {
            is_def_fn: true,
            current_line: pos.line,
            current_pos: pos,
            ..Scope::default()
        };
        let mut hparams = Vec::new();
        for p in params {
            let ty = self.param_ty(p);
            let key = self.var_key(&p.name, &p.suffix, false);
            let slot = fscope.locals.len() as u16;
            fscope.locals.push(hir::HVar {
                name: key.trim_end_matches("\u{1}AS").to_string(),
                ty: self.h_ty(&ty),
                array: false,
            });
            fscope.vars.insert(
                key,
                VarInfo {
                    ty: ty.clone(),
                    array: false,
                    explicit: true,
                    slot,
                    global: false,
                    by_ref: false,
                },
            );
            hparams.push(hir::HParam {
                name: p.name.clone(),
                ty: self.h_ty(&ty),
                array: false,
                by_ref: false,
            });
        }
        let ret_key = self.var_key(name, suffix, false);
        let ret_slot_idx = fscope.locals.len() as u16;
        fscope.locals.push(hir::HVar {
            name: name.trim_end_matches("\u{1}AS").to_string(),
            ty: self.h_ty(&ret),
            array: false,
        });
        fscope.vars.insert(
            ret_key,
            VarInfo {
                ty: ret.clone(),
                array: false,
                explicit: true,
                slot: ret_slot_idx,
                global: false,
                by_ref: false,
            },
        );

        let body = match (block, single) {
            (Some(stmts), _) => {
                self.prescan(stmts, &mut fscope, false);
                self.lower_body(stmts, &mut fscope)
            }
            (_, Some(expr)) => {
                let (e, et) = self.lower_expr(expr, &mut fscope);
                let value = self.coerce(e, &et, &ret, expr.pos());
                vec![HStmt {
                    pos: fscope.current_pos,
                    line: fscope.current_line,
                    kind: HStmtKind::Assign {
                        place: HPlace {
                            base: VarSlot::Local(ret_slot_idx),
                            base_is_ref: false,
                            indices: Vec::new(),
                            fields: Vec::new(),
                            ty: self.h_ty(&ret),
                            array_elem: None,
                        },
                        value,
                    },
                }]
            }
            _ => Vec::new(),
        };

        let hproc = hir::HProc {
            name: name.to_string(),
            kind: hir::HProcKind::DefFn,
            params: hparams,
            locals: std::mem::take(&mut fscope.locals),
            ret_slot: Some(VarSlot::Local(ret_slot_idx)),
            ret_ty: Some(self.h_ty(&ret)),
            body,
            label_count: fscope.next_label,
        };
        while self.hir_procs.len() <= id as usize {
            self.hir_procs.push(None);
        }
        self.hir_procs[id as usize] = Some(hproc);
    }

    fn common_bound(&self, expr: &Expr) -> Option<i32> {
        match self.fold_const(expr)? {
            ConstVal::Num(n)
                if n.is_finite()
                    && (i32::MIN as f64..=i32::MAX as f64).contains(&n.round_ties_even()) =>
            {
                Some(n.round_ties_even() as i32)
            }
            _ => None,
        }
    }

    fn declare(&mut self, d: &VarDecl, redim: bool, scope: &mut Scope, out: &mut Vec<HStmt>) {
        let ty = self.decl_ty(d);
        let mut hdims = Vec::new();
        if let Some(dims) = &d.dims {
            for (lo, hi) in dims {
                let lo_e = match lo {
                    Some(l) => self.lower_num_as(l, scope, NumTy::Lng),
                    None => HExpr::Lng(self.option_base as i32),
                };
                let hi_e = self.lower_num_as(hi, scope, NumTy::Lng);
                hdims.push((lo_e, hi_e));
            }
        }
        let key = self.var_key(&d.name, &d.suffix, d.as_type.is_some());
        let is_array = d.dims.is_some();
        if let Some(existing) = self.vars_of(scope).get(&key).cloned() {
            let redim_ok = redim && existing.array && is_array;
            if existing.explicit && !redim_ok {
                self.err(d.pos, "Duplicate definition");
                return;
            }
            if redim_ok && !hdims.is_empty() {
                let slot = if existing.global {
                    VarSlot::Global(existing.slot)
                } else {
                    VarSlot::Local(existing.slot)
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::Dim {
                        slot,
                        elem: self.h_ty(&ty),
                        dims: hdims,
                        redim: true,
                    },
                );
                return;
            }
        }
        let (slot, global) = self.alloc_var(scope, &key, &ty, is_array);
        self.insert_var(
            scope,
            key,
            VarInfo {
                ty: ty.clone(),
                array: is_array,
                explicit: true,
                slot,
                global,
                by_ref: false,
            },
        );
        if is_array && !hdims.is_empty() {
            let vslot = if global {
                VarSlot::Global(slot)
            } else {
                VarSlot::Local(slot)
            };
            self.push(
                out,
                scope,
                HStmtKind::Dim {
                    slot: vslot,
                    elem: self.h_ty(&ty),
                    dims: hdims,
                    redim,
                },
            );
        }
    }

    fn check_label_exists(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) {
        let found = match target {
            LabelRef::Name(n) => scope.labels.contains_key(n),
            LabelRef::Line(n) => scope.line_labels.contains_key(n),
        };
        if !found {
            self.err(pos, "Label not defined");
        }
    }

    fn label_id(&mut self, target: &LabelRef, scope: &Scope, pos: SourcePos) -> Option<LabelId> {
        let id = match target {
            LabelRef::Name(n) => scope.labels.get(n).copied(),
            LabelRef::Line(n) => scope.line_labels.get(n).copied(),
        };
        if id.is_none() {
            self.err(pos, "Label not defined");
        }
        id
    }

    /// Sprungziel im Modulrumpf auflösen (für `ON ERROR GOTO` aus einer
    /// Prozedur heraus).
    fn module_label_id(&mut self, target: &LabelRef, pos: SourcePos) -> Option<LabelId> {
        let id = match target {
            LabelRef::Name(n) => self.module_labels.get(n).copied(),
            LabelRef::Line(n) => self.module_line_labels.get(n).copied(),
        };
        if id.is_none() {
            self.err(pos, "Label not defined");
        }
        id
    }

    // ---- Aufrufe -----------------------------------------------------------

    fn lower_call_stmt(
        &mut self,
        name: &str,
        args: &[Expr],
        scope: &mut Scope,
        pos: SourcePos,
        out: &mut Vec<HStmt>,
    ) {
        if let Some(r) = &mut self.references {
            r.procedures.push((pos, name.into()));
        }
        if name == "CLIPBOARD.ADDITEM" {
            if args.len() != 1 {
                self.err(pos, "Argument-count mismatch for CLIPBOARD.ADDITEM");
                return;
            }
            let (value, _) = self.want_str(&args[0], scope);
            self.push(
                out,
                scope,
                HStmtKind::BuiltinStmt {
                    b: Builtin::ClipboardAdd,
                    args: vec![value],
                },
            );
            return;
        }
        if name == "PRINTER.PRINT" {
            self.push_ziel(out, scope, HExpr::Lng(-2));
            let items = args
                .iter()
                .map(|arg| HPrintItem::Val(self.lower_expr(arg, scope).0))
                .collect();
            self.push(
                out,
                scope,
                HStmtKind::Print {
                    items,
                    trailing: false,
                },
            );
            self.push_ziel(out, scope, HExpr::Lng(-1));
            return;
        }
        if matches!(name, "PRINTER.NEWPAGE" | "PRINTER.ENDDOC") {
            if !args.is_empty() {
                self.err(pos, format!("Argument-count mismatch for {name}"));
                return;
            }
            if name == "PRINTER.NEWPAGE" {
                self.push_ziel(out, scope, HExpr::Lng(-2));
                self.push(
                    out,
                    scope,
                    HStmtKind::Print {
                        items: vec![HPrintItem::Val(HExpr::Str("\u{c}".into()))],
                        trailing: true,
                    },
                );
                self.push_ziel(out, scope, HExpr::Lng(-1));
            }
            return;
        }
        if matches!(name, "SHOW" | "HIDE") && args.is_empty() {
            if let Some((object, _)) = self.current_form() {
                let method = forms::methods(ObjectClass::Form)
                    .iter()
                    .position(|method| *method == name)
                    .unwrap() as u16;
                self.push(
                    out,
                    scope,
                    HStmtKind::ObjectMethod {
                        object,
                        index: None,
                        method,
                        args: Vec::new(),
                    },
                );
                return;
            }
        }
        if name.contains('.') || name.contains('!') {
            if let Some((object, info, member)) = self.object_member_target(name, pos) {
                let Some(method) = forms::methods(info.class)
                    .iter()
                    .position(|m| m.eq_ignore_ascii_case(&member))
                else {
                    self.err(
                        pos,
                        format!("Unknown method '{member}' of {}", info.class.name()),
                    );
                    return;
                };
                if !forms::method_is_implemented(info.class, forms::methods(info.class)[method]) {
                    self.err(
                        pos,
                        format!("Feature unavailable: {}.{member}", info.class.name()),
                    );
                    return;
                }
                let member = forms::methods(info.class)[method];
                let (index, actual) = if info.array {
                    let Some((index, actual)) = args.split_first() else {
                        self.err(pos, format!("Object '{}' requires an index", info.name));
                        return;
                    };
                    (Some(self.lower_num_as(index, scope, NumTy::Lng)), actual)
                } else {
                    (None, args)
                };
                let (min, max) = forms::method_arity(info.class, member).unwrap();
                if !(min..=max).contains(&actual.len()) {
                    self.err(
                        pos,
                        format!("Argument-count mismatch for {}.{member}", info.class.name()),
                    );
                    for arg in actual {
                        self.lower_expr(arg, scope);
                    }
                    return;
                }
                let hargs = if info.class == ObjectClass::Form && member == "SHOW" {
                    actual
                        .iter()
                        .map(|a| self.lower_num_as(a, scope, NumTy::Lng))
                        .collect()
                } else {
                    actual.iter().map(|a| self.lower_expr(a, scope).0).collect()
                };
                self.push(
                    out,
                    scope,
                    HStmtKind::ObjectMethod {
                        object,
                        index,
                        method: method as u16,
                        args: hargs,
                    },
                );
                return;
            }
            return;
        }
        if matches!(name, "LOAD" | "UNLOAD") && args.len() == 1 {
            if let Expr::Name {
                name: object_name,
                suffix: None,
                args: index,
                pos: object_pos,
            } = &args[0]
            {
                let (parent, object_name) = object_name
                    .split_once('!')
                    .map_or((None, object_name.as_str()), |(parent, name)| {
                        (Some(parent), name)
                    });
                if let Some((object, _)) = self.scoped_object(object_name, parent, *object_pos) {
                    let Ok(index) = self.object_index(object, index, scope, *object_pos) else {
                        return;
                    };
                    self.push(
                        out,
                        scope,
                        HStmtKind::ObjectLoad {
                            object,
                            index,
                            unload: name == "UNLOAD",
                        },
                    );
                    return;
                }
                return;
            }
        }
        if let Some(info) = self.procs.get(name).cloned() {
            if info.kind != ProcKind::Sub {
                self.err(pos, "Duplicate definition");
                return;
            }
            if args.len() != info.params.len() {
                self.err(pos, "Argument-count mismatch");
            }
            let hargs = self.lower_call_args(&info, args, scope);
            self.push(
                out,
                scope,
                HStmtKind::CallSub {
                    proc: info.id,
                    args: hargs,
                },
            );
            return;
        }
        if let Some((min, max, spec)) = builtin_stmt(name) {
            self.lower_builtin_stmt(name, args, min, max, spec, scope, pos, out);
            return;
        }
        if banned_feature(name) {
            for a in args {
                self.lower_expr(a, scope);
            }
            self.err(pos, format!("Feature unavailable: {name}"));
            return;
        }
        self.err(pos, "Subprogram not defined");
    }

    /// Argumente eines SUB-/FUNCTION-Aufrufs: Variablen BYREF (exakter
    /// Typ), Ausdrücke/Klammern BYVAL mit Konvertierung.
    fn lower_call_args(&mut self, info: &ProcInfo, args: &[Expr], scope: &mut Scope) -> Vec<HArg> {
        let mut out = Vec::new();
        for (i, a) in args.iter().enumerate() {
            let (pt, p_array) = match info.params.get(i) {
                Some(p) => (p.0.clone(), p.1),
                None => (Ty::Unknown, false),
            };
            // DEF FN: alle Parameter BYVAL.
            let force_byval = info.def_fn;
            // Array-Parameter: ganzes Array als Referenz.
            if p_array {
                if let Expr::Name {
                    name,
                    suffix,
                    args: idx,
                    pos,
                } = a
                {
                    if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
                        if let Some(v) = self.resolve_var(scope, name, suffix, true, *pos) {
                            let base = if v.global {
                                VarSlot::Global(v.slot)
                            } else {
                                VarSlot::Local(v.slot)
                            };
                            out.push(HArg::ArrayRef(HPlace {
                                base,
                                base_is_ref: false,
                                indices: Vec::new(),
                                fields: Vec::new(),
                                ty: self.h_ty(&v.ty),
                                array_elem: None,
                            }));
                            continue;
                        }
                    }
                }
                self.err(a.pos(), "Parameter type mismatch");
                continue;
            }
            // BYREF-fähig: einfacher Variablen-/Element-/Feldzugriff.
            let byref_candidate = !force_byval && self.is_place_expr(a, scope);
            if byref_candidate {
                let (place, at) = self.lower_place(a, scope);
                let kind_ok = (is_num(&pt) && is_num(&at))
                    || (is_str(&pt) && is_str(&at))
                    || matches!((&pt, &at), (Ty::Udt(x), Ty::Udt(y)) if x == y)
                    || matches!(
                        (&pt, &at),
                        (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
                    )
                    || pt == Ty::Unknown
                    || at == Ty::Unknown;
                if !kind_ok {
                    self.err(a.pos(), "Parameter type mismatch");
                    continue;
                }
                // BYREF verlangt exakten Typ (Vorbild: „Parameter type
                // mismatch" bei abweichendem numerischen Typ).
                if pt != Ty::Unknown && at != Ty::Unknown && pt != at {
                    if is_num(&pt) && is_num(&at) {
                        self.err(a.pos(), "Parameter type mismatch");
                        continue;
                    }
                    // FixedStr → STRING-Parameter: BYVAL-Kopie.
                    if let Some(place) = place {
                        let e = HExpr::Load(Box::new(place));
                        let e = self.coerce(e, &at, &pt, a.pos());
                        out.push(HArg::ByVal(e));
                    }
                    continue;
                }
                if let Some(place) = place {
                    out.push(HArg::ByRef(place));
                }
                continue;
            }
            // BYVAL: Ausdruck auswerten und konvertieren.
            let (e, at) = self.lower_expr(a, scope);
            let kind_ok = (is_num(&pt) && is_num(&at))
                || (is_str(&pt) && is_str(&at))
                || matches!(
                    (&pt, &at),
                    (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
                )
                || pt == Ty::Unknown
                || at == Ty::Unknown;
            if !kind_ok {
                self.err(a.pos(), "Parameter type mismatch");
                continue;
            }
            let e = self.coerce(e, &at, &pt, a.pos());
            out.push(HArg::ByVal(e));
        }
        out
    }

    /// Ist der Ausdruck ein L-Wert (Variable/Element/Feld), keine
    /// Konstante, Funktion oder Klammerung?
    fn is_place_expr(&self, e: &Expr, scope: &Scope) -> bool {
        match e {
            Expr::Name {
                name, suffix, args, ..
            } => {
                if self.consts.contains_key(name) {
                    return false;
                }
                if (suffix.is_none() && self.find_object(name).is_some())
                    || (name.contains('!') && !self.is_udt_path(scope, name))
                {
                    return false;
                }
                match args {
                    None => {
                        // Variable oder parameterlose Funktion/Builtin?
                        let as_key = format!("{name}\u{1}AS");
                        let key = self.var_key(name, suffix, false);
                        let declared = self.visible_var(scope, &key).is_some()
                            || self
                                .visible_var(scope, &as_key)
                                .is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
                        if declared {
                            return true;
                        }
                        let full_name = match suffix {
                            Some(s) => format!("{name}{}", s.as_char()),
                            None => name.to_string(),
                        };
                        if banned_feature(&full_name) || builtin_fn(&full_name).is_some() {
                            return false;
                        }
                        if let Some(info) = self.procs.get(name) {
                            if info.kind == ProcKind::Function && info.params.is_empty() {
                                return false;
                            }
                        }
                        true // implizite Variable
                    }
                    Some(_) => {
                        // Nur deklarierte Arrays sind L-Werte.
                        let as_key = format!("{name}\u{1}AS");
                        let key = self.var_key(name, suffix, false);
                        let v = if suffix.is_none() {
                            self.visible_var(scope, &as_key)
                                .or_else(|| self.visible_var(scope, &key))
                        } else {
                            self.visible_var(scope, &key).or_else(|| {
                                self.visible_var(scope, &as_key)
                                    .filter(|v| v.ty == suffix_ty(suffix.unwrap()))
                            })
                        };
                        v.map(|v| v.array).unwrap_or(false)
                    }
                }
            }
            _ => false,
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn lower_builtin_stmt(
        &mut self,
        name: &str,
        args: &[Expr],
        min: u8,
        max: u8,
        spec: &[ArgK],
        scope: &mut Scope,
        pos: SourcePos,
        out: &mut Vec<HStmt>,
    ) {
        // Argumentprüfung wie bisher.
        let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos);

        match name {
            "BEEP" => self.push(
                out,
                scope,
                HStmtKind::BuiltinStmt {
                    b: Builtin::Beep,
                    args: vec![],
                },
            ),
            "DOEVENTS" => self.push(
                out,
                scope,
                HStmtKind::BuiltinStmt {
                    b: Builtin::Doevents,
                    args: vec![],
                },
            ),
            "RANDOMIZE" => {
                let a = lowered
                    .into_iter()
                    .next()
                    .map(|(e, t)| {
                        let conv = self.conv_num(e, &t, NumTy::Dbl);
                        vec![conv]
                    })
                    .unwrap_or_default();
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::Randomize,
                        args: a,
                    },
                );
            }
            "SLEEP" => {
                let a = lowered
                    .into_iter()
                    .next()
                    .map(|(e, t)| {
                        let conv = self.conv_num(e, &t, NumTy::Dbl);
                        vec![conv]
                    })
                    .unwrap_or_default();
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::Sleep,
                        args: a,
                    },
                );
            }
            "SWAP" => {
                // SWAP a, b → t = a : a = b : b = t (versteckter Temp).
                if args.len() == 2 {
                    let (pa, ta) = self.lower_place(&args[0], scope);
                    let (pb, tb) = self.lower_place(&args[1], scope);
                    let ok = (is_num(&ta) && is_num(&tb))
                        || (is_str(&ta) && is_str(&tb))
                        || matches!((&ta, &tb), (Ty::Udt(x), Ty::Udt(y)) if x == y)
                        || ta == Ty::Unknown
                        || tb == Ty::Unknown;
                    if !ok {
                        self.err(pos, "Type mismatch");
                        return;
                    }
                    if let (Some(pa), Some(pb)) = (pa, pb) {
                        let tmp = self.alloc_temp(scope, &ta);
                        let tmp_place = HPlace {
                            base: tmp,
                            base_is_ref: false,
                            indices: Vec::new(),
                            fields: Vec::new(),
                            ty: self.h_ty(&ta),
                            array_elem: None,
                        };
                        let load_a = HExpr::Load(Box::new(pa.clone()));
                        let load_b = HExpr::Load(Box::new(pb.clone()));
                        let b_conv = self.coerce_silent(load_b, &tb, &ta);
                        let t_load = HExpr::Load(Box::new(tmp_place.clone()));
                        let t_conv = self.coerce_silent(t_load, &ta, &tb);
                        self.push(
                            out,
                            scope,
                            HStmtKind::Assign {
                                place: tmp_place,
                                value: load_a,
                            },
                        );
                        self.push(
                            out,
                            scope,
                            HStmtKind::Assign {
                                place: pa,
                                value: b_conv,
                            },
                        );
                        self.push(
                            out,
                            scope,
                            HStmtKind::Assign {
                                place: pb,
                                value: t_conv,
                            },
                        );
                    }
                }
            }
            // Bildschirmanweisungen: ausgelassene Argumente kommen als -1
            // durch, damit die Laufzeit „weglassen" von „null" unterscheidet.
            "ENVIRON" | "CLEAR" | "TRON" | "TROFF" | "STACK" | "MSGBOX" => {
                let bt = match name {
                    "ENVIRON" => Builtin::EnvironSet,
                    "CLEAR" => Builtin::Clear,
                    "TRON" => Builtin::Tron,
                    "TROFF" => Builtin::Troff,
                    "MSGBOX" => Builtin::MsgBox,
                    _ => Builtin::StackStmt,
                };
                let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
                self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
            }
            "KILL" | "CHDIR" | "CHDRIVE" | "MKDIR" | "RMDIR" | "FILES" | "SHELL" => {
                let bt = match name {
                    "KILL" => Builtin::Kill,
                    "CHDIR" => Builtin::Chdir,
                    "CHDRIVE" => Builtin::Chdrive,
                    "MKDIR" => Builtin::Mkdir,
                    "RMDIR" => Builtin::Rmdir,
                    "FILES" => Builtin::Files,
                    _ => Builtin::ShellStmt,
                };
                let args: Vec<HExpr> = lowered.iter().map(|(e, _)| e.clone()).collect();
                self.push(out, scope, HStmtKind::BuiltinStmt { b: bt, args });
            }
            "RESET" => self.push(
                out,
                scope,
                HStmtKind::BuiltinStmt {
                    b: Builtin::CloseAll,
                    args: vec![],
                },
            ),
            "SETFORMATCC" => {
                let (e, t) = lowered[0].clone();
                let n = self.conv_num(e, &t, NumTy::Lng);
                self.push(
                    out,
                    scope,
                    HStmtKind::BuiltinStmt {
                        b: Builtin::SetFormatCc,
                        args: vec![n],
                    },
                );
            }
            "KEY" => {
                // `KEY LIST` (Makros auflisten) vs. `KEY n, text$` (zuweisen).
                let ist_list = matches!(
                    args.first(),
                    Some(Expr::Name { name, args: None, suffix: None, .. }) if name == "LIST"
                );
                if ist_list {
                    if args.len() > 1 {
                        self.err(pos, "Argument-count mismatch: KEY");
                    }
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::KeyList,
                            args: vec![],
                        },
                    );
                } else {
                    if lowered.len() != 2 {
                        self.err(pos, "Argument-count mismatch: KEY");
                    }
                    let (ne, nt) = lowered[0].clone();
                    let n = self.conv_num(ne, &nt, NumTy::Lng);
                    let t = lowered[1].0.clone();
                    self.push(
                        out,
                        scope,
                        HStmtKind::BuiltinStmt {
                            b: Builtin::KeyAssign,
                            args: vec![n, t],
                        },
                    );
                }
            }
            "CLS" | "COLOR" | "LOCATE" | "WIDTH" | "SCREEN" => {
                let b = match name {
                    "CLS" => Builtin::Cls,
                    "COLOR" => Builtin::Color,
                    "LOCATE" => Builtin::Locate,
                    "SCREEN" => Builtin::ScreenStmt,
                    _ => Builtin::Width,
                };
                let a = self.lower_opt_args(args, &lowered);
                self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
            }
            // ---- ISAM ------------------------------------------------------
            "CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE"
            | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS"
            | "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
                self.lower_isam_stmt(name, args, lowered, scope, out)
            }
            "SETUEVENT" => self.push(
                out,
                scope,
                HStmtKind::BuiltinStmt {
                    b: Builtin::SetUEvent,
                    args: vec![],
                },
            ),
            "RUN" => {
                let (target, string) = match lowered.first() {
                    None => (None, false),
                    Some((expr, ty)) if is_str(ty) => (Some(expr.clone()), true),
                    Some((expr, ty)) => (Some(self.conv_num(expr.clone(), ty, NumTy::Lng)), false),
                };
                self.push(out, scope, HStmtKind::Run { target, string });
            }
            // Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
            _ => self.push(out, scope, HStmtKind::Unsupported("Anweisung")),
        }
    }

    /// ISAM-Anweisungen absenken. Die Dateinummer ist überall das erste
    /// Argument (bei `DELETETABLE` stattdessen der Datenbankname); alle
    /// weiteren Argumente reisen unverändert durch, weil die Laufzeit sie
    /// gegen das Tabellenlayout prüfen muss.
    #[allow(clippy::too_many_arguments)]
    fn lower_isam_stmt(
        &mut self,
        name: &str,
        args: &[Expr],
        lowered: Vec<(HExpr, Ty)>,
        scope: &mut Scope,
        out: &mut Vec<HStmt>,
    ) {
        let b = match name {
            "CREATEINDEX" => Builtin::IsamCreateIndex,
            "DELETEINDEX" => Builtin::IsamDeleteIndex,
            "SETINDEX" => Builtin::IsamSetIndex,
            "INSERT" => Builtin::IsamInsert,
            "RETRIEVE" => Builtin::IsamRetrieve,
            "UPDATE" => Builtin::IsamUpdate,
            "DELETE" => Builtin::IsamDelete,
            "DELETETABLE" => Builtin::IsamDeleteTable,
            "MOVEFIRST" => Builtin::IsamMoveFirst,
            "MOVELAST" => Builtin::IsamMoveLast,
            "MOVENEXT" => Builtin::IsamMoveNext,
            "MOVEPREVIOUS" => Builtin::IsamMovePrevious,
            "SEEKEQ" => Builtin::IsamSeekEq,
            "SEEKGT" => Builtin::IsamSeekGt,
            "SEEKGE" => Builtin::IsamSeekGe,
            "BEGINTRANS" => Builtin::IsamBeginTrans,
            "COMMITTRANS" => Builtin::IsamCommitTrans,
            _ => Builtin::IsamRollback,
        };

        // Satzargument gegen den Typ der Dateinummer prüfen, sofern beide
        // literal bekannt sind (Aufgabe 2.3). Ist die Nummer erst zur
        // Laufzeit bekannt, prüft die Laufzeit gegen das Tabellenlayout.
        if matches!(name, "INSERT" | "RETRIEVE" | "UPDATE") {
            if let (Some(ConstVal::Num(n)), Some((_, Ty::Udt(hat)))) = (
                args.first().and_then(|a| self.fold_const(a)),
                lowered.get(1),
            ) {
                if let Some(soll) = self.isam_typ.get(&(n as i32)) {
                    if soll != hat {
                        self.err(args[1].pos(), "Type mismatch");
                    }
                }
            }
        }

        let mut hargs = Vec::with_capacity(lowered.len());
        for (i, (e, t)) in lowered.into_iter().enumerate() {
            // Erstes Argument ist die Dateinummer (bei DELETETABLE ein Name).
            let ist_nummer = i == 0 && name != "DELETETABLE";
            if ist_nummer || (name == "ROLLBACK" && i == 0) {
                hargs.push(self.conv_num(e, &t, NumTy::Lng));
            } else {
                hargs.push(e);
            }
        }
        self.push(out, scope, HStmtKind::BuiltinStmt { b, args: hargs });
    }

    /// Ausgabeziel von `PRINT` umschalten (−1 Bildschirm, −2 Drucker,
    /// sonst Dateinummer).
    fn push_ziel(&mut self, out: &mut Vec<HStmt>, scope: &mut Scope, n: HExpr) {
        self.push(
            out,
            scope,
            HStmtKind::BuiltinStmt {
                b: Builtin::PrintZiel,
                args: vec![n],
            },
        );
    }

    /// Argumente einer Bildschirmanweisung: ausgelassene (`LOCATE , 5`)
    /// werden zu -1, damit die Laufzeit „weglassen" erkennt (0 ist bei
    /// `COLOR` eine gültige Farbe).
    fn lower_opt_args(&mut self, args: &[Expr], lowered: &[(HExpr, Ty)]) -> Vec<HExpr> {
        args.iter()
            .enumerate()
            .map(|(i, a)| {
                if matches!(a, Expr::Missing) {
                    HExpr::Lng(-1)
                } else {
                    let (e, t) = lowered[i].clone();
                    if t == Ty::Unknown {
                        e
                    } else {
                        self.conv_num(e, &t, NumTy::Lng)
                    }
                }
            })
            .collect()
    }

    /// Zählt/prüft Builtin-Argumente und liefert die abgesenkten Ausdrücke.
    #[allow(clippy::too_many_arguments)]
    fn check_and_lower_builtin_args(
        &mut self,
        name: &str,
        args: &[Expr],
        min: u8,
        max: u8,
        spec: &[ArgK],
        scope: &mut Scope,
        pos: SourcePos,
    ) -> Vec<(HExpr, Ty)> {
        let real: Vec<&Expr> = args
            .iter()
            .filter(|a| !matches!(a, Expr::Missing))
            .collect();
        if real.len() < min as usize || args.len() > max as usize {
            // Katalogtext des Vorbilds, ergänzt um das Element: eine Diagnose
            // zu einem dokumentierten Element muss es benennen (Guiding
            // Principle) — sonst ist bei mehreren Aufrufen in einer Zeile
            // nicht erkennbar, welcher gemeint ist.
            self.err(pos, format!("Argument-count mismatch: {name}"));
        }
        // Sonderfall INSTR([start%,] s$, such$)
        let instr_with_start = name == "INSTR" && args.len() == 3;
        // Sonderfall CREATEINDEX: beliebig viele Spaltennamen ab Argument 4.
        let createindex = name == "CREATEINDEX";
        let mut out = Vec::new();
        for (i, a) in args.iter().enumerate() {
            if matches!(a, Expr::Missing) {
                out.push((HExpr::Int(0), Ty::Unknown));
                continue;
            }
            let (he, at) = self.lower_expr(a, scope);
            let kind = if instr_with_start {
                match i {
                    0 => ArgK::N,
                    _ => ArgK::S,
                }
            } else if name == "INSTR" || (createindex && i >= 3) {
                ArgK::S
            } else {
                *spec.get(i).unwrap_or(&ArgK::A)
            };
            let ok = match kind {
                ArgK::N => is_num(&at),
                ArgK::S => is_str(&at),
                ArgK::A => true,
                ArgK::R => matches!(at, Ty::Udt(_) | Ty::Unknown),
            };
            if !ok {
                self.err(a.pos(), "Type mismatch");
            }
            out.push((he, at));
        }
        out
    }

    // ---- Ausdrücke ---------------------------------------------------------

    fn want_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
        let (he, t) = self.lower_expr(e, scope);
        if !is_num(&t) {
            self.err(e.pos(), "Type mismatch");
        }
        (he, t)
    }
    /// `OPEN "COM1:" …` — serielle Schnittstelle ist deklariertes Non-Feature.
    /// Greift nur bei Stringliteralen; ein zur Laufzeit gebildeter Gerätename
    /// bleibt der Datei-E/A überlassen.
    fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
        if let Expr::StrLit(s, _) = file {
            if ist_com_geraet(s) {
                self.err(pos, format!("Feature unavailable: {s}"));
            }
        }
    }

    fn want_str(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
        let (he, t) = self.lower_expr(e, scope);
        if !is_str(&t) {
            self.err(e.pos(), "Type mismatch");
        }
        (he, t)
    }

    /// Numerischer Ausdruck, konvertiert auf Zieltyp.
    fn lower_num_as(&mut self, e: &Expr, scope: &mut Scope, to: NumTy) -> HExpr {
        let (he, t) = self.want_num(e, scope);
        if t == Ty::Unknown {
            he
        } else {
            self.conv_num(he, &t, to)
        }
    }

    fn lower_expr_num(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
        self.want_num(e, scope)
    }

    /// Bedingung: numerisch; 0 = falsch. Der Codegen testet auf ≠ 0 im
    /// jeweiligen Typ (kein Konvertierungszwang nötig — auf INTEGER
    /// konvertieren würde bei großen Werten fälschlich Fehler 6 auslösen,
    /// daher Vergleich im Operandentyp).
    fn lower_cond(&mut self, e: &Expr, scope: &mut Scope) -> HExpr {
        let (he, t) = self.want_num(e, scope);
        if t == Ty::Unknown || num_ty(&t) == NumTy::Int {
            return he;
        }
        // vergleiche ≠ 0 im Operandentyp → INTEGER-Ergebnis
        let nt = num_ty(&t);
        let zero = match nt {
            NumTy::Int => HExpr::Int(0),
            NumTy::Lng => HExpr::Lng(0),
            NumTy::Cur => HExpr::Cur(0),
            NumTy::Sng => HExpr::Sng(0.0),
            NumTy::Dbl => HExpr::Dbl(0.0),
        };
        HExpr::Cmp {
            op: hir::HCmp::Ne,
            ty: hir::CmpKind::Num(nt),
            l: Box::new(he),
            r: Box::new(zero),
        }
    }

    fn check_assign(&mut self, target: &Ty, value: &Ty, pos: SourcePos) {
        let ok = (is_num(target) && is_num(value))
            || (is_str(target) && is_str(value))
            || matches!((target, value), (Ty::Udt(a), Ty::Udt(b)) if a == b)
            || matches!(
                (target, value),
                (Ty::Control, Ty::Control) | (Ty::Form, Ty::Form)
            )
            || *target == Ty::Unknown
            || *value == Ty::Unknown;
        if !ok {
            self.err(pos, "Type mismatch");
        }
    }

    /// Wie `coerce`, aber ohne neue Diagnose (Prüfung lief bereits).
    fn coerce_silent(&mut self, e: HExpr, from: &Ty, to: &Ty) -> HExpr {
        if is_num(to) && is_num(from) && *to != Ty::Unknown && *from != Ty::Unknown {
            return self.conv_num(e, from, num_ty(to));
        }
        if let Ty::FixedStr(n) = to {
            if is_str(from) {
                return HExpr::FixStr {
                    len: *n,
                    arg: Box::new(e),
                };
            }
        }
        e
    }

    /// L-Wert absenken: liefert Platz und Typ.
    fn lower_place(&mut self, e: &Expr, scope: &mut Scope) -> (Option<HPlace>, Ty) {
        match e {
            Expr::Name {
                name,
                suffix,
                args,
                pos,
            } => {
                let indices = match args {
                    Some(idx) => {
                        let mut v = Vec::new();
                        for a in idx {
                            v.push(self.lower_num_as(a, scope, NumTy::Lng));
                        }
                        Some(v)
                    }
                    None => None,
                };
                // UDT-Feldzugriff über Punktpfad, auch auf UDT-Arrays.
                if name.contains('.') {
                    let parts: Vec<&str> = name.split('.').collect();
                    let base = parts[0];
                    let base_as = format!("{base}\u{1}AS");
                    let base_info = self
                        .visible_var(scope, &base_as)
                        .or_else(|| self.visible_var(scope, &self.var_key(base, &None, false)))
                        .cloned();
                    if let Some(info) = base_info {
                        if matches!(info.ty, Ty::Udt(_)) {
                            if info.array != indices.is_some() {
                                self.err(*pos, "Duplicate definition");
                                return (None, Ty::Unknown);
                            }
                            let (fty, fpath) = self.member_path(&info.ty, &parts[1..], *pos);
                            let hty = self.h_ty(&fty);
                            let base_slot = if info.global {
                                VarSlot::Global(info.slot)
                            } else {
                                VarSlot::Local(info.slot)
                            };
                            return (
                                Some(HPlace {
                                    base: base_slot,
                                    base_is_ref: info.by_ref,
                                    indices: indices.unwrap_or_default(),
                                    fields: fpath,
                                    ty: hty,
                                    array_elem: None,
                                }),
                                fty,
                            );
                        }
                    }
                }
                let is_array_access = indices.is_some();
                let info = self.resolve_var(scope, name, suffix, is_array_access, *pos);
                let Some(info) = info else {
                    return (None, self.name_ty(name, suffix));
                };
                let base = if info.global {
                    VarSlot::Global(info.slot)
                } else {
                    VarSlot::Local(info.slot)
                };
                let hty = self.h_ty(&info.ty);
                let idx_vec = indices.unwrap_or_default();
                let array_elem = if !idx_vec.is_empty() {
                    Some((hty.clone(), idx_vec.len() as u8))
                } else {
                    None
                };
                (
                    Some(HPlace {
                        base,
                        base_is_ref: info.by_ref,
                        indices: idx_vec,
                        fields: Vec::new(),
                        ty: hty,
                        array_elem,
                    }),
                    info.ty,
                )
            }
            _ => {
                self.err(e.pos(), "Variable required");
                (None, Ty::Unknown)
            }
        }
    }

    fn lower_expr(&mut self, e: &Expr, scope: &mut Scope) -> (HExpr, Ty) {
        match e {
            Expr::IntLit(v, _) => (HExpr::Int(*v), Ty::Int),
            Expr::LongLit(v, _) => (HExpr::Lng(*v), Ty::Lng),
            Expr::SingleLit(v, _) => (HExpr::Sng(*v), Ty::Sng),
            Expr::DoubleLit(v, _) => (HExpr::Dbl(*v), Ty::Dbl),
            Expr::CurrencyLit(v, _) => (HExpr::Cur(*v), Ty::Cur),
            Expr::StrLit(s, _) => (HExpr::Str(s.clone()), Ty::Str),
            Expr::Paren(inner) => self.lower_expr(inner, scope),
            Expr::Missing => (HExpr::Int(0), Ty::Unknown),
            Expr::TypeOf { value, class, pos } => {
                let Some(class) = ObjectClass::parse(class) else {
                    self.err(*pos, format!("Unknown control class '{class}'"));
                    self.lower_expr(value, scope);
                    return (HExpr::Int(0), Ty::Int);
                };
                let (value, ty) = self.lower_expr(value, scope);
                if !matches!(ty, Ty::Form | Ty::Control | Ty::Unknown) {
                    self.err(*pos, "Object required");
                }
                (
                    HExpr::TypeOf {
                        value: Box::new(value),
                        class,
                    },
                    Ty::Int,
                )
            }
            Expr::Unary { op, operand, pos } => {
                let (he, t) = self.lower_expr(operand, scope);
                if !is_num(&t) {
                    self.err(*pos, "Type mismatch");
                }
                match op {
                    UnOp::Neg => {
                        let nt = num_ty(&t);
                        // Literal direkt negieren (hilft FOR STEP -1).
                        match (&he, nt) {
                            (HExpr::Int(v), NumTy::Int) if *v != i16::MIN => {
                                (HExpr::Int(-v), t.clone())
                            }
                            (HExpr::Lng(v), NumTy::Lng) if *v != i32::MIN => {
                                (HExpr::Lng(-v), t.clone())
                            }
                            (HExpr::Sng(v), NumTy::Sng) => (HExpr::Sng(-v), t.clone()),
                            (HExpr::Dbl(v), NumTy::Dbl) => (HExpr::Dbl(-v), t.clone()),
                            _ => (
                                HExpr::Neg {
                                    ty: nt,
                                    arg: Box::new(he),
                                },
                                t.clone(),
                            ),
                        }
                    }
                    UnOp::Not => {
                        let kind = if num_ty(&t) == NumTy::Int {
                            IntKind::I2
                        } else {
                            IntKind::I4
                        };
                        let target = if kind == IntKind::I2 {
                            NumTy::Int
                        } else {
                            NumTy::Lng
                        };
                        let conv = if t == Ty::Unknown {
                            he
                        } else {
                            self.conv_num(he, &t, target)
                        };
                        (
                            HExpr::Not {
                                ty: kind,
                                arg: Box::new(conv),
                            },
                            if kind == IntKind::I2 {
                                Ty::Int
                            } else {
                                Ty::Lng
                            },
                        )
                    }
                }
            }
            Expr::Binary { op, lhs, rhs, pos } => {
                let (le, lt) = self.lower_expr(lhs, scope);
                let (re, rt) = self.lower_expr(rhs, scope);
                self.lower_binary(*op, le, lt, re, rt, *pos)
            }
            Expr::Name {
                name,
                suffix,
                args,
                pos,
            } => self.lower_name_expr(name, suffix, args, *pos, scope),
        }
    }

    fn lower_binary(
        &mut self,
        op: BinOp,
        le: HExpr,
        lt: Ty,
        re: HExpr,
        rt: Ty,
        pos: SourcePos,
    ) -> (HExpr, Ty) {
        use hir::{HArith, HCmp, HLogic};
        match op {
            BinOp::Add => {
                if is_str(&lt) && is_str(&rt) {
                    (HExpr::Concat(Box::new(le), Box::new(re)), Ty::Str)
                } else if is_num(&lt) && is_num(&rt) {
                    self.arith(HArith::Add, le, &lt, re, &rt)
                } else {
                    self.err(pos, "Type mismatch");
                    (HExpr::Int(0), Ty::Unknown)
                }
            }
            BinOp::Sub | BinOp::Mul => {
                if !is_num(&lt) || !is_num(&rt) {
                    self.err(pos, "Type mismatch");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                let a = if op == BinOp::Sub {
                    HArith::Sub
                } else {
                    HArith::Mul
                };
                self.arith(a, le, &lt, re, &rt)
            }
            BinOp::Div => {
                if !is_num(&lt) || !is_num(&rt) {
                    self.err(pos, "Type mismatch");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                // `/`: DOUBLE bei DOUBLE-/CURRENCY-Operand, sonst SINGLE.
                let out = if matches!(num_ty(&lt), NumTy::Dbl | NumTy::Cur)
                    || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur)
                {
                    NumTy::Dbl
                } else {
                    NumTy::Sng
                };
                let l = self.conv_num(le, &lt, out);
                let r = self.conv_num(re, &rt, out);
                (
                    HExpr::Bin {
                        op: HArith::Div,
                        ty: out,
                        l: Box::new(l),
                        r: Box::new(r),
                    },
                    ty_of_num(out),
                )
            }
            BinOp::Pow => {
                if !is_num(&lt) || !is_num(&rt) {
                    self.err(pos, "Type mismatch");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                // `^` rechnet in DOUBLE; Ergebnis SINGLE außer bei
                // DOUBLE-/CURRENCY-Operand (Festlegung: Matrix in
                // docs/tbvm-design.md; Verifikation gegen die
                // Original-Hilfe ist in PLAN.md Phase 3 eingeplant).
                let out = if matches!(num_ty(&lt), NumTy::Dbl | NumTy::Cur)
                    || matches!(num_ty(&rt), NumTy::Dbl | NumTy::Cur)
                {
                    NumTy::Dbl
                } else {
                    NumTy::Sng
                };
                let l = self.conv_num(le, &lt, NumTy::Dbl);
                let r = self.conv_num(re, &rt, NumTy::Dbl);
                let p = HExpr::Bin {
                    op: HArith::Pow,
                    ty: NumTy::Dbl,
                    l: Box::new(l),
                    r: Box::new(r),
                };
                if out == NumTy::Dbl {
                    (p, Ty::Dbl)
                } else {
                    (
                        HExpr::Conv {
                            from: NumTy::Dbl,
                            to: NumTy::Sng,
                            arg: Box::new(p),
                        },
                        Ty::Sng,
                    )
                }
            }
            BinOp::IntDiv | BinOp::Mod => {
                if !is_num(&lt) || !is_num(&rt) {
                    self.err(pos, "Type mismatch");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                // Operanden vorab auf Ganzzahl gerundet; INTEGER nur wenn
                // beide Operanden INTEGER sind.
                let out = if num_ty(&lt) == NumTy::Int && num_ty(&rt) == NumTy::Int {
                    NumTy::Int
                } else {
                    NumTy::Lng
                };
                let l = self.conv_num(le, &lt, out);
                let r = self.conv_num(re, &rt, out);
                let a = if op == BinOp::IntDiv {
                    HArith::IDiv
                } else {
                    HArith::Mod
                };
                (
                    HExpr::Bin {
                        op: a,
                        ty: out,
                        l: Box::new(l),
                        r: Box::new(r),
                    },
                    ty_of_num(out),
                )
            }
            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
                let hop = match op {
                    BinOp::Eq => HCmp::Eq,
                    BinOp::Ne => HCmp::Ne,
                    BinOp::Lt => HCmp::Lt,
                    BinOp::Le => HCmp::Le,
                    BinOp::Gt => HCmp::Gt,
                    _ => HCmp::Ge,
                };
                if is_str(&lt) && is_str(&rt) {
                    (
                        HExpr::Cmp {
                            op: hop,
                            ty: hir::CmpKind::Str,
                            l: Box::new(le),
                            r: Box::new(re),
                        },
                        Ty::Int,
                    )
                } else if is_num(&lt) && is_num(&rt) {
                    let common = promote_num(num_ty(&lt), num_ty(&rt));
                    let l = self.conv_num(le, &lt, common);
                    let r = self.conv_num(re, &rt, common);
                    (
                        HExpr::Cmp {
                            op: hop,
                            ty: hir::CmpKind::Num(common),
                            l: Box::new(l),
                            r: Box::new(r),
                        },
                        Ty::Int,
                    )
                } else {
                    self.err(pos, "Type mismatch");
                    (HExpr::Int(0), Ty::Int)
                }
            }
            BinOp::And | BinOp::Or | BinOp::Xor | BinOp::Eqv | BinOp::Imp => {
                if !is_num(&lt) || !is_num(&rt) {
                    self.err(pos, "Type mismatch");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                let hop = match op {
                    BinOp::And => HLogic::And,
                    BinOp::Or => HLogic::Or,
                    BinOp::Xor => HLogic::Xor,
                    BinOp::Eqv => HLogic::Eqv,
                    _ => HLogic::Imp,
                };
                // INTEGER-Ergebnis nur wenn beide Operanden INTEGER sind.
                let kind = if num_ty(&lt) == NumTy::Int && num_ty(&rt) == NumTy::Int {
                    IntKind::I2
                } else {
                    IntKind::I4
                };
                let target = if kind == IntKind::I2 {
                    NumTy::Int
                } else {
                    NumTy::Lng
                };
                let l = self.conv_num(le, &lt, target);
                let r = self.conv_num(re, &rt, target);
                (
                    HExpr::Logic {
                        op: hop,
                        ty: kind,
                        l: Box::new(l),
                        r: Box::new(r),
                    },
                    if kind == IntKind::I2 {
                        Ty::Int
                    } else {
                        Ty::Lng
                    },
                )
            }
        }
    }

    /// Arithmetik `+ - *` im promoteten Typ.
    fn arith(&mut self, op: hir::HArith, le: HExpr, lt: &Ty, re: HExpr, rt: &Ty) -> (HExpr, Ty) {
        if *lt == Ty::Unknown || *rt == Ty::Unknown {
            return (le, Ty::Unknown);
        }
        let out = promote_num(num_ty(lt), num_ty(rt));
        let l = self.conv_num(le, lt, out);
        let r = self.conv_num(re, rt, out);
        (
            HExpr::Bin {
                op,
                ty: out,
                l: Box::new(l),
                r: Box::new(r),
            },
            ty_of_num(out),
        )
    }

    fn lower_name_expr(
        &mut self,
        name: &str,
        suffix: &Option<Suffix>,
        args: &Option<Vec<Expr>>,
        pos: SourcePos,
        scope: &mut Scope,
    ) -> (HExpr, Ty) {
        if name == "CLIPBOARD.GETTEXT" && args.as_ref().is_none_or(Vec::is_empty) {
            return (
                HExpr::Builtin {
                    b: Builtin::ClipboardGet,
                    args: Vec::new(),
                    ret: HTy::Str,
                },
                Ty::Str,
            );
        }
        if name == "SCREEN" && args.is_some() {
            return self.lower_builtin_fn(name, args.as_deref().unwrap(), scope, pos);
        }
        if suffix.is_none() && args.is_none() && !name.contains('.') && !name.contains('!') {
            if let Some((object, property, spec)) = self.implicit_form_property(scope, name) {
                return (
                    HExpr::ObjectProperty {
                        object,
                        index: None,
                        property,
                        ty: self.h_ty(&Self::property_ty(spec)),
                    },
                    Self::property_ty(spec),
                );
            }
        }
        if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name) {
            if name.contains('!') && !name.contains('.') {
                return match self.object_member_target(name, pos) {
                    Some((object, info, _)) => {
                        let Ok(index) = self.object_index(object, args, scope, pos) else {
                            return (HExpr::Int(0), Ty::Unknown);
                        };
                        let ty = if info.class == ObjectClass::Form {
                            Ty::Form
                        } else {
                            Ty::Control
                        };
                        (
                            HExpr::ObjectRef {
                                object,
                                index: index.map(Box::new),
                                class: info.class,
                            },
                            ty,
                        )
                    }
                    None => (HExpr::Int(0), Ty::Unknown),
                };
            }
            if args.is_none() {
                match self.dynamic_property(scope, name, pos) {
                    Ok(Some((object, spec))) => {
                        let ty = Self::property_ty(spec);
                        return (
                            HExpr::DynamicObjectProperty {
                                object: Box::new(object),
                                property: spec.name.to_string(),
                            },
                            ty,
                        );
                    }
                    Err(()) => return (HExpr::Int(0), Ty::Unknown),
                    Ok(None) => {}
                }
            }
            let Some((object, info, member)) = self.object_member_target(name, pos) else {
                return (HExpr::Int(0), Ty::Unknown);
            };
            if let Some((property, spec)) = forms::property(info.class, &member) {
                if spec.name == "LIST" || spec.ty == PropertyType::IntegerArray {
                    let Some(indices) = args else {
                        self.err(pos, "Argument-count mismatch for LIST");
                        return (HExpr::Int(0), Ty::Unknown);
                    };
                    let expected = if info.array { 2 } else { 1 };
                    if indices.len() != expected {
                        self.err(pos, "Argument-count mismatch for LIST");
                        return (HExpr::Int(0), Ty::Unknown);
                    }
                    let object_index = info
                        .array
                        .then(|| Box::new(self.lower_num_as(&indices[0], scope, NumTy::Lng)));
                    let index = self.lower_num_as(&indices[expected - 1], scope, NumTy::Lng);
                    let ty = Self::property_ty(spec);
                    return (
                        HExpr::ObjectIndexedProperty {
                            object,
                            object_index,
                            property,
                            index: Box::new(index),
                            ty: self.h_ty(&ty),
                        },
                        ty,
                    );
                }
                return {
                    let Ok(index) = self.object_index(object, args, scope, pos) else {
                        return (HExpr::Int(0), Ty::Unknown);
                    };
                    (
                        HExpr::ObjectProperty {
                            object,
                            index: index.map(Box::new),
                            property,
                            ty: self.h_ty(&Self::property_ty(spec)),
                        },
                        Self::property_ty(spec),
                    )
                };
            }
            if let Some(method) = forms::methods(info.class)
                .iter()
                .position(|m| m.eq_ignore_ascii_case(&member))
            {
                let Some(return_type) =
                    forms::method_return_type(info.class, forms::methods(info.class)[method])
                else {
                    self.err(pos, format!("Method '{member}' has no return value"));
                    return (HExpr::Int(0), Ty::Unknown);
                };
                let supplied = args.as_deref().unwrap_or(&[]);
                let (index, actual) = if info.array {
                    let Some((index, actual)) = supplied.split_first() else {
                        self.err(pos, format!("Object '{}' requires an index", info.name));
                        return (HExpr::Int(0), Ty::Unknown);
                    };
                    (
                        Some(Box::new(self.lower_num_as(index, scope, NumTy::Lng))),
                        actual,
                    )
                } else {
                    (None, supplied)
                };
                let (min, max) =
                    forms::method_arity(info.class, forms::methods(info.class)[method]).unwrap();
                if !(min..=max).contains(&actual.len()) {
                    self.err(
                        pos,
                        format!("Argument-count mismatch for {}.{member}", info.class.name()),
                    );
                    return (HExpr::Int(0), Ty::Unknown);
                }
                let hargs = actual
                    .iter()
                    .map(|arg| self.lower_expr(arg, scope).0)
                    .collect();
                let ty = Self::property_ty(forms::PropertySpec {
                    name: "",
                    ty: return_type,
                    default: forms::PropertyDefault::Empty,
                    min: None,
                    max: None,
                    writable: false,
                });
                return (
                    HExpr::ObjectMethodCall {
                        object,
                        index,
                        method: method as u16,
                        args: hargs,
                        ty: self.h_ty(&ty),
                    },
                    ty,
                );
            }
            self.err(
                pos,
                format!(
                    "Unknown property or method '{member}' of {}",
                    info.class.name()
                ),
            );
            return (HExpr::Int(0), Ty::Unknown);
        }
        if let Some((object, info)) = self
            .find_object(name)
            .filter(|_| suffix.is_none())
            .map(|(id, info)| (id, info.clone()))
        {
            if args.is_none() || info.array {
                if let Some(r) = &mut self.references {
                    r.objects.push((pos, name.into(), object));
                }
                let Ok(index) = self.object_index(object, args, scope, pos) else {
                    return (HExpr::Int(0), Ty::Unknown);
                };
                let ty = if info.class == ObjectClass::Form {
                    Ty::Form
                } else {
                    Ty::Control
                };
                return (
                    HExpr::ObjectRef {
                        object,
                        index: index.map(Box::new),
                        class: info.class,
                    },
                    ty,
                );
            }
        }
        let full_name = match suffix {
            Some(s) => format!("{name}{}", s.as_char()),
            None => name.to_string(),
        };

        // 1. Konstante
        if let Some((t, v)) = self.consts.get(name).cloned() {
            if args.is_some() {
                self.err(pos, "Syntax error");
            }
            let e = match (&t, v) {
                (_, Some(ConstVal::Str(s))) => HExpr::Str(s),
                (Ty::Int, Some(ConstVal::Num(n))) => HExpr::Int(n as i16),
                (Ty::Lng, Some(ConstVal::Num(n))) => HExpr::Lng(n as i32),
                (Ty::Sng, Some(ConstVal::Num(n))) => HExpr::Sng(n as f32),
                (Ty::Cur, Some(ConstVal::Num(n))) => HExpr::Cur((n * 10_000.0).round() as i64),
                (_, Some(ConstVal::Num(n))) => HExpr::Dbl(n),
                _ => HExpr::Int(0),
            };
            return (e, t);
        }

        match args {
            Some(idx) => {
                // 2. Deklariertes Array
                let as_key = format!("{name}\u{1}AS");
                let key = self.var_key(name, suffix, false);
                let existing = if suffix.is_none() {
                    self.visible_var(scope, &as_key)
                        .or_else(|| self.visible_var(scope, &key))
                } else {
                    self.visible_var(scope, &key).or_else(|| {
                        self.visible_var(scope, &as_key)
                            .filter(|v| v.ty == suffix_ty(suffix.unwrap()))
                    })
                };
                let existing = existing.cloned();
                if let Some(v) = existing {
                    if v.array {
                        let (place, ty) = self.lower_place(
                            &Expr::Name {
                                name: name.to_string(),
                                suffix: *suffix,
                                args: Some(idx.clone()),
                                pos,
                            },
                            scope,
                        );
                        let e = match place {
                            Some(p) => HExpr::Load(Box::new(p)),
                            None => HExpr::Int(0),
                        };
                        return (e, ty);
                    }
                    // Rekursion: In einer FUNCTION ist der Name mit
                    // Argumentliste der (rekursive) Aufruf, ohne Argumente
                    // die Rückgabevariable.
                    let is_fn_call = self
                        .procs
                        .get(name)
                        .map(|p| p.kind == ProcKind::Function)
                        .unwrap_or(false);
                    if !is_fn_call {
                        self.err(pos, "Duplicate definition");
                        return (HExpr::Int(0), Ty::Unknown);
                    }
                }
                // 3. Nicht unterstützte Features → Compile-Fehler
                if banned_feature(&full_name) {
                    for a in idx {
                        self.lower_expr(a, scope);
                    }
                    self.err(pos, format!("Feature unavailable: {full_name}"));
                    return (HExpr::Int(0), Ty::Unknown);
                }
                // 4. Builtin-Funktion
                if builtin_fn(&full_name).is_some() {
                    return self.lower_builtin_fn(&full_name, idx, scope, pos);
                }
                // 5. FUNCTION / DEF FN
                if let Some(info) = self.procs.get(name).cloned() {
                    if info.kind == ProcKind::Function {
                        if idx.len() != info.params.len() {
                            self.err(pos, "Argument-count mismatch");
                        }
                        let hargs = self.lower_call_args(&info, idx, scope);
                        let ret = info.ret.clone();
                        return (
                            HExpr::FnCall {
                                proc: info.id,
                                args: hargs,
                                ret: self.h_ty(&ret),
                            },
                            ret,
                        );
                    }
                    self.err(pos, "Duplicate definition");
                    return (HExpr::Int(0), Ty::Unknown);
                }
                // 6. Implizites Array (klassisch: DIM x(10) implizit)
                if self.explicit {
                    for a in idx {
                        self.want_num(a, scope);
                    }
                    self.err(pos, "Variable not defined");
                    return (HExpr::Int(0), self.name_ty(name, suffix));
                }
                let (place, ty) = self.lower_place(
                    &Expr::Name {
                        name: name.to_string(),
                        suffix: *suffix,
                        args: Some(idx.clone()),
                        pos,
                    },
                    scope,
                );
                let e = match place {
                    Some(p) => HExpr::Load(Box::new(p)),
                    None => HExpr::Int(0),
                };
                (e, ty)
            }
            None => {
                // 2. Parameterlose Builtins (RND, ERR, INKEY$, TIMER …)
                let as_key = format!("{name}\u{1}AS");
                let key = self.var_key(name, suffix, false);
                let declared = self.visible_var(scope, &key).is_some()
                    || self
                        .visible_var(scope, &as_key)
                        .is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
                if !declared {
                    if banned_feature(&full_name) {
                        self.err(pos, format!("Feature unavailable: {full_name}"));
                        return (HExpr::Int(0), Ty::Unknown);
                    }
                    if let Some((0, _, _, _)) = builtin_fn(&full_name) {
                        return self.lower_builtin_fn(&full_name, &[], scope, pos);
                    }
                    if let Some(info) = self.procs.get(name).cloned() {
                        if info.kind == ProcKind::Function && info.params.is_empty() {
                            let ret = info.ret.clone();
                            return (
                                HExpr::FnCall {
                                    proc: info.id,
                                    args: Vec::new(),
                                    ret: self.h_ty(&ret),
                                },
                                ret,
                            );
                        }
                    }
                }
                // 3. Variable (ggf. implizit deklarieren)
                let (place, ty) = self.lower_place(
                    &Expr::Name {
                        name: name.to_string(),
                        suffix: *suffix,
                        args: None,
                        pos,
                    },
                    scope,
                );
                let e = match place {
                    Some(p) => HExpr::Load(Box::new(p)),
                    None => HExpr::Int(0),
                };
                (e, ty)
            }
        }
    }

    /// Builtin-Funktionsaufruf absenken (inkl. Argument-Konvertierungen).
    fn lower_builtin_fn(
        &mut self,
        full_name: &str,
        args: &[Expr],
        scope: &mut Scope,
        pos: SourcePos,
    ) -> (HExpr, Ty) {
        let (min, max, spec, retk) = builtin_fn(full_name).unwrap();

        // LBOUND/UBOUND brauchen das Array selbst.
        if full_name == "LBOUND" || full_name == "UBOUND" {
            if args.is_empty() || args.len() > 2 {
                self.err(pos, "Argument-count mismatch");
                return (HExpr::Lng(0), Ty::Lng);
            }
            let place = if let Expr::Name {
                name,
                suffix,
                args: idx,
                pos,
            } = &args[0]
            {
                if idx.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
                    self.resolve_var(scope, name, suffix, true, *pos)
                        .map(|v| HPlace {
                            base: if v.global {
                                VarSlot::Global(v.slot)
                            } else {
                                VarSlot::Local(v.slot)
                            },
                            base_is_ref: false,
                            indices: Vec::new(),
                            fields: Vec::new(),
                            ty: self.h_ty(&v.ty),
                            array_elem: None,
                        })
                } else {
                    None
                }
            } else {
                None
            };
            let dim = match args.get(1) {
                Some(d) => self.lower_num_as(d, scope, NumTy::Lng),
                None => HExpr::Lng(1),
            };
            match place {
                Some(p) => {
                    return (
                        HExpr::ArrayBound {
                            lower: full_name == "LBOUND",
                            place: Box::new(p),
                            dim: Box::new(dim),
                        },
                        Ty::Lng,
                    )
                }
                None => {
                    self.err(args[0].pos(), "Type mismatch");
                    return (HExpr::Lng(0), Ty::Lng);
                }
            }
        }

        let lowered =
            self.check_and_lower_builtin_args(full_name, args, min, max, spec, scope, pos);
        let get = |i: usize| -> Option<&(HExpr, Ty)> { lowered.get(i) };
        let take = |l: &[(HExpr, Ty)], i: usize| -> (HExpr, Ty) {
            l.get(i).cloned().unwrap_or((HExpr::Int(0), Ty::Unknown))
        };

        macro_rules! conv_arg {
            ($i:expr, $to:expr) => {{
                let (e, t) = take(&lowered, $i);
                if t == Ty::Unknown {
                    e
                } else {
                    self.conv_num(e, &t, $to)
                }
            }};
        }

        let b = |b: Builtin, args: Vec<HExpr>, ret: Ty, s: &Sema| -> (HExpr, Ty) {
            let rty = s.h_ty(&ret);
            (HExpr::Builtin { b, args, ret: rty }, ret)
        };

        match full_name {
            // --- Strings ---
            "LEN" => {
                let (e, t) = take(&lowered, 0);
                if is_str(&t) && t != Ty::Unknown {
                    b(Builtin::Len, vec![e], Ty::Int, self)
                } else if let Some(size) = self.fixed_width(&self.h_ty(&t)) {
                    (HExpr::Int(size), Ty::Int)
                } else {
                    // LEN(zahlvariable) = Bytegröße des Typs
                    let size: i16 = match num_ty(&t) {
                        NumTy::Int => 2,
                        NumTy::Lng | NumTy::Sng => 4,
                        NumTy::Cur | NumTy::Dbl => 8,
                    };
                    (HExpr::Int(size), Ty::Int)
                }
            }
            "LEFT$" => {
                let (s, _) = take(&lowered, 0);
                let n = conv_arg!(1, NumTy::Lng);
                b(Builtin::LeftS, vec![s, n], Ty::Str, self)
            }
            "RIGHT$" => {
                let (s, _) = take(&lowered, 0);
                let n = conv_arg!(1, NumTy::Lng);
                b(Builtin::RightS, vec![s, n], Ty::Str, self)
            }
            "MID$" => {
                let (s, _) = take(&lowered, 0);
                let start = conv_arg!(1, NumTy::Lng);
                let len = if get(2).is_some() {
                    conv_arg!(2, NumTy::Lng)
                } else {
                    HExpr::Lng(-1)
                };
                b(Builtin::MidS, vec![s, start, len], Ty::Str, self)
            }
            "INSTR" => {
                let (start, s, t) = if args.len() == 3 {
                    let st = conv_arg!(0, NumTy::Lng);
                    (st, take(&lowered, 1).0, take(&lowered, 2).0)
                } else {
                    (HExpr::Lng(1), take(&lowered, 0).0, take(&lowered, 1).0)
                };
                b(Builtin::InstrF, vec![start, s, t], Ty::Int, self)
            }
            "UCASE$" => b(Builtin::UcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
            "LCASE$" => b(Builtin::LcaseS, vec![take(&lowered, 0).0], Ty::Str, self),
            "LTRIM$" => b(Builtin::LtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
            "RTRIM$" => b(Builtin::RtrimS, vec![take(&lowered, 0).0], Ty::Str, self),
            "SPACE$" => {
                let n = conv_arg!(0, NumTy::Lng);
                b(Builtin::SpaceS, vec![n], Ty::Str, self)
            }
            "STRING$" => {
                let n = conv_arg!(0, NumTy::Lng);
                let (ch, cht) = take(&lowered, 1);
                let ch = if is_num(&cht) && cht != Ty::Unknown {
                    self.conv_num(ch, &cht, NumTy::Lng)
                } else {
                    ch
                };
                b(Builtin::StringS, vec![n, ch], Ty::Str, self)
            }
            "CHR$" => {
                let n = conv_arg!(0, NumTy::Lng);
                b(Builtin::ChrS, vec![n], Ty::Str, self)
            }
            "ASC" => b(Builtin::Asc, vec![take(&lowered, 0).0], Ty::Int, self),
            "STR$" => {
                // Typ bleibt erhalten (Formatierung je Tag).
                b(Builtin::StrS, vec![take(&lowered, 0).0], Ty::Str, self)
            }
            "VAL" => b(Builtin::Val, vec![take(&lowered, 0).0], Ty::Dbl, self),
            "HEX$" => {
                let n = conv_arg!(0, NumTy::Lng);
                b(Builtin::HexS, vec![n], Ty::Str, self)
            }
            "OCT$" => {
                let n = conv_arg!(0, NumTy::Lng);
                b(Builtin::OctS, vec![n], Ty::Str, self)
            }
            // --- Konvertierungsfunktionen → Conv-Knoten ---
            "CINT" | "CLNG" | "CSNG" | "CDBL" | "CCUR" => {
                let (e, t) = take(&lowered, 0);
                let to = match full_name {
                    "CINT" => NumTy::Int,
                    "CLNG" => NumTy::Lng,
                    "CSNG" => NumTy::Sng,
                    "CDBL" => NumTy::Dbl,
                    _ => NumTy::Cur,
                };
                let e = if t == Ty::Unknown {
                    e
                } else {
                    self.conv_num(e, &t, to)
                };
                (e, ty_of_num(to))
            }
            // --- Mathematik ---
            "ABS" | "FIX" | "INT" => {
                let (e, t) = take(&lowered, 0);
                let bt = match full_name {
                    "ABS" => Builtin::Abs,
                    "FIX" => Builtin::Fix,
                    _ => Builtin::IntF,
                };
                // Ergebnistyp = Operandentyp (Vorbild).
                let rt = if t == Ty::Unknown {
                    Ty::Sng
                } else {
                    ty_of_num(num_ty(&t))
                };
                b(bt, vec![e], rt, self)
            }
            "SGN" => b(Builtin::Sgn, vec![take(&lowered, 0).0], Ty::Int, self),
            "SQR" | "EXP" | "LOG" | "SIN" | "COS" | "TAN" | "ATN" => {
                let e = conv_arg!(0, NumTy::Dbl);
                let bt = match full_name {
                    "SQR" => Builtin::Sqr,
                    "EXP" => Builtin::Exp,
                    "LOG" => Builtin::Log,
                    "SIN" => Builtin::Sin,
                    "COS" => Builtin::Cos,
                    "TAN" => Builtin::Tan,
                    _ => Builtin::Atn,
                };
                b(bt, vec![e], Ty::Dbl, self)
            }
            "RND" => {
                let a = if !lowered.is_empty() {
                    vec![conv_arg!(0, NumTy::Sng)]
                } else {
                    vec![]
                };
                b(Builtin::Rnd, a, Ty::Sng, self)
            }
            // --- Datei-E/A ---
            "EOF" | "LOF" | "LOC" | "SEEK" | "FILEATTR" => {
                let f = conv_arg!(0, NumTy::Lng);
                match full_name {
                    "EOF" => b(Builtin::EofF, vec![f], Ty::Int, self),
                    "LOF" => b(Builtin::LofF, vec![f], Ty::Lng, self),
                    "LOC" => b(Builtin::LocF, vec![f], Ty::Lng, self),
                    "SEEK" => b(Builtin::SeekF, vec![f], Ty::Lng, self),
                    _ => {
                        let art = conv_arg!(1, NumTy::Lng);
                        b(Builtin::Fileattr, vec![f, art], Ty::Lng, self)
                    }
                }
            }
            "FREEFILE" => b(Builtin::Freefile, vec![], Ty::Int, self),
            // --- ISAM-Funktionen ---
            "GETINDEX$" => {
                let f = conv_arg!(0, NumTy::Lng);
                b(Builtin::IsamGetIndexS, vec![f], Ty::Str, self)
            }
            "BOF" => {
                let f = conv_arg!(0, NumTy::Lng);
                b(Builtin::IsamBof, vec![f], Ty::Int, self)
            }
            "SAVEPOINT" => b(Builtin::IsamSavepoint, vec![], Ty::Int, self),
            "SETMEM" => {
                let n = conv_arg!(0, NumTy::Lng);
                b(Builtin::IsamSetmem, vec![n], Ty::Lng, self)
            }
            "ENVIRON$" => b(Builtin::EnvironS, vec![take(&lowered, 0).0], Ty::Str, self),
            "FRE" => b(Builtin::Fre, vec![], Ty::Lng, self),
            "STACK" => b(Builtin::StackFn, vec![], Ty::Lng, self),
            "ERDEV" => b(Builtin::Erdev, vec![], Ty::Int, self),
            "ERDEV$" => b(Builtin::ErdevS, vec![], Ty::Str, self),
            "CURDIR$" => b(Builtin::CurdirS, vec![], Ty::Str, self),
            "DIR$" => {
                let m = if lowered.is_empty() {
                    HExpr::Str(String::new())
                } else {
                    take(&lowered, 0).0
                };
                b(Builtin::DirS, vec![m], Ty::Str, self)
            }
            "LPOS" => b(Builtin::Lpos, vec![], Ty::Int, self),
            "MKI$" | "MKL$" | "MKS$" | "MKD$" | "MKC$" | "MKSMBF$" | "MKDMBF$" => {
                // Zweites Argument wählt Breite und Typ.
                let art = match full_name {
                    "MKI$" => 0,
                    "MKL$" => 1,
                    "MKS$" | "MKSMBF$" => 2,
                    "MKD$" | "MKDMBF$" => 3,
                    _ => 4,
                };
                let wert = take(&lowered, 0).0;
                b(Builtin::MkS, vec![wert, HExpr::Lng(art)], Ty::Str, self)
            }
            "CVI" | "CVL" | "CVS" | "CVD" | "CVC" | "CVSMBF" | "CVDMBF" => {
                let art = match full_name {
                    "CVI" => 0,
                    "CVL" => 1,
                    "CVS" | "CVSMBF" => 2,
                    "CVD" | "CVDMBF" => 3,
                    _ => 4,
                };
                let ret = match art {
                    0 => Ty::Int,
                    1 => Ty::Lng,
                    2 => Ty::Sng,
                    3 => Ty::Dbl,
                    _ => Ty::Cur,
                };
                let s = take(&lowered, 0).0;
                b(Builtin::CvF, vec![s, HExpr::Lng(art)], ret, self)
            }
            "SHELL" => {
                let c = take(&lowered, 0).0;
                b(Builtin::ShellFn, vec![c], Ty::Lng, self)
            }
            // --- Finanzmathematik ---
            "FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
                let a: Vec<HExpr> = (0..5).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
                let bt = match full_name.trim_end_matches('#') {
                    "FV" => Builtin::Fv,
                    "PV" => Builtin::Pv,
                    "PMT" => Builtin::Pmt,
                    _ => Builtin::NPer,
                };
                b(bt, a, Ty::Dbl, self)
            }
            "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
                let a: Vec<HExpr> = (0..6).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
                let bt = match full_name.trim_end_matches('#') {
                    "IPMT" => Builtin::IPmt,
                    "PPMT" => Builtin::PPmt,
                    _ => Builtin::Rate,
                };
                b(bt, a, Ty::Dbl, self)
            }
            "NPV" | "NPV#" => {
                let zins = conv_arg!(0, NumTy::Dbl);
                let reihe = take(&lowered, 1).0;
                b(Builtin::Npv, vec![zins, reihe], Ty::Dbl, self)
            }
            "IRR" | "IRR#" => {
                let reihe = take(&lowered, 0).0;
                let schaetzung = conv_arg!(1, NumTy::Dbl);
                b(Builtin::Irr, vec![reihe, schaetzung], Ty::Dbl, self)
            }
            "MIRR" | "MIRR#" => {
                let reihe = take(&lowered, 0).0;
                let f = conv_arg!(1, NumTy::Dbl);
                let w = conv_arg!(2, NumTy::Dbl);
                b(Builtin::Mirr, vec![reihe, f, w], Ty::Dbl, self)
            }
            "SLN" | "SLN#" => {
                let a: Vec<HExpr> = (0..3).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
                b(Builtin::Sln, a, Ty::Dbl, self)
            }
            "SYD" | "SYD#" | "DDB" | "DDB#" => {
                let a: Vec<HExpr> = (0..4).map(|i| conv_arg!(i, NumTy::Dbl)).collect();
                let bt = if full_name.starts_with("SYD") {
                    Builtin::Syd
                } else {
                    Builtin::Ddb
                };
                b(bt, a, Ty::Dbl, self)
            }
            "NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
            "TIMEZONEKNOWN" => b(Builtin::TimezoneKnown, vec![], Ty::Int, self),
            "DATESERIAL" | "TIMESERIAL" => {
                let x = conv_arg!(0, NumTy::Lng);
                let y = conv_arg!(1, NumTy::Lng);
                let z = conv_arg!(2, NumTy::Lng);
                let bt = if full_name == "DATESERIAL" {
                    Builtin::DateSerial
                } else {
                    Builtin::TimeSerial
                };
                b(bt, vec![x, y, z], Ty::Dbl, self)
            }
            "DATEVALUE" => b(Builtin::DateValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
            "TIMEVALUE" => b(Builtin::TimeValue, vec![take(&lowered, 0).0], Ty::Dbl, self),
            "DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => {
                let e = conv_arg!(0, NumTy::Dbl);
                let bt = match full_name {
                    "DAY" => Builtin::DayF,
                    "MONTH" => Builtin::MonthF,
                    "YEAR" => Builtin::YearF,
                    "WEEKDAY" => Builtin::WeekdayF,
                    "HOUR" => Builtin::HourF,
                    "MINUTE" => Builtin::MinuteF,
                    _ => Builtin::SecondF,
                };
                b(bt, vec![e], Ty::Int, self)
            }
            "FORMAT$" => {
                let mut a = vec![take(&lowered, 0).0];
                if get(1).is_some() {
                    a.push(take(&lowered, 1).0);
                }
                b(Builtin::FormatS, a, Ty::Str, self)
            }
            // --- Bildschirm ---
            "TAB" | "SPC" => {
                // In einer PRINT-Liste werden sie eigens abgesenkt; hier
                // stehen sie außerhalb und sind dort nicht zulässig.
                self.err(pos, format!("Illegal function call: {full_name}"));
                (HExpr::Str(String::new()), Ty::Str)
            }
            "CSRLIN" => b(Builtin::Csrlin, vec![], Ty::Int, self),
            "POS" => {
                // `POS(0)` — das Argument ist im Vorbild ein Dummy.
                b(Builtin::PosFn, vec![], Ty::Int, self)
            }
            "SCREEN" => {
                let zeile = conv_arg!(0, NumTy::Lng);
                let spalte = conv_arg!(1, NumTy::Lng);
                let farbe = if get(2).is_some() {
                    conv_arg!(2, NumTy::Lng)
                } else {
                    HExpr::Lng(0)
                };
                b(Builtin::ScreenFn, vec![zeile, spalte, farbe], Ty::Int, self)
            }
            "INKEY$" => b(Builtin::InkeyS, vec![], Ty::Str, self),
            "INPUT$" => {
                let n = conv_arg!(0, NumTy::Lng);
                let mut args = vec![n];
                if get(1).is_some() {
                    args.push(conv_arg!(1, NumTy::Lng));
                }
                b(Builtin::InputS, args, Ty::Str, self)
            }
            // --- Fehlerstatus ---
            "ERR" => (HExpr::Err, Ty::Lng),
            "ERL" => (HExpr::Erl, Ty::Lng),
            // --- Sonstiges (Phase-2-Scheibe) ---
            "TIMER" => b(Builtin::Timer, vec![], Ty::Sng, self),
            "DATE$" => b(Builtin::DateS, vec![], Ty::Str, self),
            "TIME$" => b(Builtin::TimeS, vec![], Ty::Str, self),
            "COMMAND$" => b(Builtin::CommandS, vec![], Ty::Str, self),
            "DOEVENTS" => b(Builtin::Doevents, vec![], Ty::Int, self),
            "MSGBOX" => b(
                Builtin::MsgBox,
                lowered.into_iter().map(|(expr, _)| expr).collect(),
                Ty::Int,
                self,
            ),
            "INPUTBOX$" => b(
                Builtin::InputBoxS,
                lowered.into_iter().map(|(expr, _)| expr).collect(),
                Ty::Str,
                self,
            ),
            // --- Spätere Phasen: dokumentiert, aber noch nicht verfügbar ---
            _ => (HExpr::Unsupported("Funktion"), ret_ty(retk)),
        }
    }
}

/// Position einer Anweisung (für Zeileninfo der Anweisungsgrenzen).
pub fn stmt_pos(stmt: &Stmt) -> SourcePos {
    match stmt {
        Stmt::Assign { pos, .. }
        | Stmt::Print { pos, .. }
        | Stmt::Input { pos, .. }
        | Stmt::If { pos, .. }
        | Stmt::Select { pos, .. }
        | Stmt::For { pos, .. }
        | Stmt::DoLoop { pos, .. }
        | Stmt::While { pos, .. }
        | Stmt::Goto { pos, .. }
        | Stmt::Gosub { pos, .. }
        | Stmt::OnGoto { pos, .. }
        | Stmt::Return { pos, .. }
        | Stmt::Exit { pos, .. }
        | Stmt::Dim { pos, .. }
        | Stmt::SharedDecl { pos, .. }
        | Stmt::StaticDecl { pos, .. }
        | Stmt::CommonDecl { pos, .. }
        | Stmt::Erase { pos, .. }
        | Stmt::ConstDecl { pos, .. }
        | Stmt::DefType { pos, .. }
        | Stmt::OptionStmt { pos, .. }
        | Stmt::TypeDecl { pos, .. }
        | Stmt::Declare { pos, .. }
        | Stmt::Call { pos, .. }
        | Stmt::OnError { pos, .. }
        | Stmt::Resume { pos, .. }
        | Stmt::ErrorStmt { pos, .. }
        | Stmt::Data { pos, .. }
        | Stmt::ReadStmt { pos, .. }
        | Stmt::Restore { pos, .. }
        | Stmt::DefFn { pos, .. }
        | Stmt::DefFnBlock { pos, .. }
        | Stmt::Open { pos, .. }
        | Stmt::OpenLegacy { pos, .. }
        | Stmt::CloseStmt { pos, .. }
        | Stmt::FieldStmt { pos, .. }
        | Stmt::GetPut { pos, .. }
        | Stmt::LsetRset { pos, .. }
        | Stmt::WriteStmt { pos, .. }
        | Stmt::SeekStmt { pos, .. }
        | Stmt::LockStmt { pos, .. }
        | Stmt::NameStmt { pos, .. }
        | Stmt::ViewPrint { pos, .. }
        | Stmt::GraphicsLine { pos, .. }
        | Stmt::GraphicsPaint { pos, .. }
        | Stmt::GraphicsView { pos, .. }
        | Stmt::TrapDef { pos, .. }
        | Stmt::EventControl { pos, .. }
        | Stmt::Include { pos, .. }
        | Stmt::MetaArrays { pos, .. }
        | Stmt::MetaForm { pos }
        | Stmt::End(pos)
        | Stmt::StopStmt(pos)
        | Stmt::System(pos) => *pos,
        Stmt::LineNumber(_, pos) => *pos,
        Stmt::Label(_) => SourcePos::default(),
    }
}

// ---- Ereignis-Traps (Sprachreferenz §8) -------------------------------------

pub const TRAP_KEY: u8 = 0;
pub const TRAP_TIMER: u8 = 1;
pub const TRAP_UEVENT: u8 = 2;
pub const TRAP_SIGNAL: u8 = 3;

/// Quellenname → Art. `None` für die Non-Features.
fn trap_art(device: &str) -> Option<u8> {
    Some(match device {
        "KEY" => TRAP_KEY,
        "TIMER" => TRAP_TIMER,
        "UEVENT" => TRAP_UEVENT,
        "SIGNAL" => TRAP_SIGNAL,
        _ => return None,
    })
}

/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
fn const_zahl(e: &Expr) -> Option<i64> {
    match e {
        Expr::IntLit(n, _) => Some(*n as i64),
        Expr::LongLit(n, _) => Some(*n as i64),
        Expr::SingleLit(n, _) => Some(*n as i64),
        Expr::DoubleLit(n, _) => Some(*n as i64),
        Expr::Paren(inner) => const_zahl(inner),
        _ => None,
    }
}

/// Wertebereiche nach der Original-Hilfe (Belege in `umfang-und-form.md`).
fn trap_bereich_ok(art: u8, n: i64) -> bool {
    match art {
        TRAP_KEY => matches!(n, 0..=25 | 30 | 31),
        TRAP_TIMER => (1..=86_400).contains(&n),
        TRAP_SIGNAL => (1..=2).contains(&n),
        _ => true,
    }
}

fn trap_bereich_text(art: u8) -> &'static str {
    match art {
        TRAP_KEY => "ON KEY: Kennung 0, 1-25 oder 30-31 erwartet",
        TRAP_TIMER => "ON TIMER: Intervall 1 bis 86400 Sekunden erwartet",
        TRAP_SIGNAL => "ON SIGNAL: Kennung 1 (SIGINT) oder 2 (SIGTERM) erwartet",
        _ => "Kennung erwartet",
    }
}

use crate::hir::literal_value;

#[cfg(test)]
mod tests {
    use crate::forms::{FormCatalog, ObjectClass};
    use crate::{analyze_source, analyze_source_with_forms};

    fn diags(src: &str) -> Vec<String> {
        analyze_source("TEST", src)
            .diagnostics
            .into_iter()
            .map(|d| d.message)
            .collect()
    }

    #[test]
    fn typkonflikt_wird_erkannt() {
        assert!(diags("a$ = 1").contains(&"Type mismatch".to_string()));
        assert!(diags("a% = \"x\"").contains(&"Type mismatch".to_string()));
        assert!(diags("a = 1 + \"x\"").contains(&"Type mismatch".to_string()));
        assert!(diags("s$ = \"a\" + \"b\"").is_empty());
    }

    #[test]
    fn option_explicit() {
        assert!(diags("OPTION EXPLICIT\nx = 1").contains(&"Variable not defined".to_string()));
        assert!(diags("OPTION EXPLICIT\nDIM x AS INTEGER\nx = 1").is_empty());
    }

    #[test]
    fn deftype_regeln() {
        assert!(diags("DEFSTR S\ns = 5").contains(&"Type mismatch".to_string()));
        assert!(diags("DEFINT I\ni = 5").is_empty());
    }

    #[test]
    fn unbekanntes_unterprogramm() {
        assert!(diags("Foo 1").contains(&"Subprogram not defined".to_string()));
        assert!(diags("SUB Foo (a%)\nEND SUB\n' Aufruf\nFoo 1").is_empty());
    }

    #[test]
    fn argumentanzahl() {
        assert!(diags("PRINT LEFT$(\"a\")").contains(&"Argument-count mismatch: LEFT$".to_string()));
        assert!(diags("SUB Foo (a%, b%)\nEND SUB\nFoo 1")
            .contains(&"Argument-count mismatch".to_string()));
    }

    #[test]
    fn hardware_features_zur_compilezeit_abgelehnt() {
        assert!(diags("POKE 100, 1").contains(&"Feature unavailable: POKE".to_string()));
        assert!(diags("x = PEEK(100)").contains(&"Feature unavailable: PEEK".to_string()));
        assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable: VARPTR".to_string()));
        assert!(diags("SOUND 440, 10").contains(&"Feature unavailable: SOUND".to_string()));
        assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable: CHAIN".to_string()));
        assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable: PLAY".to_string()));
        assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable: CIRCLE".to_string()));
    }

    #[test]
    fn label_pruefung() {
        assert!(diags("GOTO Nirwana").contains(&"Label not defined".to_string()));
        assert!(diags("Ziel:\nGOTO Ziel").is_empty());
        assert!(diags("10 PRINT 1\nGOTO 10").is_empty());
    }

    #[test]
    fn funktionsaufruf_und_rueckgabe() {
        let d = diags("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\ny = Quad(3)");
        assert!(d.is_empty(), "{d:?}");
    }

    #[test]
    fn arrays_implizit_und_explizit() {
        assert!(diags("DIM a(10)\na(1) = 2\nPRINT a(1)").is_empty());
        assert!(diags("b(3) = 1").is_empty()); // implizites Array
        assert!(diags("DIM c(5)\nDIM c(5)").contains(&"Duplicate definition".to_string()));
        assert!(diags("REDIM d(5)\nREDIM d(9)").is_empty());
    }

    #[test]
    fn builtins_typen() {
        assert!(diags("PRINT LEN(\"abc\")").is_empty());
        assert!(diags("PRINT MID$(\"abc\", 2, 1)").is_empty());
        assert!(diags("PRINT CHR$(\"x\")").contains(&"Type mismatch".to_string()));
        assert!(diags("x$ = INKEY$").is_empty());
        assert!(diags("t! = TIMER").is_empty());
        assert!(matches!(
            super::builtin_fn("TIMEZONEKNOWN"),
            Some((0, 0, [], super::RetK::I))
        ));
        assert!(diags("PRINT TIMEZONEKNOWN").is_empty());
        for name in ["TIMEZONEKNOWN(1)", "MKL$()"] {
            let d = diags(&format!("PRINT {name}"));
            assert!(
                d.iter().any(|m| m.contains("Argument-count mismatch")
                    && m.contains(name.split('(').next().unwrap())),
                "{d:?}"
            );
        }
    }

    #[test]
    fn mid_anweisung() {
        assert!(diags("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"").is_empty());
        assert!(diags("s$ = \"hallo\"\nMID$(s$, 2) = 5").contains(&"Type mismatch".to_string()));
    }

    #[test]
    fn def_fn_blockform() {
        let d = diags("DEF FNquad (x)\nFNquad = x * x\nEND DEF\ny = FNQUAD(3)");
        assert!(d.is_empty(), "{d:?}");
    }

    #[test]
    fn udt_feldtypen() {
        let src = "TYPE Kunde\nName AS STRING * 30\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Umsatz = 5\nk.Name = \"x\"";
        assert!(diags(src).is_empty(), "{:?}", diags(src));
        let bad = "TYPE Kunde\nUmsatz AS DOUBLE\nEND TYPE\nDIM k AS Kunde\nk.Gibtsnicht = 1";
        assert!(diags(bad).contains(&"Element not defined".to_string()));
        let bad2 = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM k AS Kunde\nk.Name = 5";
        assert!(diags(bad2).contains(&"Type mismatch".to_string()));
        let array = "TYPE Kunde\nName AS STRING * 30\nEND TYPE\nDIM SHARED k(1 TO 2) AS Kunde\nSUB Setzen\nk(1).Name = \"Ada\"\nPRINT k(1).Name\nEND SUB";
        assert!(diags(array).is_empty(), "{:?}", diags(array));
    }

    #[test]
    fn shared_und_common() {
        let src =
            "DIM SHARED zaehler%\nSUB Hoch\nSHARED zaehler%\nzaehler% = zaehler% + 1\nEND SUB";
        assert!(diags(src).is_empty(), "{:?}", diags(src));
        let src = "TYPE Satz\nWert AS INTEGER\nEND TYPE\nDIM SHARED r AS Satz\nSUB Hoch\nr.Wert = r.Wert + 1\nEND SUB";
        assert!(diags(src).is_empty(), "{:?}", diags(src));
        let src = "DIM SHARED datei AS INTEGER, name AS STRING\nSUB Nutzen\ndatei% = 1\nname$ = \"x\"\nEND SUB";
        assert!(diags(src).is_empty(), "{:?}", diags(src));
        assert!(diags("COMMON SHARED /blk/ a%, b$()").is_empty());
    }

    #[test]
    fn konstantenfaltung() {
        assert!(diags("CONST PI = 3.14159, ZWEI.PI = PI * 2").is_empty());
        assert!(diags("x = 1\nCONST K = x + 1").contains(&"Invalid constant".to_string()));
    }

    #[test]
    fn datei_ea_und_events() {
        let src = "OPEN \"test.dat\" FOR RANDOM AS #1 LEN = 64\nCLOSE #1\nOPEN \"o\", #2, \"f.txt\"\nCLOSE\nTIMER ON\nKEY(5) OFF";
        assert!(diags(src).is_empty(), "{:?}", diags(src));
        assert!(diags("PEN ON").contains(&"Feature unavailable: PEN".to_string()));
    }

    fn form_catalog() -> FormCatalog {
        let mut c = FormCatalog::default();
        c.add("Form1", ObjectClass::Form, None, false);
        c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
        c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
        c.add("List1", ObjectClass::ListBox, Some("Form1"), false);
        c.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
        c
    }

    fn form_diags(src: &str) -> Vec<String> {
        analyze_source_with_forms("FORM1", src, &form_catalog())
            .diagnostics
            .into_iter()
            .map(|d| d.message)
            .collect()
    }

    #[test]
    fn objektpfade_werden_namentlich_geprueft_und_udt_bleibt_unveraendert() {
        assert!(diags("Text9.Text = \"a\"")
            .iter()
            .any(|d| d.contains("TEXT9")));
        assert!(form_diags("Text1.Farbe = 3")
            .iter()
            .any(|d| d.contains("FARBE")));
        assert!(form_diags("Text1.Text = 5")
            .iter()
            .any(|d| d.contains("TEXT")));
        assert!(form_diags("x = Text1.ListCount")
            .iter()
            .any(|d| d.contains("LISTCOUNT")));
        assert!(diags("TYPE T\nF AS INTEGER\nEND TYPE\nDIM x AS T\nx.F = 1").is_empty());
    }

    #[test]
    fn eigenschaftstyp_schreibbarkeit_bang_und_hir_index() {
        let d = form_diags("Text1.Text = 5\nx = Text1.SelLength");
        assert!(
            d.iter()
                .any(|m| m.contains("Type mismatch for property 'TEXT'")),
            "{d:?}"
        );
        let d = form_diags("SCREEN.Width = 80");
        assert!(d.iter().any(|m| m.contains("read-only")), "{d:?}");
        assert!(form_diags("Form1!Text1.Text = \"a\"").is_empty());
        let d = form_diags("Text1 = \"x\"");
        assert!(
            d.iter().any(|m| m.contains("Text1") || m.contains("TEXT1")),
            "{d:?}"
        );
        assert!(form_diags("Command1(3).Caption = \"drei\"").is_empty());
        assert!(form_diags("zeichen% = SCREEN(1, 1)").is_empty());
        assert!(form_diags("SHOW\nHIDE").is_empty());
        assert!(form_diags("Caption = \"Titel\"\nx% = Width").is_empty());
        assert!(
            form_diags("CALL Aus(Text1)\nSUB Aus(c AS CONTROL)\nc.Visible = 0\nEND SUB").is_empty()
        );
        let a = analyze_source_with_forms("FORM1", "Text1.Text = \"a\"", &form_catalog());
        let h = a.hir.unwrap();
        assert!(matches!(
            h.procs[0].body[0].kind,
            HStmtKind::SetObjectProperty { object: 1, .. }
        ));
        let d = form_diags("Text1.Move 1, 2");
        assert!(d.is_empty(), "{d:?}");
        for src in ["Form1.Hide 1", "Form1.Show 1, 2", "SCREEN.Show 1"] {
            let d = form_diags(src);
            assert!(
                d.iter().any(|m| m.contains("Argument-count mismatch")),
                "{src}: {d:?}"
            );
        }
        let d = form_diags("Form1.Show \"modal\"");
        assert!(d.iter().any(|m| m.contains("Type mismatch")), "{d:?}");
    }

    #[test]
    fn typeof_form_control_und_form_metabefehl() {
        let d = form_diags("DIM Ziel AS CONTROL\nIF TYPEOF Ziel IS CommandButton THEN PRINT 1");
        assert!(d.is_empty(), "{d:?}");
        let a = analyze_source("MeinForm", "'$FORM\nMeinForm.Caption = \"x\"");
        assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
        assert!(a
            .hir
            .unwrap()
            .objects
            .iter()
            .any(|o| o.name == "MEINFORM" && o.class == ObjectClass::Form));
    }

    #[test]
    fn dialoge_listindex_und_zeichenmessung_werden_abgesenkt() {
        let source = "MSGBOX \"Hinweis\"\nantwort% = MSGBOX(\"Weiter?\", 4, \"Frage\")\nname$ = INPUTBOX$(\"Name\")\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT Picture1.TEXTWIDTH(\"abc\")";
        let diagnostics = form_diags(source);
        assert!(diagnostics.is_empty(), "{diagnostics:?}");
        let arrays = form_diags("Command1(1).SETFOCUS\nCommand1(1).MOVE 1, 2, 3, 4");
        assert!(arrays.is_empty(), "{arrays:?}");
    }

    #[test]
    fn ereignisprozedur_signatur_und_normale_unterstrich_sub() {
        let ok = "SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nEND SUB";
        assert!(form_diags(ok).is_empty(), "{:?}", form_diags(ok));
        let bad = form_diags("SUB Form_MouseDown(X AS SINGLE)\nEND SUB");
        assert!(
            bad.iter().any(|d| d.contains("BUTTON AS INTEGER")),
            "{bad:?}"
        );
        assert!(form_diags("SUB Zins_Berechnen(n AS INTEGER)\nEND SUB").is_empty());
        let array_bad = form_diags("SUB Command1_Click()\nEND SUB");
        assert!(
            array_bad.iter().any(|d| d.contains("INDEX AS INTEGER")),
            "{array_bad:?}"
        );
    }

    #[test]
    fn dreizeiler_wird_namentlich_statt_falsch_abgewiesen() {
        let d = diags("Text1.Text = \"hallo\"\nForm1.Show 1\nUNLOAD Form1");
        assert_eq!(d.len(), 3, "{d:?}");
        assert!(d[0].contains("TEXT1"));
        assert!(d[1].contains("FORM1"));
        assert!(d[2].contains("FORM1"));
    }

    // ---- HIR-Lowering ------------------------------------------------------

    use crate::hir::{HExpr, HStmtKind, NumTy};

    fn hir_of(src: &str) -> crate::hir::HirModule {
        let a = analyze_source("TEST", src);
        assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
        a.hir.expect("HIR erwartet")
    }

    #[test]
    fn hir_konvertierung_materialisiert() {
        // d# = i% + 1.5# → Conv(INTEGER→DOUBLE) unter der DOUBLE-Addition
        let h = hir_of("i% = 2\nd# = i% + 1.5#");
        let main = &h.procs[0];
        let HStmtKind::Assign { place, value } = &main.body[1].kind else {
            panic!("Assign erwartet: {:?}", main.body[1].kind);
        };
        assert_eq!(place.ty, crate::hir::HTy::Num(NumTy::Dbl));
        let HExpr::Bin { ty, l, .. } = value else {
            panic!("Bin erwartet: {value:?}");
        };
        assert_eq!(*ty, NumTy::Dbl);
        assert!(
            matches!(
                **l,
                HExpr::Conv {
                    from: NumTy::Int,
                    to: NumTy::Dbl,
                    ..
                }
            ),
            "Conv INTEGER→DOUBLE erwartet: {l:?}"
        );
    }

    #[test]
    fn hir_slots_aufgeloest() {
        let h = hir_of("a% = 1\nb% = a%");
        assert_eq!(h.globals.len(), 2);
        let HStmtKind::Assign { value, .. } = &h.procs[0].body[1].kind else {
            panic!();
        };
        assert!(matches!(value, HExpr::Load(_)));
    }

    #[test]
    fn hir_intdiv_rundet_operanden() {
        // 7.5 \ 2 → beide Operanden Conv → LONG (SINGLE-Operand)
        let h = hir_of("x& = 7.5 \\ 2");
        let HStmtKind::Assign { value, .. } = &h.procs[0].body[0].kind else {
            panic!();
        };
        let HExpr::Bin { op, ty, l, .. } = value else {
            panic!("Bin erwartet: {value:?}");
        };
        assert_eq!(*op, crate::hir::HArith::IDiv);
        assert_eq!(*ty, NumTy::Lng);
        assert!(matches!(**l, HExpr::Conv { to: NumTy::Lng, .. }));
    }

    #[test]
    fn hir_select_wird_abgesenkt() {
        let h = hir_of("SELECT CASE 2\nCASE 1\nPRINT \"a\"\nCASE ELSE\nPRINT \"b\"\nEND SELECT");
        // Temp-Zuweisung + If-Kette
        let main = &h.procs[0];
        assert!(matches!(main.body[0].kind, HStmtKind::Assign { .. }));
        assert!(matches!(main.body[1].kind, HStmtKind::If { .. }));
    }

    #[test]
    fn hir_byref_und_byval() {
        let h = hir_of("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nInc (n%)");
        let main = &h.procs[0];
        let calls: Vec<_> = main
            .body
            .iter()
            .filter_map(|s| match &s.kind {
                HStmtKind::CallSub { args, .. } => Some(args),
                _ => None,
            })
            .collect();
        assert_eq!(calls.len(), 2);
        assert!(matches!(calls[0][0], crate::hir::HArg::ByRef(_)));
        assert!(matches!(calls[1][0], crate::hir::HArg::ByVal(_)));
    }

    #[test]
    fn byref_verlangt_exakten_typ() {
        let d = diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo n%");
        assert!(
            d.contains(&"Parameter type mismatch".to_string()),
            "BYREF mit abweichendem Typ muss abgelehnt werden: {d:?}"
        );
        // BYVAL (Klammern) konvertiert stattdessen
        assert!(diags("SUB Foo (x&)\nEND SUB\nn% = 1\nFoo (n%)").is_empty());
    }

    // ---- ISAM (Change `phase-3-isam`) -----------------------------------

    /// Satztyp und geöffnete Tabelle als Vorspann für die ISAM-Tests.
    const ISAM_KOPF: &str = "TYPE KundeTyp\n         Nummer AS LONG\n         Name AS STRING * 20\n        END TYPE\n        DIM k AS KundeTyp\n        OPEN \"db\" FOR ISAM KundeTyp \"Kunden\" AS #1\n";

    /// Aufgabe 2.1: die Anweisung parst mit `#`-Dateinummer diagnose-frei.
    #[test]
    fn isam_anweisung_parst_ohne_diagnose() {
        let d = diags(&format!("{ISAM_KOPF}SETINDEX #1, \"Name\""));
        assert!(d.is_empty(), "SETINDEX soll diagnose-frei parsen: {d:?}");
        // Auch ohne `#` (die Original-Hilfe schreibt es optional).
        let d = diags(&format!("{ISAM_KOPF}SETINDEX 1, \"Name\""));
        assert!(d.is_empty(), "{d:?}");
    }

    /// Aufgabe 2.2: falsche Argumentanzahl wird gemeldet und nennt das
    /// Element (Guiding Principle).
    #[test]
    fn isam_argumentanzahl_nennt_das_element() {
        let d = diags(&format!("{ISAM_KOPF}SEEKGT #1"));
        assert!(
            d.iter().any(|m| m.contains("SEEKGT")),
            "Diagnose muss SEEKGT nennen: {d:?}"
        );
        assert!(
            d.iter().any(|m| m.contains("Argument-count mismatch")),
            "{d:?}"
        );
    }

    /// Aufgabe 2.3: das Satzargument wird gegen den Tabellentyp geprüft.
    #[test]
    fn isam_satzargument_wird_typgeprueft() {
        let d = diags(&format!("{ISAM_KOPF}DIM x AS STRING\nRETRIEVE #1, x$"));
        assert!(
            d.contains(&"Type mismatch".to_string()),
            "String statt Satzvariable muss auffallen: {d:?}"
        );
        // Ein anderer benutzerdefinierter Typ passt ebenfalls nicht.
        let quelle = format!(
            "TYPE Anderer\n z AS INTEGER\nEND TYPE\n{ISAM_KOPF}             DIM a AS Anderer\nRETRIEVE #1, a"
        );
        assert!(
            diags(&quelle).contains(&"Type mismatch".to_string()),
            "fremder Satztyp muss auffallen: {:?}",
            diags(&quelle)
        );
        // Der richtige Typ ist diagnose-frei.
        let d = diags(&format!("{ISAM_KOPF}RETRIEVE #1, k"));
        assert!(d.is_empty(), "{d:?}");
    }

    /// Aufgabe 2.5: kein ISAM-Element endet als unbekannter Bezeichner —
    /// ein Programm, das jedes Element genau einmal verwendet.
    #[test]
    fn kein_isam_element_ist_unbekannter_bezeichner() {
        let quelle = format!(
            "{ISAM_KOPF}             CREATEINDEX #1, \"NachName\", 0, \"Name\"\n             CREATEINDEX #1, \"Zwei\", 1, \"Name\", \"-Nummer\"\n             SETINDEX #1, \"NachName\"\n             i$ = GETINDEX$(1)\n             INSERT #1, k\n             MOVEFIRST #1\n             MOVELAST #1\n             MOVENEXT #1\n             MOVEPREVIOUS #1\n             SEEKEQ #1, \"a\"\n             SEEKGT #1, \"a\"\n             SEEKGE #1, \"a\"\n             RETRIEVE #1, k\n             UPDATE #1, k\n             DELETE #1\n             e% = EOF(1)\n             b% = BOF(1)\n             BEGINTRANS\n             s% = SAVEPOINT\n             ROLLBACK s%\n             ROLLBACK\n             ROLLBACK ALL\n             COMMITTRANS\n             m& = SETMEM(1024)\n             DELETEINDEX #1, \"NachName\"\n             CLOSE #1\n             DELETETABLE \"db\", \"Kunden\"\n"
        );
        let d = diags(&quelle);
        assert!(
            !d.iter().any(|m| m.contains("not defined")),
            "ISAM-Element wird als unbekannter Bezeichner abgewiesen: {d:?}"
        );
        assert!(d.is_empty(), "unerwartete Diagnosen: {d:?}");
    }

    /// `ROLLBACK ALL` senkt auf die Sentinel-Kennung ab und ist damit von
    /// `ROLLBACK kennung` unterscheidbar.
    #[test]
    fn rollback_all_wird_als_sentinel_abgesenkt() {
        let a = crate::analyze_source("TEST", "BEGINTRANS\nROLLBACK ALL");
        assert!(a.diagnostics.is_empty(), "{:?}", a.diagnostics);
        // `ALL` darf keine Variable werden — sonst zaehlte OPTION EXPLICIT es an.
        let d = diags("OPTION EXPLICIT\nBEGINTRANS\nROLLBACK ALL");
        assert!(d.is_empty(), "{d:?}");
    }

    /// `CREATEINDEX` ist in der Spaltenzahl unbegrenzt — die Original-Hilfe
    /// nennt keine Obergrenze, also darf die Signatur auch keine setzen.
    #[test]
    fn createindex_ohne_obergrenze_der_spaltenzahl() {
        let felder: String = (1..=12).map(|i| format!(" F{i} AS LONG\n")).collect();
        let spalten: String = (1..=12).map(|i| format!(", \"F{i}\"")).collect();
        let quelle = format!(
            "TYPE Breit\n{felder}END TYPE\n             DIM b AS Breit\n             OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n             CREATEINDEX #1, \"Viel\", 0{spalten}"
        );
        let d = diags(&quelle);
        assert!(
            d.is_empty(),
            "zwölfspaltiger Index muss zulässig sein: {d:?}"
        );
        // Ein Spaltenname muss trotzdem ein String sein.
        let quelle = format!(
            "TYPE Breit\n{felder}END TYPE\n             DIM b AS Breit\n             OPEN \"db\" FOR ISAM Breit \"Tab\" AS #1\n             CREATEINDEX #1, \"Viel\", 0, \"F1\", 42"
        );
        assert!(
            diags(&quelle).contains(&"Type mismatch".to_string()),
            "Zahl als Spaltenname muss auffallen: {:?}",
            diags(&quelle)
        );
    }
}
