// 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,
}

// ---------------------------------------------------------------------------
// 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 {
    if String_Eq(name, "void") { return tyVoid; }
    if String_Eq(name, "bool") { return tyBool; }
    if String_Eq(name, "bool8") { return tyBool8; }
    if String_Eq(name, "bool16") { return tyBool16; }
    if String_Eq(name, "bool32") { return tyBool32; }
    if String_Eq(name, "char8") { return tyChar8; }
    if String_Eq(name, "char16") { return tyChar16; }
    if String_Eq(name, "char32") { return tyChar32; }
    if String_Eq(name, "String") { return tyStr; }
    if String_Eq(name, "str") { return tyStr; }
    if String_Eq(name, "int8") { return tyInt8; }
    if String_Eq(name, "int16") { return tyInt16; }
    if String_Eq(name, "int32") { return tyInt32; }
    if String_Eq(name, "int64") { return tyInt64; }
    if String_Eq(name, "int") { return tyInt; }
    if String_Eq(name, "uint8") { return tyUInt8; }
    if String_Eq(name, "uint16") { return tyUInt16; }
    if String_Eq(name, "uint32") { return tyUInt32; }
    if String_Eq(name, "uint64") { return tyUInt64; }
    if String_Eq(name, "uint") { return tyUInt; }
    if String_Eq(name, "float32") { return tyFloat32; }
    if String_Eq(name, "float64") { return tyFloat64; }
    if String_Eq(name, "float") { return tyFloat64; }
    return tyNamed;
}

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 tyTuple; }
    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;
    }

    return te;
}

// Build C function-pointer type string from a tekFunc TypeExpr, e.g. "int (*)(int)"
func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String {
    if te == null as *TypeExpr || te.kind != tekFunc { return "void (*)(void)"; }
    var retName: String = "void";
    if te.funcRet != null as *TypeExpr {
        if te.funcRet.kind == tekPointer && te.funcRet.pointerPointee != null as *TypeExpr {
            retName = String_Concat(te.funcRet.pointerPointee.typeName, "*");
        } else {
            retName = te.funcRet.typeName;
        }
        if String_Eq(retName, "") { retName = "int"; }
    }
    var result: String = retName;
    result = String_Concat(result, " (*)(");
    var cur: *TypeExprList = te.funcParams;
    var first: bool = true;
    while cur != null as *TypeExprList {
        if !first {
            result = String_Concat(result, ", ");
        }
        var pName: String = "int";
        if cur.te.kind == tekPointer && cur.te.pointerPointee != null as *TypeExpr {
            pName = String_Concat(cur.te.pointerPointee.typeName, "*");
        } else {
            pName = cur.te.typeName;
        }
        if String_Eq(pName, "") { pName = "int"; }
        result = String_Concat(result, pName);
        first = false;
        cur = cur.next;
    }
    result = String_Concat(result, ")");
    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;
}

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;
}

// ---------------------------------------------------------------------------
// 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 "";
}

