// hir_lower.bux — HIR lowering: AST → HIR transformation (ported from hir_lower.nim)
// Transforms the typed AST into a lower-level IR suitable for code generation.
module HirLower {

    extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
    extern func bux_strlen(s: String) -> uint;

    // ---------------------------------------------------------------------------
    // Lowering context
    // ---------------------------------------------------------------------------
    struct LowerCtx {
        module: *Module,
        scope: *Scope,
        funcs: *HirFunc,
        funcCount: int,
        externFuncs: *HirFunc,
        externCount: int,
        varCounter: int,
        tryCounter: int,
        closureDepth: int,
        currentClosureExpr: *Expr,
        envInstanceName: String,
        hm: *HirModule,
        // Generic monomorphization
        genFuncCount: int,
        genFuncs: *Decl,
        genStructCount: int,
        genStructs: *Decl,
        // Type substitution (active during generic instance lowering)
        substParam0: String,
        substArg0: String,
        substParam1: String,
        substArg1: String,
        // Borrow checker state
        checkedFunc: bool,
        releaseFunc: bool,
        // Pattern binding renames: source name → unique C local (shadowing-safe)
        patMapCount: int,
        patMapFrom0: String,
        patMapTo0: String,
        patMapFrom1: String,
        patMapTo1: String,
        patMapFrom2: String,
        patMapTo2: String,
        patMapFrom3: String,
        patMapTo3: String,
        patMapFrom4: String,
        patMapTo4: String,
        patMapFrom5: String,
        patMapTo5: String,
        patMapFrom6: String,
        patMapTo6: String,
        patMapFrom7: String,
        patMapTo7: String,
    }

    func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String {
        // Most recent rename wins (scan from end)
        var i: int = ctx.patMapCount - 1;
        while i >= 0 {
            var from: String = "";
            var to: String = "";
            if i == 0 { from = ctx.patMapFrom0; to = ctx.patMapTo0; }
            else if i == 1 { from = ctx.patMapFrom1; to = ctx.patMapTo1; }
            else if i == 2 { from = ctx.patMapFrom2; to = ctx.patMapTo2; }
            else if i == 3 { from = ctx.patMapFrom3; to = ctx.patMapTo3; }
            else if i == 4 { from = ctx.patMapFrom4; to = ctx.patMapTo4; }
            else if i == 5 { from = ctx.patMapFrom5; to = ctx.patMapTo5; }
            else if i == 6 { from = ctx.patMapFrom6; to = ctx.patMapTo6; }
            else if i == 7 { from = ctx.patMapFrom7; to = ctx.patMapTo7; }
            if String_Eq(from, src) { return to; }
            i = i - 1;
        }
        return "";
    }

    func Lcx_PatPush(ctx: *LowerCtx, src: String, dst: String) {
        if ctx.patMapCount >= 8 { return; }
        let i: int = ctx.patMapCount;
        if i == 0 { ctx.patMapFrom0 = src; ctx.patMapTo0 = dst; }
        else if i == 1 { ctx.patMapFrom1 = src; ctx.patMapTo1 = dst; }
        else if i == 2 { ctx.patMapFrom2 = src; ctx.patMapTo2 = dst; }
        else if i == 3 { ctx.patMapFrom3 = src; ctx.patMapTo3 = dst; }
        else if i == 4 { ctx.patMapFrom4 = src; ctx.patMapTo4 = dst; }
        else if i == 5 { ctx.patMapFrom5 = src; ctx.patMapTo5 = dst; }
        else if i == 6 { ctx.patMapFrom6 = src; ctx.patMapTo6 = dst; }
        else if i == 7 { ctx.patMapFrom7 = src; ctx.patMapTo7 = dst; }
        ctx.patMapCount = ctx.patMapCount + 1;
    }

    func Lcx_FreshPatName(ctx: *LowerCtx, src: String) -> String {
        ctx.varCounter = ctx.varCounter + 1;
        var safe: String = src;
        if String_Eq(src, "") || String_Eq(src, "_") { safe = "x"; }
        return String_Concat(String_Concat("__p", String_FromInt(ctx.varCounter as int64)),
        String_Concat("_", safe));
    }

    // Alloca unique C local + store + scope define + rename map for pattern binding.
    func Lcx_BindPatIdent(ctx: *LowerCtx, src: String, ty: String, value: *HirNode,
    line: uint32, col: uint32) -> *HirNode {
        if String_Eq(src, "") || String_Eq(src, "_") { return null as *HirNode; }
        let cName: String = Lcx_FreshPatName(ctx, src);
        Lcx_PatPush(ctx, src, cName);
        let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        alloca.kind = hAlloca;
        alloca.line = line;
        alloca.column = col;
        alloca.strValue = cName;
        alloca.typeName = ty;
        let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        store.kind = hStore;
        store.line = line;
        store.column = col;
        let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        v.kind = hVar;
        v.strValue = cName;
        store.child1 = v;
        store.child2 = value;
        alloca.child3 = store;
        var bsym: Symbol;
        bsym.kind = skVar;
        bsym.name = src;
        bsym.typeKind = tyInt;
        bsym.typeName = ty;
        bsym.refType = null as *TypeExpr;
        bsym.isMutable = false;
        bsym.isPublic = false;
        bsym.decl = null as *Decl;
        discard Scope_Define(ctx.scope, bsym);
        return alloca;
    }

    // ---------------------------------------------------------------------------
    // TypeExpr.kind → Type.kind resolver
    // TypeExpr.kind values (0-5) overlap with Type.kind values — this
    // resolves the correct Type.kind for codegen.
    // ---------------------------------------------------------------------------

    func Lcx_ResolveTypeKindFromName(name: String) -> int {
        return Type_FromName(name);
    }

    func Lcx_TypeKindToName(kind: int) -> String {
        if kind == tyVoid { return "void"; }
        if kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32 { return "bool"; }
        if kind == tyChar8 { return "char"; }
        if kind == tyChar16 { return "uint16"; }
        if kind == tyChar32 { return "uint32"; }
        if kind == tyStr { return "String"; }
        if kind == tyInt8 { return "int8"; }
        if kind == tyInt16 { return "int16"; }
        if kind == tyInt32 { return "int32"; }
        if kind == tyInt64 { return "int64"; }
        if kind == tyInt { return "int"; }
        if kind == tyUInt8 { return "uint8"; }
        if kind == tyUInt16 { return "uint16"; }
        if kind == tyUInt32 { return "uint32"; }
        if kind == tyUInt64 { return "uint64"; }
        if kind == tyUInt { return "uint"; }
        if kind == tyFloat32 { return "float32"; }
        if kind == tyFloat64 { return "float64"; }
        if kind == tyPointer { return "void*"; }
        return "int";
    }

    func Lcx_ResolveTypeKind(te: *TypeExpr) -> int {
        if te == null as *TypeExpr { return tyUnknown; }

        if te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef { return tyPointer; }
        if te.kind == tekSlice { return tySlice; }
        if te.kind == tekTuple { return tyNamed; /* Tuple_T_U is a C struct */ }
        if te.kind == tekFunc { return tyFunc; }

        return Lcx_ResolveTypeKindFromName(te.typeName);
    }

    // ---------------------------------------------------------------------------
    // Type substitution for generic monomorphization
    // ---------------------------------------------------------------------------

    func Lcx_SubstituteType(ctx: *LowerCtx, te: *TypeExpr) -> *TypeExpr {
        if te == null as *TypeExpr { return te; }

        // Generic named type with type args: check if concrete or parametric
        if te.kind == tekNamed && te.typeArgCount > 0 {
            let genStruct: *Decl = Lcx_FindGenericStruct(ctx, te.typeName);
            if genStruct != null as *Decl {
                // Check if type args are the struct's own type params (parametric)
                var isParametric: bool = false;
                if te.typeArgCount > 0 && String_Eq(te.typeArgName0, genStruct.typeParam0) { isParametric = true; }
                if te.typeArgCount > 1 && String_Eq(te.typeArgName1, genStruct.typeParam1) { isParametric = true; }

                // If parametric and NOT inside a generic instantiation, keep as generic (don't mangle)
                if isParametric && String_Eq(ctx.substParam0, "") && String_Eq(ctx.substParam1, "") {
                    return te;
                }

                // Otherwise: substitute active type params and mangle to concrete name
                let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
                r.kind = tekNamed;
                r.line = te.line;
                r.column = te.column;
                r.typeArgCount = te.typeArgCount;
                r.typeArgName0 = te.typeArgName0;
                r.typeArgName1 = te.typeArgName1;
                if String_Eq(r.typeArgName0, ctx.substParam0) { r.typeArgName0 = ctx.substArg0; }
                if String_Eq(r.typeArgName0, ctx.substParam1) { r.typeArgName0 = ctx.substArg1; }
                if String_Eq(r.typeArgName1, ctx.substParam0) { r.typeArgName1 = ctx.substArg0; }
                if String_Eq(r.typeArgName1, ctx.substParam1) { r.typeArgName1 = ctx.substArg1; }
                r.typeName = Lcx_MangleName(te.typeName, r.typeArgName0, r.typeArgName1, te.typeArgCount);
                Lcx_GenerateStructInstance(ctx, genStruct, r.typeArgName0, r.typeArgName1, te.typeArgCount);
                return r;
            }
        }

        // Named type that is a type parameter (only when in instance mode)
        if te.kind == tekNamed {
            if String_Eq(te.typeName, ctx.substParam0) {
                let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
                r.kind = tekNamed;
                r.typeName = ctx.substArg0;
                r.line = te.line;
                r.column = te.column;
                return r;
            }
            if String_Eq(te.typeName, ctx.substParam1) {
                let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
                r.kind = tekNamed;
                r.typeName = ctx.substArg1;
                r.line = te.line;
                r.column = te.column;
                return r;
            }
        }

        // Pointer type: substitute recursively
        if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
            let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
            r.kind = tekPointer;
            r.pointerPointee = Lcx_SubstituteType(ctx, te.pointerPointee);
            if r.pointerPointee != null as *TypeExpr && !String_Eq(r.pointerPointee.typeName, "") {
                r.typeName = String_Concat(r.pointerPointee.typeName, "*");
            }
            r.line = te.line;
            r.column = te.column;
            return r;
        }

        // Fat function type: func(T)->U — substitute params and return
        if te.kind == tekFunc {
            let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
            r.kind = tekFunc;
            r.line = te.line;
            r.column = te.column;
            r.funcParamCount = te.funcParamCount;
            r.funcRet = Lcx_SubstituteType(ctx, te.funcRet);
            var head: *TypeExprList = null as *TypeExprList;
            var tail: *TypeExprList = null as *TypeExprList;
            var cur: *TypeExprList = te.funcParams;
            while cur != null as *TypeExprList {
                let node: *TypeExprList = bux_alloc(sizeof(TypeExprList)) as *TypeExprList;
                node.te = Lcx_SubstituteType(ctx, cur.te);
                node.next = null as *TypeExprList;
                if head == null as *TypeExprList {
                    head = node;
                    tail = node;
                } else {
                    tail.next = node;
                    tail = node;
                }
                cur = cur.next;
            }
            r.funcParams = head;
            r.typeName = Lcx_BuildFuncTypeName(r);
            return r;
        }