// ---------------------------------------------------------------------------
// 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;

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

    // Identifier → variable reference
    if kind == ekIdent {
        // 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;
            }
        }
        n.kind = hVar;
        n.strValue = expr.strValue;
        let sym: Symbol = Scope_Lookup(ctx.scope, 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;
        }
        n.kind = hBinary;
        n.intValue = expr.intValue; // operator
        n.child1 = Lcx_LowerExpr(ctx, expr.child1);
        n.child2 = Lcx_LowerExpr(ctx, expr.child2);
        return n;
    }

    // Unary
    if kind == ekUnary {
        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 = "";
            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;
            }
            if String_Eq(receiverTypeName, "") && expr.child1.child1 != null as *Expr && expr.child1.child1.refType != null as *TypeExpr {
                receiverTypeName = expr.child1.child1.refType.typeName;
            }
            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);
            }
            // Lower receiver as first argument
            let recv: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child1);
            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
            if expr.child1 != null as *Expr && expr.child1.genericTypeArgCount > 0 {
                let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
                if genDecl != null as *Decl {
                    var typeArg0: String = expr.child1.genericTypeArg0;
                    var typeArg1: String = expr.child1.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; }
                    let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, expr.child1.genericTypeArgCount);
                    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;
            }
        }
        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;
        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 && 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;
        }

        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 && 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;
            }
        }

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

    // Closure: generate function and return address-of
    if kind == ekClosure {
        let f: *HirFunc = Lcx_LowerClosureFunc(ctx, expr);
        n.kind = hUnary;
        n.intValue = tkAmp;
        let varNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        varNode.kind = hVar;
        varNode.strValue = f.name;
        varNode.typeKind = tyFunc;
        n.child1 = varNode;
        n.typeKind = tyFunc;
        if expr.refType != null as *TypeExpr {
            n.typeName = Lcx_BuildFuncTypeName(expr.refType);
        }
        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 {
        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;
    }

    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 {
        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);
        // store the init value
        let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
        storeNode.kind = hStore;
        storeNode.child1 = alloca;
        storeNode.child2 = init;

        // If init is a closure with captures, emit capture assignments before the let
        if stmt.child1 != null as *Expr && stmt.child1.kind == ekClosure && stmt.child1.captureCount > 0 {
            let closureIdx: int = ctx.funcCount - 1;
            let envInst: String = String_Concat("__closure_env_instance_", String_FromInt(closureIdx));
            // Build a block: capture assignments + let store
            let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
            blockNode.kind = hBlock;
            blockNode.line = line;
            blockNode.column = col;
            var firstStmt: *HirNode = null as *HirNode;
            var lastStmt: *HirNode = null as *HirNode;
            var ci: int = 0;
            while ci < stmt.child1.captureCount {
                var capName: String = "";
                if ci == 0 { capName = stmt.child1.captureName0; }
                else if ci == 1 { capName = stmt.child1.captureName1; }
                else if ci == 2 { capName = stmt.child1.captureName2; }
                else if ci == 3 { capName = stmt.child1.captureName3; }
                else if ci == 4 { capName = stmt.child1.captureName4; }
                else if ci == 5 { capName = stmt.child1.captureName5; }
                else if ci == 6 { capName = stmt.child1.captureName6; }
                else if ci == 7 { capName = stmt.child1.captureName7; }
                // Build: envInst.capName = capName;
                let assignNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                assignNode.kind = hAssign;
                assignNode.line = line;
                assignNode.column = col;
                let fieldNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                fieldNode.kind = hFieldAccess;
                fieldNode.strValue = capName;
                let baseNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                baseNode.kind = hVar;
                baseNode.strValue = envInst;
                fieldNode.child1 = baseNode;
                let valNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                valNode.kind = hVar;
                valNode.strValue = capName;
                assignNode.child1 = fieldNode;
                assignNode.child2 = valNode;
                if firstStmt == null as *HirNode {
                    firstStmt = assignNode;
                    lastStmt = assignNode;
                } else {
                    lastStmt.child3 = assignNode;
                    lastStmt = assignNode;
                }
                ci = ci + 1;
            }
            // Append the let store
            if firstStmt == null as *HirNode {
                firstStmt = storeNode;
                lastStmt = storeNode;
            } else {
                lastStmt.child3 = storeNode;
                lastStmt = storeNode;
            }
            blockNode.child1 = firstStmt;
            return blockNode;
        }
        return storeNode;
    }

    // Return
    if kind == skReturn {
        n.kind = hReturn;
        if stmt.child1 != null as *Expr {
            n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
        }
        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 && iterExpr.kind == ekIdent {
            let collTypeExpr: *TypeExpr = iterExpr.refType;
            if collTypeExpr == null as *TypeExpr {
                // 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");
                if String_Eq(elemTypeName, "") {
                    // Manual prefix check for "Array_"
                    var isArrayPrefix: bool = false;
                    var isIterPrefix: bool = false;
                    let collName: String = collTypeName;
                    if collName[0] as int == 65 && collName[1] as int == 114 && collName[2] as int == 114 && collName[3] as int == 97 && collName[4] as int == 121 && collName[5] as int == 95 {
                        isArrayPrefix = true;
                    }
                    if collName[0] as int == 73 && collName[1] as int == 116 && collName[2] as int == 101 && collName[3] as int == 114 && collName[4] as int == 95 {
                        isIterPrefix = true;
                    }
                    if isArrayPrefix {
                        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 isIterPrefix {
                        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);
                        }
                    }
                }

                if !String_Eq(elemTypeName, "") {
                    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;
                        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;
                            let arrVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            arrVar.kind = hVar;
                            arrVar.strValue = iterExpr.strValue;
                            addrArr.child1 = arrVar;
                            callIter.child1 = addrArr;
                            iterInit = callIter;
                        } else {
                            // Already an iterator: __iter = arr
                            let arrVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
                            arrVar.kind = hVar;
                            arrVar.strValue = iterExpr.strValue;
                            iterInit = arrVar;
                        }

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

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

    // Build a linked list of HirNodes via child3:
    //   node1 (stmt1) → child3 → node2 (stmt2) → child3 → node3 (stmt3) → null
    // child3 is safe for chaining because:
    //   hStore: child1=alloca, child2=value, child3 unused
    //   hReturn: child1=value, child2/child3 unused
    //   hCall: child1=arg1, child2=arg2, child3 unused
    var firstNode: *HirNode = null as *HirNode;
    var prevNode: *HirNode = null as *HirNode;
    var stmt: *Stmt = block.firstStmt;
    while stmt != null as *Stmt {
        let lowered: *HirNode = Lcx_LowerStmt(ctx, stmt);
        if lowered != null as *HirNode {
            if firstNode == null as *HirNode {
                firstNode = lowered;
                prevNode = lowered;
            } else {
                prevNode.child3 = lowered;
                prevNode = lowered;
            }
        }
        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;
    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 f: *HirFunc = bux_alloc(sizeof(HirFunc)) as *HirFunc;
    f.name = decl.strValue;
    f.isPublic = decl.isPublic;
    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.retTypeName = retTe.typeName;
        f.retTypeKind = Lcx_ResolveTypeKind(retTe);
    } 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
    let prevScope: *Scope = ctx.scope;
    ctx.scope = &funcScope;
    if decl.refBody != null as *Block {
        f.body = Lcx_LowerBlock(ctx, decl.refBody, -1);
    } else {
        f.body = null as *HirNode;
    }
    ctx.scope = prevScope;
    ctx.checkedFunc = oldChecked;

    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;
    if params != null as *Decl {
        f.paramCount = params.paramCount;
        if params.paramCount > 0 { f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param0, &params.param0, ctx); }
        if params.paramCount > 1 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, &params.param1, ctx); }
        if params.paramCount > 2 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, &params.param2, ctx); }
        if params.paramCount > 3 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, &params.param3, ctx); }
        if params.paramCount > 4 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, &params.param4, ctx); }
        if params.paramCount > 5 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, &params.param5, ctx); }
        if params.paramCount > 6 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, &params.param6, ctx); }
        if params.paramCount > 7 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, &params.param7, ctx); }
        if params.paramCount > 8 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, &params.param8, ctx); }
    }

    if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc && expr.refType.funcRet != null as *TypeExpr {
        f.retTypeName = expr.refType.funcRet.typeName;
        f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet);
    } else {
        f.retTypeName = "";
        f.retTypeKind = 0;
    }

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

// ---------------------------------------------------------------------------
// 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;
        }
        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 {
            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 {
                    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;
        }
        if decl.kind == dkConst && hm.constCount < 512 {
            let ci: int = hm.constCount;
            hm.consts[ci].name = decl.strValue;
            var val: int = 0;
            if decl.constValue != null as *Expr {
                if decl.constValue.kind == ekLiteral {
                    val = decl.constValue.intValue;
                }
            }
            hm.consts[ci].value = val;
            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 {
                        hm.enums[ei].variants[vi].fieldName0 = "value";
                        hm.enums[ei].variants[vi].fieldType0 = Lcx_ResolveTypeKindFromName(v.fieldTypeName0);
                    }
                    if v.fieldCount > 1 {
                        hm.enums[ei].variants[vi].fieldName1 = "value2";
                        hm.enums[ei].variants[vi].fieldType1 = Lcx_ResolveTypeKindFromName(v.fieldTypeName1);
                    }
                }
                vi = vi + 1;
            }
            hm.enumCount = hm.enumCount + 1;
        }
        decl = decl.childDecl2;
    }


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


    return hm;
}

}