        return te;
    }

    // Sanitize a C type fragment for use inside BuxFn_* mangled names
    func Lcx_SanitizeFatPart(s: String) -> String {
        var r: String = s;
        if String_Eq(r, "String") || String_Eq(r, "str") || String_Eq(r, "const char*") {
            return "cstr";
        }
        if String_Eq(r, "unsigned int") { return "uint"; }
        // crude replacements for * and spaces
        r = String_ReplaceAll(r, "*", "Ptr");
        r = String_ReplaceAll(r, " ", "_");
        r = String_ReplaceAll(r, "(", "");
        r = String_ReplaceAll(r, ")", "");
        r = String_ReplaceAll(r, ",", "_");
        if String_Eq(r, "") { return "int"; }
        return r;
    }

    func Lcx_TypeExprFatPart(te: *TypeExpr) -> String {
        if te == null as *TypeExpr { return "void"; }
        if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
            return Lcx_SanitizeFatPart(String_Concat(te.pointerPointee.typeName, "Ptr"));
        }
        if te.kind == tekFunc {
            return Lcx_SanitizeFatPart(Lcx_BuildFuncTypeName(te));
        }
        var n: String = te.typeName;
        if String_Eq(n, "") { n = "int"; }
        return Lcx_SanitizeFatPart(n);
    }

    // Fat function-pointer type name: BuxFn_<ret>_<p0>_<p1>...
    // Enables multi-instance closures (code + env).
    func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String {
        if te == null as *TypeExpr || te.kind != tekFunc {
            return "BuxFn_void_void";
        }
        var retPart: String = "void";
        if te.funcRet != null as *TypeExpr {
            retPart = Lcx_TypeExprFatPart(te.funcRet);
        }
        var result: String = String_Concat("BuxFn_", retPart);
        var cur: *TypeExprList = te.funcParams;
        var anyParam: bool = false;
        while cur != null as *TypeExprList {
            result = String_Concat(result, "_");
            result = String_Concat(result, Lcx_TypeExprFatPart(cur.te));
            anyParam = true;
            cur = cur.next;
        }
        if !anyParam {
            result = String_Concat(result, "_void");
        }
        return result;
    }

    // ---------------------------------------------------------------------------
    // Generic monomorphization helpers
    // ---------------------------------------------------------------------------

    func Lcx_FindGenericFunc(ctx: *LowerCtx, name: String) -> *Decl {
        var i: int = 0;
        while i < ctx.genFuncCount {
            if String_Eq(ctx.genFuncs[i].strValue, name) {
                return &ctx.genFuncs[i];
            }
            i = i + 1;
        }
        return null as *Decl;
    }

    func Lcx_FindGenericStruct(ctx: *LowerCtx, name: String) -> *Decl {
        var i: int = 0;
        while i < ctx.genStructCount {
            if String_Eq(ctx.genStructs[i].strValue, name) {
                return &ctx.genStructs[i];
            }
            i = i + 1;
        }
        return null as *Decl;
    }

    // Extract element type from mangled collection name: Array_int → int, Iter_String → String
    func Lcx_ExtractElemFromName(typeName: String) -> String {
        if String_Eq(typeName, "") { return ""; }
        let len: uint = bux_strlen(typeName);
        // "Array_" prefix (6 chars)
        if len > 6 {
            let p: String = bux_str_slice(typeName, 0, 6);
            if String_Eq(p, "Array_") {
                return bux_str_slice(typeName, 6, len - 6);
            }
        }
        // "Iter_" prefix (5 chars)
        if len > 5 {
            let p: String = bux_str_slice(typeName, 0, 5);
            if String_Eq(p, "Iter_") {
                return bux_str_slice(typeName, 5, len - 5);
            }
        }
        return "";
    }

    func Lcx_MangleName(base: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
        let r: String = String_Concat(base, "_");
        r = String_Concat(r, typeArg0);
        if typeArgCount > 1 && !String_Eq(typeArg1, "") {
            r = String_Concat(r, "_");
            r = String_Concat(r, typeArg1);
        }
        return r;
    }

    func Lcx_GenerateStructInstance(ctx: *LowerCtx, genDecl: *Decl, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
        if String_Eq(genDecl.strValue, "") { return ""; }
        let mangled: String = Lcx_MangleName(genDecl.strValue, typeArg0, typeArg1, typeArgCount);

        // Check if already generated (linear search in hm.structs)
        var i: int = 0;
        while i < ctx.hm.structCount {
            if String_Eq(ctx.hm.structs[i].name, mangled) {
                return mangled;
            }
            i = i + 1;
        }

        // Save old substitution
        let oldParam0: String = ctx.substParam0;
        let oldArg0: String = ctx.substArg0;
        let oldParam1: String = ctx.substParam1;
        let oldArg1: String = ctx.substArg1;

        ctx.substParam0 = genDecl.typeParam0;
        ctx.substArg0 = typeArg0;
        ctx.substParam1 = genDecl.typeParam1;
        ctx.substArg1 = typeArg1;

        // Generate concrete HirStruct with substituted field types
        // Reserve the slot BEFORE processing fields so nested generic instantiations
        // get their own distinct indices and cannot overwrite our slot.
        let si: int = ctx.hm.structCount;
        ctx.hm.structCount = ctx.hm.structCount + 1;
        ctx.hm.structs[si].name = mangled;
        ctx.hm.structs[si].fieldCount = genDecl.fieldCount;
        ctx.hm.structs[si].fields = bux_alloc(genDecl.fieldCount as uint * sizeof(HirStructField)) as *HirStructField;
        var fi: int = 0;
        while fi < genDecl.fieldCount {
            let fname: String = genDecl.fields[fi].name;
            let ftype: *TypeExpr = genDecl.fields[fi].refFieldType;
            ctx.hm.structs[si].fields[fi].name = fname;
            if ftype != null as *TypeExpr {
                let subTe: *TypeExpr = Lcx_SubstituteType(ctx, ftype);
                if subTe.kind == tekPointer && subTe.pointerPointee != null as *TypeExpr {
                    if !String_Eq(subTe.pointerPointee.typeName, "") {
                        ctx.hm.structs[si].fields[fi].typeName = String_Concat(subTe.pointerPointee.typeName, "*");
                    }
                } else if !String_Eq(subTe.typeName, "") {
                    ctx.hm.structs[si].fields[fi].typeName = subTe.typeName;
                }
            }
            fi = fi + 1;
        }

        // Restore old substitution
        ctx.substParam0 = oldParam0;
        ctx.substArg0 = oldArg0;
        ctx.substParam1 = oldParam1;
        ctx.substArg1 = oldArg1;

        return mangled;
    }

    func Lcx_GenerateFuncInstance(ctx: *LowerCtx, genDecl: *Decl, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
        let mangled: String = Lcx_MangleName(genDecl.strValue, typeArg0, typeArg1, typeArgCount);

        // Check if already generated (linear search in ctx.funcs)
        var i: int = 0;
        while i < ctx.funcCount {
            if String_Eq(ctx.funcs[i].name, mangled) {
                return mangled;
            }
            i = i + 1;
        }

        // Save old substitution
        let oldParam0: String = ctx.substParam0;
        let oldArg0: String = ctx.substArg0;
        let oldParam1: String = ctx.substParam1;
        let oldArg1: String = ctx.substArg1;

        // Set up substitution
        ctx.substParam0 = genDecl.typeParam0;
        ctx.substArg0 = typeArg0;
        ctx.substParam1 = genDecl.typeParam1;
        ctx.substArg1 = typeArg1;

        // Lower the generic function with substitution active
        let f: *HirFunc = Lcx_LowerFunc(ctx, genDecl);
        f.name = mangled;

        // Add to module
        ctx.funcs[ctx.funcCount] = *f;
        ctx.funcCount = ctx.funcCount + 1;

        // Restore old substitution
        ctx.substParam0 = oldParam0;
        ctx.substArg0 = oldArg0;
        ctx.substParam1 = oldParam1;
        ctx.substArg1 = oldArg1;

        return mangled;
    }

    // Strip type-arg suffix from a mangled generic instance name.
    // E.g. ("Box_int", "int", "", 1) -> "Box"; ("Pair_int_String", "int", "String", 2) -> "Pair".
    func Lcx_StripTypeArgs(typeName: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
        var suffix: String = "_";
        suffix = String_Concat(suffix, typeArg0);
        if typeArgCount > 1 && !String_Eq(typeArg1, "") {
            suffix = String_Concat(suffix, "_");
            suffix = String_Concat(suffix, typeArg1);
        }
        let fullLen: int = bux_strlen(typeName) as int;
        let suffixLen: int = bux_strlen(suffix) as int;
        if fullLen > suffixLen {
            let endPart: String = bux_str_slice(typeName, (fullLen - suffixLen) as uint, suffixLen as uint);
            if String_Eq(endPart, suffix) {
                return bux_str_slice(typeName, 0, (fullLen - suffixLen) as uint);
            }
        }
        return typeName;
    }

    // ---------------------------------------------------------------------------
    // Array type helpers for bounds-checking desugaring
    // ---------------------------------------------------------------------------

    func Lcx_IsArrayTypeExpr(te: *TypeExpr) -> bool {
        if te == null as *TypeExpr { return false; }
        if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
            te = te.pointerPointee;
        }
        if te.kind == tekNamed {
            if String_Eq(te.typeName, "Array") { return true; }
            let name: String = te.typeName;
            if name[0] as int == 65 && name[1] as int == 114 && name[2] as int == 114 && name[3] as int == 97 && name[4] as int == 121 && name[5] as int == 95 {
                return true;
            }
        }
        return false;
    }

    func Lcx_GetArrayElemType(te: *TypeExpr) -> String {
        if te == null as *TypeExpr { return ""; }
        if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
            te = te.pointerPointee;
        }
        if te.kind == tekNamed {
            if String_Eq(te.typeName, "Array") && te.typeArgCount > 0 {
                return te.typeArgName0;
            }
            let name: String = te.typeName;
            if name[0] as int == 65 && name[1] as int == 114 && name[2] as int == 114 && name[3] as int == 97 && name[4] as int == 121 && name[5] as int == 95 {
                let prefixLen: uint = 6;
                let totalLen: uint = bux_strlen(name);
                if totalLen > prefixLen {
                    return bux_str_slice(name, prefixLen, totalLen - prefixLen);
                }
            }
        }
        return "";
    }

    // ---------------------------------------------------------------------------
    // Match lowering helpers
    // ---------------------------------------------------------------------------

    func Lcx_EnumHasData(ctx: *LowerCtx, enumName: String) -> bool {
        if String_Eq(enumName, "") { return false; }
        let sym: Symbol = Scope_Lookup(ctx.scope, enumName);
        if sym.decl == null as *Decl || sym.decl.kind != dkEnum { return false; }
        if sym.decl.variantCount > 0 && sym.decl.variant0.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 1 && sym.decl.variant1.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 2 && sym.decl.variant2.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 3 && sym.decl.variant3.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 4 && sym.decl.variant4.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 5 && sym.decl.variant5.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 6 && sym.decl.variant6.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 7 && sym.decl.variant7.fieldCount > 0 { return true; }
        if sym.decl.variantCount > 8 && sym.decl.variant8.fieldCount > 0 { return true; }
        return false;
    }

    func Lcx_MakeLitHir(litKind: int, litText: String, line: uint32, col: uint32) -> *HirNode {
        let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        n.kind = hLit;
        n.line = line;
        n.column = col;
        n.intValue = litKind;
        n.strValue = litText;
        return n;
    }

    func Lcx_MakeBinHir(op: int, left: *HirNode, right: *HirNode, line: uint32, col: uint32) -> *HirNode {
        let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        n.kind = hBinary;
        n.line = line;
        n.column = col;
        n.intValue = op;
        n.child1 = left;
        n.child2 = right;
        return n;
    }

    func Lcx_MakeTrueHir(line: uint32, col: uint32) -> *HirNode {
        return Lcx_MakeLitHir(tkBoolLiteral, "true", line, col);
    }

    // Build condition HirNode for a match pattern. Returns null = always-true.
    func Lcx_PatternCond(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
    subjectEnumName: String, subjectHasData: bool,
    line: uint32, col: uint32) -> *HirNode {
        if pat == null as *Pattern { return null as *HirNode; }
        let kind: int = pat.kind;

        // Guarded: condition is only the inner pattern; guard applied after binds
        if kind == pkGuarded {
            return Lcx_PatternCond(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col);
        }

        if kind == pkWildcard || kind == pkIdent {
            return null as *HirNode;
        }

        if kind == pkLiteral {
            let lit: *HirNode = Lcx_MakeLitHir(pat.patLitKind, pat.patLitText, line, col);
            return Lcx_MakeBinHir(tkEq, subject, lit, line, col);
        }

        if kind == pkRange {
            let loPat: *Pattern = pat.patChild1;
            let hiPat: *Pattern = pat.patChild2;
            if loPat == null as *Pattern || hiPat == null as *Pattern { return null as *HirNode; }
            if loPat.kind != pkLiteral || hiPat.kind != pkLiteral { return null as *HirNode; }
            let lo: *HirNode = Lcx_MakeLitHir(loPat.patLitKind, loPat.patLitText, line, col);
            let hi: *HirNode = Lcx_MakeLitHir(hiPat.patLitKind, hiPat.patLitText, line, col);
            let loOk: *HirNode = Lcx_MakeBinHir(tkGe, subject, lo, line, col);
            var hiOp: int = tkLt;
            if pat.patRangeInclusive { hiOp = tkLe; }
            let hiOk: *HirNode = Lcx_MakeBinHir(hiOp, subject, hi, line, col);
            return Lcx_MakeBinHir(tkAmpAmp, loOk, hiOk, line, col);
        }

        if kind == pkEnum {
            let path: String = pat.patEnumPath;
            // path is "Enum::Variant" or just "Variant"
            var enumName: String = "";
            var variantName: String = path;
            if String_Contains(path, "::") {
                enumName = String_SplitPart(path, "::", 0);
                variantName = String_SplitPart(path, "::", 1);
            }
            let tagName: String = String_Concat(String_Concat(enumName, "_"), variantName);
            // Prefer subject enum name when path is full
            var useEnum: String = enumName;
            if String_Eq(useEnum, "") { useEnum = subjectEnumName; }
            let fullTag: String = String_Concat(String_Concat(useEnum, "_"), variantName);

            if subjectHasData && (String_Eq(enumName, subjectEnumName) || String_Eq(enumName, "")) {
                // Algebraic: subject.tag == Enum_Variant
                let tagPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagPtr.kind = hFieldPtr;
                tagPtr.line = line;
                tagPtr.column = col;
                tagPtr.strValue = "tag";
                tagPtr.child1 = subject;
                let tagLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagLoad.kind = hLoad;
                tagLoad.line = line;
                tagLoad.column = col;
                tagLoad.child1 = tagPtr;
                let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagConst.kind = hVar;
                tagConst.line = line;
                tagConst.column = col;
                tagConst.strValue = fullTag;
                return Lcx_MakeBinHir(tkEq, tagLoad, tagConst, line, col);
            } else {
                // Simple enum: subject == Enum_Variant
                let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagConst.kind = hVar;
                tagConst.line = line;
                tagConst.column = col;
                if !String_Eq(enumName, "") {
                    tagConst.strValue = fullTag;
                } else {
                    tagConst.strValue = tagName;
                }
                return Lcx_MakeBinHir(tkEq, subject, tagConst, line, col);
            }
        }

        return null as *HirNode;
    }

    // Emit binding stmts for pattern payload: Option::Some(value) → alloca value; value = subject.data.Some_0
    // Returns head of child3-linked list of HirNodes (may be null).
    func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
    subjectEnumName: String, subjectHasData: bool,
    line: uint32, col: uint32) -> *HirNode {
        if pat == null as *Pattern { return null as *HirNode; }
        // Guarded: bind from inner pattern
        if pat.kind == pkGuarded {
            return Lcx_PatternBindings(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col);
        }
        if pat.kind == pkIdent {
            let ty: String = "int";
            if subject != null as *HirNode && !String_Eq(subject.typeName, "") {
                ty = subject.typeName;
            }
            return Lcx_BindPatIdent(ctx, pat.patIdent, ty, subject, line, col);
        }

        // Tuple: (a, b) → a = subject._0; b = subject._1
        if pat.kind == pkTuple {
            var head: *HirNode = null as *HirNode;
            var tail: *HirNode = null as *HirNode;
            var ei: int = 0;
            var elem: *Pattern = pat.patArgs;
            while elem != null as *Pattern {
                if elem.kind == pkIdent && !String_Eq(elem.patIdent, "_") {
                    var fieldName: String = "_0";
                    if ei == 1 { fieldName = "_1"; }
                    else if ei == 2 { fieldName = "_2"; }
                    else if ei == 3 { fieldName = "_3"; }
                    else if ei > 3 { fieldName = String_Concat("_", String_FromInt(ei as int64)); }
                    let fPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    fPtr.kind = hFieldPtr;
                    fPtr.line = line;
                    fPtr.column = col;
                    fPtr.strValue = fieldName;
                    fPtr.child1 = subject;
                    let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    fLoad.kind = hLoad;
                    fLoad.line = line;
                    fLoad.column = col;
                    fLoad.child1 = fPtr;
                    fLoad.typeName = "int";
                    let bound: *HirNode = Lcx_BindPatIdent(ctx, elem.patIdent, "int", fLoad, line, col);
                    if bound != null as *HirNode {
                        if head == null as *HirNode {
                            head = bound;
                            tail = bound;
                            while tail.child3 != null as *HirNode { tail = tail.child3; }
                        } else {
                            tail.child3 = bound;
                            while tail.child3 != null as *HirNode { tail = tail.child3; }
                        }
                    }
                }
                elem = elem.patNext;
                ei = ei + 1;
            }
            return head;
        }

        // Struct: Point { x: a } → a = subject.x
        if pat.kind == pkStruct {
            var head: *HirNode = null as *HirNode;
            var tail: *HirNode = null as *HirNode;
            var field: *Pattern = pat.patArgs;
            while field != null as *Pattern {
                if field.kind == pkIdent && !String_Eq(field.patIdent, "_") {
                    var fname: String = field.patFieldName;
                    if String_Eq(fname, "") { fname = field.patIdent; }
                    let fPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    fPtr.kind = hFieldPtr;
                    fPtr.line = line;
                    fPtr.column = col;
                    fPtr.strValue = fname;
                    fPtr.child1 = subject;
                    let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    fLoad.kind = hLoad;
                    fLoad.line = line;
                    fLoad.column = col;
                    fLoad.child1 = fPtr;
                    fLoad.typeName = "int";
                    let bound: *HirNode = Lcx_BindPatIdent(ctx, field.patIdent, "int", fLoad, line, col);
                    if bound != null as *HirNode {
                        if head == null as *HirNode {
                            head = bound;
                            tail = bound;
                            while tail.child3 != null as *HirNode { tail = tail.child3; }
                        } else {
                            tail.child3 = bound;
                            while tail.child3 != null as *HirNode { tail = tail.child3; }
                        }
                    }
                }
                field = field.patNext;
            }
            return head;
        }

        if pat.kind != pkEnum || !subjectHasData { return null as *HirNode; }

        var enumName: String = "";
        var variantName: String = pat.patEnumPath;
        if String_Contains(pat.patEnumPath, "::") {
            enumName = String_SplitPart(pat.patEnumPath, "::", 0);
            variantName = String_SplitPart(pat.patEnumPath, "::", 1);
        } else {
            enumName = subjectEnumName;
        }
        if String_Eq(enumName, "") || String_Eq(variantName, "") { return null as *HirNode; }

        // Look up field type names from enum decl
        var fieldType0: String = "int";
        var fieldType1: String = "int";
        var fieldCount: int = 0;
        let enumSym: Symbol = Scope_Lookup(ctx.scope, enumName);
        if enumSym.decl != null as *Decl && enumSym.decl.kind == dkEnum {
            var vi: int = 0;
            while vi < enumSym.decl.variantCount {
                var vv: *EnumVariant = null as *EnumVariant;
                if vi == 0 { vv = &enumSym.decl.variant0; }
                else if vi == 1 { vv = &enumSym.decl.variant1; }
                else if vi == 2 { vv = &enumSym.decl.variant2; }
                else if vi == 3 { vv = &enumSym.decl.variant3; }
                else if vi == 4 { vv = &enumSym.decl.variant4; }
                else if vi == 5 { vv = &enumSym.decl.variant5; }
                else if vi == 6 { vv = &enumSym.decl.variant6; }
                else if vi == 7 { vv = &enumSym.decl.variant7; }
                else if vi == 8 { vv = &enumSym.decl.variant8; }
                if vv != null as *EnumVariant && String_Eq(vv.name, variantName) {
                    fieldCount = vv.fieldCount;
                    if !String_Eq(vv.fieldTypeName0, "") { fieldType0 = vv.fieldTypeName0; }
                    if !String_Eq(vv.fieldTypeName1, "") { fieldType1 = vv.fieldTypeName1; }
                }
                vi = vi + 1;
            }
        }

        // dataLoad = subject.data
        let dataPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        dataPtr.kind = hFieldPtr;
        dataPtr.line = line;
        dataPtr.column = col;
        dataPtr.strValue = "data";
        dataPtr.child1 = subject;
        let dataLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        dataLoad.kind = hLoad;
        dataLoad.line = line;
        dataLoad.column = col;
        dataLoad.child1 = dataPtr;
        dataLoad.typeName = String_Concat(enumName, "_Data");

        // Multi-field: nested struct data.Variant (anonymous or Enum_Variant_Payload);
        // single-field: flat data.Variant_0
        var payloadBase: *HirNode = dataLoad;
        if fieldCount > 1 {
            let nestedName: String = String_Concat(String_Concat(enumName, "_"),
            String_Concat(variantName, "_Payload"));
            let vPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            vPtr.kind = hFieldPtr;
            vPtr.line = line;
            vPtr.column = col;
            vPtr.strValue = variantName;
            vPtr.child1 = dataLoad;
            let vLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            vLoad.kind = hLoad;
            vLoad.line = line;
            vLoad.column = col;
            vLoad.child1 = vPtr;
            vLoad.typeName = nestedName;
            payloadBase = vLoad;
        }

        var head: *HirNode = null as *HirNode;
        var tail: *HirNode = null as *HirNode;
        var arg: *Pattern = pat.patArgs;
        var ai: int = 0;
        while arg != null as *Pattern {
            var ftype: String = "int";
            if ai == 0 { ftype = fieldType0; }
            else if ai == 1 { ftype = fieldType1; }
            let fieldName: String = String_Concat(String_Concat(variantName, "_"), String_FromInt(ai as int64));

            let fPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            fPtr.kind = hFieldPtr;
            fPtr.line = line;
            fPtr.column = col;
            fPtr.strValue = fieldName;
            fPtr.child1 = payloadBase;
            let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            fLoad.kind = hLoad;
            fLoad.line = line;
            fLoad.column = col;
            fLoad.child1 = fPtr;
            fLoad.typeName = ftype;

            if arg.kind == pkIdent && !String_Eq(arg.patIdent, "_") {
                let bound: *HirNode = Lcx_BindPatIdent(ctx, arg.patIdent, ftype, fLoad, line, col);
                if bound != null as *HirNode {
                    if head == null as *HirNode {
                        head = bound;
                        tail = bound;
                        while tail.child3 != null as *HirNode { tail = tail.child3; }
                    } else {
                        tail.child3 = bound;
                        while tail.child3 != null as *HirNode { tail = tail.child3; }
                    }
                }
            } else if arg.kind == pkTuple || arg.kind == pkStruct || arg.kind == pkEnum {
                // Nested pattern on payload field
                let nested: *HirNode = Lcx_PatternBindings(ctx, fLoad, arg, subjectEnumName, subjectHasData, line, col);
                if nested != null as *HirNode {
                    if head == null as *HirNode {
                        head = nested;
                        tail = nested;
                        while tail.child3 != null as *HirNode { tail = tail.child3; }
                    } else {
                        tail.child3 = nested;
                        while tail.child3 != null as *HirNode { tail = tail.child3; }
                    }
                }
            }
            arg = arg.patNext;
            ai = ai + 1;
        }
        return head;
    }

    // True when n is a multi-stmt yield block (match result, etc.)
    func Lcx_IsMatchYield(n: *HirNode) -> bool {
        if n == null as *HirNode { return false; }
        if n.kind != hBlock { return false; }
        // strValue must be a real temp name — null/"" is a plain statement block
        if n.strValue == null as String { return false; }
        return !String_Eq(n.strValue, "");
    }

    func Lcx_YieldVarOf(n: *HirNode) -> *HirNode {
        let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        v.kind = hVar;
        v.strValue = n.strValue;
        v.typeName = n.typeName;
        return v;
    }

    // Append `node` at the end of a child3-linked chain starting at `head` (or its child1 if head is hBlock).
    func Lcx_AppendToChain(head: *HirNode, node: *HirNode) {
        if head == null as *HirNode || node == null as *HirNode { return; }
        var cur: *HirNode = head;
        if head.kind == hBlock && head.child1 != null as *HirNode {
            cur = head.child1;
        }
        while cur.child3 != null as *HirNode {
            cur = cur.child3;
        }
        cur.child3 = node;
    }

    // Lower match expr → sequential ifs with a found flag (no shared HIR DAG).
    // Each arm: if (!found) { if (cond) { binds; if (guard) { result=body; found=true; } } }
    // Guards see pattern bindings. Supports pkGuarded (`p if cond`).
    func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
        let line: uint32 = expr.line;
        let col: uint32 = expr.column;
        let subject: *HirNode = Lcx_LowerExpr(ctx, expr.child1);

        ctx.varCounter = ctx.varCounter + 1;
        let resultName: String = String_Concat("__match_", String_FromInt(ctx.varCounter as int64));
        ctx.varCounter = ctx.varCounter + 1;
        let foundName: String = String_Concat("__found_", String_FromInt(ctx.varCounter as int64));

        // Result type from sema refType, default int
        var typeName: String = "int";
        if expr.refType != null as *TypeExpr && !String_Eq(expr.refType.typeName, "") {
            typeName = expr.refType.typeName;
        }

        // Subject enum info
        var subjectEnumName: String = "";
        var subjectHasData: bool = false;
        if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
            if expr.child1.refType.kind == tekNamed {
                subjectEnumName = expr.child1.refType.typeName;
                subjectHasData = Lcx_EnumHasData(ctx, subjectEnumName);
            }
        }

        // Alloca result + found flag
        let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        allocaNode.kind = hAlloca;
        allocaNode.line = line;
        allocaNode.column = col;
        allocaNode.strValue = resultName;
        allocaNode.typeName = typeName;

        let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        foundAlloca.kind = hAlloca;
        foundAlloca.line = line;
        foundAlloca.column = col;
        foundAlloca.strValue = foundName;
        foundAlloca.typeName = "bool";
        allocaNode.child3 = foundAlloca;

        let foundInit: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        foundInit.kind = hStore;
        foundInit.line = line;
        foundInit.column = col;
        let foundVar0: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        foundVar0.kind = hVar;
        foundVar0.strValue = foundName;
        foundInit.child1 = foundVar0;
        foundInit.child2 = Lcx_MakeLitHir(tkBoolLiteral, "false", line, col);
        foundAlloca.child3 = foundInit;

        // Chain of arm ifs (forward order), linked via child3
        var tail: *HirNode = foundInit;
        var cur: *MatchArm = expr.matchArms;
        while cur != null as *MatchArm {
            // Snapshot rename map so this arm's bindings don't leak to later arms
            let savedMapCount: int = ctx.patMapCount;
            var bindPat: *Pattern = cur.pattern;
            var guardExpr: *Expr = null as *Expr;
            if cur.pattern != null as *Pattern && cur.pattern.kind == pkGuarded {
                bindPat = cur.pattern.patChild1;
                guardExpr = cur.pattern.patGuardExpr;
            }
            // Bindings before guard/body so renames are active and allocas precede uses
            let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col);
            var guardHirEarly: *HirNode = null as *HirNode;
            if guardExpr != null as *Expr {
                guardHirEarly = Lcx_LowerExpr(ctx, guardExpr);
            }
            let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body);
            // Pop this arm's renames (nested matches already restored themselves)
            ctx.patMapCount = savedMapCount;

            // store result = body
            let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            storeNode.kind = hStore;
            storeNode.line = line;
            storeNode.column = col;
            let resVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            resVar.kind = hVar;
            resVar.strValue = resultName;
            storeNode.child1 = resVar;
            var bodyPrefix: *HirNode = null as *HirNode;
            if Lcx_IsMatchYield(bodyHir) {
                storeNode.child2 = Lcx_YieldVarOf(bodyHir);
                bodyHir.strValue = "";
                bodyPrefix = bodyHir.child1;
            } else {
                storeNode.child2 = bodyHir;
            }

            // found = true
            let foundSet: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            foundSet.kind = hStore;
            foundSet.line = line;
            foundSet.column = col;
            let foundVar1: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            foundVar1.kind = hVar;
            foundVar1.strValue = foundName;
            foundSet.child1 = foundVar1;
            foundSet.child2 = Lcx_MakeLitHir(tkBoolLiteral, "true", line, col);
            storeNode.child3 = foundSet;

            // success block: bodyPrefix → store → found=true
            let successBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            successBlock.kind = hBlock;
            successBlock.line = line;
            successBlock.column = col;
            if bodyPrefix != null as *HirNode {
                successBlock.child1 = bodyPrefix;
                var sbt: *HirNode = bodyPrefix;
                while sbt.child3 != null as *HirNode { sbt = sbt.child3; }
                sbt.child3 = storeNode;
            } else {
                successBlock.child1 = storeNode;
            }

            // Optional guard wraps success (already lowered with renames active)
            var afterBinds: *HirNode = successBlock;
            if guardHirEarly != null as *HirNode {
                let guardIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                guardIf.kind = hIf;
                guardIf.line = line;
                guardIf.column = col;
                guardIf.child1 = guardHirEarly;
                guardIf.child2 = successBlock;
                afterBinds = guardIf;
            }

            // armInner: binds → afterBinds
            let armInner: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            armInner.kind = hBlock;
            armInner.line = line;
            armInner.column = col;
            if bindHead != null as *HirNode {
                armInner.child1 = bindHead;
                var ibt: *HirNode = bindHead;
                while ibt.child3 != null as *HirNode { ibt = ibt.child3; }
                ibt.child3 = afterBinds;
            } else {
                armInner.child1 = afterBinds;
            }

            // Optional pattern condition
            let cond: *HirNode = Lcx_PatternCond(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col);
            var armBody: *HirNode = armInner;
            if cond != null as *HirNode {
                let condIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                condIf.kind = hIf;
                condIf.line = line;
                condIf.column = col;
                condIf.child1 = cond;
                condIf.child2 = armInner;
                armBody = condIf;
            }

            // if (!found) armBody
            let foundLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            foundLoad.kind = hVar;
            foundLoad.strValue = foundName;
            foundLoad.typeName = "bool";
            let notFound: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            notFound.kind = hUnary;
            notFound.line = line;
            notFound.column = col;
            notFound.intValue = tkBang;
            notFound.child1 = foundLoad;

            let tryIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            tryIf.kind = hIf;
            tryIf.line = line;
            tryIf.column = col;
            tryIf.child1 = notFound;
            tryIf.child2 = armBody;

            tail.child3 = tryIf;
            tail = tryIf;
            cur = cur.next;
        }

        let block: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        block.kind = hBlock;
        block.line = line;
        block.column = col;
        block.child1 = allocaNode;
        // Yield marker: strValue = result var name for last-expr return / let init
        block.strValue = resultName;
        block.typeName = typeName;
        return block;
    }

    // ---------------------------------------------------------------------------
    // Expression lowering
    // ---------------------------------------------------------------------------

    func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
        if expr == null as *Expr { return null as *HirNode; }

        let line: uint32 = expr.line;
        let col: uint32 = expr.column;
        let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        n.kind = hBlock;
        n.line = line;
        n.column = col;

        let kind: int = expr.kind;

        // Match expression
        if kind == ekMatch {
            return Lcx_LowerMatch(ctx, expr);
        }

        // String interpolation: desugar to String_Concat + String_FromInt/Bool/Float
        // Parts in callArgs are interleaved text lits and expressions.
        if kind == ekStringInterp {
            var result: *HirNode = null as *HirNode;
            var part: *ExprList = expr.callArgs;
            while part != null as *ExprList {
                let pe: *Expr = part.expr;
                var piece: *HirNode = null as *HirNode;
                if pe != null as *Expr && pe.kind == ekLiteral && pe.tokKind == tkStringLiteral {
                    piece = Lcx_LowerExpr(ctx, pe);
                } else {
                    let lowered: *HirNode = Lcx_LowerExpr(ctx, pe);
                    // Convert non-string to String
                    var needConv: bool = true;
                    var convName: String = "String_FromInt";
                    if pe != null as *Expr && pe.refType != null as *TypeExpr {
                        let tn: String = pe.refType.typeName;
                        if String_Eq(tn, "String") || String_Eq(tn, "str") {
                            needConv = false;
                        } else if String_Eq(tn, "bool") {
                            convName = "String_FromBool";
                        } else if String_Eq(tn, "float64") || String_Eq(tn, "float") || String_Eq(tn, "float32") {
                            convName = "String_FromFloat";
                        } else {
                            convName = "String_FromInt";
                        }
                    }
                    if needConv {
                        let callN: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        callN.kind = hCall;
                        callN.line = line;
                        callN.column = col;
                        callN.strValue = convName;
                        callN.child1 = lowered;
                        piece = callN;
                    } else {
                        piece = lowered;
                    }
                }
                if result == null as *HirNode {
                    result = piece;
                } else {
                    let cat: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    cat.kind = hCall;
                    cat.line = line;
                    cat.column = col;
                    cat.strValue = "String_Concat";
                    cat.child1 = result;
                    cat.child2 = piece;
                    result = cat;
                }
                part = part.next;
            }
            if result == null as *HirNode {
                // empty f""
                let empty: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                empty.kind = hLit;
                empty.line = line;
                empty.column = col;
                empty.intValue = tkStringLiteral;
                empty.strValue = "\"\"";
                return empty;
            }
            return result;
        }

        // Literal
        if kind == ekLiteral {
            n.kind = hLit;
            n.intValue = expr.tokKind;
            n.strValue = expr.tokText;
            return n;
        }

        // Identifier → variable reference
        if kind == ekIdent {
            // Pattern binding rename: source name → unique C local
            let ren: String = Lcx_PatLookup(ctx, expr.strValue);
            if !String_Eq(ren, "") {
                n.kind = hVar;
                n.strValue = ren;
                let rsym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
                n.typeKind = rsym.typeKind;
                if rsym.typeName != null as String && !String_Eq(rsym.typeName, "") {
                    n.typeName = rsym.typeName;
                }
                return n;
            }
            // Capture rewriting: if inside closure body and this ident is captured,
            // emit field access on env instance instead of bare variable
            if ctx.closureDepth > 0 && ctx.currentClosureExpr != null as *Expr && !String_Eq(ctx.envInstanceName, "") {
                let capCount: int = ctx.currentClosureExpr.captureCount;
                var ci: int = 0;
                var isCaptured: bool = false;
                var capType: int = 0;
                while ci < capCount {
                    var capName: String = "";
                    if ci == 0 { capName = ctx.currentClosureExpr.captureName0; capType = ctx.currentClosureExpr.captureType0; }
                    else if ci == 1 { capName = ctx.currentClosureExpr.captureName1; capType = ctx.currentClosureExpr.captureType1; }
                    else if ci == 2 { capName = ctx.currentClosureExpr.captureName2; capType = ctx.currentClosureExpr.captureType2; }
                    else if ci == 3 { capName = ctx.currentClosureExpr.captureName3; capType = ctx.currentClosureExpr.captureType3; }
                    else if ci == 4 { capName = ctx.currentClosureExpr.captureName4; capType = ctx.currentClosureExpr.captureType4; }
                    else if ci == 5 { capName = ctx.currentClosureExpr.captureName5; capType = ctx.currentClosureExpr.captureType5; }
                    else if ci == 6 { capName = ctx.currentClosureExpr.captureName6; capType = ctx.currentClosureExpr.captureType6; }
                    else if ci == 7 { capName = ctx.currentClosureExpr.captureName7; capType = ctx.currentClosureExpr.captureType7; }
                    if String_Eq(capName, expr.strValue) {
                        isCaptured = true;
                    }
                    ci = ci + 1;
                }
                if isCaptured {
                    n.kind = hFieldAccess;
                    n.strValue = expr.strValue;
                    let baseNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    baseNode.kind = hVar;
                    baseNode.strValue = ctx.envInstanceName;
                    n.child1 = baseNode;
                    n.typeKind = capType;
                    n.typeName = Lcx_TypeKindToName(capType);
                    return n;
                }
            }
            let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
            // Named function used as a value → fat pointer via __adapt_ wrapper
            if sym.kind == skFunc {
                var fatName: String = "BuxFn_int_int";
                if sym.refType != null as *TypeExpr && sym.refType.kind == tekFunc {
                    fatName = Lcx_BuildFuncTypeName(sym.refType);
                } else if !String_Eq(sym.typeName, "") && String_StartsWith(sym.typeName, "BuxFn_") {
                    fatName = sym.typeName;
                }
                n.kind = hStructInit;
                n.strValue = fatName;
                n.typeKind = tyFunc;
                n.typeName = fatName;
                let codeField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                codeField.kind = hBlock;
                codeField.strValue = "code";
                let codeVal: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                codeVal.kind = hVar;
                codeVal.strValue = String_Concat("__adapt_", expr.strValue);
                codeField.child1 = codeVal;
                let envField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                envField.kind = hBlock;
                envField.strValue = "env";
                let nullEnv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                nullEnv.kind = hCast;
                nullEnv.typeName = "void*";
                let zero: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                zero.kind = hLit;
                zero.intValue = tkIntLiteral;
                zero.strValue = "0";
                nullEnv.child1 = zero;
                envField.child1 = nullEnv;
                codeField.child3 = envField;
                n.child1 = codeField;
                return n;
            }
            n.kind = hVar;
            n.strValue = expr.strValue;
            n.typeKind = sym.typeKind;

            if expr.refType != null as *TypeExpr {
                n.typeName = expr.refType.typeName;
            }
            if sym.typeName != null as String && !String_Eq(sym.typeName, "") {
                n.typeName = sym.typeName;
            }
            return n;
        }

        // self → variable reference named "self"
        if kind == ekSelf {
            n.kind = hVar;
            n.strValue = "self";
            let sym: Symbol = Scope_Lookup(ctx.scope, "self");
            n.typeKind = sym.typeKind;
            if sym.typeName != null as String && !String_Eq(sym.typeName, "") {
                n.typeName = sym.typeName;
            }
            return n;
        }

        // Binary
        if kind == ekBinary {
            // Assignment operator → use hAssign
            if expr.intValue == tkAssign {
                n.kind = hAssign;
                n.child1 = Lcx_LowerExpr(ctx, expr.child1);
                n.child2 = Lcx_LowerExpr(ctx, expr.child2);
                return n;
            }

            // Operator overloading: try method call
            var opMethodName: String = "";
            if expr.intValue == tkPlus { opMethodName = "operator_add"; }
            else if expr.intValue == tkMinus { opMethodName = "operator_sub"; }
            else if expr.intValue == tkStar { opMethodName = "operator_mul"; }
            else if expr.intValue == tkSlash { opMethodName = "operator_div"; }
            else if expr.intValue == tkPercent { opMethodName = "operator_mod"; }
            else if expr.intValue == tkEq { opMethodName = "operator_eq"; }
            else if expr.intValue == tkNe { opMethodName = "operator_ne"; }
            else if expr.intValue == tkLt { opMethodName = "operator_lt"; }
            else if expr.intValue == tkLe { opMethodName = "operator_le"; }
            else if expr.intValue == tkGt { opMethodName = "operator_gt"; }
            else if expr.intValue == tkGe { opMethodName = "operator_ge"; }
            else if expr.intValue == tkAmp { opMethodName = "operator_bitand"; }
            else if expr.intValue == tkPipe { opMethodName = "operator_bitor"; }
            else if expr.intValue == tkCaret { opMethodName = "operator_xor"; }
            else if expr.intValue == tkShl { opMethodName = "operator_shl"; }
            else if expr.intValue == tkShr { opMethodName = "operator_shr"; }

            if !String_Eq(opMethodName, "") {
                var receiverTypeName: String = "";
                if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
                    let refTe: *TypeExpr = expr.child1.refType;
                    if refTe.kind == tekNamed {
                        if refTe.typeName != null as String { receiverTypeName = refTe.typeName; }
                    } else if refTe.kind == tekPointer && refTe.pointerPointee != null as *TypeExpr && refTe.pointerPointee.kind == tekNamed {
                        if refTe.pointerPointee.typeName != null as String {
                            receiverTypeName = refTe.pointerPointee.typeName;
                        }
                    }
                }
                // Note: String_Eq(null, "") is false — must also reject null type names
                if receiverTypeName != null as String && !String_Eq(receiverTypeName, "") {
                    let funcName: String = String_Concat(String_Concat(receiverTypeName, "_"), opMethodName);
                    let sym: Symbol = Scope_Lookup(ctx.scope, funcName);
                    if sym.kind == skFunc && sym.decl != null as *Decl {
                        n.kind = hCall;
                        n.strValue = funcName;
                        let recv: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
                        // If method expects pointer/reference but receiver is a value, add &
                        if sym.decl.paramCount > 0 && sym.decl.param0.refParamType != null as *TypeExpr {
                            let paramKind: int = sym.decl.param0.refParamType.kind;
                            if paramKind == tekPointer || paramKind == tekRef || paramKind == tekMutRef {
                                if expr.child1.refType != null as *TypeExpr && expr.child1.refType.kind != tekPointer && expr.child1.refType.kind != tekRef && expr.child1.refType.kind != tekMutRef {
                                    let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    addrNode.kind = hUnary;
                                    addrNode.intValue = tkAmp;
                                    addrNode.child1 = recv;
                                    n.child1 = addrNode;
                                } else {
                                    n.child1 = recv;
                                }
                            } else {
                                n.child1 = recv;
                            }
                        } else {
                            n.child1 = recv;
                        }
                        n.child2 = Lcx_LowerExpr(ctx, expr.child2);
                        return n;
                    }
                }
            }

            // Overflow checking: in @[Checked] mode, lower +, -, * on signed integers to checked calls
            if ctx.checkedFunc && !ctx.releaseFunc {
                var opKind: int = expr.intValue;
                var isArithOp: bool = opKind == tkPlus || opKind == tkMinus || opKind == tkStar;
                var isSignedInt: bool = false;
                if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
                    let lhsKind: int = Lcx_ResolveTypeKind(expr.child1.refType);
                    isSignedInt = Type_IsSigned(lhsKind);
                }
                if isArithOp && isSignedInt {
                    var checkedFunc: String = "";
                    if opKind == tkPlus { checkedFunc = "bux_add_i64_checked"; }
                    else if opKind == tkMinus { checkedFunc = "bux_sub_i64_checked"; }
                    else if opKind == tkStar { checkedFunc = "bux_mul_i64_checked"; }
                    if !String_Eq(checkedFunc, "") {
                        n.kind = hCall;
                        n.strValue = checkedFunc;
                        n.child1 = Lcx_LowerExpr(ctx, expr.child1);
                        n.child2 = Lcx_LowerExpr(ctx, expr.child2);
                        return n;
                    }
                }
            }

            n.kind = hBinary;
            n.intValue = expr.intValue; // operator
            let leftHir: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
            let rightHir: *HirNode = Lcx_LowerExpr(ctx, expr.child2);
            // If either side is a match yield block, expand to:
            //   match stmts...; int __binop_N = leftVal op rightVal; yield __binop_N
            if Lcx_IsMatchYield(leftHir) || Lcx_IsMatchYield(rightHir) {
                ctx.varCounter = ctx.varCounter + 1;
                let tmpName: String = String_Concat("__binop_", String_FromInt(ctx.varCounter as int64));
                var leftVal: *HirNode = leftHir;
                var rightVal: *HirNode = rightHir;
                let outer: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                outer.kind = hBlock;
                outer.line = line;
                outer.column = col;
                outer.strValue = tmpName;
                outer.typeName = "int";
                var first: *HirNode = null as *HirNode;
                if Lcx_IsMatchYield(leftHir) {
                    leftVal = Lcx_YieldVarOf(leftHir);
                    leftHir.strValue = "";
                    first = leftHir;
                }
                if Lcx_IsMatchYield(rightHir) {
                    rightVal = Lcx_YieldVarOf(rightHir);
                    rightHir.strValue = "";
                    if first == null as *HirNode {
                        first = rightHir;
                    } else {
                        Lcx_AppendToChain(first, rightHir);
                    }
                }
                let tmpAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpAlloca.kind = hAlloca;
                tmpAlloca.line = line;
                tmpAlloca.column = col;
                tmpAlloca.strValue = tmpName;
                tmpAlloca.typeName = "int";
                let binNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                binNode.kind = hBinary;
                binNode.line = line;
                binNode.column = col;
                binNode.intValue = expr.intValue;
                binNode.child1 = leftVal;
                binNode.child2 = rightVal;
                let tmpStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpStore.kind = hStore;
                tmpStore.line = line;
                tmpStore.column = col;
                let tmpVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpVar.kind = hVar;
                tmpVar.strValue = tmpName;
                tmpStore.child1 = tmpVar;
                tmpStore.child2 = binNode;
                tmpAlloca.child3 = tmpStore;
                if first == null as *HirNode {
                    outer.child1 = tmpAlloca;
                } else {
                    outer.child1 = first;
                    Lcx_AppendToChain(first, tmpAlloca);
                }
                return outer;
            }
            n.child1 = leftHir;
            n.child2 = rightHir;
            return n;
        }

        // Unary
        if kind == ekUnary {
            // Overflow checking: in @[Checked] mode, lower negation on signed integers to checked call
            if ctx.checkedFunc && !ctx.releaseFunc && expr.intValue == tkMinus {
                var isSignedInt: bool = false;
                if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
                    let operandKind: int = Lcx_ResolveTypeKind(expr.child1.refType);
                    isSignedInt = Type_IsSigned(operandKind);
                }
                if isSignedInt {
                    n.kind = hCall;
                    n.strValue = "bux_neg_i64_checked";
                    n.child1 = Lcx_LowerExpr(ctx, expr.child1);
                    return n;
                }
            }
            n.kind = hUnary;
            n.intValue = expr.intValue;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1);
            if expr.intValue == tkAmp {
                n.typeKind = tyPointer;
                if expr.child1.refType != null as *TypeExpr && expr.child1.refType.kind == tekFunc {
                    n.typeName = Lcx_BuildFuncTypeName(expr.child1.refType);
                }
            }
            return n;
        }

        // Call
        if kind == ekCall {
            // Method call desugaring: obj.method(args) → Type_method(obj, args)
            if expr.child1 != null as *Expr && expr.child1.kind == ekField {
                n.kind = hCall;
                let methodName: String = expr.child1.strValue;
                var receiverTypeName: String = "";
                var receiverRefType: *TypeExpr = null as *TypeExpr;
                if expr.child1.child1 != null as *Expr && expr.child1.child1.kind == ekIdent {
                    let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.child1.strValue);
                    receiverTypeName = sym.typeName;
                    receiverRefType = sym.refType;
                }
                if String_Eq(receiverTypeName, "") && expr.child1.child1 != null as *Expr && expr.child1.child1.refType != null as *TypeExpr {
                    receiverTypeName = expr.child1.child1.refType.typeName;
                    receiverRefType = expr.child1.child1.refType;
                }

                var methodDecl: *Decl = null as *Decl;
                if !String_Eq(receiverTypeName, "") {
                    // Strip trailing '*' from pointer type names (e.g. "Box*" → "Box")
                    var baseName: String = receiverTypeName;
                    let len: int = bux_strlen(baseName) as int;
                    if len > 0 {
                        let lastChar: String = bux_str_slice(baseName, (len - 1) as uint, 1);
                        if String_Eq(lastChar, "*") {
                            baseName = bux_str_slice(baseName, 0, (len - 1) as uint);
                        }
                    }
                    n.strValue = String_Concat(baseName, "_");
                    n.strValue = String_Concat(n.strValue, methodName);

                    // Generic method monomorphization: Box_Get<T> on Box<int> -> Box_Get_int
                    var genericRecvType: *TypeExpr = receiverRefType;
                    if genericRecvType != null as *TypeExpr && genericRecvType.kind == tekPointer && genericRecvType.pointerPointee != null as *TypeExpr {
                        genericRecvType = genericRecvType.pointerPointee;
                    }
                    if genericRecvType != null as *TypeExpr && genericRecvType.typeArgCount > 0 {
                        let baseTypeName: String = Lcx_StripTypeArgs(genericRecvType.typeName, genericRecvType.typeArgName0, genericRecvType.typeArgName1, genericRecvType.typeArgCount);
                        let baseMethodName: String = String_Concat(String_Concat(baseTypeName, "_"), methodName);
                        let genDecl: *Decl = Lcx_FindGenericFunc(ctx, baseMethodName);
                        if genDecl != null as *Decl {
                            let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, genericRecvType.typeArgName0, genericRecvType.typeArgName1, genericRecvType.typeArgCount);
                            n.strValue = mangled;
                            methodDecl = genDecl;
                        }
                    }
                }
                // Lower receiver as first argument
                let recv: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child1);
                // Find method decl if not already found (non-generic case)
                if methodDecl == null as *Decl {
                    let sym: Symbol = Scope_Lookup(ctx.scope, n.strValue);
                    if sym.kind == skFunc && sym.decl != null as *Decl {
                        methodDecl = sym.decl;
                    }
                }
                // Auto-address if method expects pointer/reference but receiver is a value
                if methodDecl != null as *Decl && methodDecl.paramCount > 0 && methodDecl.param0.refParamType != null as *TypeExpr {
                    let paramKind: int = methodDecl.param0.refParamType.kind;
                    if paramKind == tekPointer || paramKind == tekRef || paramKind == tekMutRef {
                        if expr.child1.child1 != null as *Expr && expr.child1.child1.refType != null as *TypeExpr &&
                        expr.child1.child1.refType.kind != tekPointer && expr.child1.child1.refType.kind != tekRef && expr.child1.child1.refType.kind != tekMutRef {
                            let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            addrNode.kind = hUnary;
                            addrNode.intValue = tkAmp;
                            addrNode.child1 = recv;
                            n.child1 = addrNode;
                        } else {
                            n.child1 = recv;
                        }
                    } else {
                        n.child1 = recv;
                    }
                } else {
                    n.child1 = recv;
                }
                // Lower remaining arguments from linked list
                var arg: *ExprList = expr.callArgs;
                var argIdx: int = 0;
                while arg != null as *ExprList {
                    let lowered: *HirNode = Lcx_LowerExpr(ctx, arg.expr);
                    if argIdx == 0 {
                        n.child2 = lowered;
                    } else if argIdx == 1 {
                        // Third argument — start linked list
                        let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        firstExtra.node = lowered;
                        firstExtra.next = null as *HirArgList;
                        n.extraData = firstExtra as *void;
                        n.extraCount = 1;
                    } else {
                        // Additional args — append to linked list
                        var cur: *HirArgList = n.extraData as *HirArgList;
                        while cur.next != null as *HirArgList {
                            cur = cur.next;
                        }
                        let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        newNode.node = lowered;
                        newNode.next = null as *HirArgList;
                        cur.next = newNode;
                        n.extraCount = n.extraCount + 1;
                    }
                    arg = arg.next;
                    argIdx = argIdx + 1;
                }
                return n;
            }

            // Decide direct vs indirect call
            var isDirectFunc: bool = false;
            if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
                let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
                if sym.kind == skFunc {
                    isDirectFunc = true;
                }
            }

            if isDirectFunc {
                n.kind = hCall;
                n.strValue = expr.child1.strValue;

                // Generic call monomorphization (explicit / inferred type args)
                if expr.child1 != null as *Expr {
                    var argc: int = expr.child1.genericTypeArgCount;
                    var typeArg0: String = expr.child1.genericTypeArg0;
                    var typeArg1: String = expr.child1.genericTypeArg1;
                    // Fallback: infer T from first *Array/*Iter arg when sema left count=0
                    if argc == 0 {
                        let genTry: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
                        if genTry != null as *Decl && genTry.typeParamCount > 0 {
                            if expr.callArgs != null as *ExprList && expr.callArgs.expr != null as *Expr {
                                var a0: *Expr = expr.callArgs.expr;
                                var te: *TypeExpr = a0.refType;
                                if te == null as *TypeExpr && a0.kind == ekUnary && a0.intValue == tkAmp {
                                    if a0.child1 != null as *Expr { te = a0.child1.refType; }
                                }
                                if te != null as *TypeExpr {
                                    // Unwrap pointer
                                    if (te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef)
                                    && te.pointerPointee != null as *TypeExpr {
                                        te = te.pointerPointee;
                                    }
                                    var elem: String = "";
                                    if te.typeArgCount > 0 {
                                        elem = te.typeArgName0;
                                    } else {
                                        // Mangled Array_int / Iter_String
                                        elem = Lcx_ExtractElemFromName(te.typeName);
                                    }
                                    if !String_Eq(elem, "") {
                                        typeArg0 = elem;
                                        argc = 1;
                                        // Second type arg from func-typed second argument if needed
                                        if genTry.typeParamCount >= 2 && expr.callArgs.next != null as *ExprList {
                                            let a1e: *Expr = expr.callArgs.next.expr;
                                            if a1e != null as *Expr && a1e.kind == ekIdent {
                                                let fsym: Symbol = Scope_Lookup(ctx.scope, a1e.strValue);
                                                if fsym.kind == skFunc && fsym.decl != null as *Decl
                                                && fsym.decl.retType != null as *TypeExpr
                                                && fsym.decl.retType.kind == tekNamed {
                                                    typeArg1 = fsym.decl.retType.typeName;
                                                    argc = 2;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if argc > 0 {
                        let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
                        if genDecl != null as *Decl {
                            if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; }
                            if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; }
                            if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; }
                            if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; }
                            let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, argc);
                            n.strValue = mangled;
                        }
                    }
                }

                // Lower arguments into child1/child2/extraData
                var arg: *ExprList = expr.callArgs;
                var argIdx: int = 0;
                while arg != null as *ExprList {
                    let lowered: *HirNode = Lcx_LowerExpr(ctx, arg.expr);
                    if argIdx == 0 {
                        n.child1 = lowered;
                    } else if argIdx == 1 {
                        n.child2 = lowered;
                    } else if argIdx == 2 {
                        let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        firstExtra.node = lowered;
                        firstExtra.next = null as *HirArgList;
                        n.extraData = firstExtra as *void;
                        n.extraCount = 1;
                    } else {
                        var cur: *HirArgList = n.extraData as *HirArgList;
                        while cur.next != null as *HirArgList {
                            cur = cur.next;
                        }
                        let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        newNode.node = lowered;
                        newNode.next = null as *HirArgList;
                        cur.next = newNode;
                        n.extraCount = n.extraCount + 1;
                    }
                    arg = arg.next;
                    argIdx = argIdx + 1;
                }
            } else {
                n.kind = hCallIndirect;
                n.child1 = Lcx_LowerExpr(ctx, expr.child1);
                // Lower arguments into child2/child3/extraData (child1 is callee)
                var arg: *ExprList = expr.callArgs;
                var argIdx: int = 0;
                while arg != null as *ExprList {
                    let lowered: *HirNode = Lcx_LowerExpr(ctx, arg.expr);
                    if argIdx == 0 {
                        n.child2 = lowered;
                    } else if argIdx == 1 {
                        n.child3 = lowered;
                    } else if argIdx == 2 {
                        let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        firstExtra.node = lowered;
                        firstExtra.next = null as *HirArgList;
                        n.extraData = firstExtra as *void;
                        n.extraCount = 1;
                    } else {
                        var cur: *HirArgList = n.extraData as *HirArgList;
                        while cur.next != null as *HirArgList {
                            cur = cur.next;
                        }
                        let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        newNode.node = lowered;
                        newNode.next = null as *HirArgList;
                        cur.next = newNode;
                        n.extraCount = n.extraCount + 1;
                    }
                    arg = arg.next;
                    argIdx = argIdx + 1;
                }
            }
            return n;
        }

        // Sizeof
        if kind == ekSizeOf {
            n.kind = hSizeOf;
            if expr.refType != null as *TypeExpr {
                let substTe: *TypeExpr = Lcx_SubstituteType(ctx, expr.refType);
                if substTe != null as *TypeExpr {
                    n.typeName = substTe.typeName;
                } else {
                    n.typeName = expr.refType.typeName;
                }
            }
            return n;
        }

        // Field access
        if kind == ekField {
            // Check if this is enum variant access: Color::Green
            if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
                let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
                if sym.decl != null as *Decl && sym.decl.kind == dkEnum {
                    // Emit as variable reference: Color_Green
                    n.kind = hVar;
                    n.strValue = String_Concat(String_Concat(expr.child1.strValue, "_"), expr.strValue);
                    return n;
                }
            }
            // Simple enum .tag is the enum value itself
            if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr && expr.child1.refType.kind == tekNamed && String_Eq(expr.strValue, "tag") {
                let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.refType.typeName);
                if sym.decl != null as *Decl && sym.decl.kind == dkEnum {
                    var hasData: bool = false;
                    if sym.decl.variantCount > 0 && sym.decl.variant0.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 1 && sym.decl.variant1.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 2 && sym.decl.variant2.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 3 && sym.decl.variant3.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 4 && sym.decl.variant4.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 5 && sym.decl.variant5.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 6 && sym.decl.variant6.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 7 && sym.decl.variant7.fieldCount > 0 { hasData = true; }
                    if sym.decl.variantCount > 8 && sym.decl.variant8.fieldCount > 0 { hasData = true; }
                    if !hasData {
                        return Lcx_LowerExpr(ctx, expr.child1);
                    }
                }
            }
            n.kind = hFieldPtr;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1);
            n.strValue = expr.strValue;

            // Get struct type from base expr refType
            if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
                n.typeName = expr.child1.refType.typeName;
            }
            return n;
        }

        // spawn Callee(args)
        if kind == ekSpawn {
            n.kind = hSpawn;
            n.boolValue = expr.boolValue;
            if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
                n.strValue = expr.child1.strValue;
            }
            if expr.child2 != null as *Expr {
                n.child1 = Lcx_LowerExpr(ctx, expr.child2);
            }
            return n;
        }

        // expr.await
        if kind == ekAwait {
            n.kind = hAwait;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1);
            return n;
        }

        // Index: arr[idx]
        if kind == ekIndex {
            // In @[Checked] functions, Array access goes through Array_Get with bounds check
            if ctx.checkedFunc && !ctx.releaseFunc && expr.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.refType) {
                let elemType: String = Lcx_GetArrayElemType(expr.child1.refType);
                if !String_Eq(elemType, "") {
                    let baseNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
                    let idxNode: *HirNode = Lcx_LowerExpr(ctx, expr.child2);

                    var isPtr: bool = false;
                    if expr.child1.refType != null as *TypeExpr && expr.child1.refType.kind == tekPointer {
                        isPtr = true;
                    }

                    let callNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    callNode.kind = hCall;
                    callNode.strValue = Lcx_MangleName("Array_Get", elemType, "", 1);
                    callNode.line = line;
                    callNode.column = col;

                    let genGet: *Decl = Lcx_FindGenericFunc(ctx, "Array_Get");
                    if genGet != null as *Decl {
                        Lcx_GenerateFuncInstance(ctx, genGet, elemType, "", 1);
                    }

                    if isPtr {
                        callNode.child1 = baseNode;
                    } else {
                        let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        addrNode.kind = hUnary;
                        addrNode.intValue = tkAmp;
                        addrNode.child1 = baseNode;
                        callNode.child1 = addrNode;
                    }
                    callNode.child2 = idxNode;
                    callNode.extraCount = 0;
                    callNode.extraData = null as *void;

                    return callNode;
                }
            }

            // For Array<T> or *Array<T>, desugar arr[idx] → arr.data[idx]
            if expr.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.refType) {
                let baseNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
                let idxNode: *HirNode = Lcx_LowerExpr(ctx, expr.child2);

                let fieldPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                fieldPtr.kind = hFieldPtr;
                fieldPtr.line = line;
                fieldPtr.column = col;
                fieldPtr.strValue = "data";
                fieldPtr.child1 = baseNode;

                n.kind = hIndexPtr;
                n.child1 = fieldPtr;
                n.child2 = idxNode;
                return n;
            }

            // Operator overloading: try operator_index_get
            if expr.child1 != null as *Expr {
                var receiverTypeName: String = "";
                if expr.child1.refType != null as *TypeExpr {
                    let refTe: *TypeExpr = expr.child1.refType;
                    if refTe.kind == tekNamed {
                        receiverTypeName = refTe.typeName;
                    } else if refTe.kind == tekPointer && refTe.pointerPointee != null as *TypeExpr && refTe.pointerPointee.kind == tekNamed {
                        receiverTypeName = refTe.pointerPointee.typeName;
                    }
                }
                if !String_Eq(receiverTypeName, "") {
                    var funcName: String = String_Concat(String_Concat(receiverTypeName, "_"), "operator_index_get");
                    let sym: Symbol = Scope_Lookup(ctx.scope, funcName);
                    var didMono: bool = false;
                    // Generic monomorphization: if not found or is generic, monomorphize
                    if sym.kind != skFunc || sym.decl == null as *Decl || sym.decl.typeParamCount > 0 {
                        var typeArg0: String = "";
                        if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
                            let refTe2: *TypeExpr = expr.child1.refType;
                            if refTe2.kind == tekNamed && refTe2.typeArgCount > 0 {
                                typeArg0 = refTe2.typeArgName0;
                            } else if refTe2.kind == tekPointer && refTe2.pointerPointee != null as *TypeExpr && refTe2.pointerPointee.typeArgCount > 0 {
                                typeArg0 = refTe2.pointerPointee.typeArgName0;
                            }
                        }
                        if !String_Eq(typeArg0, "") {
                            let genericFuncName: String = String_Concat(String_Concat(receiverTypeName, "_"), "operator_index_get");
                            let genDecl: *Decl = Lcx_FindGenericFunc(ctx, genericFuncName);
                            if genDecl != null as *Decl {
                                funcName = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, "", 1);
                                didMono = true;
                            }
                        }
                    }
                    let sym2: Symbol = Scope_Lookup(ctx.scope, funcName);
                    let targetDecl: *Decl = sym2.decl;
                    if didMono {
                        targetDecl = sym.decl;
                    }
                    if sym2.kind == skFunc && sym2.decl != null as *Decl || didMono {
                        n.kind = hCall;
                        n.strValue = funcName;
                        let recv: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
                        // If method expects pointer/reference but receiver is a value, add &
                        if targetDecl != null as *Decl && targetDecl.paramCount > 0 && targetDecl.param0.refParamType != null as *TypeExpr {
                            let paramKind: int = targetDecl.param0.refParamType.kind;
                            if paramKind == tekPointer || paramKind == tekRef || paramKind == tekMutRef {
                                if expr.child1.refType != null as *TypeExpr && expr.child1.refType.kind != tekPointer && expr.child1.refType.kind != tekRef && expr.child1.refType.kind != tekMutRef {
                                    let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    addrNode.kind = hUnary;
                                    addrNode.intValue = tkAmp;
                                    addrNode.child1 = recv;
                                    n.child1 = addrNode;
                                } else {
                                    n.child1 = recv;
                                }
                            } else {
                                n.child1 = recv;
                            }
                        } else {
                            n.child1 = recv;
                        }
                        n.child2 = Lcx_LowerExpr(ctx, expr.child2);
                        return n;
                    }
                }
            }

            n.kind = hIndexPtr;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1);
            n.child2 = Lcx_LowerExpr(ctx, expr.child2);
            return n;
        }

        // Assign: target = value
        if kind == ekAssign {
            // Array bounds-checking for write in @[Checked]: arr[idx] = val → Array_Set_T(&arr, idx, val)
            // Only for plain assignment (=), not compound operators (+=, -=, etc.)
            if expr.intValue == tkAssign && ctx.checkedFunc && !ctx.releaseFunc && expr.child1 != null as *Expr && expr.child1.kind == ekIndex && expr.child1.child1 != null as *Expr && Lcx_IsArrayTypeExpr(expr.child1.child1.refType) {
                let elemType: String = Lcx_GetArrayElemType(expr.child1.child1.refType);
                if !String_Eq(elemType, "") {
                    let baseNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child1);
                    let idxNode: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child2);
                    let valNode: *HirNode = Lcx_LowerExpr(ctx, expr.child2);

                    var isPtr: bool = false;
                    if expr.child1.child1.refType != null as *TypeExpr && expr.child1.child1.refType.kind == tekPointer {
                        isPtr = true;
                    }

                    let callNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    callNode.kind = hCall;
                    callNode.strValue = Lcx_MangleName("Array_Set", elemType, "", 1);
                    callNode.line = line;
                    callNode.column = col;

                    let genSet: *Decl = Lcx_FindGenericFunc(ctx, "Array_Set");
                    if genSet != null as *Decl {
                        Lcx_GenerateFuncInstance(ctx, genSet, elemType, "", 1);
                    }

                    if isPtr {
                        callNode.child1 = baseNode;
                    } else {
                        let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        addrNode.kind = hUnary;
                        addrNode.intValue = tkAmp;
                        addrNode.child1 = baseNode;
                        callNode.child1 = addrNode;
                    }
                    callNode.child2 = idxNode;

                    let extra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                    extra.node = valNode;
                    extra.next = null as *HirArgList;
                    callNode.extraData = extra as *void;
                    callNode.extraCount = 1;

                    return callNode;
                }
            }

            // Operator overloading: try operator_index_set
            if expr.intValue == tkAssign && expr.child1 != null as *Expr && expr.child1.kind == ekIndex && expr.child1.child1 != null as *Expr {
                var receiverTypeName: String = "";
                let objExpr: *Expr = expr.child1.child1;
                if objExpr.refType != null as *TypeExpr {
                    let refTe: *TypeExpr = objExpr.refType;
                    if refTe.kind == tekNamed {
                        receiverTypeName = refTe.typeName;
                    } else if refTe.kind == tekPointer && refTe.pointerPointee != null as *TypeExpr && refTe.pointerPointee.kind == tekNamed {
                        receiverTypeName = refTe.pointerPointee.typeName;
                    }
                }
                if !String_Eq(receiverTypeName, "") {
                    var funcName: String = String_Concat(String_Concat(receiverTypeName, "_"), "operator_index_set");
                    let sym: Symbol = Scope_Lookup(ctx.scope, funcName);
                    var didMono: bool = false;
                    // Generic monomorphization: if not found or is generic, monomorphize
                    if sym.kind != skFunc || sym.decl == null as *Decl || sym.decl.typeParamCount > 0 {
                        var typeArg0: String = "";
                        if objExpr != null as *Expr && objExpr.refType != null as *TypeExpr {
                            let refTe2: *TypeExpr = objExpr.refType;
                            if refTe2.kind == tekNamed && refTe2.typeArgCount > 0 {
                                typeArg0 = refTe2.typeArgName0;
                            } else if refTe2.kind == tekPointer && refTe2.pointerPointee != null as *TypeExpr && refTe2.pointerPointee.typeArgCount > 0 {
                                typeArg0 = refTe2.pointerPointee.typeArgName0;
                            }
                        }
                        if !String_Eq(typeArg0, "") {
                            let genericFuncName: String = String_Concat(String_Concat(receiverTypeName, "_"), "operator_index_set");
                            let genDecl: *Decl = Lcx_FindGenericFunc(ctx, genericFuncName);
                            if genDecl != null as *Decl {
                                funcName = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, "", 1);
                                didMono = true;
                            }
                        }
                    }
                    let sym2: Symbol = Scope_Lookup(ctx.scope, funcName);
                    let targetDecl: *Decl = sym2.decl;
                    if didMono {
                        targetDecl = sym.decl;
                    }
                    if sym2.kind == skFunc && sym2.decl != null as *Decl || didMono {
                        let callNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        callNode.kind = hCall;
                        callNode.strValue = funcName;
                        callNode.line = line;
                        callNode.column = col;
                        let recv: *HirNode = Lcx_LowerExpr(ctx, objExpr);
                        // If method expects pointer/reference but receiver is a value, add &
                        if targetDecl != null as *Decl && targetDecl.paramCount > 0 && targetDecl.param0.refParamType != null as *TypeExpr {
                            let paramKind: int = targetDecl.param0.refParamType.kind;
                            if paramKind == tekPointer || paramKind == tekRef || paramKind == tekMutRef {
                                if objExpr.refType != null as *TypeExpr && objExpr.refType.kind != tekPointer && objExpr.refType.kind != tekRef && objExpr.refType.kind != tekMutRef {
                                    let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    addrNode.kind = hUnary;
                                    addrNode.intValue = tkAmp;
                                    addrNode.child1 = recv;
                                    callNode.child1 = addrNode;
                                } else {
                                    callNode.child1 = recv;
                                }
                            } else {
                                callNode.child1 = recv;
                            }
                        } else {
                            callNode.child1 = recv;
                        }
                        callNode.child2 = Lcx_LowerExpr(ctx, expr.child1.child2);
                        let extra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        extra.node = Lcx_LowerExpr(ctx, expr.child2);
                        extra.next = null as *HirArgList;
                        callNode.extraData = extra as *void;
                        callNode.extraCount = 1;
                        return callNode;
                    }
                }
            }

            n.kind = hAssign;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1); // target
            n.child2 = Lcx_LowerExpr(ctx, expr.child2); // value
            return n;
        }

        // Tuple expression (a, b, ...) → struct init Tuple_int_int { ._0 = a, ._1 = b }
        if kind == ekTuple {
            var tname: String = "Tuple";
            var ti: int = 0;
            while ti < expr.callArgCount {
                tname = String_Concat(tname, "_int");
                ti = ti + 1;
            }
            if expr.callArgCount == 0 { tname = "Tuple_Empty"; }
            if expr.refType != null as *TypeExpr && !String_Eq(expr.refType.typeName, "") {
                tname = expr.refType.typeName;
            }
            n.kind = hStructInit;
            n.strValue = tname;
            n.typeKind = tyNamed;
            n.typeName = tname;
            var firstField: *HirNode = null as *HirNode;
            var lastField: *HirNode = null as *HirNode;
            var tcur: *ExprList = expr.callArgs;
            var tidx: int = 0;
            while tcur != null as *ExprList {
                let fNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                fNode.kind = hBlock;
                fNode.strValue = String_Concat("_", String_FromInt(tidx as int64));
                fNode.child1 = Lcx_LowerExpr(ctx, tcur.expr);
                if firstField == null as *HirNode {
                    firstField = fNode;
                    lastField = fNode;
                } else {
                    lastField.child3 = fNode;
                    lastField = fNode;
                }
                tcur = tcur.next;
                tidx = tidx + 1;
            }
            n.child1 = firstField;
            return n;
        }

        // Closure: fat function pointer (multi-instance via heap env + maker)
        if kind == ekClosure {
            let f: *HirFunc = Lcx_LowerClosureFunc(ctx, expr);
            var fatName: String = "BuxFn_int_int";
            if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc {
                fatName = Lcx_BuildFuncTypeName(expr.refType);
            } else if f.paramCount >= 2 {
                // thunk has __env + user params; approximate from ret + user arity
                fatName = "BuxFn_int_int";
                if f.paramCount == 3 { fatName = "BuxFn_int_int_int"; }
                if f.paramCount == 1 { fatName = "BuxFn_int_void"; }
            }
            if f.captureCount > 0 {
                // Call __make_<closure>(captures...) which heap-allocs env
                n.kind = hCall;
                n.strValue = String_Concat("__make_", f.name);
                n.typeKind = tyFunc;
                n.typeName = fatName;
                var ci: int = 0;
                while ci < f.captureCount {
                    var capName: String = "";
                    if ci == 0 { capName = f.captureName0; }
                    else if ci == 1 { capName = f.captureName1; }
                    else if ci == 2 { capName = f.captureName2; }
                    else if ci == 3 { capName = f.captureName3; }
                    else if ci == 4 { capName = f.captureName4; }
                    else if ci == 5 { capName = f.captureName5; }
                    else if ci == 6 { capName = f.captureName6; }
                    else if ci == 7 { capName = f.captureName7; }
                    let capVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    capVar.kind = hVar;
                    capVar.strValue = capName;
                    if ci == 0 { n.child1 = capVar; }
                    else if ci == 1 { n.child2 = capVar; }
                    else if ci == 2 {
                        let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        firstExtra.node = capVar;
                        firstExtra.next = null as *HirArgList;
                        n.extraData = firstExtra as *void;
                        n.extraCount = 1;
                    } else {
                        var cur: *HirArgList = n.extraData as *HirArgList;
                        while cur.next != null as *HirArgList { cur = cur.next; }
                        let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
                        newNode.node = capVar;
                        newNode.next = null as *HirArgList;
                        cur.next = newNode;
                        n.extraCount = n.extraCount + 1;
                    }
                    ci = ci + 1;
                }
                return n;
            }
            // Capture-less: compound literal fat pointer with NULL env
            n.kind = hStructInit;
            n.strValue = fatName;
            n.typeKind = tyFunc;
            n.typeName = fatName;
            let codeField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            codeField.kind = hBlock;
            codeField.strValue = "code";
            let codeVal: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            codeVal.kind = hVar;
            codeVal.strValue = f.name;
            codeField.child1 = codeVal;
            let envField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            envField.kind = hBlock;
            envField.strValue = "env";
            let nullEnv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            nullEnv.kind = hCast;
            nullEnv.typeName = "void*";
            let zero: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            zero.kind = hLit;
            zero.intValue = tkIntLiteral;
            zero.strValue = "0";
            nullEnv.child1 = zero;
            envField.child1 = nullEnv;
            codeField.child3 = envField;
            n.child1 = codeField;
            return n;
        }

        // Cast
        if kind == ekCast {
            n.kind = hCast;
            n.child1 = Lcx_LowerExpr(ctx, expr.child1);
            if expr.refType != null as *TypeExpr {
                let substTe: *TypeExpr = Lcx_SubstituteType(ctx, expr.refType);
                if substTe == null as *TypeExpr { substTe = expr.refType; }
                let resolvedKind: int = Lcx_ResolveTypeKind(substTe);
                n.typeKind = resolvedKind;
                // For pointer types, construct "PointeeType*"
                if substTe.kind == tekPointer && substTe.pointerPointee != null as *TypeExpr {
                    n.typeName = String_Concat(substTe.pointerPointee.typeName, "*");
                } else if !String_Eq(substTe.typeName, "") {
                    n.typeName = substTe.typeName;
                }
            }
            return n;
        }

        // Struct init: TypeName { field: value, ... }
        if kind == ekStructInit {
            // Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
            let enumSym: Symbol = Scope_Lookup(ctx.scope, expr.structName);
            if enumSym.decl != null as *Decl && enumSym.decl.kind == dkEnum {
                var hasData: bool = false;
                if enumSym.decl.variantCount > 0 && enumSym.decl.variant0.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 1 && enumSym.decl.variant1.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 2 && enumSym.decl.variant2.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 3 && enumSym.decl.variant3.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 4 && enumSym.decl.variant4.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 5 && enumSym.decl.variant5.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 6 && enumSym.decl.variant6.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 7 && enumSym.decl.variant7.fieldCount > 0 { hasData = true; }
                if enumSym.decl.variantCount > 8 && enumSym.decl.variant8.fieldCount > 0 { hasData = true; }
                if !hasData && expr.child1 != null as *Expr && String_Eq(expr.child1.strValue, "tag") {
                    return Lcx_LowerExpr(ctx, expr.child1.child1);
                }
            }
            n.kind = hStructInit;
            var structName: String = expr.structName;
            // Generic struct monomorphization
            if expr.genericTypeArgCount > 0 {
                let genDecl: *Decl = Lcx_FindGenericStruct(ctx, expr.structName);
                if genDecl != null as *Decl {
                    var typeArg0: String = expr.genericTypeArg0;
                    var typeArg1: String = expr.genericTypeArg1;
                    if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; }
                    if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; }
                    if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; }
                    if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; }
                    structName = Lcx_GenerateStructInstance(ctx, genDecl, typeArg0, typeArg1, expr.genericTypeArgCount);
                }
            }
            n.strValue = structName;
            // Lower each field (fields are chained via child3 on synthetic ekField exprs)
            var field: *Expr = expr.child1;
            var firstField: *HirNode = null as *HirNode;
            var lastField: *HirNode = null as *HirNode;
            while field != null as *Expr {
                let fNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                fNode.kind = hBlock; // placeholder, field is identified by name+value
                fNode.line = expr.line;
                fNode.column = expr.column;
                fNode.strValue = field.strValue;  // field name
                fNode.child1 = Lcx_LowerExpr(ctx, field.child1); // field value
                if firstField == null as *HirNode {
                    firstField = fNode;
                    lastField = fNode;
                } else {
                    lastField.child3 = fNode;
                    lastField = fNode;
                }
                field = field.child3;
            }
            n.child1 = firstField;
            return n;
        }

        // Block expression (boolValue = true means unsafe block)
        // retTypeKind -2 → yield last expression as block value
        if kind == ekBlock {
            if expr.refBlock != null as *Block {
                if expr.boolValue {
                    let oldChecked: bool = ctx.checkedFunc;
                    let oldRelease: bool = ctx.releaseFunc;
                    ctx.checkedFunc = false;
                    ctx.releaseFunc = false;
                    let blockNode: *HirNode = Lcx_LowerBlock(ctx, expr.refBlock, -2);
                    ctx.checkedFunc = oldChecked;
                    ctx.releaseFunc = oldRelease;
                    return blockNode;
                } else {
                    return Lcx_LowerBlock(ctx, expr.refBlock, -2);
                }
            }
            return n;
        }

        return n;
    }

    // ---------------------------------------------------------------------------
    // Statement lowering
    // ---------------------------------------------------------------------------

    func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
        if stmt == null as *Stmt { return null as *HirNode; }

        let line: uint32 = stmt.line;
        let col: uint32 = stmt.column;
        let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        n.kind = hBlock;
        n.line = line;
        n.column = col;

        let kind: int = stmt.kind;

        // Let/var → alloca + store
        if kind == skLet {
            // Try operator: let x: T = operand?  ->  tmp = operand; if tmp.tag == Err { return tmp; } let x = tmp.data.Ok;
            if stmt.child1 != null as *Expr && stmt.child1.kind == ekTry {
                let tryExpr: *Expr = stmt.child1;
                let operandExpr: *Expr = tryExpr.child1;
                let operandTypeExpr: *TypeExpr = operandExpr.refType;
                var typeName: String = "Result";
                var errTag: String = "Result_Err";
                var okField: String = "Ok_0";
                if operandTypeExpr != null as *TypeExpr && operandTypeExpr.kind == tekNamed {
                    typeName = operandTypeExpr.typeName;
                    if String_Eq(typeName, "Option") {
                        errTag = "Option_None";
                        okField = "Some_0";
                    } else if !String_Eq(typeName, "Result") {
                        errTag = String_Concat(String_Concat(typeName, "_"), "Err");
                        okField = "Ok_0";
                    }
                }
                let tmpName: String = String_Concat("__try_tmp_", String_FromInt(ctx.tryCounter as int64));
                ctx.tryCounter = ctx.tryCounter + 1;

                // alloca tmp
                let tmpAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpAlloca.kind = hAlloca;
                tmpAlloca.line = line;
                tmpAlloca.column = col;
                tmpAlloca.strValue = tmpName;
                tmpAlloca.typeName = typeName;

                // tmp = operand
                let tmpStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpStore.kind = hStore;
                tmpStore.line = line;
                tmpStore.column = col;
                let tmpVarRef: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tmpVarRef.kind = hVar;
                tmpVarRef.strValue = tmpName;
                tmpStore.child1 = tmpVarRef;
                tmpStore.child2 = Lcx_LowerExpr(ctx, operandExpr);

                // if (tmp.tag == errTag) return tmp
                let tagPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagPtr.kind = hFieldPtr;
                tagPtr.line = line;
                tagPtr.column = col;
                tagPtr.strValue = "tag";
                tagPtr.child1 = tmpVarRef;
                let tagLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                tagLoad.kind = hLoad;
                tagLoad.line = line;
                tagLoad.column = col;
                tagLoad.child1 = tagPtr;
                let errConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                errConst.kind = hVar;
                errConst.strValue = errTag;
                let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                cond.kind = hBinary;
                cond.line = line;
                cond.column = col;
                cond.intValue = tkEq;
                cond.child1 = tagLoad;
                cond.child2 = errConst;
                let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                retNode.kind = hReturn;
                retNode.line = line;
                retNode.column = col;
                retNode.child1 = tmpVarRef;
                let thenBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                thenBlock.kind = hBlock;
                thenBlock.line = line;
                thenBlock.column = col;
                thenBlock.child1 = retNode;
                let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                ifNode.kind = hIf;
                ifNode.line = line;
                ifNode.column = col;
                ifNode.child1 = cond;
                ifNode.child2 = thenBlock;

                // New initializer: tmp.data.OkField
                let dataPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                dataPtr.kind = hFieldPtr;
                dataPtr.line = line;
                dataPtr.column = col;
                dataPtr.strValue = "data";
                dataPtr.child1 = tmpVarRef;
                let dataLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                dataLoad.kind = hLoad;
                dataLoad.line = line;
                dataLoad.column = col;
                dataLoad.child1 = dataPtr;
                let okPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                okPtr.kind = hFieldPtr;
                okPtr.line = line;
                okPtr.column = col;
                okPtr.strValue = okField;
                okPtr.child1 = dataLoad;
                let okLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                okLoad.kind = hLoad;
                okLoad.line = line;
                okLoad.column = col;
                okLoad.child1 = okPtr;

                // alloca x
                let xAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                xAlloca.kind = hAlloca;
                xAlloca.line = line;
                xAlloca.column = col;
                xAlloca.strValue = stmt.strValue;
                xAlloca.typeName = "";
                let letTe: *TypeExpr = Lcx_SubstituteType(ctx, stmt.refStmtType);
                if letTe != null as *TypeExpr {
                    xAlloca.intValue = letTe.kind;
                    xAlloca.typeKind = Lcx_ResolveTypeKind(letTe);
                    if letTe.kind == tekFunc {
                        xAlloca.typeName = Lcx_BuildFuncTypeName(letTe);
                    } else if letTe.kind == tekPointer && letTe.pointerPointee != null as *TypeExpr {
                        xAlloca.typeName = String_Concat(letTe.pointerPointee.typeName, "*");
                    } else if !String_Eq(letTe.typeName, "") {
                        xAlloca.typeName = letTe.typeName;
                    }
                }
                var xSym: Symbol;
                xSym.kind = skVar;
                xSym.name = stmt.strValue;
                xSym.typeKind = xAlloca.typeKind;
                xSym.typeName = xAlloca.typeName;
                xSym.refType = letTe;
                xSym.isMutable = false;
                xSym.isPublic = false;
                xSym.decl = null as *Decl;
                discard Scope_Define(ctx.scope, xSym);

                let xStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                xStore.kind = hStore;
                xStore.line = line;
                xStore.column = col;
                xStore.child1 = xAlloca;
                xStore.child2 = okLoad;

                tmpAlloca.child3 = tmpStore;
                tmpStore.child3 = ifNode;
                ifNode.child3 = xStore;
                let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                blockNode.kind = hBlock;
                blockNode.line = line;
                blockNode.column = col;
                blockNode.child1 = tmpAlloca;
                return blockNode;
            }

            let init: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
            // alloca for the variable
            let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            alloca.kind = hAlloca;
            alloca.line = line;
            alloca.column = col;
            alloca.strValue = stmt.strValue;
            // Set type from the declared type expression (with generic substitution)
            alloca.typeName = "";
            let letTe: *TypeExpr = Lcx_SubstituteType(ctx, stmt.refStmtType);
            if letTe != null as *TypeExpr {
                alloca.intValue = letTe.kind;
                alloca.typeKind = Lcx_ResolveTypeKind(letTe);
                // For function types, build C function-pointer syntax
                if letTe.kind == tekFunc {
                    alloca.typeName = Lcx_BuildFuncTypeName(letTe);
                } else if letTe.kind == tekPointer && letTe.pointerPointee != null as *TypeExpr {
                    alloca.typeName = String_Concat(letTe.pointerPointee.typeName, "*");
                } else if !String_Eq(letTe.typeName, "") {
                    alloca.typeName = letTe.typeName;
                }
            }
            // Add to scope for field offset lookups (skip if already defined)
            var sym: Symbol;
            sym.kind = skVar;
            sym.name = stmt.strValue;
            sym.typeKind = alloca.typeKind;
            sym.typeName = alloca.typeName;
            sym.refType = letTe;
            sym.isMutable = false;
            sym.isPublic = false;
            sym.decl = null as *Decl;
            discard Scope_Define(ctx.scope, sym);

            // Match (or other multi-stmt yield) as let initializer:
            //   match stmts...; Type x = __match_N;
            // instead of illegal `Type x = <block>;`
            if Lcx_IsMatchYield(init) {
                let yieldName: String = init.strValue;
                init.strValue = "";
                let yieldVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                yieldVar.kind = hVar;
                yieldVar.strValue = yieldName;
                let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                storeNode.kind = hStore;
                storeNode.line = line;
                storeNode.column = col;
                storeNode.child1 = alloca;
                storeNode.child2 = yieldVar;
                Lcx_AppendToChain(init, storeNode);
                return init;
            }

            // store the init value
            let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            storeNode.kind = hStore;
            storeNode.child1 = alloca;
            storeNode.child2 = init;

            // Auto-Drop for @[Drop] types and heap-allocated stdlib types
            var deferNode: *HirNode = null as *HirNode;
            if !String_Eq(alloca.typeName, "") {
                let typeName: String = alloca.typeName;
                let freeName: String = Lcx_BuildAutoDropFree(ctx, typeName);
                if !String_Eq(freeName, "") {
                    let varRef: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    varRef.kind = hVar;
                    varRef.strValue = stmt.strValue;
                    let addrNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    addrNode.kind = hUnary;
                    addrNode.intValue = tkAmp;
                    addrNode.child1 = varRef;
                    let callNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    callNode.kind = hCall;
                    callNode.strValue = freeName;
                    callNode.child1 = addrNode;
                    deferNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    deferNode.kind = hDefer;
                    deferNode.child1 = callNode;
                }
            }

            // Capturing closures allocate env via __make_* at ekClosure site.
            // Wrap with defer if present
            if deferNode != null as *HirNode {
                storeNode.child3 = deferNode;
                let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                blockNode.kind = hBlock;
                blockNode.line = line;
                blockNode.column = col;
                blockNode.child1 = storeNode;
                return blockNode;
            }
            return storeNode;
        }

        // Return
        if kind == skReturn {
            if stmt.child1 != null as *Expr {
                let retVal: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
                // `return match { ... }` / other multi-stmt yields: expand stmts then return result var
                if retVal != null as *HirNode && Lcx_IsMatchYield(retVal) {
                    let retVar: *HirNode = Lcx_YieldVarOf(retVal);
                    let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    retNode.kind = hReturn;
                    retNode.line = line;
                    retNode.column = col;
                    retNode.child1 = retVar;
                    retVal.strValue = "";
                    var lastIn: *HirNode = retVal.child1;
                    if lastIn == null as *HirNode {
                        retVal.child1 = retNode;
                    } else {
                        while lastIn.child3 != null as *HirNode {
                            lastIn = lastIn.child3;
                        }
                        lastIn.child3 = retNode;
                    }
                    return retVal;
                }
                n.kind = hReturn;
                n.child1 = retVal;
                return n;
            }
            n.kind = hReturn;
            return n;
        }

        // Expression statement
        if kind == skExpr && stmt.child1 != null as *Expr {
            return Lcx_LowerExpr(ctx, stmt.child1);
        }

        // If
        if kind == skIf {
            n.kind = hIf;
            n.child1 = Lcx_LowerExpr(ctx, stmt.child1); // condition
            if stmt.refStmtBlock != null as *Block {
                n.child2 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
            }
            if stmt.refStmtElse != null as *Block {
                n.extraData = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1) as *void;
            }
            return n;
        }

        // While
        if kind == skWhile {
            n.kind = hWhile;
            n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
            if stmt.refStmtBlock != null as *Block {
                n.child2 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
            }
            return n;
        }

        // Loop
        if kind == skLoop {
            n.kind = hLoop;
            if stmt.refStmtBlock != null as *Block {
                n.child1 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
            }
            return n;
        }

        // For
        if kind == skFor {
            let iterExpr: *Expr = stmt.child1;
            let varName: String = stmt.strValue;
            let body: *Block = stmt.refStmtBlock;

            // Range-based for: for i in lo..hi { body }
            // (selfhost parses .. as ekBinary; bootstrap parses as ekRange)
            let isRangeExpr: bool = iterExpr != null as *Expr && (iterExpr.kind == ekRange || (iterExpr.kind == ekBinary && (iterExpr.intValue == tkDotDot || iterExpr.intValue == tkDotDotEqual)));
            if isRangeExpr {
                let lo: *HirNode = Lcx_LowerExpr(ctx, iterExpr.child1);
                let hi: *HirNode = Lcx_LowerExpr(ctx, iterExpr.child2);
                var inclusive: bool = iterExpr.boolValue;
                if iterExpr.kind == ekBinary && iterExpr.intValue == tkDotDotEqual {
                    inclusive = true;
                }

                let varTypeKind: int = tyInt;
                let varTypeName: String = "int";

                // alloca for loop variable
                let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                alloca.kind = hAlloca;
                alloca.line = line;
                alloca.column = col;
                alloca.strValue = varName;
                alloca.typeName = varTypeName;
                alloca.typeKind = varTypeKind;

                // store init value
                let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                store.kind = hStore;
                store.child1 = alloca;
                store.child2 = lo;

                // var node for reading in condition
                let varRead: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                varRead.kind = hVar;
                varRead.strValue = varName;
                varRead.typeName = varTypeName;
                varRead.typeKind = varTypeKind;

                // condition: var < hi (or <=)
                let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                cond.kind = hBinary;
                if inclusive {
                    cond.intValue = tkLe;
                } else {
                    cond.intValue = tkLt;
                }
                cond.child1 = varRead;
                cond.child2 = hi;

                // Build while body: original body + increment
                let bodyBlock: *HirNode = Lcx_LowerBlock(ctx, body, -1);

                // increment: var = var + 1
                let varRead2: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                varRead2.kind = hVar;
                varRead2.strValue = varName;
                varRead2.typeName = varTypeName;
                varRead2.typeKind = varTypeKind;

                let one: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                one.kind = hLit;
                one.intValue = tkIntLiteral;
                one.strValue = "1";
                one.typeKind = varTypeKind;

                let inc: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                inc.kind = hBinary;
                inc.intValue = tkPlus;
                inc.child1 = varRead2;
                inc.child2 = one;

                let storeInc: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                storeInc.kind = hStore;
                // Use hVar (not alloca) for assignment to existing variable
                let varForInc: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                varForInc.kind = hVar;
                varForInc.strValue = varName;
                varForInc.typeName = varTypeName;
                varForInc.typeKind = varTypeKind;
                storeInc.child1 = varForInc;
                storeInc.child2 = inc;

                // Append storeInc to body block chain
                if bodyBlock != null as *HirNode && bodyBlock.kind == hBlock {
                    if bodyBlock.child1 != null as *HirNode {
                        var last: *HirNode = bodyBlock.child1;
                        while last.child3 != null as *HirNode {
                            last = last.child3;
                        }
                        last.child3 = storeInc;
                    } else {
                        bodyBlock.child1 = storeInc;
                    }
                } else if bodyBlock != null as *HirNode {
                    // Wrap single node into a block
                    let wrapBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    wrapBlock.kind = hBlock;
                    wrapBlock.child1 = bodyBlock;
                    var last: *HirNode = bodyBlock;
                    while last.child3 != null as *HirNode {
                        last = last.child3;
                    }
                    last.child3 = storeInc;
                    bodyBlock = wrapBlock;
                } else {
                    let wrapBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                    wrapBlock.kind = hBlock;
                    wrapBlock.child1 = storeInc;
                    bodyBlock = wrapBlock;
                }

                // while node
                let whileNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                whileNode.kind = hWhile;
                whileNode.child1 = cond;
                whileNode.child2 = bodyBlock;

                // Chain: store (contains alloca as child1) -> while
                // C backend emits hStore with hAlloca child1 as "Type x = value;"
                store.child3 = whileNode;

                let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                blockNode.kind = hBlock;
                blockNode.line = line;
                blockNode.column = col;
                blockNode.child1 = store;
                return blockNode;
            }

            // Collection-based for: for x in arr { body }
            // Desugar to:
            //   let __iter = Array_Iter_T(&arr);
            //   while Iter_HasNext_T(&__iter) {
            //       let x = Iter_Next_T(&__iter);
            //       body
            //   }
            if iterExpr != null as *Expr {
                let collTypeExpr: *TypeExpr = iterExpr.refType;
                if collTypeExpr == null as *TypeExpr && iterExpr.kind == ekIdent {
                    // Fallback: try scope lookup (may have substituted type)
                    let collSym: Symbol = Scope_Lookup(ctx.scope, iterExpr.strValue);
                    collTypeExpr = collSym.refType;
                }
                if collTypeExpr != null as *TypeExpr && collTypeExpr.kind == tekNamed {
                    let collTypeName: String = collTypeExpr.typeName;
                    var elemTypeName: String = "";
                    var elemTypeKind: int = tyInt;
                    if collTypeExpr.typeArgCount > 0 {
                        elemTypeName = collTypeExpr.typeArgName0;
                        elemTypeKind = Lcx_ResolveTypeKindFromName(elemTypeName);
                    }

                    // Handle already-monomorphized types like Array_int, Iter_string
                    var isArray: bool = String_Eq(collTypeName, "Array");
                    var isIter: bool = String_Eq(collTypeName, "Iter");
                    var isChannel: bool = String_Eq(collTypeName, "Channel");

                    // Also check for mangled names like Array_int, Iter_string
                    if !isArray && !isIter && !isChannel {
                        if String_StartsWith(collTypeName, "Array_") {
                            isArray = true;
                            let prefixLen: uint = 6; // len("Array_")
                            let totalLen: uint = bux_strlen(collTypeName);
                            if totalLen > prefixLen {
                                elemTypeName = bux_str_slice(collTypeName, prefixLen, totalLen - prefixLen);
                                elemTypeKind = Lcx_ResolveTypeKindFromName(elemTypeName);
                            }
                        } else if String_StartsWith(collTypeName, "Iter_") {
                            isIter = true;
                            let prefixLen: uint = 5; // len("Iter_")
                            let totalLen: uint = bux_strlen(collTypeName);
                            if totalLen > prefixLen {
                                elemTypeName = bux_str_slice(collTypeName, prefixLen, totalLen - prefixLen);
                                elemTypeKind = Lcx_ResolveTypeKindFromName(elemTypeName);
                            }
                        } else if String_StartsWith(collTypeName, "Channel_") {
                            isChannel = true;
                            let prefixLen: uint = 8; // len("Channel_")
                            let totalLen: uint = bux_strlen(collTypeName);
                            if totalLen > prefixLen {
                                elemTypeName = bux_str_slice(collTypeName, prefixLen, totalLen - prefixLen);
                                elemTypeKind = Lcx_ResolveTypeKindFromName(elemTypeName);
                            }
                        }
                    }

                    if !String_Eq(elemTypeName, "") {
                        if isChannel {
                            // Channel-based for: for x in ch { body }
                            // Desugar to:
                            //   while true {
                            //       let x: T;
                            //       if !Channel_Recv_Ok_T(&ch, &x) { break; }
                            //       body
                            //   }
                            let recvOkFuncName: String = Lcx_MangleName("Channel_Recv_Ok", elemTypeName, "", 1);
                            let genRecvOk: *Decl = Lcx_FindGenericFunc(ctx, "Channel_Recv_Ok");
                            if genRecvOk != null as *Decl {
                                Lcx_GenerateFuncInstance(ctx, genRecvOk, elemTypeName, "", 1);
                            }

                            let bodyBlock: *HirNode = Lcx_LowerBlock(ctx, body, -1);

                            // alloca for x
                            let xAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            xAlloca.kind = hAlloca;
                            xAlloca.line = line;
                            xAlloca.column = col;
                            xAlloca.strValue = varName;
                            xAlloca.typeName = elemTypeName;
                            xAlloca.typeKind = elemTypeKind;

                            // call Channel_Recv_Ok_T(&ch, &x)
                            let recvOkCall: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            recvOkCall.kind = hCall;
                            recvOkCall.strValue = recvOkFuncName;
                            let addrCh: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            addrCh.kind = hUnary;
                            addrCh.intValue = tkAmp;
                            if iterExpr.kind == ekIdent {
                                let chVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                chVar.kind = hVar;
                                chVar.strValue = iterExpr.strValue;
                                addrCh.child1 = chVar;
                            } else {
                                addrCh.child1 = Lcx_LowerExpr(ctx, iterExpr);
                            }
                            let addrX: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            addrX.kind = hUnary;
                            addrX.intValue = tkAmp;
                            let xVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            xVar.kind = hVar;
                            xVar.strValue = varName;
                            addrX.child1 = xVar;
                            recvOkCall.child1 = addrCh;
                            recvOkCall.child2 = addrX;

                            // if !recvOk { break; }
                            let notRecvOk: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            notRecvOk.kind = hUnary;
                            notRecvOk.intValue = tkBang;
                            notRecvOk.child1 = recvOkCall;
                            let breakNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            breakNode.kind = hBreak;
                            let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            ifNode.kind = hIf;
                            ifNode.child1 = notRecvOk;
                            ifNode.child2 = breakNode;

                            // Build while body block: ifNode -> bodyBlock
                            let whileBody: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            whileBody.kind = hBlock;
                            whileBody.child1 = xAlloca;
                            xAlloca.child3 = ifNode;
                            if bodyBlock != null as *HirNode && bodyBlock.kind == hBlock {
                                ifNode.child3 = bodyBlock.child1;
                            } else if bodyBlock != null as *HirNode {
                                ifNode.child3 = bodyBlock;
                            }

                            // while true
                            let trueLit: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            trueLit.kind = hLit;
                            trueLit.intValue = tkBoolLiteral;
                            trueLit.strValue = "true";
                            let whileNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            whileNode.kind = hWhile;
                            whileNode.child1 = trueLit;
                            whileNode.child2 = whileBody;

                            let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            blockNode.kind = hBlock;
                            blockNode.line = line;
                            blockNode.column = col;
                            blockNode.child1 = whileNode;
                            return blockNode;
                        }
                        if isArray || isIter {
                            let iterVarName: String = String_Concat("__iter_", varName);
                            let iterTypeName: String = Lcx_MangleName("Iter", elemTypeName, "", 1);

                            // Ensure struct instances exist
                            let iterGenStruct: *Decl = Lcx_FindGenericStruct(ctx, "Iter");
                            if iterGenStruct != null as *Decl {
                                Lcx_GenerateStructInstance(ctx, iterGenStruct, elemTypeName, "", 1);
                            }

                            // Ensure function instances exist
                            let genIter: *Decl = Lcx_FindGenericFunc(ctx, "Array_Iter");
                            let genHasNext: *Decl = Lcx_FindGenericFunc(ctx, "Iter_HasNext");
                            let genNext: *Decl = Lcx_FindGenericFunc(ctx, "Iter_Next");
                            let iterFuncName: String = Lcx_MangleName("Array_Iter", elemTypeName, "", 1);
                            let hasNextFuncName: String = Lcx_MangleName("Iter_HasNext", elemTypeName, "", 1);
                            let nextFuncName: String = Lcx_MangleName("Iter_Next", elemTypeName, "", 1);
                            if genIter != null as *Decl {
                                Lcx_GenerateFuncInstance(ctx, genIter, elemTypeName, "", 1);
                            }
                            if genHasNext != null as *Decl {
                                Lcx_GenerateFuncInstance(ctx, genHasNext, elemTypeName, "", 1);
                            }
                            if genNext != null as *Decl {
                                Lcx_GenerateFuncInstance(ctx, genNext, elemTypeName, "", 1);
                            }

                            // alloca for __iter
                            let iterAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            iterAlloca.kind = hAlloca;
                            iterAlloca.line = line;
                            iterAlloca.column = col;
                            iterAlloca.strValue = iterVarName;
                            iterAlloca.typeName = iterTypeName;
                            iterAlloca.typeKind = tyNamed;

                            // __iter = Array_Iter_T(&arr)  or  just copy if already Iter
                            var iterInit: *HirNode = null as *HirNode;
                            var collStore: *HirNode = null as *HirNode;
                            if isArray {
                                let callIter: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                callIter.kind = hCall;
                                callIter.strValue = iterFuncName;
                                let addrArr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                addrArr.kind = hUnary;
                                addrArr.intValue = tkAmp;
                                if iterExpr.kind == ekIdent {
                                    let arrVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    arrVar.kind = hVar;
                                    arrVar.strValue = iterExpr.strValue;
                                    addrArr.child1 = arrVar;
                                } else {
                                    // Non-identifier: create temp variable for collection
                                    ctx.varCounter = ctx.varCounter + 1;
                                    let tmpName: String = String_Concat("__tmp_coll_", String_FromInt(ctx.varCounter));
                                    // Ensure Array<T> struct instance exists and get mangled name
                                    let arrayGenStruct: *Decl = Lcx_FindGenericStruct(ctx, "Array");
                                    var arrayMangledName: String = collTypeName;
                                    if arrayGenStruct != null as *Decl {
                                        arrayMangledName = Lcx_GenerateStructInstance(ctx, arrayGenStruct, elemTypeName, "", 1);
                                    }
                                    let collAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    collAlloca.kind = hAlloca;
                                    collAlloca.strValue = tmpName;
                                    collAlloca.typeName = arrayMangledName;
                                    collAlloca.typeKind = tyNamed;
                                    collStore = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    collStore.kind = hStore;
                                    collStore.child1 = collAlloca;
                                    collStore.child2 = Lcx_LowerExpr(ctx, iterExpr);
                                    let collVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    collVar.kind = hVar;
                                    collVar.strValue = tmpName;
                                    addrArr.child1 = collVar;
                                }
                                callIter.child1 = addrArr;
                                iterInit = callIter;
                            } else {
                                // Already an iterator: __iter = arr
                                if iterExpr.kind == ekIdent {
                                    let arrVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                                    arrVar.kind = hVar;
                                    arrVar.strValue = iterExpr.strValue;
                                    iterInit = arrVar;
                                } else {
                                    iterInit = Lcx_LowerExpr(ctx, iterExpr);
                                }
                            }

                            let iterStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            iterStore.kind = hStore;
                            iterStore.child1 = iterAlloca;
                            iterStore.child2 = iterInit;

                            // Chain collStore -> iterStore if temp was created
                            if collStore != null as *HirNode {
                                collStore.child3 = iterStore;
                            }

                            // condition: Iter_HasNext_T(&__iter)
                            let condCall: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            condCall.kind = hCall;
                            condCall.strValue = hasNextFuncName;
                            let addrIter: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            addrIter.kind = hUnary;
                            addrIter.intValue = tkAmp;
                            let iterVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            iterVar.kind = hVar;
                            iterVar.strValue = iterVarName;
                            addrIter.child1 = iterVar;
                            condCall.child1 = addrIter;

                            // while body: alloca x + store x = Iter_Next_T(&__iter) + original body
                            let bodyBlock: *HirNode = Lcx_LowerBlock(ctx, body, -1);

                            // alloca for x
                            let xAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            xAlloca.kind = hAlloca;
                            xAlloca.line = line;
                            xAlloca.column = col;
                            xAlloca.strValue = varName;
                            xAlloca.typeName = elemTypeName;
                            xAlloca.typeKind = elemTypeKind;

                            // x = Iter_Next_T(&__iter)
                            let nextCall: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            nextCall.kind = hCall;
                            nextCall.strValue = nextFuncName;
                            let addrIter2: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            addrIter2.kind = hUnary;
                            addrIter2.intValue = tkAmp;
                            let iterVar2: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            iterVar2.kind = hVar;
                            iterVar2.strValue = iterVarName;
                            addrIter2.child1 = iterVar2;
                            nextCall.child1 = addrIter2;

                            let xStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            xStore.kind = hStore;
                            xStore.child1 = xAlloca;
                            xStore.child2 = nextCall;

                            // Build while body block: xStore -> bodyBlock
                            let whileBody: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            whileBody.kind = hBlock;
                            whileBody.child1 = xStore;
                            if bodyBlock != null as *HirNode && bodyBlock.kind == hBlock {
                                xStore.child3 = bodyBlock.child1;
                            } else if bodyBlock != null as *HirNode {
                                xStore.child3 = bodyBlock;
                            }

                            // while node
                            let whileNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            whileNode.kind = hWhile;
                            whileNode.child1 = condCall;
                            whileNode.child2 = whileBody;

                            // Chain: iterStore -> whileNode
                            iterStore.child3 = whileNode;

                            let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            blockNode.kind = hBlock;
                            blockNode.line = line;
                            blockNode.column = col;
                            if collStore != null as *HirNode {
                                blockNode.child1 = collStore;
                            } else {
                                blockNode.child1 = iterStore;
                            }
                            return blockNode;
                        }
                    }
                }
            }

            // Fallback: infinite loop
            n.kind = hLoop;
            if stmt.refStmtBlock != null as *Block {
                n.child1 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
            }
            return n;
        }

        // Break
        if kind == skBreak {
            n.kind = hBreak;
            return n;
        }

        // Continue
        if kind == skContinue {
            n.kind = hContinue;
            return n;
        }

        // Defer
        if kind == skDefer {
            n.kind = hDefer;
            if stmt.child1 != null as *Expr {
                n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
            }
            return n;
        }

        // Switch — desugar to if-else chain
        if kind == skSwitch {
            let subject: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
            var current: *HirNode = null as *HirNode;
            // Default first (bottom of chain)
            if stmt.refStmtElse != null as *Block {
                current = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1);
            }
            // Cases in reverse order (from caseBlock)
            if stmt.refStmtBlock != null as *Block {
                let caseBlock: *Block = stmt.refStmtBlock;
                var caseCount: int = caseBlock.stmtCount;
                // Collect cases into array for reverse iteration
                var c0: *Stmt = null as *Stmt;
                var c1: *Stmt = null as *Stmt;
                var c2: *Stmt = null as *Stmt;
                var c3: *Stmt = null as *Stmt;
                var c4: *Stmt = null as *Stmt;
                var c5: *Stmt = null as *Stmt;
                var c6: *Stmt = null as *Stmt;
                var c7: *Stmt = null as *Stmt;
                var ci: int = 0;
                var cs: *Stmt = caseBlock.firstStmt;
                while cs != null as *Stmt && ci < 8 {
                    if ci == 0 { c0 = cs; }
                    if ci == 1 { c1 = cs; }
                    if ci == 2 { c2 = cs; }
                    if ci == 3 { c3 = cs; }
                    if ci == 4 { c4 = cs; }
                    if ci == 5 { c5 = cs; }
                    if ci == 6 { c6 = cs; }
                    if ci == 7 { c7 = cs; }
                    ci = ci + 1;
                    cs = cs.nextStmt;
                }
                while caseCount > 0 {
                    caseCount = caseCount - 1;
                    var c: *Stmt = null as *Stmt;
                    if caseCount == 0 { c = c0; }
                    if caseCount == 1 { c = c1; }
                    if caseCount == 2 { c = c2; }
                    if caseCount == 3 { c = c3; }
                    if caseCount == 4 { c = c4; }
                    if caseCount == 5 { c = c5; }
                    if caseCount == 6 { c = c6; }
                    if caseCount == 7 { c = c7; }
                    if c != null as *Stmt {
                        let caseVal: *HirNode = Lcx_LowerExpr(ctx, c.child1);
                        let caseBody: *HirNode = Lcx_LowerBlock(ctx, c.refStmtBlock, -1);
                        let cond: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        cond.kind = hBinary;
                        cond.intValue = 74;  // tkEq
                        cond.child1 = subject;
                        cond.child2 = caseVal;
                        let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        ifNode.kind = hIf;
                        ifNode.child1 = cond;
                        ifNode.child2 = caseBody;
                        ifNode.child3 = current;
                        current = ifNode;
                    }
                }
            }
            return current;
        }

        return n;
    }

    // ---------------------------------------------------------------------------
    // Block lowering
    // ---------------------------------------------------------------------------

    func Lcx_LowerBlock(ctx: *LowerCtx, block: *Block, retTypeKind: int) -> *HirNode {
        if block == null as *Block { return null as *HirNode; }
        if block.stmtCount == 0 { return null as *HirNode; }

        // retTypeKind:
        //   >= 0  → function body; last skExpr becomes return
        //   -1    → statement block (if/while/for); no return, no yield
        //   -2    → block-as-expression (match arm / let { ... }); yield last skExpr
        let asExpr: bool = retTypeKind == -2;
        var yieldName: String = "";
        if asExpr {
            ctx.varCounter = ctx.varCounter + 1;
            yieldName = String_Concat("__blk_", String_FromInt(ctx.varCounter as int64));
        }

        // Build a linked list of HirNodes via child3
        var firstNode: *HirNode = null as *HirNode;
        var prevNode: *HirNode = null as *HirNode;
        var stmt: *Stmt = block.firstStmt;
        while stmt != null as *Stmt {
            let isLast: bool = stmt.nextStmt == null as *Stmt;
            var lowered: *HirNode = null as *HirNode;

            // Last expression statement in a non-void function → implicit return
            if isLast && !asExpr && retTypeKind != tyVoid && retTypeKind != tyUnknown && retTypeKind >= 0
            && stmt.kind == skExpr && stmt.child1 != null as *Expr {
                let exprNode: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
                if exprNode != null as *HirNode {
                    if exprNode.kind == hBlock && !String_Eq(exprNode.strValue, "") {
                        let retVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        retVar.kind = hVar;
                        retVar.strValue = exprNode.strValue;
                        let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        retNode.kind = hReturn;
                        retNode.line = stmt.line;
                        retNode.column = stmt.column;
                        retNode.child1 = retVar;
                        var lastInBlock: *HirNode = exprNode.child1;
                        if lastInBlock == null as *HirNode {
                            exprNode.child1 = retNode;
                        } else {
                            while lastInBlock.child3 != null as *HirNode {
                                lastInBlock = lastInBlock.child3;
                            }
                            lastInBlock.child3 = retNode;
                        }
                        exprNode.strValue = "";
                        lowered = exprNode;
                    } else if exprNode.kind == hReturn {
                        lowered = exprNode;
                    } else {
                        let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                        retNode.kind = hReturn;
                        retNode.line = stmt.line;
                        retNode.column = stmt.column;
                        retNode.child1 = exprNode;
                        lowered = retNode;
                    }
                }
            } else if isLast && asExpr && stmt.kind == skExpr && stmt.child1 != null as *Expr {
                // Block-as-expression: last expr is the yield value
                let exprNode: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
                // alloca yield temp
                let allocaN: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                allocaN.kind = hAlloca;
                allocaN.line = stmt.line;
                allocaN.column = stmt.column;
                allocaN.strValue = yieldName;
                allocaN.typeName = "int";
                if exprNode != null as *HirNode && exprNode.typeName != null as String
                && !String_Eq(exprNode.typeName, "") {
                    allocaN.typeName = exprNode.typeName;
                }
                // store: yield = value (handle nested match yield)
                let storeN: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                storeN.kind = hStore;
                storeN.line = stmt.line;
                storeN.column = stmt.column;
                let yv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                yv.kind = hVar;
                yv.strValue = yieldName;
                storeN.child1 = yv;
                if exprNode != null as *HirNode && Lcx_IsMatchYield(exprNode) {
                    // Expand nested match stmts then store its result var
                    let yvar: *HirNode = Lcx_YieldVarOf(exprNode);
                    storeN.child2 = yvar;
                    exprNode.strValue = "";
                    // chain: exprNode stmts → alloca → store
                    var lastIn: *HirNode = exprNode.child1;
                    if lastIn == null as *HirNode {
                        exprNode.child1 = allocaN;
                        allocaN.child3 = storeN;
                        lowered = exprNode;
                    } else {
                        while lastIn.child3 != null as *HirNode {
                            lastIn = lastIn.child3;
                        }
                        lastIn.child3 = allocaN;
                        allocaN.child3 = storeN;
                        lowered = exprNode;
                    }
                } else {
                    storeN.child2 = exprNode;
                    allocaN.child3 = storeN;
                    lowered = allocaN;
                }
            } else {
                lowered = Lcx_LowerStmt(ctx, stmt);
            }
            if lowered != null as *HirNode {
                if firstNode == null as *HirNode {
                    firstNode = lowered;
                    prevNode = lowered;
                } else {
                    // Walk to end of chain (lowered may itself be a multi-node chain)
                    prevNode.child3 = lowered;
                    prevNode = lowered;
                    while prevNode.child3 != null as *HirNode {
                        prevNode = prevNode.child3;
                    }
                }
            }
            stmt = stmt.nextStmt;
        }

        // Wrap in an hBlock node with child1 = first statement in chain
        let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        n.kind = hBlock;
        n.line = block.line;
        n.column = block.column;
        n.boolValue = true;
        n.child1 = firstNode;
        if asExpr && !String_Eq(yieldName, "") {
            n.strValue = yieldName;
            n.typeName = "int";
        }
        return n;
    }

    // ---------------------------------------------------------------------------
    // Param → HirParam conversion
    // ---------------------------------------------------------------------------

    func Lcx_LowerParam(out: *HirParam, p: *Param, ctx: *LowerCtx) {
        out.name = p.name;
        var te: *TypeExpr = p.refParamType;
        if ctx != null as *LowerCtx {
            te = Lcx_SubstituteType(ctx, te);
        }
        if te != null as *TypeExpr {
            out.typeKind = Lcx_ResolveTypeKind(te);
            // Function type: build C function-pointer syntax
            if te.kind == tekFunc {
                out.typeName = Lcx_BuildFuncTypeName(te);
            } else if !String_Eq(te.typeName, "") {
                out.typeName = te.typeName;
            } else if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
                out.typeName = String_Concat(te.pointerPointee.typeName, "*");
            } else {
                out.typeName = "";
            }
        } else {
            out.typeKind = 0;
            out.typeName = "";
        }
    }

    // ---------------------------------------------------------------------------
    // Function lowering
    // ---------------------------------------------------------------------------

    func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc {
        let oldChecked: bool = ctx.checkedFunc;
        ctx.checkedFunc = decl.isChecked != 0;
        let oldRelease: bool = ctx.releaseFunc;
        ctx.releaseFunc = decl.isRelease != 0;

        let f: *HirFunc = bux_alloc(sizeof(HirFunc)) as *HirFunc;
        f.name = decl.strValue;
        f.isPublic = decl.isPublic;
        f.checkedFunc = ctx.checkedFunc;
        f.paramCount = decl.paramCount;
        f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param0, &decl.param0, ctx);
        f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param1, &decl.param1, ctx);
        f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param2, &decl.param2, ctx);
        f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param3, &decl.param3, ctx);
        f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param4, &decl.param4, ctx);
        f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param5, &decl.param5, ctx);
        f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param6, &decl.param6, ctx);
        f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param7, &decl.param7, ctx);
        f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam;
        Lcx_LowerParam(f.param8, &decl.param8, ctx);

        let retTe: *TypeExpr = Lcx_SubstituteType(ctx, decl.retType);
        if retTe != null as *TypeExpr {
            f.retTypeKind = Lcx_ResolveTypeKind(retTe);
            if retTe.kind == tekFunc {
                f.retTypeName = Lcx_BuildFuncTypeName(retTe);
            } else if !String_Eq(retTe.typeName, "") {
                f.retTypeName = retTe.typeName;
            } else if retTe.kind == tekPointer && retTe.pointerPointee != null as *TypeExpr {
                f.retTypeName = String_Concat(retTe.pointerPointee.typeName, "*");
            } else {
                f.retTypeName = "";
            }
        } else {
            f.retTypeName = "";
            f.retTypeKind = 0;
        }

        // Create function scope as child of current scope
        var funcScope: Scope = Scope_NewChild(ctx.scope);

        // Add parameters to function scope for field offset lookups
        var pi: int = 0;
        while pi < decl.paramCount {
            var p: *Param = null as *Param;
            if pi == 0 { p = &decl.param0; }
            else if pi == 1 { p = &decl.param1; }
            else if pi == 2 { p = &decl.param2; }
            else if pi == 3 { p = &decl.param3; }
            else if pi == 4 { p = &decl.param4; }
            else if pi == 5 { p = &decl.param5; }
            else if pi == 6 { p = &decl.param6; }
            else if pi == 7 { p = &decl.param7; }
            else if pi == 8 { p = &decl.param8; }
            if p != null as *Param && p.refParamType != null as *TypeExpr {
                let pTe: *TypeExpr = Lcx_SubstituteType(ctx, p.refParamType);
                var sym: Symbol;
                sym.kind = skVar;
                sym.name = p.name;
                sym.typeKind = Lcx_ResolveTypeKind(pTe);
                sym.refType = pTe;
                // Build typeName same as Lcx_LowerParam
                if pTe.kind == tekFunc {
                    sym.typeName = Lcx_BuildFuncTypeName(pTe);
                } else if !String_Eq(pTe.typeName, "") {
                    sym.typeName = pTe.typeName;
                } else if pTe.kind == tekPointer && pTe.pointerPointee != null as *TypeExpr {
                    sym.typeName = String_Concat(pTe.pointerPointee.typeName, "*");
                } else {
                    sym.typeName = "";
                }
                sym.isMutable = false;
                sym.isPublic = false;
                sym.decl = null as *Decl;
                discard Scope_Define(&funcScope, sym);
            }
            pi = pi + 1;
        }

        // Lower body with function scope active (pass ret kind for last-expr return)
        let prevScope: *Scope = ctx.scope;
        ctx.scope = &funcScope;
        if decl.refBody != null as *Block {
            f.body = Lcx_LowerBlock(ctx, decl.refBody, f.retTypeKind);
        } else {
            f.body = null as *HirNode;
        }
        ctx.scope = prevScope;
        ctx.checkedFunc = oldChecked;
        ctx.releaseFunc = oldRelease;

        return f;
    }

    // ---------------------------------------------------------------------------
    // Closure lowering — generate a global function for a closure expression
    // ---------------------------------------------------------------------------

    func Lcx_LowerClosureFunc(ctx: *LowerCtx, expr: *Expr) -> *HirFunc {
        let f: *HirFunc = bux_alloc(sizeof(HirFunc)) as *HirFunc;

        // Generate unique name
        let numStr: String = String_FromInt(ctx.funcCount);
        f.name = String_Concat("__closure_", numStr);
        f.isPublic = false;

        let params: *Decl = expr.closureParams;
        // Fat-func ABI: leading void* __env, then user params
        var userCount: int = 0;
        if params != null as *Decl { userCount = params.paramCount; }
        f.paramCount = userCount + 1;
        f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam;
        f.param0.name = "__env";
        f.param0.typeKind = tyPointer;
        f.param0.typeName = "void*";
        if userCount > 0 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, &params.param0, ctx); }
        if userCount > 1 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, &params.param1, ctx); }
        if userCount > 2 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, &params.param2, ctx); }
        if userCount > 3 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, &params.param3, ctx); }
        if userCount > 4 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, &params.param4, ctx); }
        if userCount > 5 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, &params.param5, ctx); }
        if userCount > 6 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, &params.param6, ctx); }
        if userCount > 7 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, &params.param7, ctx); }

        if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc {
            // Return type of the *thunk* is the closure's return type (not the fat type)
            if expr.refType.funcRet != null as *TypeExpr {
                f.retTypeName = expr.refType.funcRet.typeName;
                if String_Eq(f.retTypeName, "") { f.retTypeName = "int"; }
                f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet);
            } else {
                f.retTypeName = "void";
                f.retTypeKind = tyVoid;
            }
        } else {
            f.retTypeName = "int";
            f.retTypeKind = tyInt;
        }

        // Copy capture metadata from AST
        f.captureCount = expr.captureCount;
        f.captureName0 = expr.captureName0;
        f.captureName1 = expr.captureName1;
        f.captureName2 = expr.captureName2;
        f.captureName3 = expr.captureName3;
        f.captureName4 = expr.captureName4;
        f.captureName5 = expr.captureName5;
        f.captureName6 = expr.captureName6;
        f.captureName7 = expr.captureName7;
        f.captureType0 = expr.captureType0;
        f.captureType1 = expr.captureType1;
        f.captureType2 = expr.captureType2;
        f.captureType3 = expr.captureType3;
        f.captureType4 = expr.captureType4;
        f.captureType5 = expr.captureType5;
        f.captureType6 = expr.captureType6;
        f.captureType7 = expr.captureType7;

        // Generate env struct and instance names if there are captures
        var envStructName: String = "";
        var envInstanceName: String = "";
        if f.captureCount > 0 {
            envStructName = String_Concat("__closure_env_", numStr);
            envInstanceName = String_Concat("__closure_env_instance_", numStr);
            f.envStructName = envStructName;
            f.envInstanceName = envInstanceName;

            // Env struct is emitted directly by C backend from func capture metadata
        }

        // Create function scope
        var funcScope: Scope = Scope_NewChild(ctx.scope);
        var pi: int = 0;
        while pi < params.paramCount {
            var p: *Param = null as *Param;
            if pi == 0 { p = &params.param0; }
            else if pi == 1 { p = &params.param1; }
            else if pi == 2 { p = &params.param2; }
            else if pi == 3 { p = &params.param3; }
            else if pi == 4 { p = &params.param4; }
            else if pi == 5 { p = &params.param5; }
            else if pi == 6 { p = &params.param6; }
            else if pi == 7 { p = &params.param7; }
            else if pi == 8 { p = &params.param8; }
            if p != null as *Param && p.refParamType != null as *TypeExpr {
                let pTe: *TypeExpr = Lcx_SubstituteType(ctx, p.refParamType);
                var sym: Symbol;
                sym.kind = skVar;
                sym.name = p.name;
                sym.typeKind = Lcx_ResolveTypeKind(pTe);
                sym.refType = pTe;
                if pTe.kind == tekFunc {
                    sym.typeName = Lcx_BuildFuncTypeName(pTe);
                } else if !String_Eq(pTe.typeName, "") {
                    sym.typeName = pTe.typeName;
                } else if pTe.kind == tekPointer && pTe.pointerPointee != null as *TypeExpr {
                    sym.typeName = String_Concat(pTe.pointerPointee.typeName, "*");
                } else {
                    sym.typeName = "";
                }
                sym.isMutable = false;
                sym.isPublic = false;
                sym.decl = null as *Decl;
                discard Scope_Define(&funcScope, sym);
            }
            pi = pi + 1;
        }

        let prevScope: *Scope = ctx.scope;
        let prevClosureDepth: int = ctx.closureDepth;
        let prevClosureExpr: *Expr = ctx.currentClosureExpr;
        let prevEnvInstanceName: String = ctx.envInstanceName;
        ctx.scope = &funcScope;
        ctx.closureDepth = ctx.closureDepth + 1;
        ctx.currentClosureExpr = expr;
        ctx.envInstanceName = envInstanceName;
        if expr.refBlock != null as *Block {
            f.body = Lcx_LowerBlock(ctx, expr.refBlock, -1);
        } else {
            f.body = null as *HirNode;
        }
        ctx.scope = prevScope;
        ctx.closureDepth = prevClosureDepth;
        ctx.currentClosureExpr = prevClosureExpr;
        ctx.envInstanceName = prevEnvInstanceName;

        // Add to module functions
        ctx.funcs[ctx.funcCount] = *f;
        ctx.funcCount = ctx.funcCount + 1;

        return f;
    }

    // ---------------------------------------------------------------------------
    // Compile-Time Function Execution (CTFE) — constant expression evaluator
    // ---------------------------------------------------------------------------

    const CTFE_MAX_LOCALS: int = 64;

    struct CtfeLocal {
        name: String,
        value: int,
    }

    struct CtfeEnv {
        locals: *CtfeLocal,
        count: int,
    }

    struct CtVal {
        value: int,
        isReturn: bool,
    }

    func CtfeEnv_New() -> CtfeEnv {
        let locals: *CtfeLocal = bux_alloc(CTFE_MAX_LOCALS as uint * sizeof(CtfeLocal)) as *CtfeLocal;
        return CtfeEnv { locals: locals, count: 0 };
    }

    func CtfeEnv_Get(env: *CtfeEnv, name: String) -> int {
        var i: int = env.count - 1;
        while i >= 0 {
            if String_Eq(env.locals[i].name, name) {
                return env.locals[i].value;
            }
            i = i - 1;
        }
        return 0;
    }

    func CtfeEnv_Set(env: *CtfeEnv, name: String, value: int) {
        if env.count >= CTFE_MAX_LOCALS { return; }
        env.locals[env.count].name = name;
        env.locals[env.count].value = value;
        env.count = env.count + 1;
    }

    func Lcx_FindConstFunc(ctx: *LowerCtx, name: String) -> *Decl {
        var decl: *Decl = ctx.module.firstItem;
        while decl != null as *Decl {
            if decl.kind == dkFunc && decl.isConst == 1 && String_Eq(decl.strValue, name) {
                return decl;
            }
            decl = decl.childDecl2;
        }
        return null as *Decl;
    }

    func Lcx_ParamName(fd: *Decl, idx: int) -> String {
        if fd == null as *Decl { return ""; }
        if idx == 0 { return fd.param0.name; }
        if idx == 1 { return fd.param1.name; }
        if idx == 2 { return fd.param2.name; }
        if idx == 3 { return fd.param3.name; }
        if idx == 4 { return fd.param4.name; }
        if idx == 5 { return fd.param5.name; }
        if idx == 6 { return fd.param6.name; }
        if idx == 7 { return fd.param7.name; }
        if idx == 8 { return fd.param8.name; }
        return "";
    }

    func CtVal_Make(value: int) -> CtVal {
        return CtVal { value: value, isReturn: false };
    }

    func CtVal_Return(value: int) -> CtVal {
        return CtVal { value: value, isReturn: true };
    }

    func Lcx_EvalConstExprEnv(ctx: *LowerCtx, expr: *Expr, env: *CtfeEnv) -> CtVal {
        if expr == null as *Expr {
            return CtVal_Make(0);
        }

        // Literal integer
        if expr.kind == ekLiteral {
            return CtVal_Make(expr.intValue);
        }

        // Reference to local or another constant
        if expr.kind == ekIdent {
            let localVal: int = CtfeEnv_Get(env, expr.strValue);
            // Local takes precedence; 0 from Get could mean not found, but global const 0
            // will still be looked up below if needed. For Factorial parameters this works
            // because params are always set in env.
            if localVal != 0 {
                return CtVal_Make(localVal);
            }
            let name: String = expr.strValue;
            var i: int = 0;
            while i < ctx.hm.constCount {
                if String_Eq(ctx.hm.consts[i].name, name) {
                    return CtVal_Make(ctx.hm.consts[i].value);
                }
                i = i + 1;
            }
            return CtVal_Make(0);
        }

        // Unary operators
        if expr.kind == ekUnary {
            let operand: CtVal = Lcx_EvalConstExprEnv(ctx, expr.child1, env);
            let op: int = expr.intValue;
            if op == tkMinus { return CtVal_Make(-operand.value); }
            if op == tkBang { return CtVal_Make((operand.value == 0) as int); }
            if op == tkTilde { return CtVal_Make(~operand.value); }
            return CtVal_Make(operand.value);
        }

        // Binary operators
        if expr.kind == ekBinary {
            let left: CtVal = Lcx_EvalConstExprEnv(ctx, expr.child1, env);
            let right: CtVal = Lcx_EvalConstExprEnv(ctx, expr.child2, env);
            let op: int = expr.intValue;
            if op == tkPlus { return CtVal_Make(left.value + right.value); }
            if op == tkMinus { return CtVal_Make(left.value - right.value); }
            if op == tkStar { return CtVal_Make(left.value * right.value); }
            if op == tkSlash {
                if right.value == 0 { return CtVal_Make(0); }
                return CtVal_Make(left.value / right.value);
            }
            if op == tkPercent {
                if right.value == 0 { return CtVal_Make(0); }
                return CtVal_Make(left.value % right.value);
            }
            if op == tkLt { return CtVal_Make((left.value < right.value) as int); }
            if op == tkLe { return CtVal_Make((left.value <= right.value) as int); }
            if op == tkGt { return CtVal_Make((left.value > right.value) as int); }
            if op == tkGe { return CtVal_Make((left.value >= right.value) as int); }
            if op == tkEq { return CtVal_Make((left.value == right.value) as int); }
            if op == tkNe { return CtVal_Make((left.value != right.value) as int); }
            if op == tkAmp { return CtVal_Make(left.value & right.value); }
            if op == tkPipe { return CtVal_Make(left.value | right.value); }
            if op == tkCaret { return CtVal_Make(left.value ^ right.value); }
            if op == tkShl { return CtVal_Make(left.value << right.value); }
            if op == tkShr { return CtVal_Make(left.value >> right.value); }
            if op == tkAmpAmp { return CtVal_Make((left.value != 0 && right.value != 0) as int); }
            if op == tkPipePipe { return CtVal_Make((left.value != 0 || right.value != 0) as int); }
            return CtVal_Make(0);
        }

        // Ternary operator
        if expr.kind == ekTernary {
            let cond: CtVal = Lcx_EvalConstExprEnv(ctx, expr.child1, env);
            if cond.value != 0 {
                return Lcx_EvalConstExprEnv(ctx, expr.child2, env);
            } else {
                return Lcx_EvalConstExprEnv(ctx, expr.child3, env);
            }
        }

        // Cast — evaluate operand (types don't affect integer values)
        if expr.kind == ekCast {
            return Lcx_EvalConstExprEnv(ctx, expr.child1, env);
        }

        // Const function call
        if expr.kind == ekCall {
            if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
                let funcName: String = expr.child1.strValue;
                let fd: *Decl = Lcx_FindConstFunc(ctx, funcName);
                if fd != null as *Decl && fd.refBody != null as *Block {
                    // Evaluate arguments
                    var argExpr: *ExprList = expr.callArgs;
                    var ai: int = 0;
                    var argVals: *int = bux_alloc(fd.paramCount as uint * sizeof(int)) as *int;
                    while ai < fd.paramCount {
                        argVals[ai] = 0;
                        ai = ai + 1;
                    }
                    ai = 0;
                    while argExpr != null as *ExprList && ai < fd.paramCount {
                        let av: CtVal = Lcx_EvalConstExprEnv(ctx, argExpr.expr, env);
                        argVals[ai] = av.value;
                        argExpr = argExpr.next;
                        ai = ai + 1;
                    }
                    // Set up call environment
                    let callEnv: CtfeEnv = CtfeEnv_New();
                    var pi: int = 0;
                    while pi < fd.paramCount {
                        CtfeEnv_Set(&callEnv, Lcx_ParamName(fd, pi), argVals[pi]);
                        pi = pi + 1;
                    }
                    let result: CtVal = Lcx_EvalConstBlock(ctx, fd.refBody, &callEnv);
                    return CtVal_Make(result.value);
                }
            }
            return CtVal_Make(0);
        }

        return CtVal_Make(0);
    }

    func Lcx_EvalConstBlock(ctx: *LowerCtx, block: *Block, env: *CtfeEnv) -> CtVal {
        if block == null as *Block {
            return CtVal_Make(0);
        }
        var stmt: *Stmt = block.firstStmt;
        while stmt != null as *Stmt {
            if stmt.kind == skLet && stmt.child1 != null as *Expr {
                let val: CtVal = Lcx_EvalConstExprEnv(ctx, stmt.child1, env);
                CtfeEnv_Set(env, stmt.strValue, val.value);
            } else if stmt.kind == skIf {
                let cond: CtVal = Lcx_EvalConstExprEnv(ctx, stmt.child1, env);
                if cond.value != 0 {
                    let r: CtVal = Lcx_EvalConstBlock(ctx, stmt.refStmtBlock, env);
                    if r.isReturn { return r; }
                } else if stmt.refStmtElse != null as *Block {
                    let r: CtVal = Lcx_EvalConstBlock(ctx, stmt.refStmtElse, env);
                    if r.isReturn { return r; }
                }
            } else if stmt.kind == skReturn {
                if stmt.child1 != null as *Expr {
                    let val: CtVal = Lcx_EvalConstExprEnv(ctx, stmt.child1, env);
                    return CtVal_Return(val.value);
                }
                return CtVal_Return(0);
            } else if stmt.kind == skExpr {
                discard Lcx_EvalConstExprEnv(ctx, stmt.child1, env);
            }
            stmt = stmt.nextStmt;
        }
        return CtVal_Make(0);
    }

    func Lcx_EvalConstExpr(ctx: *LowerCtx, expr: *Expr) -> int {
        let env: CtfeEnv = CtfeEnv_New();
        let result: CtVal = Lcx_EvalConstExprEnv(ctx, expr, &env);
        return result.value;
    }

    // ---------------------------------------------------------------------------
    // Auto-Drop: build the Free function name for a given type
    // ---------------------------------------------------------------------------

    func Lcx_BuildAutoDropFree(ctx: *LowerCtx, typeName: String) -> String {
        if String_StartsWith(typeName, "Array_") {
            let elemType: String = bux_str_slice(typeName, 6, bux_strlen(typeName) - 6);
            // Ensure inner free is also monomorphized since Drop calls Free
            let genFree: *Decl = Lcx_FindGenericFunc(ctx, "Array_Free");
            if genFree != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genFree, elemType, "", 1);
            }
            let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Array_Drop");
            if genDrop != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1);
            }
            return String_Concat("Array_Drop_", elemType);
        }
        if String_StartsWith(typeName, "Channel_") {
            let elemType: String = bux_str_slice(typeName, 8, bux_strlen(typeName) - 8);
            let genFree: *Decl = Lcx_FindGenericFunc(ctx, "Channel_Free");
            if genFree != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genFree, elemType, "", 1);
            }
            let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Channel_Drop");
            if genDrop != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1);
            }
            return String_Concat("Channel_Drop_", elemType);
        }
        if String_StartsWith(typeName, "Set_") {
            let elemType: String = bux_str_slice(typeName, 4, bux_strlen(typeName) - 4);
            let genFree: *Decl = Lcx_FindGenericFunc(ctx, "Set_Free");
            if genFree != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genFree, elemType, "", 1);
            }
            let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Set_Drop");
            if genDrop != null as *Decl {
                Lcx_GenerateFuncInstance(ctx, genDrop, elemType, "", 1);
            }
            return String_Concat("Set_Drop_", elemType);
        }
        if String_StartsWith(typeName, "Map_") {
            let rest: String = bux_str_slice(typeName, 4, bux_strlen(typeName) - 4);
            // Find underscore separator between K and V
            var ki: int = 0;
            var klen: int = bux_strlen(rest) as int;
            while ki < klen {
                if (rest[ki] as int) == ('_' as int) {
                    break;
                }
                ki = ki + 1;
            }
            if ki < klen {
                let kType: String = bux_str_slice(rest, 0, ki);
                let vType: String = bux_str_slice(rest, ki + 1, klen - ki - 1);
                let genFree: *Decl = Lcx_FindGenericFunc(ctx, "Map_Free");
                if genFree != null as *Decl {
                    Lcx_GenerateFuncInstance(ctx, genFree, kType, vType, 2);
                }
                let genDrop: *Decl = Lcx_FindGenericFunc(ctx, "Map_Drop");
                if genDrop != null as *Decl {
                    Lcx_GenerateFuncInstance(ctx, genDrop, kType, vType, 2);
                }
                return String_Concat(String_Concat("Map_Drop_", kType), String_Concat("_", vType));
            }
        }
        // User-defined types with @[Drop] OR an explicit TypeName_Drop method
        let typeSym: Symbol = Scope_Lookup(ctx.scope, typeName);
        if typeSym.kind == skType && typeSym.decl != null as *Decl {
            let dropName: String = String_Concat(typeName, "_Drop");
            let dropSym: Symbol = Scope_Lookup(ctx.scope, dropName);
            let hasAttr: bool = typeSym.decl.isDrop != 0;
            let hasMethod: bool = dropSym.decl != null as *Decl;
            if hasAttr || hasMethod {
                return dropName;
            }
        }
        return "";
    }

    // ---------------------------------------------------------------------------
    // Module lowering — main entry point
    // ---------------------------------------------------------------------------

    func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule {
        let ctx: *LowerCtx = bux_alloc(sizeof(LowerCtx)) as *LowerCtx;
        ctx.module = mod;
        ctx.scope = sema.scope;
        ctx.funcs = bux_alloc(512 as uint * sizeof(HirFunc)) as *HirFunc;
        ctx.funcCount = 0;
        ctx.externFuncs = bux_alloc(512 as uint * sizeof(HirFunc)) as *HirFunc;
        ctx.externCount = 0;
        ctx.varCounter = 0;
        ctx.genFuncCount = 0;
        ctx.genFuncs = bux_alloc(256 as uint * sizeof(Decl)) as *Decl;
        ctx.genStructCount = 0;
        ctx.genStructs = bux_alloc(256 as uint * sizeof(Decl)) as *Decl;
        ctx.substParam0 = "";
        ctx.substArg0 = "";
        ctx.substParam1 = "";
        ctx.substArg1 = "";

        let hm: *HirModule = bux_alloc(sizeof(HirModule)) as *HirModule;
        hm.funcCount = 0;
        hm.funcs = ctx.funcs;
        hm.structCount = 0;
        hm.structs = bux_alloc(64 as uint * sizeof(HirStruct)) as *HirStruct;
        hm.enumCount = 0;
        hm.enums = bux_alloc(64 as uint * sizeof(HirEnum)) as *HirEnum;
        hm.constCount = 0;
        hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst;
        ctx.hm = hm;

        // First pass: count structs (to allocate field arrays later)
        // Second pass: actually collect them
        // For simplicity, do single pass with pre-allocated field arrays

        // Pass 1: collect generic declarations for monomorphization
        var decl: *Decl = mod.firstItem;
        while decl != null as *Decl {
            if decl.kind == dkFunc && decl.typeParamCount > 0 {
                ctx.genFuncs[ctx.genFuncCount] = *decl;
                ctx.genFuncCount = ctx.genFuncCount + 1;
            }
            if decl.kind == dkStruct && decl.typeParamCount > 0 {
                ctx.genStructs[ctx.genStructCount] = *decl;
                ctx.genStructCount = ctx.genStructCount + 1;
            }
            // Generic impl/extend blocks: methods inherit the impl's type params
            if decl.kind == dkImpl && decl.typeParamCount > 0 {
                let implTypeName: String = decl.strValue;
                var implDecl: *Decl = decl.childDecl1;
                while implDecl != null as *Decl {
                    if implDecl.kind == dkFunc {
                        let renamed: String = String_Concat(String_Concat(implTypeName, "_"), implDecl.strValue);
                        var copy: Decl = *implDecl;
                        copy.strValue = renamed;
                        copy.typeParam0 = decl.typeParam0;
                        copy.typeParam1 = decl.typeParam1;
                        copy.typeParamCount = decl.typeParamCount;
                        ctx.genFuncs[ctx.genFuncCount] = copy;
                        ctx.genFuncCount = ctx.genFuncCount + 1;
                    }
                    implDecl = implDecl.childDecl2;
                }
            }
            decl = decl.childDecl2;
        }

        // Pass 2: lower all declarations
        decl = mod.firstItem;
        while decl != null as *Decl {
            if decl.kind == dkStruct && decl.typeParamCount == 0 && !String_Eq(decl.strValue, "") {
                // Collect struct definition for C codegen
                let si: int = hm.structCount;
                hm.structs[si].name = decl.strValue;
                hm.structs[si].fieldCount = decl.fieldCount;
                hm.structs[si].fields = bux_alloc(decl.fieldCount as uint * sizeof(HirStructField)) as *HirStructField;
                var fi: int = 0;
                while fi < decl.fieldCount {
                    var fname: String = "";
                    var ftype: *TypeExpr = null as *TypeExpr;
                    fname = decl.fields[fi].name;
                    ftype = decl.fields[fi].refFieldType;
                    // Skip empty field names
                    if String_Eq(fname, "") {
                        fi = fi + 1;
                        continue;
                    }
                    hm.structs[si].fields[fi].name = fname;
                    if ftype != null as *TypeExpr {
                        if ftype.kind == tekPointer && ftype.pointerPointee != null as *TypeExpr {
                            // Pointer type: emit "TypeName*"
                            if !String_Eq(ftype.pointerPointee.typeName, "") {
                                hm.structs[si].fields[fi].typeName = String_Concat(ftype.pointerPointee.typeName, "*");
                            }
                        } else if !String_Eq(ftype.typeName, "") {
                            hm.structs[si].fields[fi].typeName = ftype.typeName;
                        }
                    }
                    fi = fi + 1;
                }
                hm.structCount = hm.structCount + 1;
            }
            if decl.kind == dkFunc && decl.refBody != null as *Block && decl.typeParamCount == 0 {
                let f: *HirFunc = Lcx_LowerFunc(ctx, decl);
                ctx.funcs[ctx.funcCount] = *f;
                ctx.funcCount = ctx.funcCount + 1;
            }
            if decl.kind == dkImpl {
                let implTypeName: String = decl.strValue;
                var implDecl: *Decl = decl.childDecl1;
                while implDecl != null as *Decl {
                    if implDecl.kind == dkFunc && implDecl.refBody != null as *Block {
                        // Generic impl methods are monomorphized on demand; skip direct lowering
                        if decl.typeParamCount > 0 {
                            implDecl = implDecl.childDecl2;
                            continue;
                        }
                        let mangled: String = String_Concat(implTypeName, "_");
                        implDecl.strValue = String_Concat(mangled, implDecl.strValue);
                        let f: *HirFunc = Lcx_LowerFunc(ctx, implDecl);
                        ctx.funcs[ctx.funcCount] = *f;
                        ctx.funcCount = ctx.funcCount + 1;
                    }
                    implDecl = implDecl.childDecl2;
                }
            }
            if decl.kind == dkExternFunc {
                let f: *HirFunc = Lcx_LowerFunc(ctx, decl);
                ctx.externFuncs[ctx.externCount] = *f;
                ctx.externCount = ctx.externCount + 1;
            }
            // Pass 1: collect const names (expressions evaluated in Pass 2)
            if decl.kind == dkConst && hm.constCount < 512 {
                let ci: int = hm.constCount;
                hm.consts[ci].name = decl.strValue;
                hm.consts[ci].value = 0;
                hm.constCount = hm.constCount + 1;
            }
            if decl.kind == dkEnum {
                let ei: int = hm.enumCount;
                hm.enums[ei].name = decl.strValue;
                // Populate variants
                hm.enums[ei].variantCount = decl.variantCount;
                if decl.variantCount > 0 {
                    hm.enums[ei].variants = bux_alloc(decl.variantCount as uint * sizeof(HirEnumVariant)) as *HirEnumVariant;
                }
                var vi: int = 0;
                while vi < decl.variantCount {
                    var v: *EnumVariant = null as *EnumVariant;
                    if vi == 0 { v = &decl.variant0; }
                    if vi == 1 { v = &decl.variant1; }
                    if vi == 2 { v = &decl.variant2; }
                    if vi == 3 { v = &decl.variant3; }
                    if vi == 4 { v = &decl.variant4; }
                    if vi == 5 { v = &decl.variant5; }
                    if vi == 6 { v = &decl.variant6; }
                    if vi == 7 { v = &decl.variant7; }
                    if vi == 8 { v = &decl.variant8; }
                    if v != null as *EnumVariant {
                        hm.enums[ei].variants[vi].name = v.name;
                        hm.enums[ei].variants[vi].fieldCount = v.fieldCount;
                        if v.fieldCount > 0 {
                            // Positional field names: Variant_0, Variant_1 (matches data.Variant_i / nested struct)
                            hm.enums[ei].variants[vi].fieldName0 = String_Concat(v.name, "_0");
                            hm.enums[ei].variants[vi].fieldType0 = Lcx_ResolveTypeKindFromName(v.fieldTypeName0);
                            hm.enums[ei].variants[vi].fieldTypeName0 = v.fieldTypeName0;
                        }
                        if v.fieldCount > 1 {
                            hm.enums[ei].variants[vi].fieldName1 = String_Concat(v.name, "_1");
                            hm.enums[ei].variants[vi].fieldType1 = Lcx_ResolveTypeKindFromName(v.fieldTypeName1);
                            hm.enums[ei].variants[vi].fieldTypeName1 = v.fieldTypeName1;
                        }
                    }
                    vi = vi + 1;
                }
                hm.enumCount = hm.enumCount + 1;
            }
            decl = decl.childDecl2;
        }


        // Pass 2: evaluate all const expressions (multiple passes for forward refs)
        var changed: bool = true;
        var maxPasses: int = 10;
        var pass: int = 0;
        while changed && pass < maxPasses {
            changed = false;
            decl = mod.firstItem;
            var ci2: int = 0;
            while decl != null as *Decl && ci2 < hm.constCount {
                if decl.kind == dkConst {
                    if decl.constValue != null as *Expr {
                        let newVal: int = Lcx_EvalConstExpr(ctx, decl.constValue);
                        if newVal != hm.consts[ci2].value {
                            hm.consts[ci2].value = newVal;
                            changed = true;
                        }
                    }
                    ci2 = ci2 + 1;
                }
                decl = decl.childDecl2;
            }
            pass = pass + 1;
        }

        hm.funcCount = ctx.funcCount;
        hm.funcs = ctx.funcs;
        hm.externCount = ctx.externCount;
        hm.externFuncs = ctx.externFuncs;


        return hm;
    }

}
