diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2c6179a..d87c88a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -46,6 +46,8 @@ const lower_objc_class = @import("lower/objc_class.zig"); const lower_call = @import("lower/call.zig"); const lower_pack = @import("lower/pack.zig"); const lower_generic = @import("lower/generic.zig"); +const lower_expr = @import("lower/expr.zig"); +const lower_closure = @import("lower/closure.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -370,7 +372,7 @@ pub const Lowering = struct { enum_tag: struct { ty: TypeId, tag: u32 }, }; - const StructConstInfo = struct { + pub const StructConstInfo = struct { value: *const Node, ty: ?TypeId, // null if no type annotation (inferred) }; @@ -500,2983 +502,12 @@ pub const Lowering = struct { // ── Public entry point ────────────────────────────────────────── - pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { - // Stamp this node's source span onto the instructions it emits (ERR - // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ - // restore so a parent's later emits keep the parent's span after a - // child lowers. Skip the empty default so synthetic nodes don't reset - // a meaningful enclosing span to offset 0. - const saved_span = self.builder.current_span; - defer self.builder.current_span = saved_span; - if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end }; - // A node carrying an explicit `source_file` is one spliced into a body - // from another module — a substituted caller comptime-`$`-arg (stamped - // at the `cpn` build site in lowerComptimeCall / monomorphizePackFn). - // Resolve its bare names in THAT module's visibility context, overriding - // the body's defining-module pin, then restore so sibling callee nodes - // keep the enclosing context. Ordinary expression nodes never carry a - // `source_file`, so this is a no-op on the hot path. - const restore_source = node.source_file != null; - const saved_source = self.current_source_file; - if (node.source_file) |sf| self.setCurrentSourceFile(sf); - defer if (restore_source) self.setCurrentSourceFile(saved_source); - return switch (node.data) { - // Bare `$` in expression position → an `[]Type` slice - // value where each element is a `const_type(arg_types[i])`. - // Per `Type → .any` mapping in type_bridge, the IR slice - // type is `[]Any`; the interp stores raw `.type_tag` Values - // (NOT Any-boxed) so `args[i]` reads back as a Type value - // directly. Step 4 final slice — lets builder fns walk the - // whole pack at interp time. - .comptime_pack_ref => |cpr| blk: { - // `$` is overloaded in expression position: - // - Inside a pack-fn mono (or a `tryPackImplMatch` - // impl mono), `name` is a pack binding → slice of - // element types (`[]Type` lowered as `[]Any`). - // - Inside an impl mono whose impl pattern bound a - // single-type generic (`$R: Type` in - // `Closure(..$args) -> $R`), `name` is in - // `type_bindings` → single `const_type(R)` value. - // Pack arg types are checked first (the slice form), - // then pack_bindings (the impl-mono mirror), then - // type_bindings (single-type binding); only if all - // miss is it a real "outside an active binding" error. - if (self.pack_arg_types) |pat| { - if (pat.get(cpr.pack_name)) |arg_tys| { - break :blk self.buildPackSliceValue(arg_tys); - } - } - if (self.pack_bindings) |pb| { - if (pb.get(cpr.pack_name)) |arg_tys| { - break :blk self.buildPackSliceValue(arg_tys); - } - } - if (self.type_bindings) |tb| { - if (tb.get(cpr.pack_name)) |ty| { - break :blk self.builder.constType(ty); - } - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name}); - } - break :blk self.builder.constNull(self.module.types.sliceOf(.any)); - }, - // Pack-index in expression position: `$[]` → - // `const_type(arg_types[index])`. Yields a comptime-only - // Type value (`Value.type_tag(TypeId)` in the interp). - // OOB / no-active-pack-binding → focused diagnostic; the - // emitted Ref is a const_type(.void) placeholder so the - // verifier downstream catches misuse rather than silently - // succeeding with .void. - .pack_index_type_expr => |pi| blk: { - if (self.pack_arg_types) |pat| { - if (pat.get(pi.pack_name)) |arg_tys| { - if (pi.index < arg_tys.len) { - break :blk self.builder.constType(arg_tys[pi.index]); - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ - pi.pack_name, pi.index, pi.pack_name, arg_tys.len, - if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), - }); - } - break :blk self.builder.constType(.void); - } - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ - pi.pack_name, pi.index, - }); - } - break :blk self.builder.constType(.void); - }, - .int_literal => |lit| { - // If target is a float type, emit as float literal - if (self.target_type) |tt| { - if (tt == .f32 or tt == .f64) { - return self.builder.constFloat(@floatFromInt(lit.value), tt); - } - } - const ty = if (self.target_type) |tt| blk: { - break :blk if (self.isIntEx(tt)) tt else .s64; - } else .s64; - return self.builder.constInt(lit.value, ty); - }, - .float_literal => |lit| { - const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64; - return self.builder.constFloat(lit.value, fty); - }, - .bool_literal => |lit| self.builder.constBool(lit.value), - .string_literal => |lit| blk: { - const str = if (lit.is_raw) - lit.raw - else - unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; - const sid = self.module.types.internString(str); - break :blk self.builder.constString(sid); - }, - // A bare `null` / `---` with no surrounding type expectation is a - // legitimate typeless literal, not a failed lookup: `.void` is its - // intentional default (emitConstNull/emitConstUndef handle void as - // null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire. - .null_literal => self.builder.constNull(self.target_type orelse .void), - .undef_literal => self.builder.constUndef(self.target_type orelse .void), - - .identifier => |id| blk: { - // A bare pack name in value position has no runtime - // representation (Decision 1). Projections (`xs.len`, `xs[i]`, - // `xs.value`) are field/index nodes handled elsewhere, so a bare - // `xs` reaching here is always a pack-as-value misuse. - if (self.isPackName(id.name)) { - break :blk self.diagPackAsValue(id.name, node.span, .generic); - } - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - if (binding.is_alloca) { - break :blk self.builder.load(binding.ref, binding.ty); - } - break :blk binding.ref; - } - } - // Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals - if (self.comptime_constants.get(id.name)) |cv| { - switch (cv) { - .int_val => |iv| break :blk self.builder.constInt(iv, .s64), - .enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty), - } - } - // `context` resolves to a load through the lowering's - // current `__sx_ctx` pointer. Every sx function (and - // every `push Context.{...}` body) sets `current_ctx_ref` - // to a `*Context` it owns, so this is one indirection. - if (std.mem.eql(u8, id.name, "context")) { - if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { - break :blk self.diagnoseMissingContext("the `context` identifier"); - } - const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { - break :blk self.diagnoseMissingContext("the `context` identifier"); - }; - break :blk self.builder.load(self.current_ctx_ref, ctx_ty); - } - // Check globals (#run constants) - if (self.program_index.global_names.get(id.name)) |gi| { - break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); - } - // Check module-level value constants (e.g. AF_INET :s32: 2) - if (self.program_index.module_const_map.get(id.name)) |ci_global| { - if (!self.isNameVisible(id.name)) { - if (self.diagnostics) |d| - d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name}); - break :blk self.emitError(id.name, node.span); - } - // F2: emit the SOURCE-AWARE author's value (own-wins), not the - // global last-wins `ci_global`. ≥2 flat-visible same-name const - // authors → a loud ambiguity (issue 0105 / 0760), never a silent - // pick. `.none` after a visible name is the registration-only - // author (no per-source partition) — emit its global value. - switch (self.selectModuleConst(id.name)) { - .resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source), - .ambiguous => { - if (self.diagnostics) |d| - d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); - break :blk self.emitPlaceholder(id.name); - }, - .none => break :blk self.emitModuleConst(ci_global, null), - } - } - // Check if it's a function name — produce function pointer reference - // Resolve mangled name for block-local functions - const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; - if (self.program_index.fn_ast_map.contains(eff_fn_name)) { - // Visibility check only for user-typed bare names (id.name - // == eff_fn_name) without a UFCS alias. Mangled local- - // scope names and UFCS rewrites are compiler indirections - // and stay exempt. - if (std.mem.eql(u8, eff_fn_name, id.name) and - self.program_index.ufcs_alias_map.get(id.name) == null and - !self.isNameVisible(eff_fn_name)) - { - if (self.diagnostics) |d| - d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name}); - break :blk self.emitError(eff_fn_name, node.span); - } - // Type-as-value: if target is Any (Type variable), produce a type name string - if (self.target_type == .any) { - const fd = self.program_index.fn_ast_map.get(eff_fn_name).?; - const fn_type_str = self.formatFnTypeString(fd); - const sid = self.module.types.internString(fn_type_str); - const str = self.builder.constString(sid); - break :blk self.builder.boxAny(str, .string); - } - // fix-0102d site 2: taking a bare same-name fn as a VALUE - // (func_ref, fn-ptr / closure coercion) must capture the - // RESOLVED author's FuncId for a genuine flat collision, not - // the first-wins winner's. Plain bare name only; `.ambiguous` - // → loud diagnostic; `.none` → existing first-wins path. The - // winner is lazily lowered ONLY on `.none` — a rerouted value - // never uses the winner, so its body must not be lowered. - const value_fid: ?FuncId = blk_fv: { - if (std.mem.eql(u8, eff_fn_name, id.name) and - self.program_index.ufcs_alias_map.get(id.name) == null and - (if (self.scope) |scope| scope.lookup(id.name) == null else true)) - { - if (self.current_source_file) |caller_file| { - switch (self.selectPlainCallableAuthor(id.name, caller_file)) { - .func => |sf| { - var selected = sf; - break :blk_fv self.selectedFuncId(&selected, id.name); - }, - .ambiguous => { - if (self.diagnostics) |d| - d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name}); - break :blk self.emitError(id.name, node.span); - }, - .none => {}, - } - } - } - if (!self.lowered_functions.contains(eff_fn_name)) { - self.lazyLowerFunction(eff_fn_name); - } - break :blk_fv self.resolveFuncByName(eff_fn_name); - }; - if (value_fid) |fid| { - // Auto-promote bare function → closure when target_type is closure - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const tt_info = self.module.types.get(tt); - if (tt_info == .closure) { - const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure); - break :blk self.builder.closureCreate(tramp_id, Ref.none, tt); - } - // Coercing a bare fn name to a fn-pointer - // type — the call_conv must match. A - // default-conv sx fn assigned to a - // callconv(.c) slot (e.g. passed to - // pthread_create) would otherwise crash at - // runtime when the C caller doesn't supply - // the implicit __sx_ctx arg. - if (tt_info == .function) { - const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv; - if (func_cc != tt_info.function.call_conv) { - if (self.diagnostics) |d| { - const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention"; - const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention"; - d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc }); - } - break :blk self.emitPlaceholder(eff_fn_name); - } - } - // NOTE: `xx : *void` (e.g. - // `class_addMethod(_, _, xx my_imp, _)`) - // is intentionally NOT diagnosed here. - // Manually-constructed Closure values - // legitimately store default-conv sx fns - // into a `*void` slot for sx-side dispatch - // through the closure trampoline ABI. The - // compiler can't distinguish C-side vs - // sx-side use from the cast alone. - // examples/50-smoke.sx has both shapes. - } - } - break :blk self.builder.emit(.{ .func_ref = fid }, .s64); - } - } - // Type-as-value: a name that resolves to a TypeId - // (primitive, alias, registered struct/enum/union, - // generic-struct instantiation) evaluates to a - // `const_type` in expression position. Works for - // direct assignment to a `Type`-typed slot - // (`x: Type = Vec4`), comparison (`x == Vec4`), and - // pack-arg / Any context (boxing happens at the - // consumer). - // E4 single-hop visibility + ambiguity gate: a bare type name used - // as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over - // 2+ flat hops is not bare-visible (consistent with annotations / - // 0763); ≥2 direct flat same-name authors are ambiguous (loud - // diagnostic, 0755/0767). A single source-keyed author — including - // the querying source's OWN author over a same-name flat import - // (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name - // author a global `findByName` would pick. A value name / generic - // param / undeclared name → `.proceed`, falling through below. - const ty = blk_ty: { - switch (self.headTypeGate(id.name, node.span)) { - .ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name), - .resolved => |tid| break :blk_ty tid, - .proceed => {}, - } - if (self.type_bindings) |tb| { - if (tb.get(id.name)) |t| break :blk_ty t; - } - if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t; - if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t; - const name_id = self.module.types.internString(id.name); - if (self.module.types.findByName(name_id)) |t| break :blk_ty t; - break :blk_ty TypeId.void; - }; - if (ty != .void) { - break :blk self.builder.constType(ty); - } - // Unknown identifier - break :blk self.emitError(id.name, node.span); - }, - - .binary_op => |bop| self.lowerBinaryOp(&bop), - - .unary_op => |uop| blk: { - // `xx ` with a slice target materializes the comptime - // pack into a runtime `[]elem` (issue 0053). Must run before the - // operand is lowered (a bare pack name otherwise hits the - // pack-as-value error). - if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) { - const pname = uop.operand.data.identifier.name; - if (self.target_type) |tt| { - if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) { - break :blk self.lowerPackToSlice(pname, tt); - } - } - break :blk self.diagPackAsValue(pname, node.span, .generic); - } - // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of - if (uop.op == .address_of and uop.operand.data == .index_expr) { - const ie = &uop.operand.data.index_expr; - const idx = self.lowerExpr(ie.index); - const obj_ty = self.inferExprType(ie.object); - const elem_ty = self.getElementType(obj_ty); - const ptr_ty = self.module.types.ptrTo(elem_ty); - // For array targets, use the storage pointer (alloca for a - // local, global_addr for a module global) so the resulting - // pointer is into live storage, not a loaded copy. - const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; - const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); - break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); - } - // address_of(field_access) → use lowerExprAsPtr for GEP chain - // Handles all cases: pointer-based, index-based, nested field access - if (uop.op == .address_of and uop.operand.data == .field_access) { - const inner_ty = self.inferExprType(uop.operand); - const ptr_ty = self.module.types.ptrTo(inner_ty); - const ptr = self.lowerExprAsPtr(uop.operand); - break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty); - } - // address_of(identifier) → return alloca directly (pointer to variable) - if (uop.op == .address_of and uop.operand.data == .identifier) { - const id_name = uop.operand.data.identifier.name; - if (self.scope) |scope| { - if (scope.lookup(id_name)) |binding| { - if (binding.is_alloca) { - const ptr_ty = self.module.types.ptrTo(binding.ty); - break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); - } - } - } - // address_of(global) → emit global_addr (pointer to global, not load) - if (self.program_index.global_names.get(id_name)) |gi| { - const ptr_ty = self.module.types.ptrTo(gi.ty); - break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); - } - } - const operand = self.lowerExpr(uop.operand); - break :blk switch (uop.op) { - .negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)), - .not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool), - .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)), - .xx => self.lowerXX(operand, uop.operand), - .address_of => blk2: { - const inner_ty = self.inferExprType(uop.operand); - const ptr_ty = self.module.types.ptrTo(inner_ty); - break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty); - }, - }; - }, - - .if_expr => |ie| self.lowerIfExpr(&ie), - .match_expr => |me| self.lowerMatch(&me), - .while_expr => |we| self.lowerWhile(&we), - .for_expr => |fe| self.lowerFor(&fe), - .break_expr => self.lowerBreak(), - .continue_expr => self.lowerContinue(), - .call => |c| self.lowerCall(&c), - .ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic), - .field_access => |fa| self.lowerFieldAccess(&fa, node.span), - .struct_literal => |sl| self.lowerStructLiteral(&sl, node.span), - .array_literal => |al| self.lowerArrayLiteral(&al), - .index_expr => |ie| self.lowerIndexExpr(&ie), - .slice_expr => |se| self.lowerSliceExpr(&se), - .lambda => |lam| self.lowerLambda(&lam), - .force_unwrap => |fu| self.lowerForceUnwrap(&fu), - .null_coalesce => |nc| self.lowerNullCoalesce(&nc), - .deref_expr => |de| self.lowerDerefExpr(&de), - .enum_literal => |el| self.lowerEnumLiteral(&el), - .comptime_expr => |ct| self.lowerInlineComptime(ct.expr), - .insert_expr => |ins| blk: { - break :blk self.lowerInsertExprValue(ins.expr); - }, - .tuple_literal => |tl| self.lowerTupleLiteral(&tl), - .spread_expr => self.emitError("spread_expr", node.span), - .chained_comparison => |cc| self.lowerChainedComparison(&cc), - - // `#jni_env(env) { body }` in expression position — the block's - // value becomes the env-scope's value. Save→set→body-value→restore. - .jni_env_block => |eb| blk: { - const env_ref = self.lowerExpr(eb.env); - const fids = self.getJniEnvTlFids(); - const ptr_ty = self.module.types.ptrTo(.void); - const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); - const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable; - _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void); - self.jni_env_stack.append(self.alloc, env_ref) catch unreachable; - const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void); - _ = self.jni_env_stack.pop(); - const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable; - _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void); - break :blk value; - }, - - // Statements that can appear in expression position - .block => |blk| blk: { - // Create a child scope for block-level variable shadowing - var block_scope = Scope.init(self.alloc, self.scope); - const saved_scope = self.scope; - self.scope = &block_scope; - const saved_defer_len = self.defer_stack.items.len; - defer { - self.emitBlockDefers(saved_defer_len); - self.scope = saved_scope; - block_scope.deinit(); - } - // This block sits in value position (lowerExpr is reached only - // for value contexts — statement blocks go through lowerBlock). - // If its last expression's value is discarded by a `;`, the - // surrounding expression has no value to use: report it. - if (!blk.produces_value and blk.discarded_semi != null) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{}); - } - } - // A block in expression position yields its last statement's - // value only when it produces one (no trailing `;`); otherwise - // it runs as statements and evaluates to void. - if (blk.produces_value and blk.stmts.len > 0) { - for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { - self.lowerStmt(stmt); - } - break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse - self.builder.constInt(0, .void); - } - for (blk.stmts) |stmt| { - self.lowerStmt(stmt); - } - break :blk self.builder.constInt(0, .void); - }, - - // type_expr can appear as a variable reference when the name collides - // with a builtin type name (e.g. s2, u8). Check scope first. - .type_expr => |te| blk: { - if (self.scope) |scope| { - if (scope.lookup(te.name)) |binding| { - if (binding.is_alloca) { - break :blk self.builder.load(binding.ref, binding.ty); - } - break :blk binding.ref; - } - } - if (self.program_index.global_names.get(te.name)) |gi| { - break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); - } - // Type literal in expression position → first-class - // `const_type` Value (i64 = TypeId.index()). Makes - // `t : Type = f64;` store a real TypeId; lets - // `t == f64` icmp at runtime against the same TypeId. - if (self.isKnownTypeName(te.name)) { - const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - break :blk self.builder.constType(ty); - } - break :blk self.emitError(te.name, node.span); - }, - - .try_expr => |te| self.lowerTry(te.operand, node.span), - .catch_expr => |ce| self.lowerCatch(&ce, node.span), - .caller_location => self.lowerCallerLocation(node), - else => self.emitError("unknown_expr", node.span), - }; - } - - /// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns - /// the element (pointee) type so a value-position use can auto-deref it. - pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId { - if (node.data != .identifier) return null; - const scope = self.scope orelse return null; - const binding = scope.lookup(node.data.identifier.name) orelse return null; - if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null; - const info = self.module.types.get(binding.ty); - return if (info == .pointer) info.pointer.pointee else null; - } - - fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { - // Short-circuit: `a and b` → if a then b else false - if (bop.op == .and_op) { - const lhs = self.lowerExpr(bop.lhs); - const rhs_bb = self.freshBlock("and.rhs"); - const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool}); - const false_val = self.builder.constBool(false); - self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val}); - self.builder.switchToBlock(rhs_bb); - const rhs = self.lowerExpr(bop.rhs); - self.builder.br(merge_bb, &.{rhs}); - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, .bool); - } - // Short-circuit: `a or b` → if a then true else b - if (bop.op == .or_op) { - // A failable `or` (value-terminator or chain) routes to the error- - // handling lowering, not the optional/boolean unwrap below. Detected - // structurally (a `try`-chain's value type is non-failable `T`, so a - // type-only `exprIsFailable(lhs)` would miss nested chains). - if (self.orIsFailableChain(bop)) { - return self.lowerFailableOr(bop); - } - const lhs = self.lowerExpr(bop.lhs); - const rhs_bb = self.freshBlock("or.rhs"); - const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool}); - const true_val = self.builder.constBool(true); - self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{}); - self.builder.switchToBlock(rhs_bb); - const rhs = self.lowerExpr(bop.rhs); - self.builder.br(merge_bb, &.{rhs}); - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, .bool); - } - - // Type-literal comparison fold: when both sides are type-shaped - // AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to - // a static TypeId at lower time (`type_of(x)` for any - // statically-typed `x`), resolve each and emit a `const_bool`. - // Same semantic as `type_eq(A, B)` but using the standard `==` - // operator — the user's intuition. Without the fold, both - // sides lower as `const_type` undef-i64 and the runtime icmp - // returns garbage. - if (bop.op == .eq or bop.op == .neq) { - if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) { - const lhs_ty = self.resolveTypeArg(bop.lhs); - const rhs_ty = self.resolveTypeArg(bop.rhs); - const eq_result = lhs_ty == rhs_ty; - return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result); - } - } - - // Any-shaped `==` (e.g. `t == s64` where `t: Type`): both - // operands are 16-byte `{tag, value}` aggregates. LLVM - // doesn't accept `icmp` on aggregates directly. Decompose - // via `unbox_any` (which extracts the value field at - // `.s64`) and compare the i64s. Tag fields are stable - // across compilations of the same source so value-only - // identity is enough. - if (bop.op == .eq or bop.op == .neq) { - const lhs_ty = self.inferExprType(bop.lhs); - const rhs_ty = self.inferExprType(bop.rhs); - if (lhs_ty == .any and rhs_ty == .any) { - const lhs = self.lowerExpr(bop.lhs); - const rhs = self.lowerExpr(bop.rhs); - const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64); - const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64); - if (bop.op == .eq) { - return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); - } else { - return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); - } - } - } - - // Special case: optional == null / optional != null - if (bop.op == .eq or bop.op == .neq) { - const lhs_is_null = bop.lhs.data == .null_literal; - const rhs_is_null = bop.rhs.data == .null_literal; - if (lhs_is_null or rhs_is_null) { - const opt_node = if (rhs_is_null) bop.lhs else bop.rhs; - const opt_ty = self.inferExprType(opt_node); - if (!opt_ty.isBuiltin()) { - const info = self.module.types.get(opt_ty); - if (info == .optional) { - const opt_val = self.lowerExpr(opt_node); - const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); - // == null → !has_value, != null → has_value - return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has; - } - } - } - } - - // Error-set equality: an error-set value compares only with an - // `error.X` tag literal or another error-set value. Comparing to a raw - // integer is a type error (coerce with `xx`). `e == error.X` resolves - // X against e's set and validates membership. - if (bop.op == .eq or bop.op == .neq) { - if (self.tryLowerErrorSetEquality(bop)) |result| return result; - } - - // Set target_type for null literals to match the other operand's type. - // This ensures null gets the same LLVM type as the value being compared. - if (bop.op == .eq or bop.op == .neq) { - const null_on_rhs = bop.rhs.data == .null_literal; - const null_on_lhs = bop.lhs.data == .null_literal; - if (null_on_rhs or null_on_lhs) { - var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); - // Lower the non-null side first when its type isn't statically - // inferable, and take the null's type from the lowered value — - // never a guess. - var pre_lowered: ?Ref = null; - if (other_ty == .unresolved) { - pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs); - other_ty = self.builder.getRefType(pre_lowered.?); - } - if (other_ty != .void and other_ty != .unresolved) { - const saved_tt = self.target_type; - self.target_type = other_ty; - const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?; - const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?; - self.target_type = saved_tt; - const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }; - return self.builder.emit(cmp_op, .bool); - } - } - } - var lhs = self.lowerExpr(bop.lhs); - // A `for xs: (*x)` capture is a pointer; in a value position (here, an - // operand) it auto-derefs to the element. - const lhs_ref_pointee = self.refCapturePointee(bop.lhs); - if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p); - // Set target_type from LHS so enum literals on RHS resolve correctly. - // When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use - // the lowered operand's concrete type rather than a guess. - const lhs_ty = blk: { - if (lhs_ref_pointee) |p| break :blk p; - const it = self.inferExprType(bop.lhs); - break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it; - }; - const saved_tt = self.target_type; - if (lhs_ty != .void) { - if (!lhs_ty.isBuiltin()) { - const lhs_info = self.module.types.get(lhs_ty); - if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) { - self.target_type = lhs_ty; - } - } else if (lhs_ty == .f32 or lhs_ty == .f64) { - self.target_type = lhs_ty; - } - } - var rhs = self.lowerExpr(bop.rhs); - const rhs_ref_pointee = self.refCapturePointee(bop.rhs); - if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p); - self.target_type = saved_tt; - // Result type follows the shared promotion rule: an int LHS with a - // float RHS promotes to the float (`s64 * f32` → `f32`); vectors / - // structs keep the LHS type. `inferExprType` reuses the same helper - // so static typing agrees with the value produced here. - const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs); - var ty = arithResultType(lhs_ty, rhs_inferred); - - // Auto-unwrap optional operands for arithmetic/comparison - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .optional) { - ty = info.optional.child; - lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty); - } - } - const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs); - if (!rhs_ty.isBuiltin()) { - const rhs_info = self.module.types.get(rhs_ty); - if (rhs_info == .optional) { - rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child); - } - } - - // String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison - if (ty == .string and (bop.op == .eq or bop.op == .neq)) { - return if (bop.op == .eq) - self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool) - else - self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool); - } - - // Tuple operators - if (!ty.isBuiltin()) { - const lhs_info = self.module.types.get(ty); - if (lhs_info == .tuple) { - return self.lowerTupleOp(bop, lhs, rhs, ty); - } - } - // Tuple membership: value in (tuple) - if (bop.op == .in_op) { - const rhs_ty_raw = self.inferExprType(bop.rhs); - if (!rhs_ty_raw.isBuiltin()) { - const rhs_info_raw = self.module.types.get(rhs_ty_raw); - if (rhs_info_raw == .tuple) { - return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple); - } - } - } - - // Reject scalar ops on incompatible operand types (e.g. - // `s64 + string`, `s64 < string`, `s64 & string`). The result type - // `ty` is derived from the LHS, so without this the op lowers as - // ` : ` and either reinterprets the RHS bytes (arithmetic - // / bitwise → garbage) or feeds mismatched LLVM types to `icmp` - // (ordering → verifier failure). - { - const group: enum { none, arith, ordering, bitwise } = switch (bop.op) { - .add, .sub, .mul, .div, .mod => .arith, - .lt, .lte, .gt, .gte => .ordering, - .bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise, - else => .none, - }; - if (group != .none) { - const eff_rhs_ty = blk: { - if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs); - if (!rhs_ty.isBuiltin()) { - const ri = self.module.types.get(rhs_ty); - if (ri == .optional) break :blk ri.optional.child; - } - break :blk rhs_ty; - }; - const ok = switch (group) { - .arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty), - .ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty), - .bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty), - .none => true, - }; - if (!ok) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{ - binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty), - }); - } - return self.emitPlaceholder("operand-type-mismatch"); - } - } - } - - return switch (bop.op) { - .add => self.builder.add(lhs, rhs, ty), - .sub => self.builder.sub(lhs, rhs, ty), - .mul => self.builder.mul(lhs, rhs, ty), - .div => self.builder.div(lhs, rhs, ty), - .mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), - .eq => self.builder.cmpEq(lhs, rhs), - .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .lt => self.builder.cmpLt(lhs, rhs), - .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .gt => self.builder.cmpGt(lhs, rhs), - .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), - .bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), - .bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), - .shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), - .shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), - .in_op => self.emitError("in_op", bop.lhs.span), - }; - } - - /// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=) - fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref { - const lhs_info = self.module.types.get(lhs_ty); - const lhs_fields = lhs_info.tuple.fields; - - switch (bop.op) { - .add => { - // Tuple concatenation: (a, b) + (c, d) → (a, b, c, d) - const rhs_ty = self.inferExprType(bop.rhs); - const rhs_fields = if (!rhs_ty.isBuiltin()) blk: { - const ri = self.module.types.get(rhs_ty); - break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{}; - } else &[_]TypeId{}; - - var all_fields = std.ArrayList(TypeId).empty; - defer all_fields.deinit(self.alloc); - var all_vals = std.ArrayList(Ref).empty; - defer all_vals.deinit(self.alloc); - - for (lhs_fields, 0..) |f, i| { - all_fields.append(self.alloc, f) catch unreachable; - all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; - } - for (rhs_fields, 0..) |f, i| { - all_fields.append(self.alloc, f) catch unreachable; - all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable; - } - - const result_ty = self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, - .names = null, - } }); - const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; - return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); - }, - .mul => { - // Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b) - const count: usize = switch (bop.rhs.data) { - .int_literal => |il| @intCast(@as(u64, @bitCast(il.value))), - else => 1, - }; - - var all_fields = std.ArrayList(TypeId).empty; - defer all_fields.deinit(self.alloc); - var all_vals = std.ArrayList(Ref).empty; - defer all_vals.deinit(self.alloc); - - for (0..count) |_| { - for (lhs_fields, 0..) |f, i| { - all_fields.append(self.alloc, f) catch unreachable; - all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; - } - } - - const result_ty = self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, - .names = null, - } }); - const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; - return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); - }, - .eq, .neq => { - // Element-wise equality (or single-element tuple vs scalar) - const rhs_is_tuple = blk: { - const rt = self.inferExprType(bop.rhs); - if (!rt.isBuiltin()) { - break :blk self.module.types.get(rt) == .tuple; - } - break :blk false; - }; - if (!rhs_is_tuple and lhs_fields.len == 1) { - // Single-element tuple vs scalar: unwrap and compare - const lf = self.builder.structGet(lhs, 0, lhs_fields[0]); - const eq = self.builder.cmpEq(lf, rhs); - return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq; - } - var result = self.builder.constBool(true); - for (lhs_fields, 0..) |f, i| { - const lf = self.builder.structGet(lhs, @intCast(i), f); - const rf = self.builder.structGet(rhs, @intCast(i), f); - const eq = self.builder.cmpEq(lf, rf); - result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool); - } - return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result; - }, - .lt, .lte, .gt, .gte => { - // Lexicographic comparison - return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields); - }, - else => return self.builder.constInt(0, .s64), - } - } - - fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref { - // Lexicographic comparison using boolean logic. - // (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1) - // (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1) - if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte); - - const n = fields.len; - // Start with the last field using the actual op - const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]); - const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]); - var result = switch (op) { - .lt => self.builder.cmpLt(lf_last, rf_last), - .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), - .gt => self.builder.cmpGt(lf_last, rf_last), - .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), - else => unreachable, - }; - - // Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result) - if (n > 1) { - var i: usize = n - 1; - while (i > 0) { - i -= 1; - const lf = self.builder.structGet(lhs, @intCast(i), fields[i]); - const rf = self.builder.structGet(rhs, @intCast(i), fields[i]); - const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf); - const eq = self.builder.cmpEq(lf, rf); - const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool); - result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool); - } - } - return result; - } - - fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref { - // value in (a, b, c) → value == a || value == b || value == c - var result = self.builder.constBool(false); - for (tuple_info.fields, 0..) |f, i| { - const elem = self.builder.structGet(tuple, @intCast(i), f); - const eq = self.builder.cmpEq(value, elem); - result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool); - } - return result; - } - - // ── Control flow ──────────────────────────────────────────────── - - fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { - // Check for tagged enum construction: .Variant.{ payload_fields } - // This happens when type_expr is an enum_literal and target_type is a union - if (sl.type_expr) |te| { - if (te.data == .enum_literal) { - const variant_name = te.data.enum_literal.name; - const union_ty = self.target_type orelse .unresolved; - if (!union_ty.isBuiltin()) { - const union_info = self.module.types.get(union_ty); - if (union_info == .tagged_union) { - return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); - } - } - } - } - - // `.{ name = ... }` against a tagged-union target_type. Reject: - // the only valid construction forms are `.variant(payload)` and - // `.variant.{ field, ... }`. Falling through would lower the - // user's values straight into the `(tag, payload_bytes)` slot - // pair and emit IR that LLVM later rejects. - if (sl.type_expr == null and sl.struct_name == null) { - const tu_ty = self.target_type orelse .unresolved; - if (!tu_ty.isBuiltin()) { - const tu_info = self.module.types.get(tu_ty); - if (tu_info == .tagged_union) { - if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { - const first_name = sl.field_inits[0].name.?; - if (self.diagnostics) |diags| { - const ty_name = self.formatTypeName(tu_ty); - if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { - diags.addFmt( - .err, - span, - "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", - .{ ty_name, first_name, first_name, first_name }, - ); - } else { - self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); - } - } - return self.builder.enumInit(0, Ref.none, tu_ty); - } - } - } - } - - const ty: TypeId = if (sl.struct_name) |name| - // Source-aware (E2): a bare struct-literal type name resolves to the - // querying source's OWN same-name author, not the global `findByName` - // first-match — so `Box.{...}` in module B builds B's `Box`, never a - // flat-imported A's. `.undeclared`/`.pending` keep the empty-struct - // stub (byte-identical to the legacy `findByName orelse intern`); - // `.ambiguous`/`.not_visible` surface their loud diagnostic + poison. - self.resolveNominalLeaf(name, false, span) - else if (sl.type_expr) |te| - // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr - self.resolveTypeWithBindings(te) - else self.target_type orelse .unresolved; - - // Get struct field types for coercion and ordering - const struct_fields = self.getStructFields(ty); - - // Look up field defaults from AST - const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: { - const ti = self.module.types.get(ty); - break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null); - } else @as(?[]const u8, null); - const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn| - (self.struct_defaults_map.get(sn) orelse &.{}) - else - &.{}; - - // Check if any field_init has a name (named literal) - const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; - - if (has_names and struct_fields.len > 0) { - // Named literal: reorder fields to match struct declaration order - // First, lower all field values in source order (to preserve evaluation order) - var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty; - defer lowered.deinit(self.alloc); - for (sl.field_inits) |fi| { - const saved_tt = self.target_type; - // Set target_type to the field's declared type so array literals - // know if the target is a vector, etc. - if (fi.name) |fname| { - for (struct_fields) |sf| { - if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { - self.target_type = sf.ty; - break; - } - } - } - const val = self.lowerExpr(fi.value); - self.target_type = saved_tt; - lowered.append(self.alloc, .{ - .val = val, - .name = fi.name orelse "", - .node = fi.value, - }) catch unreachable; - } - - // Build fields in declaration order - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - for (struct_fields, 0..) |sf, fi| { - const sf_name = self.module.types.getString(sf.name); - // Find the matching lowered value - var found = false; - for (lowered.items) |l| { - if (std.mem.eql(u8, l.name, sf_name)) { - var val = l.val; - const src_ty = self.builder.getRefType(val); - val = self.coerceToType(val, src_ty, sf.ty); - fields.append(self.alloc, val) catch unreachable; - found = true; - break; - } - } - if (!found) { - // Field not specified — use default if available, else zero - if (fi < field_defaults.len) { - if (field_defaults[fi]) |default_expr| { - // Coerce the default to the field type at the IR - // level (the implicit narrowing rule) so a float - // default folds/errors here instead of being - // silently bit-coerced by the backend. - fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; - } else { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } else { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - } - - const result = self.builder.structInit(fields.items, ty); - if (sl.init_block) |ib| { - return self.lowerInitBlock(result, ty, ib); - } - return result; - } - - // Positional literal: use source order - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - - for (sl.field_inits, 0..) |fi, i| { - var val = self.lowerExpr(fi.value); - // Coerce field value to match struct field type - if (i < struct_fields.len) { - const src_ty = self.inferExprType(fi.value); - val = self.coerceToType(val, src_ty, struct_fields[i].ty); - } - fields.append(self.alloc, val) catch unreachable; - } - - // Pad missing fields with defaults or zeroes - if (fields.items.len < struct_fields.len) { - for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { - if (fi < field_defaults.len) { - if (field_defaults[fi]) |default_expr| { - fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; - continue; - } - } - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - - const result = self.builder.structInit(fields.items, ty); - - // Lower init block if present - if (sl.init_block) |ib| { - return self.lowerInitBlock(result, ty, ib); - } - - return result; - } - - /// Lower an init block: store struct value to alloca, bind `self`, execute block, reload. - fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref { - // Store struct value to a temporary alloca - const ptr_ty = self.module.types.ptrTo(ty); - const slot = self.builder.alloca(ty); - self.builder.store(slot, struct_val); - - // Create a nested scope with `self` bound to the alloca pointer - var init_scope = Scope.init(self.alloc, self.scope); - defer init_scope.deinit(); - const saved_scope = self.scope; - self.scope = &init_scope; - - // `self` is the pointer to the struct (not an alloca itself — it IS the pointer value) - init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false }); - - // Lower the init block body - self.lowerBlock(ib); - - // Restore scope - self.scope = saved_scope; - - // Load and return the (possibly modified) struct value - return self.builder.load(slot, ty); - } - - /// Get the field list for a struct TypeId, or empty if not a struct. - pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field { - if (ty.isBuiltin()) return &.{}; - var resolved = ty; - const info = self.module.types.get(resolved); - // Dereference pointer types to get to the underlying struct - if (info == .pointer) { - resolved = info.pointer.pointee; - if (resolved.isBuiltin()) return &.{}; - const inner = self.module.types.get(resolved); - return switch (inner) { - .@"struct" => |s| s.fields, - else => &.{}, - }; - } - return switch (info) { - .@"struct" => |s| s.fields, - else => &.{}, - }; - } - - /// If a method's first param expects a pointer (*T) but we're passing T by value, - /// swap the first arg with the alloca address (implicit address-of). - pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { - // Skip the implicit __sx_ctx param when inspecting the receiver slot. - const skip: usize = if (func.has_implicit_ctx) 1 else 0; - if (func.params.len <= skip) return; - const first_param_ty = func.params[skip].ty; - // Check if first param expects a pointer - if (!first_param_ty.isBuiltin()) { - const pi = self.module.types.get(first_param_ty); - if (pi == .pointer) { - // If obj is already a pointer type, it's already correct (no addr_of needed) - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer) return; // already a pointer - } - // Method expects *T — pass the address of the receiver (value type in alloca) - if (obj_node.data == .identifier) { - if (self.scope) |scope| { - if (scope.lookup(obj_node.data.identifier.name)) |binding| { - if (binding.is_alloca) { - const ptr_ty = self.module.types.ptrTo(binding.ty); - method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); - return; - } - } - } - } - // Field access: obj.field.method() → GEP to field, pass pointer directly. - // This avoids copying the struct value (mutations through *T must be visible). - if (obj_node.data == .field_access) { - const gep_ref = self.lowerExprAsPtr(obj_node); - // GEP returns a pointer in LLVM but its IR type is the field value type. - // Wrap with addr_of (no-op in LLVM) to set the IR type to *T, - // preventing coerceCallArgs from doing a spurious alloca+store. - const ptr_ty = self.module.types.ptrTo(obj_ty); - method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty); - return; - } - // General case: alloca+store the value and pass the alloca pointer - { - const slot = self.builder.alloca(obj_ty); - self.builder.store(slot, method_args.items[0]); - method_args.items[0] = slot; - } - } else { - // Method expects a value `T` but the receiver is a `*T` (e.g. a - // `for xs: (*x)` by-ref capture) — deref to pass the value. - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer and oi.pointer.pointee == first_param_ty) { - method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty); - } - } - } - } - } - - /// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. - pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { - if (ty.isBuiltin()) { - // Map builtin types to their names for method resolution (e.g., s64.eq) - return builtinTypeName(ty); - } - var resolved = ty; - const info = self.module.types.get(resolved); - if (info == .pointer) { - resolved = info.pointer.pointee; - if (resolved.isBuiltin()) return builtinTypeName(resolved); - } - const ri = self.module.types.get(resolved); - return switch (ri) { - .@"struct" => |s| self.module.types.getString(s.name), - else => null, - }; - } - - fn builtinTypeName(ty: TypeId) ?[]const u8 { - return switch (ty) { - .s8 => "s8", - .s16 => "s16", - .s32 => "s32", - .s64 => "s64", - .u8 => "u8", - .u16 => "u16", - .u32 => "u32", - .u64 => "u64", - .f32 => "f32", - .f64 => "f64", - .bool => "bool", - .string => "string", - else => null, - }; - } - - /// Resolve the type of a named field on a given type. - fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { - if (std.mem.eql(u8, field, "len")) return .s64; - if (std.mem.eql(u8, field, "ptr")) { - const elem_ty = self.getElementType(ty); - return self.module.types.manyPtrTo(elem_ty); - } - const field_name_id = self.module.types.internString(field); - // Check union fields + promoted fields - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { - .@"union" => |u| u.fields, - .tagged_union => |u| u.fields, - else => null, - }; - if (u_fields) |ufields| { - for (ufields) |f| { - if (f.name == field_name_id) return f.ty; - // Check promoted fields from anonymous struct variants - if (!f.ty.isBuiltin()) { - const fi = self.module.types.get(f.ty); - if (fi == .@"struct") { - for (fi.@"struct".fields) |sf| { - if (sf.name == field_name_id) return sf.ty; - } - } - } - } - } - } - // Check tuple fields - if (!ty.isBuiltin()) { - const ti = self.module.types.get(ty); - if (ti == .tuple) { - const tuple = ti.tuple; - // Try named fields - if (tuple.names) |names| { - for (names, 0..) |name_id, i| { - if (name_id == field_name_id) return tuple.fields[i]; - } - } - // Try numeric index - const idx = std.fmt.parseInt(usize, field, 10) catch { - return .unresolved; - }; - if (idx < tuple.fields.len) return tuple.fields[idx]; - return .unresolved; - } - } - const struct_fields = self.getStructFields(ty); - for (struct_fields) |f| { - if (f.name == field_name_id) return f.ty; - } - return .unresolved; - } - - fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { - // `error.X` — an error-tag literal. The `error` keyword in expression - // position parses as identifier "error" (E0.2), so `error.X` is a - // field access we intercept here. `error` is reserved, so this is - // unambiguous (no struct/pack can be named `error`). - if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) { - return self.lowerErrorTagLiteral(fa.field, span); - } - - // Pack-arity intercept: `.len` in a pack-fn mono's - // body resolves to the comptime-known N. The mono doesn't - // materialise the `[]Any` slice that the inline path used, so - // `args` isn't in scope as a value. - if (self.pack_param_count) |ppc| { - if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { - if (ppc.get(fa.object.data.identifier.name)) |n| { - return self.builder.constInt(@as(i64, @intCast(n)), .s64); - } - } - } - - // Pack value projection: `xs.` where `` is a (zero-arg) method of - // the pack's constraint protocol projects it over every element → - // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) - if (self.pack_constraint) |pcon| { - if (fa.object.data == .identifier) { - if (pcon.get(fa.object.data.identifier.name)) |proto| { - if (self.lookupProtocolField(proto, fa.field) != null) { - return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); - } - } - } - } - - // Interface-only enforcement (Decision): a member access on a - // constrained pack element `xs[i].` may only name a method of the - // constraint protocol — not an arbitrary concrete field. Checked here, - // on the `xs[i]` (index_expr) base, BEFORE substitution erases the - // "constrained to P" context. Protocol method CALLS go through the call - // path; a method name passes this check (it's in the protocol). - if (self.pack_constraint) |pcon| { - if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { - const base_name = fa.object.data.index_expr.object.data.identifier.name; - if (pcon.get(base_name)) |proto| { - if (self.lookupProtocolField(proto, fa.field) == null) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); - } - return self.builder.constInt(0, .void); - } - } - } - } - - // Check for struct constant access: Struct.CONST - if (fa.object.data == .identifier) { - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; - if (self.struct_const_map.get(qualified)) |info| { - return self.lowerStructConstant(info); - } - } - - // Numeric-limit accessor: `.min` / `.max` folds to a comptime - // const of the queried type (sibling of the identifier-receiver - // intercepts above). Placed AFTER `Struct.CONST` so a user const named - // `min`/`max` wins on its own struct; a builtin type name can never - // name a user struct (reserved — issue 0076), so they never collide. - if (self.lowerNumericLimit(fa, span)) |ref| return ref; - - // M1.3 — `obj.class` on any Obj-C-class pointer lowers to - // `object_getClass(obj)`. Sugar; the receiver is opaque so - // we don't auto-deref. Returns `Class` (alias for *void; - // typed Class(T) parameterization is M1.1.b). - if (std.mem.eql(u8, fa.field, "class")) { - const expr_ty = self.inferExprType(fa.object); - if (self.objc().isObjcClassPointer(expr_ty)) { - const obj_ref = self.lowerExpr(fa.object); - const ptr_void = self.module.types.ptrTo(.void); - const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); - const args = self.alloc.alloc(Ref, 1) catch unreachable; - args[0] = obj_ref; - return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); - } - } - - // M2.2 — `obj.field` where `field` is declared with `#property` - // on a foreign Obj-C class lowers as `[obj field]` (the synthesized - // getter). Receiver stays opaque — no auto-deref. - if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { - return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span); - } - - // M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class - // pointer for a plain instance field (NOT a #property) lowers as - // `object_getIvar(obj, load(___state_ivar))` + struct_gep on - // the state struct + load. The receiver is the opaque Obj-C id - // (matching Apple's `self` semantics); the state lives in the - // hidden `__sx_state` ivar. - if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { - return self.lowerObjcDefinedStateFieldRead(fa.object, info); - } - - var obj = self.lowerExpr(fa.object); - var obj_ty = self.inferExprType(fa.object); - - // Auto-deref: if the object is a pointer to a struct, load through it - if (!obj_ty.isBuiltin()) { - const ptr_info = self.module.types.get(obj_ty); - if (ptr_info == .pointer) { - const pointee = ptr_info.pointer.pointee; - obj = self.builder.load(obj, pointee); - obj_ty = pointee; - } - } - - // Special fields on slices/strings (NOT structs with .len/.ptr fields) - if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) { - // Only use length/data_ptr for slice, string, array, vector types - const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { - const info = self.module.types.get(obj_ty); - break :blk info == .slice or info == .array or info == .vector; - } else false); - - if (is_special) { - if (std.mem.eql(u8, fa.field, "len")) { - return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); - } - { - const elem_ty = self.getElementType(obj_ty); - const mp_ty = self.module.types.manyPtrTo(elem_ty); - return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty); - } - } - } - - // Optional chaining: p?.field - if (fa.is_optional) { - return self.lowerOptionalChain(obj, fa, span); - } - - return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); - } - - /// True when an `.identifier` receiver text resolves to an in-scope VALUE - /// binding rather than a builtin type. A backtick raw identifier (F0.6) can - /// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``); - /// such a value is reachable through the same three sources the ordinary - /// identifier field-access path consults (see `expr_typer` `.identifier` - /// arm): lexical `scope`, program `global_names`, and module value - /// constants `module_const_map`. The numeric-limit intercept must defer to - /// ordinary field access whenever ANY of the three binds the name, so a - /// raw value field read is never hijacked into a numeric-limit fold - /// (issues 0092 local / 0093 global + module-const). A single helper used - /// by both lowering and inference keeps the two resolvers in lockstep - /// (issue-0083 two-resolver defect class). - pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool { - if (self.scope) |scope| { - if (scope.lookup(name) != null) return true; - } - if (self.program_index.global_names.get(name) != null) return true; - if (self.program_index.module_const_map.get(name) != null) return true; - return false; - } - - /// Numeric-limit accessor intercept (`.min`/`.max`/`.epsilon`/ - /// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` / - /// `Struct.CONST` / pack-arity identifier-receiver intercepts in - /// `lowerFieldAccess`. Folds the limit to a comptime const of the queried - /// type via the shared `TypeResolver` logic (no second computor) + the - /// existing `constInt` / `constFloat` const paths: - /// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`); - /// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/ - /// `.nan` → `constFloat` (via `floatLimitFor`). - /// Returns null when the field is not a limit accessor, or the receiver is not - /// a builtin type (a user struct → ordinary field lowering reports - /// field-not-found). Two clean diagnostics (then a placeholder, so lowering - /// finishes and `hasErrors()` aborts the build): - /// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`); - /// - any accessor on a builtin NON-numeric receiver - /// (`bool`/`string`/`void`/`Any`/`noreturn`). - fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref { - const name = switch (fa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => return null, - }; - if (!TypeResolver.isLimitField(fa.field)) return null; - const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null; - - // A backtick raw identifier (F0.6) can bind a value whose spelling - // shadows a builtin type name (`` `f64 := … ``). Field access on that - // value is an ordinary field read, not a numeric-limit fold — defer to - // the normal field-access path when the receiver identifier resolves to - // a value binding through any of scope / globals / module consts - // (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type - // and can never be value-shadowed. - if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null; - - if (TypeResolver.integerLimitFor(name, fa.field)) |value| { - return self.builder.constInt(value, ty); - } - if (TypeResolver.floatLimitFor(name, fa.field)) |value| { - return self.builder.constFloat(value, ty); - } - // The field is a limit accessor, but it does not apply to this type. - if (self.diagnostics) |d| { - if (TypeResolver.integerWidthSign(name) != null) { - // Integer receiver + a float-only accessor. - d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field }); - } else { - // Non-numeric builtin receiver (bool/string/void/Any/noreturn). - d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field }); - } - } - return self.emitPlaceholder(fa.field); - } - - /// Lower a struct-level constant value (e.g., Phys.GRAVITY). - fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { - const val_node = info.value; - return switch (val_node.data) { - .int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64), - .float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64), - .bool_literal => |lit| self.builder.constBool(lit.value), - .string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)), - else => self.lowerExpr(val_node), - }; - } - - /// Lower optional chaining: `p?.field` where p is ?T - /// Produces ?FieldType: some(unwrap(p).field) if p has value, else null - /// If FieldType is already optional (?U), flattens to ?U (no double wrapping) - fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { - const obj_ty = self.inferExprType(fa.object); - // Get the inner (non-optional) type - const inner_ty = if (!obj_ty.isBuiltin()) blk: { - const info = self.module.types.get(obj_ty); - break :blk if (info == .optional) info.optional.child else obj_ty; - } else obj_ty; - - // Get the field type on the inner type - const field_ty = self.resolveFieldType(inner_ty, fa.field); - // If field is already optional, flatten (don't double-wrap) - const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false; - const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty); - - // Check if optional has value - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool); - - // Create blocks - const some_bb = self.freshBlock("chain.some"); - const none_bb = self.freshBlock("chain.none"); - const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty}); - - self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); - - // Some: unwrap, access field (already ?FieldType if flattened, else wrap) - self.builder.switchToBlock(some_bb); - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); - const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); - const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); - self.builder.br(merge_bb, &.{some_result}); - - // None: produce null optional - self.builder.switchToBlock(none_bb); - const none_result = self.builder.constNull(result_ty); - self.builder.br(merge_bb, &.{none_result}); - - // Merge - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, result_ty); - } - - /// Field access on a known type (shared by regular field access and optional chaining) - /// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour - /// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any - /// other field name so the read path (`lowerFieldAccessOnType`) and the - /// write path (`lowerAssignment`) share one resolver and reject a - /// non-lane field identically (issue 0086). - pub fn vectorLaneIndex(field: []const u8) ?u32 { - if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0; - if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1; - if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2; - if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3; - return null; - } - - fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { - const field_name_id = self.module.types.internString(field); - - // Check if it's a union type - if (!obj_ty.isBuiltin()) { - const info = self.module.types.get(obj_ty); - switch (info) { - .tagged_union => |u| { - // .tag → extract the enum tag value with the correct tag type - if (std.mem.eql(u8, field, "tag")) { - return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); - } - // Tagged union — use enum_payload - for (u.fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); - } - } - // Check promoted fields from anonymous struct variants - for (u.fields) |f| { - if (!f.ty.isBuiltin()) { - const field_info = self.module.types.get(f.ty); - if (field_info == .@"struct") { - for (field_info.@"struct".fields, 0..) |sf, si| { - if (sf.name == field_name_id) { - const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); - return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); - } - } - } - } - } - }, - .@"union" => |u| { - // Untagged union — use union_get to reinterpret bytes - for (u.fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); - } - } - // Check promoted fields from anonymous struct variants - for (u.fields) |f| { - if (!f.ty.isBuiltin()) { - const field_info = self.module.types.get(f.ty); - if (field_info == .@"struct") { - for (field_info.@"struct".fields, 0..) |sf, si| { - if (sf.name == field_name_id) { - const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); - return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); - } - } - } - } - } - }, - else => {}, - } - } - - // Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) → - // lane 0/1/2/3. Shares lane-index resolution with the write path - // (lowerAssignment) via vectorLaneIndex; a non-lane field falls - // through to the field-not-found error below. - if (!obj_ty.isBuiltin()) { - const vinfo = self.module.types.get(obj_ty); - if (vinfo == .vector) { - if (Lowering.vectorLaneIndex(field)) |vidx| { - return self.builder.structGet(obj, vidx, vinfo.vector.element); - } - } - } - - // Closure field access: .fn_ptr → field 0, .env → field 1 - if (!obj_ty.isBuiltin()) { - const cinfo = self.module.types.get(obj_ty); - if (cinfo == .closure) { - if (std.mem.eql(u8, field, "fn_ptr")) { - const fn_ptr_ty = self.module.types.ptrTo(.void); - return self.builder.structGet(obj, 0, fn_ptr_ty); - } else if (std.mem.eql(u8, field, "env")) { - const env_ty = self.module.types.ptrTo(.void); - return self.builder.structGet(obj, 1, env_ty); - } - } - } - - // Tuple field access: .0, .1, etc. or named fields - if (!obj_ty.isBuiltin()) { - const tinfo = self.module.types.get(obj_ty); - if (tinfo == .tuple) { - const tuple = tinfo.tuple; - // Try named fields first - if (tuple.names) |names| { - for (names, 0..) |name_id, i| { - if (name_id == field_name_id) { - return self.builder.structGet(obj, @intCast(i), tuple.fields[i]); - } - } - } - // Try numeric index (e.g., "0", "1") - const idx = std.fmt.parseInt(u32, field, 10) catch { - return self.emitFieldError(obj_ty, field, span); - }; - if (idx < tuple.fields.len) { - return self.builder.structGet(obj, idx, tuple.fields[idx]); - } - return self.emitFieldError(obj_ty, field, span); - } - } - - // Resolve struct field index and type - const struct_fields = self.getStructFields(obj_ty); - for (struct_fields, 0..) |f, i| { - if (f.name == field_name_id) { - return self.builder.structGet(obj, @intCast(i), f.ty); - } - } - - return self.emitFieldError(obj_ty, field, span); - } - - fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { - const target = self.target_type orelse .unresolved; - const tag = self.resolveVariantValue(target, el.name); - return self.builder.enumInit(tag, Ref.none, target); - } - - /// Lower an `error.X` tag literal to its global tag id (a `u32`). When the - /// destination context (`target_type`) is a named error set, the value is - /// typed as that set and `X`'s membership is validated; otherwise the value - /// is the raw `u32` global tag id (per the spec's context rule). - fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref { - const tag_id = self.module.types.internTag(tag_name); - if (self.target_type) |t| { - if (!t.isBuiltin()) { - const info = self.module.types.get(t); - if (info == .error_set) { - // The bare-`!` inferred placeholder (reserved name "!") accepts - // any tag — its members aren't known until the whole-program SCC - // pass (E1.4) folds in every raised tag. Skip membership for it. - if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) { - var in_set = false; - for (info.error_set.tags) |member| { - if (member == tag_id) { - in_set = true; - break; - } - } - if (!in_set) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) }); - } - } - } - return self.builder.constInt(@as(i64, @intCast(tag_id)), t); - } - } - } - return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32); - } - - /// Lower a tagged enum construction: .Variant.{ field_inits } - /// The struct literal provides the payload fields; we wrap them in an enum_init. - fn lowerTaggedEnumLiteral( - self: *Lowering, - sl: *const ast.StructLiteral, - variant_name: []const u8, - union_ty: TypeId, - union_info: types.TypeInfo.TaggedUnionInfo, - span: ast.Span, - ) Ref { - if (self.findTaggedVariant(union_info, variant_name) == null) { - self.emitBadVariant(union_ty, union_info, variant_name, span); - return self.builder.enumInit(0, Ref.none, union_ty); - } - - const tag = self.resolveVariantValue(union_ty, variant_name); - const name_id = self.module.types.internString(variant_name); - - // Find the payload type for this variant - var payload_ty: TypeId = .void; - for (union_info.fields) |f| { - if (f.name == name_id) { - payload_ty = f.ty; - break; - } - } - - if (payload_ty == .void or sl.field_inits.len == 0) { - // No payload or no fields — just tag - return self.builder.enumInit(tag, Ref.none, union_ty); - } - - // Lower the payload as a struct init of the payload type - const saved_tt = self.target_type; - self.target_type = payload_ty; - const payload_fields = self.getStructFields(payload_ty); - - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - - for (sl.field_inits, 0..) |fi, i| { - if (i < payload_fields.len) { - const saved_inner = self.target_type; - self.target_type = payload_fields[i].ty; - var val = self.lowerExpr(fi.value); - self.target_type = saved_inner; - const src_ty = self.inferExprType(fi.value); - val = self.coerceToType(val, src_ty, payload_fields[i].ty); - fields.append(self.alloc, val) catch unreachable; - } else { - fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable; - } - } - - // Pad missing payload fields with zeroes - if (fields.items.len < payload_fields.len) { - for (payload_fields[fields.items.len..]) |sf| { - fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; - } - } - - const payload = self.builder.structInit(fields.items, payload_ty); - self.target_type = saved_tt; - - return self.builder.enumInit(tag, payload, union_ty); - } - - fn findTaggedVariant( - self: *Lowering, - union_info: types.TypeInfo.TaggedUnionInfo, - variant_name: []const u8, - ) ?usize { - const name_id = self.module.types.internString(variant_name); - for (union_info.fields, 0..) |f, i| { - if (f.name == name_id) return i; - } - return null; - } - - fn emitBadVariant( - self: *Lowering, - union_ty: TypeId, - union_info: types.TypeInfo.TaggedUnionInfo, - variant_name: []const u8, - span: ast.Span, - ) void { - const diags = self.diagnostics orelse return; - const ty_name = self.formatTypeName(union_ty); - var list: std.ArrayList(u8) = .empty; - for (union_info.fields, 0..) |f, i| { - if (i > 0) list.appendSlice(self.alloc, ", ") catch return; - list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; - } - diags.addFmt( - .err, - span, - "'{s}' is not a variant of '{s}' (variants are: {s})", - .{ variant_name, ty_name, list.items }, - ); - } - - /// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). - fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { - if (ty.isBuiltin()) return 0; - const info = self.module.types.get(ty); - const name_id = self.module.types.internString(variant_name); - switch (info) { - .@"enum" => |e| { - for (e.variants, 0..) |v, i| { - if (v == name_id) { - if (e.explicit_values) |vals| { - if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); - } - return @intCast(i); - } - } - }, - .tagged_union => |u| { - for (u.fields, 0..) |f, i| { - if (f.name == name_id) { - if (u.explicit_tag_values) |vals| { - if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); - } - return @intCast(i); - } - } - }, - else => {}, - } - return 0; - } - - /// Resolve a variant name to its tag index within an enum or union type. - pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { - if (ty.isBuiltin()) return 0; - const info = self.module.types.get(ty); - const name_id = self.module.types.internString(variant_name); - switch (info) { - .tagged_union => |u| { - for (u.fields, 0..) |f, i| { - if (f.name == name_id) return @intCast(i); - } - }, - .@"enum" => |e| { - for (e.variants, 0..) |v, i| { - if (v == name_id) return @intCast(i); - } - }, - else => {}, - } - return 0; - } - - fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref { - var elems = std.ArrayList(Ref).empty; - defer elems.deinit(self.alloc); - - // Determine element type: explicit type_expr > target_type > inference - var elem_ty: TypeId = .unresolved; - var from_target = false; - var is_vector = false; - - // First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3]) - if (al.type_expr) |te| { - const resolved = self.resolveArrayLiteralType(te); - if (resolved != .unresolved) { - if (!resolved.isBuiltin()) { - const info = self.module.types.get(resolved); - switch (info) { - .array => |a| { - elem_ty = a.element; - from_target = true; - }, - .vector => |v| { - elem_ty = v.element; - from_target = true; - is_vector = true; - }, - .slice => |s| { - elem_ty = s.element; - from_target = true; - }, - else => {}, - } - } - } - } - - if (!from_target) { - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const info = self.module.types.get(tt); - switch (info) { - .array => |a| { - elem_ty = a.element; - from_target = true; - }, - .slice => |s| { - elem_ty = s.element; - from_target = true; - }, - .vector => |v| { - elem_ty = v.element; - from_target = true; - is_vector = true; - }, - else => {}, - } - } - } - } - if (!from_target and al.elements.len > 0) { - const inferred = self.inferExprType(al.elements[0]); - if (inferred != .void) elem_ty = inferred; - } - - for (al.elements) |elem| { - const old_tt = self.target_type; - self.target_type = elem_ty; - var val = self.lowerExpr(elem); - self.target_type = old_tt; - // A nested `.[...]` element at a slice element type lowers to an - // aggregate array `[N]U` (lowerArrayLiteral always yields an array - // value); materialize it into a `[]U` slice so the element is a real - // {ptr,len} header rather than a raw array the callee would read its - // header off of (issue 0085). This per-element coercion recurses with - // the literal nesting, so `[][]T` and deeper coerce at every level. - if (!elem_ty.isBuiltin()) { - const ei = self.module.types.get(elem_ty); - if (ei == .slice) { - const val_ty = self.builder.getRefType(val); - if (!val_ty.isBuiltin()) { - const vi = self.module.types.get(val_ty); - if (vi == .array and vi.array.element == ei.slice.element) { - val = self.coerceToType(val, val_ty, elem_ty); - } - } - } - } - elems.append(self.alloc, val) catch unreachable; - } - - const result_ty = if (is_vector) - self.module.types.vectorOf(elem_ty, @intCast(al.elements.len)) - else - self.module.types.arrayOf(elem_ty, @intCast(al.elements.len)); - return self.builder.structInit(elems.items, result_ty); - } - - /// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]). - /// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr. - fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { - switch (te.data) { - .call => |cl| { - // Vector(3, f32) or Module.Vector(3, f32) - const callee_name = switch (cl.callee.data) { - .identifier => |id| id.name, - .field_access => |fa| fa.field, - else => return .unresolved, - }; - if (std.mem.eql(u8, callee_name, "Vector")) { - if (cl.args.len == 2) { - const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; - const elem = self.resolveTypeWithBindings(cl.args[1]); - return self.module.types.vectorOf(elem, length); - } - } - // Generic-struct typed-literal head (`Box(s64).[...]`): route - // through the single layout choke-point (CP-1). A qualified head - // `a.Box(s64).[...]` selects a's OWN template via the namespace edge - // (Counter-1: was the global last-wins map); a bare head selects the - // single bare-VISIBLE author. - if (headNameOfCallee(cl.callee)) |hn| { - switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { - .template => |t| return self.instantiateGenericStruct(&t, cl.args), - .poisoned => return .unresolved, - .not_generic => {}, - } - } - return .unresolved; - }, - .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), - .identifier => |id| { - // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type - // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is - // not bare-visible (consistent with annotations / 0763); ≥2 direct - // flat same-name authors are ambiguous (loud diagnostic, consistent - // with the leaf / 0755); a single source-keyed author resolves to - // ITS TypeId instead of a global `findByName` first-/last-wins pick. - switch (self.headTypeGate(id.name, te.span)) { - .ambiguous, .not_visible => return .unresolved, - .resolved => |tid| return tid, - .proceed => {}, - } - const name_id = self.module.types.internString(id.name); - return self.module.types.findByName(name_id) orelse .unresolved; - }, - .type_expr => |inner| { - if (self.headTypeLeak(inner.name, te.span)) return .unresolved; - return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - }, - .field_access => |fa| { - // Module.Type — try to resolve the field as a type name - const name_id = self.module.types.internString(fa.field); - return self.module.types.findByName(name_id) orelse .unresolved; - }, - else => return .unresolved, - } - } - - fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { - // Pack-arg substitution: `args[]` inside a body - // whose enclosing comptime call bound `args` as a pack name. - // Lowering the i-th call-site arg directly gives the concrete - // call-arg type — bypasses the `[]Any` slice boxing that would - // otherwise lose the type. Non-literal indices fall through to - // the standard slice indexing path. - if (self.packArgNodeAt(ie)) |arg_node| { - return self.lowerExpr(arg_node); - } - // Out-of-bounds pack indexing: object IS a pack name + index - // IS a comptime int literal but exceeds the pack arity. Emit - // a focused diagnostic so the user gets "pack index 2 out of - // bounds" instead of the generic "unresolved 'args'" that the - // fall-through scope-lookup would produce. - if (self.diagPackIndexOOB(ie)) { - return self.builder.constInt(0, .s64); - } - // Runtime index into a comptime-only pack (Decision 1): a pack has no - // runtime representation, so the index must be a compile-time constant. - // A runtime index is a hard error — clearer than the "unresolved - // ''" the slice-index fall-through would otherwise produce. - if (self.pack_param_count) |ppc| { - if (ie.object.data == .identifier) { - const pname = ie.object.data.identifier.name; - if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname}); - } - return self.builder.constInt(0, .s64); - } - } - } - const obj = self.lowerExpr(ie.object); - const idx = self.lowerExpr(ie.index); - // Infer element type from the object's slice/array type - const obj_ty = self.inferExprType(ie.object); - const elem_ty = self.getElementType(obj_ty); - return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); - } - - fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { - const obj = self.lowerExpr(se.object); - const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); - const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); - // Infer result slice type from the object - const obj_ty = self.inferExprType(se.object); - // Subslice of string stays string (same {ptr, i64} layout, correct type category) - if (obj_ty == .string) { - return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); - } - const elem_ty = self.getElementType(obj_ty); - const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); - return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); - } - - fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { - var elems = std.ArrayList(Ref).empty; - defer elems.deinit(self.alloc); - var field_type_ids = std.ArrayList(TypeId).empty; - defer field_type_ids.deinit(self.alloc); - var name_ids = std.ArrayList(types.StringId).empty; - defer name_ids.deinit(self.alloc); - var has_names = false; - - // A tuple_init's element values must match its field types exactly - // (LLVM `insertvalue` does no implicit conversion). When a contextual - // target tuple of matching arity is in scope (annotation, assignment - // LHS, call/return slot), its field types drive element lowering so an - // ambient scalar `target_type` (e.g. the enclosing fn's int return - // type) can't narrow an element below its field width. Otherwise each - // element's type is inferred independently. - // A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields, - // so element-count ≠ field-count and a contextual target tuple can't be - // aligned by index — infer field types from the expanded refs instead. - var has_spread = false; - for (tl.elements) |elem| { - if (elem.value.data == .spread_expr) has_spread = true; - } - - // Contextual target tuple field types. Without a spread we require - // exact arity (existing behavior); with a spread we index positionally - // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces - // / erases each spliced element to its slot's type). - var target_fields: ?[]const TypeId = null; - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const tinfo = self.module.types.get(tt); - if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) { - target_fields = tinfo.tuple.fields; - } - } - } - - const saved_target = self.target_type; - var out_idx: usize = 0; - for (tl.elements) |elem| { - // Pack-spread element → splice its per-element values as fields. - if (elem.value.data == .spread_expr) { - const sp_operand = elem.value.data.spread_expr.operand; - if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| { - defer self.alloc.free(refs); - // Element AST nodes (for protocol-erasure lvalue/name fallback) - // when the spread is a bare pack name. - const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null) - self.pack_arg_nodes.?.get(sp_operand.data.identifier.name) - else - null; - for (refs, 0..) |r, ri| { - var val = r; - var vty = self.builder.getRefType(r); - if (target_fields) |tf| { - if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) { - const want = tf[out_idx]; - const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value; - val = self.coerceOrErase(r, vty, want, node); - vty = want; - } - } - elems.append(self.alloc, val) catch unreachable; - field_type_ids.append(self.alloc, vty) catch unreachable; - name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; - out_idx += 1; - } - continue; - } - // Not a pack spread (e.g. tuple-value spread) — not yet handled. - _ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic - continue; - } - const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value); - self.target_type = field_ty; - var val = self.lowerExpr(elem.value); - self.target_type = saved_target; - const val_ty = self.builder.getRefType(val); - if (val_ty != field_ty and val_ty != .void) { - val = self.coerceToType(val, val_ty, field_ty); - } - elems.append(self.alloc, val) catch unreachable; - field_type_ids.append(self.alloc, field_ty) catch unreachable; - if (elem.name) |name| { - name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable; - has_names = true; - } else { - name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; - } - out_idx += 1; - } - - // Reuse the contextual target tuple type when it drove lowering so the - // value's type identity (incl. field names) matches the destination - // slot; otherwise build the tuple type from the inferred fields. - const tuple_ty = if (target_fields != null and self.target_type != null) - self.target_type.? - else - self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable, - .names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null, - } }); - - const owned = self.alloc.dupe(Ref, elems.items) catch unreachable; - return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); - } - - fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref { - const ptr = self.lowerExpr(de.operand); - // Resolve pointee type from the pointer type. - const ptr_ty = self.inferExprType(de.operand); - if (!ptr_ty.isBuiltin()) { - const info = self.module.types.get(ptr_ty); - if (info == .pointer) { - return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee); - } - } - // Operand isn't a pointer — `.*` is invalid. Diagnose here instead of - // emitting a `.deref` with an `.unresolved` result type, which would - // otherwise slip through to emit_llvm's "unresolved type reached LLVM - // emission" panic with no source location. - if (self.diagnostics) |d| { - d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)}); - } - return ptr; - } - - fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref { - const val = self.lowerExpr(fu.operand); - const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand)); - return self.builder.optionalUnwrap(val, inner_ty); - } - - fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { - const lhs = self.lowerExpr(nc.lhs); - const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); - - // Short-circuit: only evaluate RHS if LHS is null. - // IMPORTANT: optional_unwrap must be in the "has value" branch, - // not before the condBr — the interpreter errors on unwrapping null. - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); - - const then_bb = self.freshBlock("nc.has"); - const rhs_bb = self.freshBlock("nc.rhs"); - const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); - - // If has value, go to then_bb to unwrap; else go to rhs_bb - self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); - - // Then block: unwrap LHS and branch to merge - self.builder.switchToBlock(then_bb); - const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); - self.builder.br(merge_bb, &.{unwrapped}); - - // RHS block: evaluate fallback and branch to merge - self.builder.switchToBlock(rhs_bb); - var rhs = self.lowerExpr(nc.rhs); - const rhs_ty = self.builder.getRefType(rhs); - if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { - rhs = self.coerceToType(rhs, rhs_ty, inner_ty); - } - self.builder.br(merge_bb, &.{rhs}); - - // Continue at merge - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, inner_ty); - } - - fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .optional) return info.optional.child; - } - return .unresolved; - } - - // ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ - - fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { - // Lower the lambda body as a new anonymous function - var buf: [64]u8 = undefined; - const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda"; - self.block_counter += 1; - - // Collect lambda param names for exclusion from captures - var param_names = std.StringHashMap(void).init(self.alloc); - defer param_names.deinit(); - for (lam.params) |p| { - param_names.put(p.name, {}) catch {}; - } - - // Pre-scan lambda body AST for free variables (captures) - var captures = std.ArrayList(CaptureInfo).empty; - defer captures.deinit(self.alloc); - self.collectCaptures(lam.body, ¶m_names, &captures); - - // Deduplicate captures - var seen = std.StringHashMap(void).init(self.alloc); - defer seen.deinit(); - var deduped = std.ArrayList(CaptureInfo).empty; - defer deduped.deinit(self.alloc); - for (captures.items) |cap| { - if (!seen.contains(cap.name)) { - seen.put(cap.name, {}) catch {}; - deduped.append(self.alloc, cap) catch {}; - } - } - const capture_list = deduped.items; - - // Build env struct type if there are captures - var env_struct_ty: TypeId = .void; - if (capture_list.len > 0) { - const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable; - for (capture_list, 0..) |cap, i| { - var nbuf: [32]u8 = undefined; - const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap"; - env_field_data[i] = .{ - .name = self.module.types.internString(fname), - .ty = cap.ty, - }; - } - const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env"; - const env_name_id = self.module.types.internString(env_name); - env_struct_ty = self.module.types.intern(.{ .@"struct" = .{ - .name = env_name_id, - .fields = env_field_data, - } }); - } - - // Save current builder state - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - const saved_scope = self.scope; - - // Build param list. Convention when implicit_ctx is enabled: - // slot 0 = __sx_ctx: *void - // slot 1 = env: *void - // slot 2+ = user params - // Without implicit_ctx, env is slot 0 and user params follow. - var params = std.ArrayList(Function.Param).empty; - const env_ptr_ty = self.module.types.ptrTo(.void); - const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c; - if (lambda_wants_ctx) { - params.append(self.alloc, .{ - .name = self.module.types.internString("__sx_ctx"), - .ty = env_ptr_ty, - }) catch unreachable; - } - params.append(self.alloc, .{ - .name = self.module.types.internString("env"), - .ty = env_ptr_ty, - }) catch unreachable; - // Get target closure param types for inference (from Closure(T1, T2) -> R annotations) - const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: { - if (!tt.isBuiltin()) { - const tti = self.module.types.get(tt); - if (tti == .closure) break :blk tti.closure.params; - // Unwrap ?Closure(...) → Closure(...) - if (tti == .optional) { - const inner = tti.optional.child; - if (!inner.isBuiltin()) { - const inner_info = self.module.types.get(inner); - if (inner_info == .closure) break :blk inner_info.closure.params; - } - } - } - break :blk null; - } else null; - // User params follow the ctx (optional) + env slots in `params`. - const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1; - for (lam.params, 0..) |p, pi| { - const pty: TypeId = blk: { - // Unannotated lambda params take their type positionally from - // the target `Closure(T0, …)` signature. Resolve them here so - // `resolveParamType` (which would diagnose a missing annotation) - // is only called for params that carry one. - if (p.type_expr.data == .inferred_type) { - if (target_closure_params != null and pi < target_closure_params.?.len) { - break :blk target_closure_params.?[pi]; - } - if (self.diagnostics) |d| { - d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name}); - } - break :blk .unresolved; - } - break :blk self.resolveParamType(&p); - }; - params.append(self.alloc, .{ - .name = self.module.types.internString(p.name), - .ty = pty, - }) catch unreachable; - } - - const ret_ty = blk: { - if (lam.return_type) |rt| { - break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); - } - // Use target closure return type if available — but only when it's - // a resolved type. An `.unresolved` ret comes from an unbound - // generic (`Closure(..) -> $R`); fall through to infer it from the - // body so the concrete return drives `$R` inference at the call site. - if (self.target_type) |tt| { - if (!tt.isBuiltin()) { - const tti = self.module.types.get(tt); - if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret; - // Unwrap ?Closure(...) → Closure(...) - if (tti == .optional) { - const inner = tti.optional.child; - if (!inner.isBuiltin()) { - const inner_info = self.module.types.get(inner); - if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret; - } - } - } - } - // Arrow lambda without explicit return type — infer from body expression - // Temporarily bind params in scope so inferExprType can resolve param types - var temp_scope = Scope.init(self.alloc, self.scope); - const saved = self.scope; - self.scope = &temp_scope; - for (lam.params, 0..) |p, i| { - const pty = params.items[user_param_base + i].ty; - temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); - } - const inferred = self.inferExprType(lam.body); - self.scope = saved; - temp_scope.deinit(); - break :blk inferred; - }; - const name_id = self.module.types.internString(name); - const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); - if (lam.call_conv == .c) { - self.module.getFunctionMut(func_id).call_conv = .c; - } - self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx; - - // Param-slot layout: ctx at 0 (if present), env at ctx_slots, - // user args at ctx_slots+1. - const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0; - const env_param_idx: u32 = lambda_ctx_slots; - const user_param_base_lam: u32 = lambda_ctx_slots + 1; - - // Save + rebind current_ctx_ref so the body's sx-to-sx calls - // forward the trampoline's own ctx (slot 0). - const saved_ctx_ref_lam = self.current_ctx_ref; - defer self.current_ctx_ref = saved_ctx_ref_lam; - if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - - // A lambda is its own function: its `return` must drain only ITS OWN - // `defer`s, not the enclosing function's. Open a fresh defer window - // (like `lowerFunction`/`monomorphizeFunction`) and restore on exit — - // otherwise lowering a closure literal inside a `defer` body re-enters - // the enclosing function's defer drain (infinite recursion — issue 0073). - const saved_func_defer_base = self.func_defer_base; - const saved_defer_len = self.defer_stack.items.len; - defer { - self.func_defer_base = saved_func_defer_base; - self.defer_stack.shrinkRetainingCapacity(saved_defer_len); - } - self.func_defer_base = saved_defer_len; - - // Create entry block - const entry_name = self.module.types.internString("entry"); - const entry = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry); - - // Create scope WITHOUT parent — captures are bound from env, not parent scope - var lambda_scope = Scope.init(self.alloc, null); - self.scope = &lambda_scope; - - // Bind captures from env struct (at env_param_idx) - if (capture_list.len > 0) { - const env_param_ref = Ref.fromIndex(env_param_idx); - // Alloca env struct locally so struct_gep can resolve the type - const env_local = self.builder.alloca(env_struct_ty); - // Compute env size - const env_byte_size_inner = self.computeEnvSize(capture_list); - const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64); - // memcpy(local_alloca, env_param, size) - _ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void)); - - for (capture_list, 0..) |cap, i| { - // GEP into env struct to get field pointer - const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); - // Load the captured value into a local alloca - const loaded = self.builder.load(field_ptr, cap.ty); - const slot = self.builder.alloca(cap.ty); - self.builder.store(slot, loaded); - lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true }); - } - } - - // Also need parent scope for function lookups (but not variable lookups) - // Set up fn_names from parent scope chain - { - var s: ?*Scope = saved_scope; - while (s) |scope| { - var it = scope.fn_names.iterator(); - while (it.next()) |e| { - if (!lambda_scope.fn_names.contains(e.key_ptr.*)) { - lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {}; - } - } - s = scope.parent; - } - } - - // Bind params (user args start at user_param_base_lam, shifted past ctx + env). - // Use the signature types computed above (`params`), which already - // applied contextual typing from the target closure to untyped params — - // `resolveParamType` alone would drop it and default each to s64. - for (lam.params, 0..) |p, i| { - const pty = params.items[user_param_base + i].ty; - const slot = self.builder.alloca(pty); - const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i))); - self.builder.store(slot, param_ref); - lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); - } - - // Lower body — capture last expression as return value. The - // `in_lambda_body` flag scopes the lambda-specific `raise`-not-failable - // hint; save/restore so a lambda nested inside a regular function (or a - // lambda inside a lambda) restores the enclosing context. - const saved_in_lambda = self.in_lambda_body; - self.in_lambda_body = true; - if (ret_ty != .void) { - if (self.lowerBlockValue(lam.body)) |val| { - if (!self.currentBlockHasTerminator()) { - const val_ty = self.builder.getRefType(val); - // A value-carrying failable arrow lambda (`-> (T, !) => expr`) - // yields the bare success value; the compiler appends the - // no-error slot (0) — same as a `return v` in a block body. - if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) { - self.lowerFailableSuccessReturn(val, ret_ty, lam.body.span); - } else { - const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; - self.builder.ret(coerced, ret_ty); - } - } - } - } else { - self.lowerBlock(lam.body); - } - self.in_lambda_body = saved_in_lambda; - self.ensureTerminator(ret_ty); - self.builder.finalize(); - - // Restore builder state - self.scope = saved_scope; - lambda_scope.deinit(); - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - // Restore the caller's `current_ctx_ref` BEFORE we emit the env - // alloc/memcpy below — those run in the caller's scope, and - // `allocViaContext` reads `current_ctx_ref` to find the - // installed allocator. Without this, the env_heap dispatch - // would still see `Ref.fromIndex(0)` (the lambda's own ctx - // param), which doesn't exist in the caller's frame and - // silently routes through the default context instead of any - // surrounding `push Context.{ allocator = ... }`. - self.current_ctx_ref = saved_ctx_ref_lam; - - // Closure flowing into a BARE function-pointer slot (`(T) -> U`, no env): - // the slot is called without the closure env arg, so the closure fn can't - // be passed directly. For a capture-free closure whose return type matches - // the slot, emit an adapter with the bare ABI. Reject the cases the bare - // ABI can't represent: a capturing closure (env has nowhere to live), and - // a failable closure into a non-failable slot (foreign code can't observe - // the error channel — ERR E5.1 FFI-boundary rule). - if (self.target_type) |tt| { - if (!tt.isBuiltin() and self.module.types.get(tt) == .function) { - const slot_ret = self.module.types.get(tt).function.ret; - const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty; - if (capture_list.len > 0) { - if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{}); - } else if (ret_ty == slot_ret or widen_ok) { - // Matching ABI, or a non-failable closure widening into a - // failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}. - const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span); - return self.builder.emit(.{ .func_ref = adapter }, tt); - } else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) { - if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{}); - } else if (self.diagnostics) |d| { - d.addFmt(.err, lam.body.span, "closure return type does not match the function-type slot", .{}); - } - } - } - - // Create proper closure type (user-visible params only — skip ctx + env). - const skip_count: usize = if (lambda_wants_ctx) 2 else 1; - var param_types_list = std.ArrayList(TypeId).empty; - for (params.items[skip_count..]) |p| { - param_types_list.append(self.alloc, p.ty) catch unreachable; - } - const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty); - - // Build env and closure in the caller's scope - if (capture_list.len > 0) { - // Alloca env struct on stack (so struct_gep can resolve the type) - const env_local = self.builder.alloca(env_struct_ty); - - // Store captured values into env struct fields - for (capture_list, 0..) |cap, i| { - const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); - const val = if (cap.is_alloca) - self.builder.load(cap.ref, cap.ty) - else - cap.ref; - self.builder.store(gep, val); - } - - // Copy env to heap (so it outlives the stack frame). - // Route through `context.allocator.alloc` rather than calling - // libc malloc directly so closures respect a surrounding - // `push Context.{ allocator = ... }` and a tracker / arena - // counts the env allocation alongside everything else. - const env_byte_size = self.computeEnvSize(capture_list); - const env_size = self.builder.constInt(@intCast(env_byte_size), .s64); - const ptr_void = self.module.types.ptrTo(.void); - const env_heap = self.allocViaContext(env_size, ptr_void); - // memcpy(heap, stack_alloca, size) - _ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void); - - return self.builder.closureCreate(func_id, env_heap, closure_ty); - } else { - return self.builder.closureCreate(func_id, Ref.none, closure_ty); - } - } - - /// Create a trampoline function that wraps a bare function for closure auto-promotion. - /// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the - /// bare function with `(args...)`, ignoring the env parameter. - pub fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId { - // Build trampoline params: [__sx_ctx]? + env + closure params. - // When the program uses Context, every sx-side trampoline carries - // the implicit ctx at slot 0 and forwards it to the wrapped - // function (which is also sx-side and expects it at slot 0). - var params = std.ArrayList(inst_mod.Function.Param).empty; - defer params.deinit(self.alloc); - const void_ptr_ty = self.module.types.ptrTo(.void); - const wants_ctx = self.implicit_ctx_enabled; - if (wants_ctx) { - params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; - } - const env_name = self.module.types.internString("env"); - params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable; - for (closure_info.params, 0..) |pty, i| { - var buf: [32]u8 = undefined; - const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; - params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; - } - - // Generate unique trampoline name - const bare_func = self.module.functions.items[bare_func_id.index()]; - const bare_name = self.module.types.getString(bare_func.name); - var name_buf: [128]u8 = undefined; - const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp"; - const tramp_name_id = self.module.types.internString(tramp_name); - - // Save builder state - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - - // Create function - const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; - var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret); - func.has_implicit_ctx = wants_ctx; - const func_id = self.module.addFunction(func); - self.builder.func = func_id; - self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1 - const entry_name = self.module.types.internString("entry"); - const entry_block = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry_block); - - // Build call args: forward [__sx_ctx]? + user_params (skip env). - // Trampoline slots: 0=ctx (if present), {0|1}=env, then user args. - const ctx_slots: usize = if (wants_ctx) 1 else 0; - const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env - var call_args = std.ArrayList(Ref).empty; - defer call_args.deinit(self.alloc); - if (wants_ctx and bare_func.has_implicit_ctx) { - call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx - } - for (closure_info.params, 0..) |_, i| { - call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable; - } - const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; - const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret); - - // Return result (or void) - if (closure_info.ret != .void) { - self.builder.ret(result, closure_info.ret); - } else { - self.builder.retVoid(); - } - self.builder.finalize(); - - // Restore builder state - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - - return func_id; - } - - /// Adapter for coercing a closure into a BARE function-pointer slot - /// (`(T) -> U`, no env). The closure's underlying function has signature - /// `[ctx?] + env + user-params`, but a bare fn-ptr slot is *called* without - /// the env arg — so the closure fn can't be used directly (the env slot - /// would swallow the first user arg). This adapter carries the bare ABI - /// (`[ctx?] + user-params`) and forwards to the closure fn with a null env. - /// Only sound for capture-free closures (a null env is correct iff the body - /// reads no captures); the caller rejects capturing closures. - /// - /// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening - /// case (a non-failable closure into a failable slot): the closure returns - /// the success value and the adapter wraps it into the slot's `{value, 0}` - /// failable tuple (ERR E5.1 non-failable→failable widening). - fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId { - var params = std.ArrayList(inst_mod.Function.Param).empty; - defer params.deinit(self.alloc); - const void_ptr_ty = self.module.types.ptrTo(.void); - const wants_ctx = self.implicit_ctx_enabled; - if (wants_ctx) { - params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; - } - for (fn_info.params, 0..) |pty, i| { - var buf: [32]u8 = undefined; - const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; - params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; - } - - const closure_func = self.module.functions.items[closure_func_id.index()]; - const closure_name = self.module.types.getString(closure_func.name); - var name_buf: [128]u8 = undefined; - const adapter_name = std.fmt.bufPrint(&name_buf, "__cl2fn_{s}", .{closure_name}) catch "__cl2fn"; - const adapter_name_id = self.module.types.internString(adapter_name); - - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - - const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; - var func = inst_mod.Function.init(adapter_name_id, owned_params, fn_info.ret); - func.has_implicit_ctx = wants_ctx; - const func_id = self.module.addFunction(func); - self.builder.func = func_id; - self.builder.inst_counter = @intCast(owned_params.len); - const entry_name = self.module.types.internString("entry"); - const entry_block = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry_block); - - // Forward [ctx?] + null env + user params to the closure fn. - const ctx_slots: usize = if (wants_ctx) 1 else 0; - var call_args = std.ArrayList(Ref).empty; - defer call_args.deinit(self.alloc); - if (wants_ctx) call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; - call_args.append(self.alloc, self.builder.constNull(void_ptr_ty)) catch unreachable; - for (fn_info.params, 0..) |_, i| { - call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable; - } - const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; - const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret); - if (closure_ret == fn_info.ret) { - if (fn_info.ret != .void) { - self.builder.ret(result, fn_info.ret); - } else { - self.builder.retVoid(); - } - } else { - // ∅-widening: closure returns the success value; wrap `{value, 0}` - // into the slot's failable tuple. - self.lowerFailableSuccessReturn(result, fn_info.ret, span); - } - self.builder.finalize(); - - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - return func_id; - } - - /// Walk an AST node and collect free variable references (identifiers that are - /// in the current scope but not in lambda params). - fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void { - switch (node.data) { - .identifier => |id| { - // Skip lambda params - if (param_names.contains(id.name)) return; - // Skip function names - if (self.program_index.fn_ast_map.contains(id.name)) return; - // Skip type names - if (self.program_index.struct_template_map.contains(id.name)) return; - // Check if it's a variable in the parent scope - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - captures.append(self.alloc, .{ - .name = id.name, - .ty = binding.ty, - .ref = binding.ref, - .is_alloca = binding.is_alloca, - }) catch {}; - } - } - }, - .binary_op => |bo| { - self.collectCaptures(bo.lhs, param_names, captures); - self.collectCaptures(bo.rhs, param_names, captures); - }, - .unary_op => |uo| { - self.collectCaptures(uo.operand, param_names, captures); - }, - .call => |cl| { - self.collectCaptures(cl.callee, param_names, captures); - for (cl.args) |arg| { - self.collectCaptures(arg, param_names, captures); - } - }, - .block => |blk| { - for (blk.stmts) |stmt| { - self.collectCaptures(stmt, param_names, captures); - } - }, - .if_expr => |ie| { - self.collectCaptures(ie.condition, param_names, captures); - self.collectCaptures(ie.then_branch, param_names, captures); - if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures); - }, - .while_expr => |we| { - self.collectCaptures(we.condition, param_names, captures); - self.collectCaptures(we.body, param_names, captures); - }, - .return_stmt => |rs| { - if (rs.value) |v| self.collectCaptures(v, param_names, captures); - }, - .var_decl => |vd| { - if (vd.value) |v| self.collectCaptures(v, param_names, captures); - // Register the local var name so it's not captured - param_names.put(vd.name, {}) catch {}; - }, - .const_decl => |cd| { - self.collectCaptures(cd.value, param_names, captures); - param_names.put(cd.name, {}) catch {}; - }, - .assignment => |a| { - self.collectCaptures(a.target, param_names, captures); - self.collectCaptures(a.value, param_names, captures); - }, - .destructure_decl => |dd| { - self.collectCaptures(dd.value, param_names, captures); - for (dd.names) |name| { - param_names.put(name, {}) catch {}; - } - }, - .field_access => |fa| { - self.collectCaptures(fa.object, param_names, captures); - }, - .index_expr => |ie| { - self.collectCaptures(ie.object, param_names, captures); - self.collectCaptures(ie.index, param_names, captures); - }, - .struct_literal => |sl| { - for (sl.field_inits) |fi| { - self.collectCaptures(fi.value, param_names, captures); - } - }, - .array_literal => |al| { - for (al.elements) |elem| { - self.collectCaptures(elem, param_names, captures); - } - }, - .lambda => |inner_lam| { - // For nested lambdas, the inner lambda captures from our scope too - // But its own params should be excluded - var inner_params = std.StringHashMap(void).init(self.alloc); - defer inner_params.deinit(); - // Copy current param_names - var it = param_names.iterator(); - while (it.next()) |e| { - inner_params.put(e.key_ptr.*, {}) catch {}; - } - for (inner_lam.params) |p| { - inner_params.put(p.name, {}) catch {}; - } - self.collectCaptures(inner_lam.body, &inner_params, captures); - }, - .match_expr => |me| { - self.collectCaptures(me.subject, param_names, captures); - for (me.arms) |arm| { - self.collectCaptures(arm.body, param_names, captures); - } - }, - .null_coalesce => |nc| { - self.collectCaptures(nc.lhs, param_names, captures); - self.collectCaptures(nc.rhs, param_names, captures); - }, - .deref_expr => |de| { - self.collectCaptures(de.operand, param_names, captures); - }, - .for_expr => |fe| { - self.collectCaptures(fe.iterable, param_names, captures); - // Register capture name as local so it's not captured - param_names.put(fe.capture_name, {}) catch {}; - self.collectCaptures(fe.body, param_names, captures); - }, - .slice_expr => |se| { - self.collectCaptures(se.object, param_names, captures); - if (se.start) |s| self.collectCaptures(s, param_names, captures); - if (se.end) |e| self.collectCaptures(e, param_names, captures); - }, - .tuple_literal => |tl| { - for (tl.elements) |elem| { - self.collectCaptures(elem.value, param_names, captures); - } - }, - .force_unwrap => |fu| { - self.collectCaptures(fu.operand, param_names, captures); - }, - .chained_comparison => |cc| { - for (cc.operands) |op| { - self.collectCaptures(op, param_names, captures); - } - }, - .defer_stmt => |ds| { - self.collectCaptures(ds.expr, param_names, captures); - }, - .ffi_intrinsic_call => |fic| { - self.collectCaptures(fic.return_type, param_names, captures); - for (fic.args) |arg| { - self.collectCaptures(arg, param_names, captures); - } - }, - else => {}, - } - } - - /// Compute the byte size of the env struct based on captured value types. - fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize { - // Must match LLVM's struct layout: fields are aligned to their natural alignment - var offset: usize = 0; - var max_align: usize = 1; - for (capture_list) |cap| { - const field_size = self.typeSizeBytes(cap.ty); - const field_align = self.typeAlignBytes(cap.ty); - if (field_align > max_align) max_align = field_align; - // Align offset to field alignment - offset = (offset + field_align - 1) & ~(field_align - 1); - offset += field_size; - } - // Align total to max field alignment (matches LLVM's struct alignment) - return (offset + max_align - 1) & ~(max_align - 1); - } - /// Byte size of an IR type matching LLVM's type layout. pub fn typeSizeBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeSizeBytes(ty); } - fn typeAlignBytes(self: *Lowering, ty: TypeId) usize { + pub fn typeAlignBytes(self: *Lowering, ty: TypeId) usize { return self.module.types.typeAlignBytes(ty); } @@ -3487,44 +518,6 @@ pub const Lowering = struct { // ── Chained comparison ────────────────────────────────────────── - fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref { - // a < b < c → (a < b) and (b < c) - // Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice. - if (cc.operands.len < 2 or cc.ops.len == 0) { - return self.builder.constBool(true); - } - - var refs = std.ArrayList(Ref).empty; - defer refs.deinit(self.alloc); - for (cc.operands) |op| { - refs.append(self.alloc, self.lowerExpr(op)) catch unreachable; - } - - var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]); - - var i: usize = 1; - while (i < cc.ops.len) : (i += 1) { - const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]); - result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool); - } - - return result; - } - - fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref { - return switch (op) { - .eq => self.builder.cmpEq(lhs, rhs), - .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .lt => self.builder.cmpLt(lhs, rhs), - .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), - .gt => self.builder.cmpGt(lhs, rhs), - .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), - else => self.builder.constBool(false), - }; - } - - // ── Defer/Push/MultiAssign ────────────────────────────────────── - pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); @@ -4124,7 +1117,7 @@ pub const Lowering = struct { /// arithmetic). `.unresolved` returns true so a type we couldn't infer /// is never diagnosed — the check only fires on a concretely /// incompatible operand (e.g. `string`, a struct, an enum). - fn isArithOperand(self: *Lowering, ty: TypeId) bool { + pub fn isArithOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or isFloat(ty)) return true; if (ty.isBuiltin()) return false; @@ -4139,7 +1132,7 @@ pub const Lowering = struct { /// order), bool, and SIMD vectors. NOT strings (no lexicographic `<` /// lowering exists) or any other aggregate. `.unresolved` passes so an /// un-inferable operand is never falsely diagnosed. - fn isOrderingOperand(self: *Lowering, ty: TypeId) bool { + pub fn isOrderingOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or isFloat(ty) or ty == .bool) return true; if (ty.isBuiltin()) return false; @@ -4153,7 +1146,7 @@ pub const Lowering = struct { /// (incl. custom widths), enums (flags are int-backed), bool, and SIMD /// vectors. NOT floats, strings, pointers, or aggregates. `.unresolved` /// passes (see `isOrderingOperand`). - fn isBitwiseOperand(self: *Lowering, ty: TypeId) bool { + pub fn isBitwiseOperand(self: *Lowering, ty: TypeId) bool { if (ty == .unresolved) return true; if (isInt(ty) or ty == .bool) return true; if (ty.isBuiltin()) return false; @@ -4188,7 +1181,7 @@ pub const Lowering = struct { return "an expression of an incompatible type"; } - fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 { + pub fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 { return switch (op) { .add => "+", .sub => "-", @@ -4539,7 +1532,7 @@ pub const Lowering = struct { pub const lookupGlobalIdByName = lower_objc_class.lookupGlobalIdByName; // --- moved to lower/call.zig (lower_call) --- - pub const CaptureInfo = lower_call.CaptureInfo; + pub const CaptureInfo = lower_closure.CaptureInfo; pub const lowerCall = lower_call.lowerCall; pub const diagnoseMissingContext = lower_call.diagnoseMissingContext; pub const allocViaContext = lower_call.allocViaContext; @@ -4620,4 +1613,53 @@ pub const Lowering = struct { pub const canonicalIntConstraintName = lower_generic.canonicalIntConstraintName; pub const diagValueParamNotConst = lower_generic.diagValueParamNotConst; pub const diagValueParamRange = lower_generic.diagValueParamRange; + + // --- moved to lower/expr.zig (lower_expr) --- + pub const lowerStructLiteral = lower_expr.lowerStructLiteral; + pub const lowerInitBlock = lower_expr.lowerInitBlock; + pub const getStructFields = lower_expr.getStructFields; + pub const fixupMethodReceiver = lower_expr.fixupMethodReceiver; + pub const getStructTypeName = lower_expr.getStructTypeName; + pub const builtinTypeName = lower_expr.builtinTypeName; + pub const resolveFieldType = lower_expr.resolveFieldType; + pub const lowerFieldAccess = lower_expr.lowerFieldAccess; + pub const identifierBindsValue = lower_expr.identifierBindsValue; + pub const lowerNumericLimit = lower_expr.lowerNumericLimit; + pub const lowerStructConstant = lower_expr.lowerStructConstant; + pub const lowerOptionalChain = lower_expr.lowerOptionalChain; + pub const vectorLaneIndex = lower_expr.vectorLaneIndex; + pub const lowerFieldAccessOnType = lower_expr.lowerFieldAccessOnType; + pub const lowerEnumLiteral = lower_expr.lowerEnumLiteral; + pub const lowerErrorTagLiteral = lower_expr.lowerErrorTagLiteral; + pub const lowerTaggedEnumLiteral = lower_expr.lowerTaggedEnumLiteral; + pub const findTaggedVariant = lower_expr.findTaggedVariant; + pub const emitBadVariant = lower_expr.emitBadVariant; + pub const resolveVariantValue = lower_expr.resolveVariantValue; + pub const resolveVariantIndex = lower_expr.resolveVariantIndex; + pub const lowerArrayLiteral = lower_expr.lowerArrayLiteral; + pub const resolveArrayLiteralType = lower_expr.resolveArrayLiteralType; + pub const lowerIndexExpr = lower_expr.lowerIndexExpr; + pub const lowerSliceExpr = lower_expr.lowerSliceExpr; + pub const lowerTupleLiteral = lower_expr.lowerTupleLiteral; + pub const lowerDerefExpr = lower_expr.lowerDerefExpr; + pub const lowerForceUnwrap = lower_expr.lowerForceUnwrap; + pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce; + pub const resolveOptionalInner = lower_expr.resolveOptionalInner; + + // --- moved to lower/expr.zig (lower_expr) --- + pub const lowerExpr = lower_expr.lowerExpr; + pub const refCapturePointee = lower_expr.refCapturePointee; + pub const lowerBinaryOp = lower_expr.lowerBinaryOp; + pub const lowerTupleOp = lower_expr.lowerTupleOp; + pub const lowerTupleLexCompare = lower_expr.lowerTupleLexCompare; + pub const lowerTupleMembership = lower_expr.lowerTupleMembership; + pub const lowerChainedComparison = lower_expr.lowerChainedComparison; + pub const emitCmp = lower_expr.emitCmp; + + // --- moved to lower/closure.zig (lower_closure) --- + pub const lowerLambda = lower_closure.lowerLambda; + pub const createBareFnTrampoline = lower_closure.createBareFnTrampoline; + pub const createClosureToBareFnAdapter = lower_closure.createClosureToBareFnAdapter; + pub const collectCaptures = lower_closure.collectCaptures; + pub const computeEnvSize = lower_closure.computeEnvSize; }; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 2ed9ca6..052e6d5 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -43,7 +43,6 @@ const Function = inst_mod.Function; const Module = mod_mod.Module; const Builder = mod_mod.Builder; - const lower = @import("../lower.zig"); const Lowering = lower.Lowering; const Scope = lower.Scope; @@ -1206,13 +1205,6 @@ pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { // ── Lambda/closure ──────────────────────────────────────────── -pub const CaptureInfo = struct { - name: []const u8, - ty: TypeId, - ref: Ref, // alloca or value ref in the parent scope - is_alloca: bool, -}; - /// Build `tp.name -> TypeId` bindings for a generic call. /// `args_ast` must be parallel to `fd.params`; for dot-calls the caller /// prepends the receiver's AST node so positions align with `fd.params[0] = self`. diff --git a/src/ir/lower/closure.zig b/src/ir/lower/closure.zig new file mode 100644 index 0000000..8bc40ed --- /dev/null +++ b/src/ir/lower/closure.zig @@ -0,0 +1,733 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; + +pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { + // Lower the lambda body as a new anonymous function + var buf: [64]u8 = undefined; + const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda"; + self.block_counter += 1; + + // Collect lambda param names for exclusion from captures + var param_names = std.StringHashMap(void).init(self.alloc); + defer param_names.deinit(); + for (lam.params) |p| { + param_names.put(p.name, {}) catch {}; + } + + // Pre-scan lambda body AST for free variables (captures) + var captures = std.ArrayList(CaptureInfo).empty; + defer captures.deinit(self.alloc); + self.collectCaptures(lam.body, ¶m_names, &captures); + + // Deduplicate captures + var seen = std.StringHashMap(void).init(self.alloc); + defer seen.deinit(); + var deduped = std.ArrayList(CaptureInfo).empty; + defer deduped.deinit(self.alloc); + for (captures.items) |cap| { + if (!seen.contains(cap.name)) { + seen.put(cap.name, {}) catch {}; + deduped.append(self.alloc, cap) catch {}; + } + } + const capture_list = deduped.items; + + // Build env struct type if there are captures + var env_struct_ty: TypeId = .void; + if (capture_list.len > 0) { + const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable; + for (capture_list, 0..) |cap, i| { + var nbuf: [32]u8 = undefined; + const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap"; + env_field_data[i] = .{ + .name = self.module.types.internString(fname), + .ty = cap.ty, + }; + } + const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env"; + const env_name_id = self.module.types.internString(env_name); + env_struct_ty = self.module.types.intern(.{ .@"struct" = .{ + .name = env_name_id, + .fields = env_field_data, + } }); + } + + // Save current builder state + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + const saved_scope = self.scope; + + // Build param list. Convention when implicit_ctx is enabled: + // slot 0 = __sx_ctx: *void + // slot 1 = env: *void + // slot 2+ = user params + // Without implicit_ctx, env is slot 0 and user params follow. + var params = std.ArrayList(Function.Param).empty; + const env_ptr_ty = self.module.types.ptrTo(.void); + const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c; + if (lambda_wants_ctx) { + params.append(self.alloc, .{ + .name = self.module.types.internString("__sx_ctx"), + .ty = env_ptr_ty, + }) catch unreachable; + } + params.append(self.alloc, .{ + .name = self.module.types.internString("env"), + .ty = env_ptr_ty, + }) catch unreachable; + // Get target closure param types for inference (from Closure(T1, T2) -> R annotations) + const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: { + if (!tt.isBuiltin()) { + const tti = self.module.types.get(tt); + if (tti == .closure) break :blk tti.closure.params; + // Unwrap ?Closure(...) → Closure(...) + if (tti == .optional) { + const inner = tti.optional.child; + if (!inner.isBuiltin()) { + const inner_info = self.module.types.get(inner); + if (inner_info == .closure) break :blk inner_info.closure.params; + } + } + } + break :blk null; + } else null; + // User params follow the ctx (optional) + env slots in `params`. + const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1; + for (lam.params, 0..) |p, pi| { + const pty: TypeId = blk: { + // Unannotated lambda params take their type positionally from + // the target `Closure(T0, …)` signature. Resolve them here so + // `resolveParamType` (which would diagnose a missing annotation) + // is only called for params that carry one. + if (p.type_expr.data == .inferred_type) { + if (target_closure_params != null and pi < target_closure_params.?.len) { + break :blk target_closure_params.?[pi]; + } + if (self.diagnostics) |d| { + d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name}); + } + break :blk .unresolved; + } + break :blk self.resolveParamType(&p); + }; + params.append(self.alloc, .{ + .name = self.module.types.internString(p.name), + .ty = pty, + }) catch unreachable; + } + + const ret_ty = blk: { + if (lam.return_type) |rt| { + break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + } + // Use target closure return type if available — but only when it's + // a resolved type. An `.unresolved` ret comes from an unbound + // generic (`Closure(..) -> $R`); fall through to infer it from the + // body so the concrete return drives `$R` inference at the call site. + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const tti = self.module.types.get(tt); + if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret; + // Unwrap ?Closure(...) → Closure(...) + if (tti == .optional) { + const inner = tti.optional.child; + if (!inner.isBuiltin()) { + const inner_info = self.module.types.get(inner); + if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret; + } + } + } + } + // Arrow lambda without explicit return type — infer from body expression + // Temporarily bind params in scope so inferExprType can resolve param types + var temp_scope = Scope.init(self.alloc, self.scope); + const saved = self.scope; + self.scope = &temp_scope; + for (lam.params, 0..) |p, i| { + const pty = params.items[user_param_base + i].ty; + temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false }); + } + const inferred = self.inferExprType(lam.body); + self.scope = saved; + temp_scope.deinit(); + break :blk inferred; + }; + const name_id = self.module.types.internString(name); + const func_id = self.builder.beginFunction(name_id, params.items, ret_ty); + if (lam.call_conv == .c) { + self.module.getFunctionMut(func_id).call_conv = .c; + } + self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx; + + // Param-slot layout: ctx at 0 (if present), env at ctx_slots, + // user args at ctx_slots+1. + const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0; + const env_param_idx: u32 = lambda_ctx_slots; + const user_param_base_lam: u32 = lambda_ctx_slots + 1; + + // Save + rebind current_ctx_ref so the body's sx-to-sx calls + // forward the trampoline's own ctx (slot 0). + const saved_ctx_ref_lam = self.current_ctx_ref; + defer self.current_ctx_ref = saved_ctx_ref_lam; + if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + + // A lambda is its own function: its `return` must drain only ITS OWN + // `defer`s, not the enclosing function's. Open a fresh defer window + // (like `lowerFunction`/`monomorphizeFunction`) and restore on exit — + // otherwise lowering a closure literal inside a `defer` body re-enters + // the enclosing function's defer drain (infinite recursion — issue 0073). + const saved_func_defer_base = self.func_defer_base; + const saved_defer_len = self.defer_stack.items.len; + defer { + self.func_defer_base = saved_func_defer_base; + self.defer_stack.shrinkRetainingCapacity(saved_defer_len); + } + self.func_defer_base = saved_defer_len; + + // Create entry block + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + + // Create scope WITHOUT parent — captures are bound from env, not parent scope + var lambda_scope = Scope.init(self.alloc, null); + self.scope = &lambda_scope; + + // Bind captures from env struct (at env_param_idx) + if (capture_list.len > 0) { + const env_param_ref = Ref.fromIndex(env_param_idx); + // Alloca env struct locally so struct_gep can resolve the type + const env_local = self.builder.alloca(env_struct_ty); + // Compute env size + const env_byte_size_inner = self.computeEnvSize(capture_list); + const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64); + // memcpy(local_alloca, env_param, size) + _ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void)); + + for (capture_list, 0..) |cap, i| { + // GEP into env struct to get field pointer + const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); + // Load the captured value into a local alloca + const loaded = self.builder.load(field_ptr, cap.ty); + const slot = self.builder.alloca(cap.ty); + self.builder.store(slot, loaded); + lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true }); + } + } + + // Also need parent scope for function lookups (but not variable lookups) + // Set up fn_names from parent scope chain + { + var s: ?*Scope = saved_scope; + while (s) |scope| { + var it = scope.fn_names.iterator(); + while (it.next()) |e| { + if (!lambda_scope.fn_names.contains(e.key_ptr.*)) { + lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {}; + } + } + s = scope.parent; + } + } + + // Bind params (user args start at user_param_base_lam, shifted past ctx + env). + // Use the signature types computed above (`params`), which already + // applied contextual typing from the target closure to untyped params — + // `resolveParamType` alone would drop it and default each to s64. + for (lam.params, 0..) |p, i| { + const pty = params.items[user_param_base + i].ty; + const slot = self.builder.alloca(pty); + const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i))); + self.builder.store(slot, param_ref); + lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); + } + + // Lower body — capture last expression as return value. The + // `in_lambda_body` flag scopes the lambda-specific `raise`-not-failable + // hint; save/restore so a lambda nested inside a regular function (or a + // lambda inside a lambda) restores the enclosing context. + const saved_in_lambda = self.in_lambda_body; + self.in_lambda_body = true; + if (ret_ty != .void) { + if (self.lowerBlockValue(lam.body)) |val| { + if (!self.currentBlockHasTerminator()) { + const val_ty = self.builder.getRefType(val); + // A value-carrying failable arrow lambda (`-> (T, !) => expr`) + // yields the bare success value; the compiler appends the + // no-error slot (0) — same as a `return v` in a block body. + if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) { + self.lowerFailableSuccessReturn(val, ret_ty, lam.body.span); + } else { + const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; + self.builder.ret(coerced, ret_ty); + } + } + } + } else { + self.lowerBlock(lam.body); + } + self.in_lambda_body = saved_in_lambda; + self.ensureTerminator(ret_ty); + self.builder.finalize(); + + // Restore builder state + self.scope = saved_scope; + lambda_scope.deinit(); + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + // Restore the caller's `current_ctx_ref` BEFORE we emit the env + // alloc/memcpy below — those run in the caller's scope, and + // `allocViaContext` reads `current_ctx_ref` to find the + // installed allocator. Without this, the env_heap dispatch + // would still see `Ref.fromIndex(0)` (the lambda's own ctx + // param), which doesn't exist in the caller's frame and + // silently routes through the default context instead of any + // surrounding `push Context.{ allocator = ... }`. + self.current_ctx_ref = saved_ctx_ref_lam; + + // Closure flowing into a BARE function-pointer slot (`(T) -> U`, no env): + // the slot is called without the closure env arg, so the closure fn can't + // be passed directly. For a capture-free closure whose return type matches + // the slot, emit an adapter with the bare ABI. Reject the cases the bare + // ABI can't represent: a capturing closure (env has nowhere to live), and + // a failable closure into a non-failable slot (foreign code can't observe + // the error channel — ERR E5.1 FFI-boundary rule). + if (self.target_type) |tt| { + if (!tt.isBuiltin() and self.module.types.get(tt) == .function) { + const slot_ret = self.module.types.get(tt).function.ret; + const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty; + if (capture_list.len > 0) { + if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{}); + } else if (ret_ty == slot_ret or widen_ok) { + // Matching ABI, or a non-failable closure widening into a + // failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}. + const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span); + return self.builder.emit(.{ .func_ref = adapter }, tt); + } else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) { + if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{}); + } else if (self.diagnostics) |d| { + d.addFmt(.err, lam.body.span, "closure return type does not match the function-type slot", .{}); + } + } + } + + // Create proper closure type (user-visible params only — skip ctx + env). + const skip_count: usize = if (lambda_wants_ctx) 2 else 1; + var param_types_list = std.ArrayList(TypeId).empty; + for (params.items[skip_count..]) |p| { + param_types_list.append(self.alloc, p.ty) catch unreachable; + } + const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty); + + // Build env and closure in the caller's scope + if (capture_list.len > 0) { + // Alloca env struct on stack (so struct_gep can resolve the type) + const env_local = self.builder.alloca(env_struct_ty); + + // Store captured values into env struct fields + for (capture_list, 0..) |cap, i| { + const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty); + const val = if (cap.is_alloca) + self.builder.load(cap.ref, cap.ty) + else + cap.ref; + self.builder.store(gep, val); + } + + // Copy env to heap (so it outlives the stack frame). + // Route through `context.allocator.alloc` rather than calling + // libc malloc directly so closures respect a surrounding + // `push Context.{ allocator = ... }` and a tracker / arena + // counts the env allocation alongside everything else. + const env_byte_size = self.computeEnvSize(capture_list); + const env_size = self.builder.constInt(@intCast(env_byte_size), .s64); + const ptr_void = self.module.types.ptrTo(.void); + const env_heap = self.allocViaContext(env_size, ptr_void); + // memcpy(heap, stack_alloca, size) + _ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void); + + return self.builder.closureCreate(func_id, env_heap, closure_ty); + } else { + return self.builder.closureCreate(func_id, Ref.none, closure_ty); + } +} + +/// Create a trampoline function that wraps a bare function for closure auto-promotion. +/// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the +/// bare function with `(args...)`, ignoring the env parameter. +pub fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId { + // Build trampoline params: [__sx_ctx]? + env + closure params. + // When the program uses Context, every sx-side trampoline carries + // the implicit ctx at slot 0 and forwards it to the wrapped + // function (which is also sx-side and expects it at slot 0). + var params = std.ArrayList(inst_mod.Function.Param).empty; + defer params.deinit(self.alloc); + const void_ptr_ty = self.module.types.ptrTo(.void); + const wants_ctx = self.implicit_ctx_enabled; + if (wants_ctx) { + params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; + } + const env_name = self.module.types.internString("env"); + params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable; + for (closure_info.params, 0..) |pty, i| { + var buf: [32]u8 = undefined; + const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; + params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; + } + + // Generate unique trampoline name + const bare_func = self.module.functions.items[bare_func_id.index()]; + const bare_name = self.module.types.getString(bare_func.name); + var name_buf: [128]u8 = undefined; + const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp"; + const tramp_name_id = self.module.types.internString(tramp_name); + + // Save builder state + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + + // Create function + const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; + var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret); + func.has_implicit_ctx = wants_ctx; + const func_id = self.module.addFunction(func); + self.builder.func = func_id; + self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1 + const entry_name = self.module.types.internString("entry"); + const entry_block = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry_block); + + // Build call args: forward [__sx_ctx]? + user_params (skip env). + // Trampoline slots: 0=ctx (if present), {0|1}=env, then user args. + const ctx_slots: usize = if (wants_ctx) 1 else 0; + const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env + var call_args = std.ArrayList(Ref).empty; + defer call_args.deinit(self.alloc); + if (wants_ctx and bare_func.has_implicit_ctx) { + call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx + } + for (closure_info.params, 0..) |_, i| { + call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable; + } + const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; + const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret); + + // Return result (or void) + if (closure_info.ret != .void) { + self.builder.ret(result, closure_info.ret); + } else { + self.builder.retVoid(); + } + self.builder.finalize(); + + // Restore builder state + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + + return func_id; +} + +/// Adapter for coercing a closure into a BARE function-pointer slot +/// (`(T) -> U`, no env). The closure's underlying function has signature +/// `[ctx?] + env + user-params`, but a bare fn-ptr slot is *called* without +/// the env arg — so the closure fn can't be used directly (the env slot +/// would swallow the first user arg). This adapter carries the bare ABI +/// (`[ctx?] + user-params`) and forwards to the closure fn with a null env. +/// Only sound for capture-free closures (a null env is correct iff the body +/// reads no captures); the caller rejects capturing closures. +/// +/// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening +/// case (a non-failable closure into a failable slot): the closure returns +/// the success value and the adapter wraps it into the slot's `{value, 0}` +/// failable tuple (ERR E5.1 non-failable→failable widening). +pub fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId { + var params = std.ArrayList(inst_mod.Function.Param).empty; + defer params.deinit(self.alloc); + const void_ptr_ty = self.module.types.ptrTo(.void); + const wants_ctx = self.implicit_ctx_enabled; + if (wants_ctx) { + params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable; + } + for (fn_info.params, 0..) |pty, i| { + var buf: [32]u8 = undefined; + const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg"; + params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable; + } + + const closure_func = self.module.functions.items[closure_func_id.index()]; + const closure_name = self.module.types.getString(closure_func.name); + var name_buf: [128]u8 = undefined; + const adapter_name = std.fmt.bufPrint(&name_buf, "__cl2fn_{s}", .{closure_name}) catch "__cl2fn"; + const adapter_name_id = self.module.types.internString(adapter_name); + + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + + const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable; + var func = inst_mod.Function.init(adapter_name_id, owned_params, fn_info.ret); + func.has_implicit_ctx = wants_ctx; + const func_id = self.module.addFunction(func); + self.builder.func = func_id; + self.builder.inst_counter = @intCast(owned_params.len); + const entry_name = self.module.types.internString("entry"); + const entry_block = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry_block); + + // Forward [ctx?] + null env + user params to the closure fn. + const ctx_slots: usize = if (wants_ctx) 1 else 0; + var call_args = std.ArrayList(Ref).empty; + defer call_args.deinit(self.alloc); + if (wants_ctx) call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; + call_args.append(self.alloc, self.builder.constNull(void_ptr_ty)) catch unreachable; + for (fn_info.params, 0..) |_, i| { + call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable; + } + const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; + const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret); + if (closure_ret == fn_info.ret) { + if (fn_info.ret != .void) { + self.builder.ret(result, fn_info.ret); + } else { + self.builder.retVoid(); + } + } else { + // ∅-widening: closure returns the success value; wrap `{value, 0}` + // into the slot's failable tuple. + self.lowerFailableSuccessReturn(result, fn_info.ret, span); + } + self.builder.finalize(); + + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + return func_id; +} + +/// Walk an AST node and collect free variable references (identifiers that are +/// in the current scope but not in lambda params). +pub fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void { + switch (node.data) { + .identifier => |id| { + // Skip lambda params + if (param_names.contains(id.name)) return; + // Skip function names + if (self.program_index.fn_ast_map.contains(id.name)) return; + // Skip type names + if (self.program_index.struct_template_map.contains(id.name)) return; + // Check if it's a variable in the parent scope + if (self.scope) |scope| { + if (scope.lookup(id.name)) |binding| { + captures.append(self.alloc, .{ + .name = id.name, + .ty = binding.ty, + .ref = binding.ref, + .is_alloca = binding.is_alloca, + }) catch {}; + } + } + }, + .binary_op => |bo| { + self.collectCaptures(bo.lhs, param_names, captures); + self.collectCaptures(bo.rhs, param_names, captures); + }, + .unary_op => |uo| { + self.collectCaptures(uo.operand, param_names, captures); + }, + .call => |cl| { + self.collectCaptures(cl.callee, param_names, captures); + for (cl.args) |arg| { + self.collectCaptures(arg, param_names, captures); + } + }, + .block => |blk| { + for (blk.stmts) |stmt| { + self.collectCaptures(stmt, param_names, captures); + } + }, + .if_expr => |ie| { + self.collectCaptures(ie.condition, param_names, captures); + self.collectCaptures(ie.then_branch, param_names, captures); + if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures); + }, + .while_expr => |we| { + self.collectCaptures(we.condition, param_names, captures); + self.collectCaptures(we.body, param_names, captures); + }, + .return_stmt => |rs| { + if (rs.value) |v| self.collectCaptures(v, param_names, captures); + }, + .var_decl => |vd| { + if (vd.value) |v| self.collectCaptures(v, param_names, captures); + // Register the local var name so it's not captured + param_names.put(vd.name, {}) catch {}; + }, + .const_decl => |cd| { + self.collectCaptures(cd.value, param_names, captures); + param_names.put(cd.name, {}) catch {}; + }, + .assignment => |a| { + self.collectCaptures(a.target, param_names, captures); + self.collectCaptures(a.value, param_names, captures); + }, + .destructure_decl => |dd| { + self.collectCaptures(dd.value, param_names, captures); + for (dd.names) |name| { + param_names.put(name, {}) catch {}; + } + }, + .field_access => |fa| { + self.collectCaptures(fa.object, param_names, captures); + }, + .index_expr => |ie| { + self.collectCaptures(ie.object, param_names, captures); + self.collectCaptures(ie.index, param_names, captures); + }, + .struct_literal => |sl| { + for (sl.field_inits) |fi| { + self.collectCaptures(fi.value, param_names, captures); + } + }, + .array_literal => |al| { + for (al.elements) |elem| { + self.collectCaptures(elem, param_names, captures); + } + }, + .lambda => |inner_lam| { + // For nested lambdas, the inner lambda captures from our scope too + // But its own params should be excluded + var inner_params = std.StringHashMap(void).init(self.alloc); + defer inner_params.deinit(); + // Copy current param_names + var it = param_names.iterator(); + while (it.next()) |e| { + inner_params.put(e.key_ptr.*, {}) catch {}; + } + for (inner_lam.params) |p| { + inner_params.put(p.name, {}) catch {}; + } + self.collectCaptures(inner_lam.body, &inner_params, captures); + }, + .match_expr => |me| { + self.collectCaptures(me.subject, param_names, captures); + for (me.arms) |arm| { + self.collectCaptures(arm.body, param_names, captures); + } + }, + .null_coalesce => |nc| { + self.collectCaptures(nc.lhs, param_names, captures); + self.collectCaptures(nc.rhs, param_names, captures); + }, + .deref_expr => |de| { + self.collectCaptures(de.operand, param_names, captures); + }, + .for_expr => |fe| { + self.collectCaptures(fe.iterable, param_names, captures); + // Register capture name as local so it's not captured + param_names.put(fe.capture_name, {}) catch {}; + self.collectCaptures(fe.body, param_names, captures); + }, + .slice_expr => |se| { + self.collectCaptures(se.object, param_names, captures); + if (se.start) |s| self.collectCaptures(s, param_names, captures); + if (se.end) |e| self.collectCaptures(e, param_names, captures); + }, + .tuple_literal => |tl| { + for (tl.elements) |elem| { + self.collectCaptures(elem.value, param_names, captures); + } + }, + .force_unwrap => |fu| { + self.collectCaptures(fu.operand, param_names, captures); + }, + .chained_comparison => |cc| { + for (cc.operands) |op| { + self.collectCaptures(op, param_names, captures); + } + }, + .defer_stmt => |ds| { + self.collectCaptures(ds.expr, param_names, captures); + }, + .ffi_intrinsic_call => |fic| { + self.collectCaptures(fic.return_type, param_names, captures); + for (fic.args) |arg| { + self.collectCaptures(arg, param_names, captures); + } + }, + else => {}, + } +} + +/// Compute the byte size of the env struct based on captured value types. +pub fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize { + // Must match LLVM's struct layout: fields are aligned to their natural alignment + var offset: usize = 0; + var max_align: usize = 1; + for (capture_list) |cap| { + const field_size = self.typeSizeBytes(cap.ty); + const field_align = self.typeAlignBytes(cap.ty); + if (field_align > max_align) max_align = field_align; + // Align offset to field alignment + offset = (offset + field_align - 1) & ~(field_align - 1); + offset += field_size; + } + // Align total to max field alignment (matches LLVM's struct alignment) + return (offset + max_align - 1) & ~(max_align - 1); +} + +pub const CaptureInfo = struct { + name: []const u8, + ty: TypeId, + ref: Ref, // alloca or value ref in the parent scope + is_alloca: bool, +}; diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig new file mode 100644 index 0000000..5fd11db --- /dev/null +++ b/src/ir/lower/expr.zig @@ -0,0 +1,2385 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; +const binOpSymbol = Lowering.binOpSymbol; +const arithResultType = Lowering.arithResultType; +const exprIsFailable = Lowering.exprIsFailable; +const headNameOfCallee = Lowering.headNameOfCallee; +const StructConstInfo = Lowering.StructConstInfo; + +pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { + // Check for tagged enum construction: .Variant.{ payload_fields } + // This happens when type_expr is an enum_literal and target_type is a union + if (sl.type_expr) |te| { + if (te.data == .enum_literal) { + const variant_name = te.data.enum_literal.name; + const union_ty = self.target_type orelse .unresolved; + if (!union_ty.isBuiltin()) { + const union_info = self.module.types.get(union_ty); + if (union_info == .tagged_union) { + return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); + } + } + } + } + + // `.{ name = ... }` against a tagged-union target_type. Reject: + // the only valid construction forms are `.variant(payload)` and + // `.variant.{ field, ... }`. Falling through would lower the + // user's values straight into the `(tag, payload_bytes)` slot + // pair and emit IR that LLVM later rejects. + if (sl.type_expr == null and sl.struct_name == null) { + const tu_ty = self.target_type orelse .unresolved; + if (!tu_ty.isBuiltin()) { + const tu_info = self.module.types.get(tu_ty); + if (tu_info == .tagged_union) { + if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { + const first_name = sl.field_inits[0].name.?; + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(tu_ty); + if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { + diags.addFmt( + .err, + span, + "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", + .{ ty_name, first_name, first_name, first_name }, + ); + } else { + self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); + } + } + return self.builder.enumInit(0, Ref.none, tu_ty); + } + } + } + } + + const ty: TypeId = if (sl.struct_name) |name| + // Source-aware (E2): a bare struct-literal type name resolves to the + // querying source's OWN same-name author, not the global `findByName` + // first-match — so `Box.{...}` in module B builds B's `Box`, never a + // flat-imported A's. `.undeclared`/`.pending` keep the empty-struct + // stub (byte-identical to the legacy `findByName orelse intern`); + // `.ambiguous`/`.not_visible` surface their loud diagnostic + poison. + self.resolveNominalLeaf(name, false, span) + else if (sl.type_expr) |te| + // Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr + self.resolveTypeWithBindings(te) + else self.target_type orelse .unresolved; + + // Get struct field types for coercion and ordering + const struct_fields = self.getStructFields(ty); + + // Look up field defaults from AST + const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: { + const ti = self.module.types.get(ty); + break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null); + } else @as(?[]const u8, null); + const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn| + (self.struct_defaults_map.get(sn) orelse &.{}) + else + &.{}; + + // Check if any field_init has a name (named literal) + const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + + if (has_names and struct_fields.len > 0) { + // Named literal: reorder fields to match struct declaration order + // First, lower all field values in source order (to preserve evaluation order) + var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty; + defer lowered.deinit(self.alloc); + for (sl.field_inits) |fi| { + const saved_tt = self.target_type; + // Set target_type to the field's declared type so array literals + // know if the target is a vector, etc. + if (fi.name) |fname| { + for (struct_fields) |sf| { + if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { + self.target_type = sf.ty; + break; + } + } + } + const val = self.lowerExpr(fi.value); + self.target_type = saved_tt; + lowered.append(self.alloc, .{ + .val = val, + .name = fi.name orelse "", + .node = fi.value, + }) catch unreachable; + } + + // Build fields in declaration order + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + for (struct_fields, 0..) |sf, fi| { + const sf_name = self.module.types.getString(sf.name); + // Find the matching lowered value + var found = false; + for (lowered.items) |l| { + if (std.mem.eql(u8, l.name, sf_name)) { + var val = l.val; + const src_ty = self.builder.getRefType(val); + val = self.coerceToType(val, src_ty, sf.ty); + fields.append(self.alloc, val) catch unreachable; + found = true; + break; + } + } + if (!found) { + // Field not specified — use default if available, else zero + if (fi < field_defaults.len) { + if (field_defaults[fi]) |default_expr| { + // Coerce the default to the field type at the IR + // level (the implicit narrowing rule) so a float + // default folds/errors here instead of being + // silently bit-coerced by the backend. + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; + } else { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } else { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + } + + const result = self.builder.structInit(fields.items, ty); + if (sl.init_block) |ib| { + return self.lowerInitBlock(result, ty, ib); + } + return result; + } + + // Positional literal: use source order + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + + for (sl.field_inits, 0..) |fi, i| { + var val = self.lowerExpr(fi.value); + // Coerce field value to match struct field type + if (i < struct_fields.len) { + const src_ty = self.inferExprType(fi.value); + val = self.coerceToType(val, src_ty, struct_fields[i].ty); + } + fields.append(self.alloc, val) catch unreachable; + } + + // Pad missing fields with defaults or zeroes + if (fields.items.len < struct_fields.len) { + for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| { + if (fi < field_defaults.len) { + if (field_defaults[fi]) |default_expr| { + fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable; + continue; + } + } + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + + const result = self.builder.structInit(fields.items, ty); + + // Lower init block if present + if (sl.init_block) |ib| { + return self.lowerInitBlock(result, ty, ib); + } + + return result; +} + +/// Lower an init block: store struct value to alloca, bind `self`, execute block, reload. +pub fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref { + // Store struct value to a temporary alloca + const ptr_ty = self.module.types.ptrTo(ty); + const slot = self.builder.alloca(ty); + self.builder.store(slot, struct_val); + + // Create a nested scope with `self` bound to the alloca pointer + var init_scope = Scope.init(self.alloc, self.scope); + defer init_scope.deinit(); + const saved_scope = self.scope; + self.scope = &init_scope; + + // `self` is the pointer to the struct (not an alloca itself — it IS the pointer value) + init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false }); + + // Lower the init block body + self.lowerBlock(ib); + + // Restore scope + self.scope = saved_scope; + + // Load and return the (possibly modified) struct value + return self.builder.load(slot, ty); +} + +/// Get the field list for a struct TypeId, or empty if not a struct. +pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field { + if (ty.isBuiltin()) return &.{}; + var resolved = ty; + const info = self.module.types.get(resolved); + // Dereference pointer types to get to the underlying struct + if (info == .pointer) { + resolved = info.pointer.pointee; + if (resolved.isBuiltin()) return &.{}; + const inner = self.module.types.get(resolved); + return switch (inner) { + .@"struct" => |s| s.fields, + else => &.{}, + }; + } + return switch (info) { + .@"struct" => |s| s.fields, + else => &.{}, + }; +} + +/// If a method's first param expects a pointer (*T) but we're passing T by value, +/// swap the first arg with the alloca address (implicit address-of). +pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void { + // Skip the implicit __sx_ctx param when inspecting the receiver slot. + const skip: usize = if (func.has_implicit_ctx) 1 else 0; + if (func.params.len <= skip) return; + const first_param_ty = func.params[skip].ty; + // Check if first param expects a pointer + if (!first_param_ty.isBuiltin()) { + const pi = self.module.types.get(first_param_ty); + if (pi == .pointer) { + // If obj is already a pointer type, it's already correct (no addr_of needed) + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .pointer) return; // already a pointer + } + // Method expects *T — pass the address of the receiver (value type in alloca) + if (obj_node.data == .identifier) { + if (self.scope) |scope| { + if (scope.lookup(obj_node.data.identifier.name)) |binding| { + if (binding.is_alloca) { + const ptr_ty = self.module.types.ptrTo(binding.ty); + method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); + return; + } + } + } + } + // Field access: obj.field.method() → GEP to field, pass pointer directly. + // This avoids copying the struct value (mutations through *T must be visible). + if (obj_node.data == .field_access) { + const gep_ref = self.lowerExprAsPtr(obj_node); + // GEP returns a pointer in LLVM but its IR type is the field value type. + // Wrap with addr_of (no-op in LLVM) to set the IR type to *T, + // preventing coerceCallArgs from doing a spurious alloca+store. + const ptr_ty = self.module.types.ptrTo(obj_ty); + method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty); + return; + } + // General case: alloca+store the value and pass the alloca pointer + { + const slot = self.builder.alloca(obj_ty); + self.builder.store(slot, method_args.items[0]); + method_args.items[0] = slot; + } + } else { + // Method expects a value `T` but the receiver is a `*T` (e.g. a + // `for xs: (*x)` by-ref capture) — deref to pass the value. + if (!obj_ty.isBuiltin()) { + const oi = self.module.types.get(obj_ty); + if (oi == .pointer and oi.pointer.pointee == first_param_ty) { + method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty); + } + } + } + } +} + +/// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types. +pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 { + if (ty.isBuiltin()) { + // Map builtin types to their names for method resolution (e.g., s64.eq) + return builtinTypeName(ty); + } + var resolved = ty; + const info = self.module.types.get(resolved); + if (info == .pointer) { + resolved = info.pointer.pointee; + if (resolved.isBuiltin()) return builtinTypeName(resolved); + } + const ri = self.module.types.get(resolved); + return switch (ri) { + .@"struct" => |s| self.module.types.getString(s.name), + else => null, + }; +} + +pub fn builtinTypeName(ty: TypeId) ?[]const u8 { + return switch (ty) { + .s8 => "s8", + .s16 => "s16", + .s32 => "s32", + .s64 => "s64", + .u8 => "u8", + .u16 => "u16", + .u32 => "u32", + .u64 => "u64", + .f32 => "f32", + .f64 => "f64", + .bool => "bool", + .string => "string", + else => null, + }; +} + +/// Resolve the type of a named field on a given type. +pub fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { + if (std.mem.eql(u8, field, "len")) return .s64; + if (std.mem.eql(u8, field, "ptr")) { + const elem_ty = self.getElementType(ty); + return self.module.types.manyPtrTo(elem_ty); + } + const field_name_id = self.module.types.internString(field); + // Check union fields + promoted fields + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) { + .@"union" => |u| u.fields, + .tagged_union => |u| u.fields, + else => null, + }; + if (u_fields) |ufields| { + for (ufields) |f| { + if (f.name == field_name_id) return f.ty; + // Check promoted fields from anonymous struct variants + if (!f.ty.isBuiltin()) { + const fi = self.module.types.get(f.ty); + if (fi == .@"struct") { + for (fi.@"struct".fields) |sf| { + if (sf.name == field_name_id) return sf.ty; + } + } + } + } + } + } + // Check tuple fields + if (!ty.isBuiltin()) { + const ti = self.module.types.get(ty); + if (ti == .tuple) { + const tuple = ti.tuple; + // Try named fields + if (tuple.names) |names| { + for (names, 0..) |name_id, i| { + if (name_id == field_name_id) return tuple.fields[i]; + } + } + // Try numeric index + const idx = std.fmt.parseInt(usize, field, 10) catch { + return .unresolved; + }; + if (idx < tuple.fields.len) return tuple.fields[idx]; + return .unresolved; + } + } + const struct_fields = self.getStructFields(ty); + for (struct_fields) |f| { + if (f.name == field_name_id) return f.ty; + } + return .unresolved; +} + +pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { + // `error.X` — an error-tag literal. The `error` keyword in expression + // position parses as identifier "error" (E0.2), so `error.X` is a + // field access we intercept here. `error` is reserved, so this is + // unambiguous (no struct/pack can be named `error`). + if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) { + return self.lowerErrorTagLiteral(fa.field, span); + } + + // Pack-arity intercept: `.len` in a pack-fn mono's + // body resolves to the comptime-known N. The mono doesn't + // materialise the `[]Any` slice that the inline path used, so + // `args` isn't in scope as a value. + if (self.pack_param_count) |ppc| { + if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) { + if (ppc.get(fa.object.data.identifier.name)) |n| { + return self.builder.constInt(@as(i64, @intCast(n)), .s64); + } + } + } + + // Pack value projection: `xs.` where `` is a (zero-arg) method of + // the pack's constraint protocol projects it over every element → + // a tuple `(xs[0].(), …, xs[N-1].())`. (`xs.len` handled above.) + if (self.pack_constraint) |pcon| { + if (fa.object.data == .identifier) { + if (pcon.get(fa.object.data.identifier.name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) != null) { + return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span); + } + } + } + } + + // Interface-only enforcement (Decision): a member access on a + // constrained pack element `xs[i].` may only name a method of the + // constraint protocol — not an arbitrary concrete field. Checked here, + // on the `xs[i]` (index_expr) base, BEFORE substitution erases the + // "constrained to P" context. Protocol method CALLS go through the call + // path; a method name passes this check (it's in the protocol). + if (self.pack_constraint) |pcon| { + if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) { + const base_name = fa.object.data.index_expr.object.data.identifier.name; + if (pcon.get(base_name)) |proto| { + if (self.lookupProtocolField(proto, fa.field) == null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto }); + } + return self.builder.constInt(0, .void); + } + } + } + } + + // Check for struct constant access: Struct.CONST + if (fa.object.data == .identifier) { + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field; + if (self.struct_const_map.get(qualified)) |info| { + return self.lowerStructConstant(info); + } + } + + // Numeric-limit accessor: `.min` / `.max` folds to a comptime + // const of the queried type (sibling of the identifier-receiver + // intercepts above). Placed AFTER `Struct.CONST` so a user const named + // `min`/`max` wins on its own struct; a builtin type name can never + // name a user struct (reserved — issue 0076), so they never collide. + if (self.lowerNumericLimit(fa, span)) |ref| return ref; + + // M1.3 — `obj.class` on any Obj-C-class pointer lowers to + // `object_getClass(obj)`. Sugar; the receiver is opaque so + // we don't auto-deref. Returns `Class` (alias for *void; + // typed Class(T) parameterization is M1.1.b). + if (std.mem.eql(u8, fa.field, "class")) { + const expr_ty = self.inferExprType(fa.object); + if (self.objc().isObjcClassPointer(expr_ty)) { + const obj_ref = self.lowerExpr(fa.object); + const ptr_void = self.module.types.ptrTo(.void); + const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); + const args = self.alloc.alloc(Ref, 1) catch unreachable; + args[0] = obj_ref; + return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); + } + } + + // M2.2 — `obj.field` where `field` is declared with `#property` + // on a foreign Obj-C class lowers as `[obj field]` (the synthesized + // getter). Receiver stays opaque — no auto-deref. + if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| { + return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span); + } + + // M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class + // pointer for a plain instance field (NOT a #property) lowers as + // `object_getIvar(obj, load(___state_ivar))` + struct_gep on + // the state struct + load. The receiver is the opaque Obj-C id + // (matching Apple's `self` semantics); the state lives in the + // hidden `__sx_state` ivar. + if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| { + return self.lowerObjcDefinedStateFieldRead(fa.object, info); + } + + var obj = self.lowerExpr(fa.object); + var obj_ty = self.inferExprType(fa.object); + + // Auto-deref: if the object is a pointer to a struct, load through it + if (!obj_ty.isBuiltin()) { + const ptr_info = self.module.types.get(obj_ty); + if (ptr_info == .pointer) { + const pointee = ptr_info.pointer.pointee; + obj = self.builder.load(obj, pointee); + obj_ty = pointee; + } + } + + // Special fields on slices/strings (NOT structs with .len/.ptr fields) + if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) { + // Only use length/data_ptr for slice, string, array, vector types + const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: { + const info = self.module.types.get(obj_ty); + break :blk info == .slice or info == .array or info == .vector; + } else false); + + if (is_special) { + if (std.mem.eql(u8, fa.field, "len")) { + return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); + } + { + const elem_ty = self.getElementType(obj_ty); + const mp_ty = self.module.types.manyPtrTo(elem_ty); + return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty); + } + } + } + + // Optional chaining: p?.field + if (fa.is_optional) { + return self.lowerOptionalChain(obj, fa, span); + } + + return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span); +} + +/// True when an `.identifier` receiver text resolves to an in-scope VALUE +/// binding rather than a builtin type. A backtick raw identifier (F0.6) can +/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``); +/// such a value is reachable through the same three sources the ordinary +/// identifier field-access path consults (see `expr_typer` `.identifier` +/// arm): lexical `scope`, program `global_names`, and module value +/// constants `module_const_map`. The numeric-limit intercept must defer to +/// ordinary field access whenever ANY of the three binds the name, so a +/// raw value field read is never hijacked into a numeric-limit fold +/// (issues 0092 local / 0093 global + module-const). A single helper used +/// by both lowering and inference keeps the two resolvers in lockstep +/// (issue-0083 two-resolver defect class). +pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool { + if (self.scope) |scope| { + if (scope.lookup(name) != null) return true; + } + if (self.program_index.global_names.get(name) != null) return true; + if (self.program_index.module_const_map.get(name) != null) return true; + return false; +} + +/// Numeric-limit accessor intercept (`.min`/`.max`/`.epsilon`/ +/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` / +/// `Struct.CONST` / pack-arity identifier-receiver intercepts in +/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried +/// type via the shared `TypeResolver` logic (no second computor) + the +/// existing `constInt` / `constFloat` const paths: +/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`); +/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/ +/// `.nan` → `constFloat` (via `floatLimitFor`). +/// Returns null when the field is not a limit accessor, or the receiver is not +/// a builtin type (a user struct → ordinary field lowering reports +/// field-not-found). Two clean diagnostics (then a placeholder, so lowering +/// finishes and `hasErrors()` aborts the build): +/// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`); +/// - any accessor on a builtin NON-numeric receiver +/// (`bool`/`string`/`void`/`Any`/`noreturn`). +pub fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref { + const name = switch (fa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => return null, + }; + if (!TypeResolver.isLimitField(fa.field)) return null; + const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null; + + // A backtick raw identifier (F0.6) can bind a value whose spelling + // shadows a builtin type name (`` `f64 := … ``). Field access on that + // value is an ordinary field read, not a numeric-limit fold — defer to + // the normal field-access path when the receiver identifier resolves to + // a value binding through any of scope / globals / module consts + // (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type + // and can never be value-shadowed. + if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null; + + if (TypeResolver.integerLimitFor(name, fa.field)) |value| { + return self.builder.constInt(value, ty); + } + if (TypeResolver.floatLimitFor(name, fa.field)) |value| { + return self.builder.constFloat(value, ty); + } + // The field is a limit accessor, but it does not apply to this type. + if (self.diagnostics) |d| { + if (TypeResolver.integerWidthSign(name) != null) { + // Integer receiver + a float-only accessor. + d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field }); + } else { + // Non-numeric builtin receiver (bool/string/void/Any/noreturn). + d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field }); + } + } + return self.emitPlaceholder(fa.field); +} + +/// Lower a struct-level constant value (e.g., Phys.GRAVITY). +pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { + const val_node = info.value; + return switch (val_node.data) { + .int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64), + .float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64), + .bool_literal => |lit| self.builder.constBool(lit.value), + .string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)), + else => self.lowerExpr(val_node), + }; +} + +/// Lower optional chaining: `p?.field` where p is ?T +/// Produces ?FieldType: some(unwrap(p).field) if p has value, else null +/// If FieldType is already optional (?U), flattens to ?U (no double wrapping) +pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref { + const obj_ty = self.inferExprType(fa.object); + // Get the inner (non-optional) type + const inner_ty = if (!obj_ty.isBuiltin()) blk: { + const info = self.module.types.get(obj_ty); + break :blk if (info == .optional) info.optional.child else obj_ty; + } else obj_ty; + + // Get the field type on the inner type + const field_ty = self.resolveFieldType(inner_ty, fa.field); + // If field is already optional, flatten (don't double-wrap) + const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false; + const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty); + + // Check if optional has value + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool); + + // Create blocks + const some_bb = self.freshBlock("chain.some"); + const none_bb = self.freshBlock("chain.none"); + const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty}); + + self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); + + // Some: unwrap, access field (already ?FieldType if flattened, else wrap) + self.builder.switchToBlock(some_bb); + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty); + const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span); + const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty); + self.builder.br(merge_bb, &.{some_result}); + + // None: produce null optional + self.builder.switchToBlock(none_bb); + const none_result = self.builder.constNull(result_ty); + self.builder.br(merge_bb, &.{none_result}); + + // Merge + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, result_ty); +} + +/// Field access on a known type (shared by regular field access and optional chaining) +/// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour +/// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any +/// other field name so the read path (`lowerFieldAccessOnType`) and the +/// write path (`lowerAssignment`) share one resolver and reject a +/// non-lane field identically (issue 0086). +pub fn vectorLaneIndex(field: []const u8) ?u32 { + if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0; + if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1; + if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2; + if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3; + return null; +} + +pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref { + const field_name_id = self.module.types.internString(field); + + // Check if it's a union type + if (!obj_ty.isBuiltin()) { + const info = self.module.types.get(obj_ty); + switch (info) { + .tagged_union => |u| { + // .tag → extract the enum tag value with the correct tag type + if (std.mem.eql(u8, field, "tag")) { + return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type); + } + // Tagged union — use enum_payload + for (u.fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); + } + } + // Check promoted fields from anonymous struct variants + for (u.fields) |f| { + if (!f.ty.isBuiltin()) { + const field_info = self.module.types.get(f.ty); + if (field_info == .@"struct") { + for (field_info.@"struct".fields, 0..) |sf, si| { + if (sf.name == field_name_id) { + const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); + return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); + } + } + } + } + } + }, + .@"union" => |u| { + // Untagged union — use union_get to reinterpret bytes + for (u.fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty); + } + } + // Check promoted fields from anonymous struct variants + for (u.fields) |f| { + if (!f.ty.isBuiltin()) { + const field_info = self.module.types.get(f.ty); + if (field_info == .@"struct") { + for (field_info.@"struct".fields, 0..) |sf, si| { + if (sf.name == field_name_id) { + const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty); + return self.builder.structGet(reinterpreted, @intCast(si), sf.ty); + } + } + } + } + } + }, + else => {}, + } + } + + // Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) → + // lane 0/1/2/3. Shares lane-index resolution with the write path + // (lowerAssignment) via vectorLaneIndex; a non-lane field falls + // through to the field-not-found error below. + if (!obj_ty.isBuiltin()) { + const vinfo = self.module.types.get(obj_ty); + if (vinfo == .vector) { + if (Lowering.vectorLaneIndex(field)) |vidx| { + return self.builder.structGet(obj, vidx, vinfo.vector.element); + } + } + } + + // Closure field access: .fn_ptr → field 0, .env → field 1 + if (!obj_ty.isBuiltin()) { + const cinfo = self.module.types.get(obj_ty); + if (cinfo == .closure) { + if (std.mem.eql(u8, field, "fn_ptr")) { + const fn_ptr_ty = self.module.types.ptrTo(.void); + return self.builder.structGet(obj, 0, fn_ptr_ty); + } else if (std.mem.eql(u8, field, "env")) { + const env_ty = self.module.types.ptrTo(.void); + return self.builder.structGet(obj, 1, env_ty); + } + } + } + + // Tuple field access: .0, .1, etc. or named fields + if (!obj_ty.isBuiltin()) { + const tinfo = self.module.types.get(obj_ty); + if (tinfo == .tuple) { + const tuple = tinfo.tuple; + // Try named fields first + if (tuple.names) |names| { + for (names, 0..) |name_id, i| { + if (name_id == field_name_id) { + return self.builder.structGet(obj, @intCast(i), tuple.fields[i]); + } + } + } + // Try numeric index (e.g., "0", "1") + const idx = std.fmt.parseInt(u32, field, 10) catch { + return self.emitFieldError(obj_ty, field, span); + }; + if (idx < tuple.fields.len) { + return self.builder.structGet(obj, idx, tuple.fields[idx]); + } + return self.emitFieldError(obj_ty, field, span); + } + } + + // Resolve struct field index and type + const struct_fields = self.getStructFields(obj_ty); + for (struct_fields, 0..) |f, i| { + if (f.name == field_name_id) { + return self.builder.structGet(obj, @intCast(i), f.ty); + } + } + + return self.emitFieldError(obj_ty, field, span); +} + +pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { + const target = self.target_type orelse .unresolved; + const tag = self.resolveVariantValue(target, el.name); + return self.builder.enumInit(tag, Ref.none, target); +} + +/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the +/// destination context (`target_type`) is a named error set, the value is +/// typed as that set and `X`'s membership is validated; otherwise the value +/// is the raw `u32` global tag id (per the spec's context rule). +pub fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref { + const tag_id = self.module.types.internTag(tag_name); + if (self.target_type) |t| { + if (!t.isBuiltin()) { + const info = self.module.types.get(t); + if (info == .error_set) { + // The bare-`!` inferred placeholder (reserved name "!") accepts + // any tag — its members aren't known until the whole-program SCC + // pass (E1.4) folds in every raised tag. Skip membership for it. + if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) { + var in_set = false; + for (info.error_set.tags) |member| { + if (member == tag_id) { + in_set = true; + break; + } + } + if (!in_set) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) }); + } + } + } + return self.builder.constInt(@as(i64, @intCast(tag_id)), t); + } + } + } + return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32); +} + +/// Lower a tagged enum construction: .Variant.{ field_inits } +/// The struct literal provides the payload fields; we wrap them in an enum_init. +pub fn lowerTaggedEnumLiteral( + self: *Lowering, + sl: *const ast.StructLiteral, + variant_name: []const u8, + union_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + span: ast.Span, +) Ref { + if (self.findTaggedVariant(union_info, variant_name) == null) { + self.emitBadVariant(union_ty, union_info, variant_name, span); + return self.builder.enumInit(0, Ref.none, union_ty); + } + + const tag = self.resolveVariantValue(union_ty, variant_name); + const name_id = self.module.types.internString(variant_name); + + // Find the payload type for this variant + var payload_ty: TypeId = .void; + for (union_info.fields) |f| { + if (f.name == name_id) { + payload_ty = f.ty; + break; + } + } + + if (payload_ty == .void or sl.field_inits.len == 0) { + // No payload or no fields — just tag + return self.builder.enumInit(tag, Ref.none, union_ty); + } + + // Lower the payload as a struct init of the payload type + const saved_tt = self.target_type; + self.target_type = payload_ty; + const payload_fields = self.getStructFields(payload_ty); + + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + + for (sl.field_inits, 0..) |fi, i| { + if (i < payload_fields.len) { + const saved_inner = self.target_type; + self.target_type = payload_fields[i].ty; + var val = self.lowerExpr(fi.value); + self.target_type = saved_inner; + const src_ty = self.inferExprType(fi.value); + val = self.coerceToType(val, src_ty, payload_fields[i].ty); + fields.append(self.alloc, val) catch unreachable; + } else { + fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable; + } + } + + // Pad missing payload fields with zeroes + if (fields.items.len < payload_fields.len) { + for (payload_fields[fields.items.len..]) |sf| { + fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable; + } + } + + const payload = self.builder.structInit(fields.items, payload_ty); + self.target_type = saved_tt; + + return self.builder.enumInit(tag, payload, union_ty); +} + +pub fn findTaggedVariant( + self: *Lowering, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, +) ?usize { + const name_id = self.module.types.internString(variant_name); + for (union_info.fields, 0..) |f, i| { + if (f.name == name_id) return i; + } + return null; +} + +pub fn emitBadVariant( + self: *Lowering, + union_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, + span: ast.Span, +) void { + const diags = self.diagnostics orelse return; + const ty_name = self.formatTypeName(union_ty); + var list: std.ArrayList(u8) = .empty; + for (union_info.fields, 0..) |f, i| { + if (i > 0) list.appendSlice(self.alloc, ", ") catch return; + list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; + } + diags.addFmt( + .err, + span, + "'{s}' is not a variant of '{s}' (variants are: {s})", + .{ variant_name, ty_name, list.items }, + ); +} + +/// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). +pub fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { + if (ty.isBuiltin()) return 0; + const info = self.module.types.get(ty); + const name_id = self.module.types.internString(variant_name); + switch (info) { + .@"enum" => |e| { + for (e.variants, 0..) |v, i| { + if (v == name_id) { + if (e.explicit_values) |vals| { + if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); + } + return @intCast(i); + } + } + }, + .tagged_union => |u| { + for (u.fields, 0..) |f, i| { + if (f.name == name_id) { + if (u.explicit_tag_values) |vals| { + if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i]))); + } + return @intCast(i); + } + } + }, + else => {}, + } + return 0; +} + +/// Resolve a variant name to its tag index within an enum or union type. +pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { + if (ty.isBuiltin()) return 0; + const info = self.module.types.get(ty); + const name_id = self.module.types.internString(variant_name); + switch (info) { + .tagged_union => |u| { + for (u.fields, 0..) |f, i| { + if (f.name == name_id) return @intCast(i); + } + }, + .@"enum" => |e| { + for (e.variants, 0..) |v, i| { + if (v == name_id) return @intCast(i); + } + }, + else => {}, + } + return 0; +} + +pub fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref { + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + + // Determine element type: explicit type_expr > target_type > inference + var elem_ty: TypeId = .unresolved; + var from_target = false; + var is_vector = false; + + // First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3]) + if (al.type_expr) |te| { + const resolved = self.resolveArrayLiteralType(te); + if (resolved != .unresolved) { + if (!resolved.isBuiltin()) { + const info = self.module.types.get(resolved); + switch (info) { + .array => |a| { + elem_ty = a.element; + from_target = true; + }, + .vector => |v| { + elem_ty = v.element; + from_target = true; + is_vector = true; + }, + .slice => |s| { + elem_ty = s.element; + from_target = true; + }, + else => {}, + } + } + } + } + + if (!from_target) { + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const info = self.module.types.get(tt); + switch (info) { + .array => |a| { + elem_ty = a.element; + from_target = true; + }, + .slice => |s| { + elem_ty = s.element; + from_target = true; + }, + .vector => |v| { + elem_ty = v.element; + from_target = true; + is_vector = true; + }, + else => {}, + } + } + } + } + if (!from_target and al.elements.len > 0) { + const inferred = self.inferExprType(al.elements[0]); + if (inferred != .void) elem_ty = inferred; + } + + for (al.elements) |elem| { + const old_tt = self.target_type; + self.target_type = elem_ty; + var val = self.lowerExpr(elem); + self.target_type = old_tt; + // A nested `.[...]` element at a slice element type lowers to an + // aggregate array `[N]U` (lowerArrayLiteral always yields an array + // value); materialize it into a `[]U` slice so the element is a real + // {ptr,len} header rather than a raw array the callee would read its + // header off of (issue 0085). This per-element coercion recurses with + // the literal nesting, so `[][]T` and deeper coerce at every level. + if (!elem_ty.isBuiltin()) { + const ei = self.module.types.get(elem_ty); + if (ei == .slice) { + const val_ty = self.builder.getRefType(val); + if (!val_ty.isBuiltin()) { + const vi = self.module.types.get(val_ty); + if (vi == .array and vi.array.element == ei.slice.element) { + val = self.coerceToType(val, val_ty, elem_ty); + } + } + } + } + elems.append(self.alloc, val) catch unreachable; + } + + const result_ty = if (is_vector) + self.module.types.vectorOf(elem_ty, @intCast(al.elements.len)) + else + self.module.types.arrayOf(elem_ty, @intCast(al.elements.len)); + return self.builder.structInit(elems.items, result_ty); +} + +/// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]). +/// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr. +pub fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { + switch (te.data) { + .call => |cl| { + // Vector(3, f32) or Module.Vector(3, f32) + const callee_name = switch (cl.callee.data) { + .identifier => |id| id.name, + .field_access => |fa| fa.field, + else => return .unresolved, + }; + if (std.mem.eql(u8, callee_name, "Vector")) { + if (cl.args.len == 2) { + const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved; + const elem = self.resolveTypeWithBindings(cl.args[1]); + return self.module.types.vectorOf(elem, length); + } + } + // Generic-struct typed-literal head (`Box(s64).[...]`): route + // through the single layout choke-point (CP-1). A qualified head + // `a.Box(s64).[...]` selects a's OWN template via the namespace edge + // (Counter-1: was the global last-wins map); a bare head selects the + // single bare-VISIBLE author. + if (headNameOfCallee(cl.callee)) |hn| { + switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) { + .template => |t| return self.instantiateGenericStruct(&t, cl.args), + .poisoned => return .unresolved, + .not_generic => {}, + } + } + return .unresolved; + }, + .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), + .identifier => |id| { + // E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type + // name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is + // not bare-visible (consistent with annotations / 0763); ≥2 direct + // flat same-name authors are ambiguous (loud diagnostic, consistent + // with the leaf / 0755); a single source-keyed author resolves to + // ITS TypeId instead of a global `findByName` first-/last-wins pick. + switch (self.headTypeGate(id.name, te.span)) { + .ambiguous, .not_visible => return .unresolved, + .resolved => |tid| return tid, + .proceed => {}, + } + const name_id = self.module.types.internString(id.name); + return self.module.types.findByName(name_id) orelse .unresolved; + }, + .type_expr => |inner| { + if (self.headTypeLeak(inner.name, te.span)) return .unresolved; + return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + }, + .field_access => |fa| { + // Module.Type — try to resolve the field as a type name + const name_id = self.module.types.internString(fa.field); + return self.module.types.findByName(name_id) orelse .unresolved; + }, + else => return .unresolved, + } +} + +pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { + // Pack-arg substitution: `args[]` inside a body + // whose enclosing comptime call bound `args` as a pack name. + // Lowering the i-th call-site arg directly gives the concrete + // call-arg type — bypasses the `[]Any` slice boxing that would + // otherwise lose the type. Non-literal indices fall through to + // the standard slice indexing path. + if (self.packArgNodeAt(ie)) |arg_node| { + return self.lowerExpr(arg_node); + } + // Out-of-bounds pack indexing: object IS a pack name + index + // IS a comptime int literal but exceeds the pack arity. Emit + // a focused diagnostic so the user gets "pack index 2 out of + // bounds" instead of the generic "unresolved 'args'" that the + // fall-through scope-lookup would produce. + if (self.diagPackIndexOOB(ie)) { + return self.builder.constInt(0, .s64); + } + // Runtime index into a comptime-only pack (Decision 1): a pack has no + // runtime representation, so the index must be a compile-time constant. + // A runtime index is a hard error — clearer than the "unresolved + // ''" the slice-index fall-through would otherwise produce. + if (self.pack_param_count) |ppc| { + if (ie.object.data == .identifier) { + const pname = ie.object.data.identifier.name; + if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname}); + } + return self.builder.constInt(0, .s64); + } + } + } + const obj = self.lowerExpr(ie.object); + const idx = self.lowerExpr(ie.index); + // Infer element type from the object's slice/array type + const obj_ty = self.inferExprType(ie.object); + const elem_ty = self.getElementType(obj_ty); + return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); +} + +pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { + const obj = self.lowerExpr(se.object); + const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); + const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64); + // Infer result slice type from the object + const obj_ty = self.inferExprType(se.object); + // Subslice of string stays string (same {ptr, i64} layout, correct type category) + if (obj_ty == .string) { + return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); + } + const elem_ty = self.getElementType(obj_ty); + const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); + return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); +} + +pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { + var elems = std.ArrayList(Ref).empty; + defer elems.deinit(self.alloc); + var field_type_ids = std.ArrayList(TypeId).empty; + defer field_type_ids.deinit(self.alloc); + var name_ids = std.ArrayList(types.StringId).empty; + defer name_ids.deinit(self.alloc); + var has_names = false; + + // A tuple_init's element values must match its field types exactly + // (LLVM `insertvalue` does no implicit conversion). When a contextual + // target tuple of matching arity is in scope (annotation, assignment + // LHS, call/return slot), its field types drive element lowering so an + // ambient scalar `target_type` (e.g. the enclosing fn's int return + // type) can't narrow an element below its field width. Otherwise each + // element's type is inferred independently. + // A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields, + // so element-count ≠ field-count and a contextual target tuple can't be + // aligned by index — infer field types from the expanded refs instead. + var has_spread = false; + for (tl.elements) |elem| { + if (elem.value.data == .spread_expr) has_spread = true; + } + + // Contextual target tuple field types. Without a spread we require + // exact arity (existing behavior); with a spread we index positionally + // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces + // / erases each spliced element to its slot's type). + var target_fields: ?[]const TypeId = null; + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const tinfo = self.module.types.get(tt); + if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) { + target_fields = tinfo.tuple.fields; + } + } + } + + const saved_target = self.target_type; + var out_idx: usize = 0; + for (tl.elements) |elem| { + // Pack-spread element → splice its per-element values as fields. + if (elem.value.data == .spread_expr) { + const sp_operand = elem.value.data.spread_expr.operand; + if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| { + defer self.alloc.free(refs); + // Element AST nodes (for protocol-erasure lvalue/name fallback) + // when the spread is a bare pack name. + const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null) + self.pack_arg_nodes.?.get(sp_operand.data.identifier.name) + else + null; + for (refs, 0..) |r, ri| { + var val = r; + var vty = self.builder.getRefType(r); + if (target_fields) |tf| { + if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) { + const want = tf[out_idx]; + const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value; + val = self.coerceOrErase(r, vty, want, node); + vty = want; + } + } + elems.append(self.alloc, val) catch unreachable; + field_type_ids.append(self.alloc, vty) catch unreachable; + name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; + out_idx += 1; + } + continue; + } + // Not a pack spread (e.g. tuple-value spread) — not yet handled. + _ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic + continue; + } + const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value); + self.target_type = field_ty; + var val = self.lowerExpr(elem.value); + self.target_type = saved_target; + const val_ty = self.builder.getRefType(val); + if (val_ty != field_ty and val_ty != .void) { + val = self.coerceToType(val, val_ty, field_ty); + } + elems.append(self.alloc, val) catch unreachable; + field_type_ids.append(self.alloc, field_ty) catch unreachable; + if (elem.name) |name| { + name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable; + has_names = true; + } else { + name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable; + } + out_idx += 1; + } + + // Reuse the contextual target tuple type when it drove lowering so the + // value's type identity (incl. field names) matches the destination + // slot; otherwise build the tuple type from the inferred fields. + const tuple_ty = if (target_fields != null and self.target_type != null) + self.target_type.? + else + self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable, + .names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null, + } }); + + const owned = self.alloc.dupe(Ref, elems.items) catch unreachable; + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); +} + +pub fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref { + const ptr = self.lowerExpr(de.operand); + // Resolve pointee type from the pointer type. + const ptr_ty = self.inferExprType(de.operand); + if (!ptr_ty.isBuiltin()) { + const info = self.module.types.get(ptr_ty); + if (info == .pointer) { + return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee); + } + } + // Operand isn't a pointer — `.*` is invalid. Diagnose here instead of + // emitting a `.deref` with an `.unresolved` result type, which would + // otherwise slip through to emit_llvm's "unresolved type reached LLVM + // emission" panic with no source location. + if (self.diagnostics) |d| { + d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)}); + } + return ptr; +} + +pub fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref { + const val = self.lowerExpr(fu.operand); + const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand)); + return self.builder.optionalUnwrap(val, inner_ty); +} + +pub fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref { + const lhs = self.lowerExpr(nc.lhs); + const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs)); + + // Short-circuit: only evaluate RHS if LHS is null. + // IMPORTANT: optional_unwrap must be in the "has value" branch, + // not before the condBr — the interpreter errors on unwrapping null. + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool); + + const then_bb = self.freshBlock("nc.has"); + const rhs_bb = self.freshBlock("nc.rhs"); + const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty}); + + // If has value, go to then_bb to unwrap; else go to rhs_bb + self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{}); + + // Then block: unwrap LHS and branch to merge + self.builder.switchToBlock(then_bb); + const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty); + self.builder.br(merge_bb, &.{unwrapped}); + + // RHS block: evaluate fallback and branch to merge + self.builder.switchToBlock(rhs_bb); + var rhs = self.lowerExpr(nc.rhs); + const rhs_ty = self.builder.getRefType(rhs); + if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) { + rhs = self.coerceToType(rhs, rhs_ty, inner_ty); + } + self.builder.br(merge_bb, &.{rhs}); + + // Continue at merge + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, inner_ty); +} + +pub fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .optional) return info.optional.child; + } + return .unresolved; +} + +// ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─ + +pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { + // Stamp this node's source span onto the instructions it emits (ERR + // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ + // restore so a parent's later emits keep the parent's span after a + // child lowers. Skip the empty default so synthetic nodes don't reset + // a meaningful enclosing span to offset 0. + const saved_span = self.builder.current_span; + defer self.builder.current_span = saved_span; + if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end }; + // A node carrying an explicit `source_file` is one spliced into a body + // from another module — a substituted caller comptime-`$`-arg (stamped + // at the `cpn` build site in lowerComptimeCall / monomorphizePackFn). + // Resolve its bare names in THAT module's visibility context, overriding + // the body's defining-module pin, then restore so sibling callee nodes + // keep the enclosing context. Ordinary expression nodes never carry a + // `source_file`, so this is a no-op on the hot path. + const restore_source = node.source_file != null; + const saved_source = self.current_source_file; + if (node.source_file) |sf| self.setCurrentSourceFile(sf); + defer if (restore_source) self.setCurrentSourceFile(saved_source); + return switch (node.data) { + // Bare `$` in expression position → an `[]Type` slice + // value where each element is a `const_type(arg_types[i])`. + // Per `Type → .any` mapping in type_bridge, the IR slice + // type is `[]Any`; the interp stores raw `.type_tag` Values + // (NOT Any-boxed) so `args[i]` reads back as a Type value + // directly. Step 4 final slice — lets builder fns walk the + // whole pack at interp time. + .comptime_pack_ref => |cpr| blk: { + // `$` is overloaded in expression position: + // - Inside a pack-fn mono (or a `tryPackImplMatch` + // impl mono), `name` is a pack binding → slice of + // element types (`[]Type` lowered as `[]Any`). + // - Inside an impl mono whose impl pattern bound a + // single-type generic (`$R: Type` in + // `Closure(..$args) -> $R`), `name` is in + // `type_bindings` → single `const_type(R)` value. + // Pack arg types are checked first (the slice form), + // then pack_bindings (the impl-mono mirror), then + // type_bindings (single-type binding); only if all + // miss is it a real "outside an active binding" error. + if (self.pack_arg_types) |pat| { + if (pat.get(cpr.pack_name)) |arg_tys| { + break :blk self.buildPackSliceValue(arg_tys); + } + } + if (self.pack_bindings) |pb| { + if (pb.get(cpr.pack_name)) |arg_tys| { + break :blk self.buildPackSliceValue(arg_tys); + } + } + if (self.type_bindings) |tb| { + if (tb.get(cpr.pack_name)) |ty| { + break :blk self.builder.constType(ty); + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name}); + } + break :blk self.builder.constNull(self.module.types.sliceOf(.any)); + }, + // Pack-index in expression position: `$[]` → + // `const_type(arg_types[index])`. Yields a comptime-only + // Type value (`Value.type_tag(TypeId)` in the interp). + // OOB / no-active-pack-binding → focused diagnostic; the + // emitted Ref is a const_type(.void) placeholder so the + // verifier downstream catches misuse rather than silently + // succeeding with .void. + .pack_index_type_expr => |pi| blk: { + if (self.pack_arg_types) |pat| { + if (pat.get(pi.pack_name)) |arg_tys| { + if (pi.index < arg_tys.len) { + break :blk self.builder.constType(arg_tys[pi.index]); + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ + pi.pack_name, pi.index, pi.pack_name, arg_tys.len, + if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), + }); + } + break :blk self.builder.constType(.void); + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ + pi.pack_name, pi.index, + }); + } + break :blk self.builder.constType(.void); + }, + .int_literal => |lit| { + // If target is a float type, emit as float literal + if (self.target_type) |tt| { + if (tt == .f32 or tt == .f64) { + return self.builder.constFloat(@floatFromInt(lit.value), tt); + } + } + const ty = if (self.target_type) |tt| blk: { + break :blk if (self.isIntEx(tt)) tt else .s64; + } else .s64; + return self.builder.constInt(lit.value, ty); + }, + .float_literal => |lit| { + const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64; + return self.builder.constFloat(lit.value, fty); + }, + .bool_literal => |lit| self.builder.constBool(lit.value), + .string_literal => |lit| blk: { + const str = if (lit.is_raw) + lit.raw + else + unescape.unescapeString(self.alloc, lit.raw) catch lit.raw; + const sid = self.module.types.internString(str); + break :blk self.builder.constString(sid); + }, + // A bare `null` / `---` with no surrounding type expectation is a + // legitimate typeless literal, not a failed lookup: `.void` is its + // intentional default (emitConstNull/emitConstUndef handle void as + // null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire. + .null_literal => self.builder.constNull(self.target_type orelse .void), + .undef_literal => self.builder.constUndef(self.target_type orelse .void), + + .identifier => |id| blk: { + // A bare pack name in value position has no runtime + // representation (Decision 1). Projections (`xs.len`, `xs[i]`, + // `xs.value`) are field/index nodes handled elsewhere, so a bare + // `xs` reaching here is always a pack-as-value misuse. + if (self.isPackName(id.name)) { + break :blk self.diagPackAsValue(id.name, node.span, .generic); + } + if (self.scope) |scope| { + if (scope.lookup(id.name)) |binding| { + if (binding.is_alloca) { + break :blk self.builder.load(binding.ref, binding.ty); + } + break :blk binding.ref; + } + } + // Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals + if (self.comptime_constants.get(id.name)) |cv| { + switch (cv) { + .int_val => |iv| break :blk self.builder.constInt(iv, .s64), + .enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty), + } + } + // `context` resolves to a load through the lowering's + // current `__sx_ctx` pointer. Every sx function (and + // every `push Context.{...}` body) sets `current_ctx_ref` + // to a `*Context` it owns, so this is one indirection. + if (std.mem.eql(u8, id.name, "context")) { + if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) { + break :blk self.diagnoseMissingContext("the `context` identifier"); + } + const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse { + break :blk self.diagnoseMissingContext("the `context` identifier"); + }; + break :blk self.builder.load(self.current_ctx_ref, ctx_ty); + } + // Check globals (#run constants) + if (self.program_index.global_names.get(id.name)) |gi| { + break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); + } + // Check module-level value constants (e.g. AF_INET :s32: 2) + if (self.program_index.module_const_map.get(id.name)) |ci_global| { + if (!self.isNameVisible(id.name)) { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name}); + break :blk self.emitError(id.name, node.span); + } + // F2: emit the SOURCE-AWARE author's value (own-wins), not the + // global last-wins `ci_global`. ≥2 flat-visible same-name const + // authors → a loud ambiguity (issue 0105 / 0760), never a silent + // pick. `.none` after a visible name is the registration-only + // author (no per-source partition) — emit its global value. + switch (self.selectModuleConst(id.name)) { + .resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source), + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name}); + break :blk self.emitPlaceholder(id.name); + }, + .none => break :blk self.emitModuleConst(ci_global, null), + } + } + // Check if it's a function name — produce function pointer reference + // Resolve mangled name for block-local functions + const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name; + if (self.program_index.fn_ast_map.contains(eff_fn_name)) { + // Visibility check only for user-typed bare names (id.name + // == eff_fn_name) without a UFCS alias. Mangled local- + // scope names and UFCS rewrites are compiler indirections + // and stay exempt. + if (std.mem.eql(u8, eff_fn_name, id.name) and + self.program_index.ufcs_alias_map.get(id.name) == null and + !self.isNameVisible(eff_fn_name)) + { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name}); + break :blk self.emitError(eff_fn_name, node.span); + } + // Type-as-value: if target is Any (Type variable), produce a type name string + if (self.target_type == .any) { + const fd = self.program_index.fn_ast_map.get(eff_fn_name).?; + const fn_type_str = self.formatFnTypeString(fd); + const sid = self.module.types.internString(fn_type_str); + const str = self.builder.constString(sid); + break :blk self.builder.boxAny(str, .string); + } + // fix-0102d site 2: taking a bare same-name fn as a VALUE + // (func_ref, fn-ptr / closure coercion) must capture the + // RESOLVED author's FuncId for a genuine flat collision, not + // the first-wins winner's. Plain bare name only; `.ambiguous` + // → loud diagnostic; `.none` → existing first-wins path. The + // winner is lazily lowered ONLY on `.none` — a rerouted value + // never uses the winner, so its body must not be lowered. + const value_fid: ?FuncId = blk_fv: { + if (std.mem.eql(u8, eff_fn_name, id.name) and + self.program_index.ufcs_alias_map.get(id.name) == null and + (if (self.scope) |scope| scope.lookup(id.name) == null else true)) + { + if (self.current_source_file) |caller_file| { + switch (self.selectPlainCallableAuthor(id.name, caller_file)) { + .func => |sf| { + var selected = sf; + break :blk_fv self.selectedFuncId(&selected, id.name); + }, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name}); + break :blk self.emitError(id.name, node.span); + }, + .none => {}, + } + } + } + if (!self.lowered_functions.contains(eff_fn_name)) { + self.lazyLowerFunction(eff_fn_name); + } + break :blk_fv self.resolveFuncByName(eff_fn_name); + }; + if (value_fid) |fid| { + // Auto-promote bare function → closure when target_type is closure + if (self.target_type) |tt| { + if (!tt.isBuiltin()) { + const tt_info = self.module.types.get(tt); + if (tt_info == .closure) { + const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure); + break :blk self.builder.closureCreate(tramp_id, Ref.none, tt); + } + // Coercing a bare fn name to a fn-pointer + // type — the call_conv must match. A + // default-conv sx fn assigned to a + // callconv(.c) slot (e.g. passed to + // pthread_create) would otherwise crash at + // runtime when the C caller doesn't supply + // the implicit __sx_ctx arg. + if (tt_info == .function) { + const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv; + if (func_cc != tt_info.function.call_conv) { + if (self.diagnostics) |d| { + const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention"; + const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention"; + d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc }); + } + break :blk self.emitPlaceholder(eff_fn_name); + } + } + // NOTE: `xx : *void` (e.g. + // `class_addMethod(_, _, xx my_imp, _)`) + // is intentionally NOT diagnosed here. + // Manually-constructed Closure values + // legitimately store default-conv sx fns + // into a `*void` slot for sx-side dispatch + // through the closure trampoline ABI. The + // compiler can't distinguish C-side vs + // sx-side use from the cast alone. + // examples/50-smoke.sx has both shapes. + } + } + break :blk self.builder.emit(.{ .func_ref = fid }, .s64); + } + } + // Type-as-value: a name that resolves to a TypeId + // (primitive, alias, registered struct/enum/union, + // generic-struct instantiation) evaluates to a + // `const_type` in expression position. Works for + // direct assignment to a `Type`-typed slot + // (`x: Type = Vec4`), comparison (`x == Vec4`), and + // pack-arg / Any context (boxing happens at the + // consumer). + // E4 single-hop visibility + ambiguity gate: a bare type name used + // as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over + // 2+ flat hops is not bare-visible (consistent with annotations / + // 0763); ≥2 direct flat same-name authors are ambiguous (loud + // diagnostic, 0755/0767). A single source-keyed author — including + // the querying source's OWN author over a same-name flat import + // (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name + // author a global `findByName` would pick. A value name / generic + // param / undeclared name → `.proceed`, falling through below. + const ty = blk_ty: { + switch (self.headTypeGate(id.name, node.span)) { + .ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name), + .resolved => |tid| break :blk_ty tid, + .proceed => {}, + } + if (self.type_bindings) |tb| { + if (tb.get(id.name)) |t| break :blk_ty t; + } + if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t; + if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t; + const name_id = self.module.types.internString(id.name); + if (self.module.types.findByName(name_id)) |t| break :blk_ty t; + break :blk_ty TypeId.void; + }; + if (ty != .void) { + break :blk self.builder.constType(ty); + } + // Unknown identifier + break :blk self.emitError(id.name, node.span); + }, + + .binary_op => |bop| self.lowerBinaryOp(&bop), + + .unary_op => |uop| blk: { + // `xx ` with a slice target materializes the comptime + // pack into a runtime `[]elem` (issue 0053). Must run before the + // operand is lowered (a bare pack name otherwise hits the + // pack-as-value error). + if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) { + const pname = uop.operand.data.identifier.name; + if (self.target_type) |tt| { + if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) { + break :blk self.lowerPackToSlice(pname, tt); + } + } + break :blk self.diagPackAsValue(pname, node.span, .generic); + } + // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of + if (uop.op == .address_of and uop.operand.data == .index_expr) { + const ie = &uop.operand.data.index_expr; + const idx = self.lowerExpr(ie.index); + const obj_ty = self.inferExprType(ie.object); + const elem_ty = self.getElementType(obj_ty); + const ptr_ty = self.module.types.ptrTo(elem_ty); + // For array targets, use the storage pointer (alloca for a + // local, global_addr for a module global) so the resulting + // pointer is into live storage, not a loaded copy. + const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; + const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); + break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); + } + // address_of(field_access) → use lowerExprAsPtr for GEP chain + // Handles all cases: pointer-based, index-based, nested field access + if (uop.op == .address_of and uop.operand.data == .field_access) { + const inner_ty = self.inferExprType(uop.operand); + const ptr_ty = self.module.types.ptrTo(inner_ty); + const ptr = self.lowerExprAsPtr(uop.operand); + break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty); + } + // address_of(identifier) → return alloca directly (pointer to variable) + if (uop.op == .address_of and uop.operand.data == .identifier) { + const id_name = uop.operand.data.identifier.name; + if (self.scope) |scope| { + if (scope.lookup(id_name)) |binding| { + if (binding.is_alloca) { + const ptr_ty = self.module.types.ptrTo(binding.ty); + break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); + } + } + } + // address_of(global) → emit global_addr (pointer to global, not load) + if (self.program_index.global_names.get(id_name)) |gi| { + const ptr_ty = self.module.types.ptrTo(gi.ty); + break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); + } + } + const operand = self.lowerExpr(uop.operand); + break :blk switch (uop.op) { + .negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)), + .not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool), + .bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)), + .xx => self.lowerXX(operand, uop.operand), + .address_of => blk2: { + const inner_ty = self.inferExprType(uop.operand); + const ptr_ty = self.module.types.ptrTo(inner_ty); + break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty); + }, + }; + }, + + .if_expr => |ie| self.lowerIfExpr(&ie), + .match_expr => |me| self.lowerMatch(&me), + .while_expr => |we| self.lowerWhile(&we), + .for_expr => |fe| self.lowerFor(&fe), + .break_expr => self.lowerBreak(), + .continue_expr => self.lowerContinue(), + .call => |c| self.lowerCall(&c), + .ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic), + .field_access => |fa| self.lowerFieldAccess(&fa, node.span), + .struct_literal => |sl| self.lowerStructLiteral(&sl, node.span), + .array_literal => |al| self.lowerArrayLiteral(&al), + .index_expr => |ie| self.lowerIndexExpr(&ie), + .slice_expr => |se| self.lowerSliceExpr(&se), + .lambda => |lam| self.lowerLambda(&lam), + .force_unwrap => |fu| self.lowerForceUnwrap(&fu), + .null_coalesce => |nc| self.lowerNullCoalesce(&nc), + .deref_expr => |de| self.lowerDerefExpr(&de), + .enum_literal => |el| self.lowerEnumLiteral(&el), + .comptime_expr => |ct| self.lowerInlineComptime(ct.expr), + .insert_expr => |ins| blk: { + break :blk self.lowerInsertExprValue(ins.expr); + }, + .tuple_literal => |tl| self.lowerTupleLiteral(&tl), + .spread_expr => self.emitError("spread_expr", node.span), + .chained_comparison => |cc| self.lowerChainedComparison(&cc), + + // `#jni_env(env) { body }` in expression position — the block's + // value becomes the env-scope's value. Save→set→body-value→restore. + .jni_env_block => |eb| blk: { + const env_ref = self.lowerExpr(eb.env); + const fids = self.getJniEnvTlFids(); + const ptr_ty = self.module.types.ptrTo(.void); + const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty); + const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable; + _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void); + self.jni_env_stack.append(self.alloc, env_ref) catch unreachable; + const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void); + _ = self.jni_env_stack.pop(); + const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable; + _ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void); + break :blk value; + }, + + // Statements that can appear in expression position + .block => |blk| blk: { + // Create a child scope for block-level variable shadowing + var block_scope = Scope.init(self.alloc, self.scope); + const saved_scope = self.scope; + self.scope = &block_scope; + const saved_defer_len = self.defer_stack.items.len; + defer { + self.emitBlockDefers(saved_defer_len); + self.scope = saved_scope; + block_scope.deinit(); + } + // This block sits in value position (lowerExpr is reached only + // for value contexts — statement blocks go through lowerBlock). + // If its last expression's value is discarded by a `;`, the + // surrounding expression has no value to use: report it. + if (!blk.produces_value and blk.discarded_semi != null) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{}); + } + } + // A block in expression position yields its last statement's + // value only when it produces one (no trailing `;`); otherwise + // it runs as statements and evaluates to void. + if (blk.produces_value and blk.stmts.len > 0) { + for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { + self.lowerStmt(stmt); + } + break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse + self.builder.constInt(0, .void); + } + for (blk.stmts) |stmt| { + self.lowerStmt(stmt); + } + break :blk self.builder.constInt(0, .void); + }, + + // type_expr can appear as a variable reference when the name collides + // with a builtin type name (e.g. s2, u8). Check scope first. + .type_expr => |te| blk: { + if (self.scope) |scope| { + if (scope.lookup(te.name)) |binding| { + if (binding.is_alloca) { + break :blk self.builder.load(binding.ref, binding.ty); + } + break :blk binding.ref; + } + } + if (self.program_index.global_names.get(te.name)) |gi| { + break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty); + } + // Type literal in expression position → first-class + // `const_type` Value (i64 = TypeId.index()). Makes + // `t : Type = f64;` store a real TypeId; lets + // `t == f64` icmp at runtime against the same TypeId. + if (self.isKnownTypeName(te.name)) { + const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + break :blk self.builder.constType(ty); + } + break :blk self.emitError(te.name, node.span); + }, + + .try_expr => |te| self.lowerTry(te.operand, node.span), + .catch_expr => |ce| self.lowerCatch(&ce, node.span), + .caller_location => self.lowerCallerLocation(node), + else => self.emitError("unknown_expr", node.span), + }; +} + +/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns +/// the element (pointee) type so a value-position use can auto-deref it. +pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId { + if (node.data != .identifier) return null; + const scope = self.scope orelse return null; + const binding = scope.lookup(node.data.identifier.name) orelse return null; + if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null; + const info = self.module.types.get(binding.ty); + return if (info == .pointer) info.pointer.pointee else null; +} + +pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { + // Short-circuit: `a and b` → if a then b else false + if (bop.op == .and_op) { + const lhs = self.lowerExpr(bop.lhs); + const rhs_bb = self.freshBlock("and.rhs"); + const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool}); + const false_val = self.builder.constBool(false); + self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val}); + self.builder.switchToBlock(rhs_bb); + const rhs = self.lowerExpr(bop.rhs); + self.builder.br(merge_bb, &.{rhs}); + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, .bool); + } + // Short-circuit: `a or b` → if a then true else b + if (bop.op == .or_op) { + // A failable `or` (value-terminator or chain) routes to the error- + // handling lowering, not the optional/boolean unwrap below. Detected + // structurally (a `try`-chain's value type is non-failable `T`, so a + // type-only `exprIsFailable(lhs)` would miss nested chains). + if (self.orIsFailableChain(bop)) { + return self.lowerFailableOr(bop); + } + const lhs = self.lowerExpr(bop.lhs); + const rhs_bb = self.freshBlock("or.rhs"); + const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool}); + const true_val = self.builder.constBool(true); + self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{}); + self.builder.switchToBlock(rhs_bb); + const rhs = self.lowerExpr(bop.rhs); + self.builder.br(merge_bb, &.{rhs}); + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, .bool); + } + + // Type-literal comparison fold: when both sides are type-shaped + // AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to + // a static TypeId at lower time (`type_of(x)` for any + // statically-typed `x`), resolve each and emit a `const_bool`. + // Same semantic as `type_eq(A, B)` but using the standard `==` + // operator — the user's intuition. Without the fold, both + // sides lower as `const_type` undef-i64 and the runtime icmp + // returns garbage. + if (bop.op == .eq or bop.op == .neq) { + if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) { + const lhs_ty = self.resolveTypeArg(bop.lhs); + const rhs_ty = self.resolveTypeArg(bop.rhs); + const eq_result = lhs_ty == rhs_ty; + return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result); + } + } + + // Any-shaped `==` (e.g. `t == s64` where `t: Type`): both + // operands are 16-byte `{tag, value}` aggregates. LLVM + // doesn't accept `icmp` on aggregates directly. Decompose + // via `unbox_any` (which extracts the value field at + // `.s64`) and compare the i64s. Tag fields are stable + // across compilations of the same source so value-only + // identity is enough. + if (bop.op == .eq or bop.op == .neq) { + const lhs_ty = self.inferExprType(bop.lhs); + const rhs_ty = self.inferExprType(bop.rhs); + if (lhs_ty == .any and rhs_ty == .any) { + const lhs = self.lowerExpr(bop.lhs); + const rhs = self.lowerExpr(bop.rhs); + const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64); + const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64); + if (bop.op == .eq) { + return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); + } else { + return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool); + } + } + } + + // Special case: optional == null / optional != null + if (bop.op == .eq or bop.op == .neq) { + const lhs_is_null = bop.lhs.data == .null_literal; + const rhs_is_null = bop.rhs.data == .null_literal; + if (lhs_is_null or rhs_is_null) { + const opt_node = if (rhs_is_null) bop.lhs else bop.rhs; + const opt_ty = self.inferExprType(opt_node); + if (!opt_ty.isBuiltin()) { + const info = self.module.types.get(opt_ty); + if (info == .optional) { + const opt_val = self.lowerExpr(opt_node); + const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); + // == null → !has_value, != null → has_value + return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has; + } + } + } + } + + // Error-set equality: an error-set value compares only with an + // `error.X` tag literal or another error-set value. Comparing to a raw + // integer is a type error (coerce with `xx`). `e == error.X` resolves + // X against e's set and validates membership. + if (bop.op == .eq or bop.op == .neq) { + if (self.tryLowerErrorSetEquality(bop)) |result| return result; + } + + // Set target_type for null literals to match the other operand's type. + // This ensures null gets the same LLVM type as the value being compared. + if (bop.op == .eq or bop.op == .neq) { + const null_on_rhs = bop.rhs.data == .null_literal; + const null_on_lhs = bop.lhs.data == .null_literal; + if (null_on_rhs or null_on_lhs) { + var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs); + // Lower the non-null side first when its type isn't statically + // inferable, and take the null's type from the lowered value — + // never a guess. + var pre_lowered: ?Ref = null; + if (other_ty == .unresolved) { + pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs); + other_ty = self.builder.getRefType(pre_lowered.?); + } + if (other_ty != .void and other_ty != .unresolved) { + const saved_tt = self.target_type; + self.target_type = other_ty; + const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?; + const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?; + self.target_type = saved_tt; + const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }; + return self.builder.emit(cmp_op, .bool); + } + } + } + var lhs = self.lowerExpr(bop.lhs); + // A `for xs: (*x)` capture is a pointer; in a value position (here, an + // operand) it auto-derefs to the element. + const lhs_ref_pointee = self.refCapturePointee(bop.lhs); + if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p); + // Set target_type from LHS so enum literals on RHS resolve correctly. + // When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use + // the lowered operand's concrete type rather than a guess. + const lhs_ty = blk: { + if (lhs_ref_pointee) |p| break :blk p; + const it = self.inferExprType(bop.lhs); + break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it; + }; + const saved_tt = self.target_type; + if (lhs_ty != .void) { + if (!lhs_ty.isBuiltin()) { + const lhs_info = self.module.types.get(lhs_ty); + if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) { + self.target_type = lhs_ty; + } + } else if (lhs_ty == .f32 or lhs_ty == .f64) { + self.target_type = lhs_ty; + } + } + var rhs = self.lowerExpr(bop.rhs); + const rhs_ref_pointee = self.refCapturePointee(bop.rhs); + if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p); + self.target_type = saved_tt; + // Result type follows the shared promotion rule: an int LHS with a + // float RHS promotes to the float (`s64 * f32` → `f32`); vectors / + // structs keep the LHS type. `inferExprType` reuses the same helper + // so static typing agrees with the value produced here. + const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs); + var ty = arithResultType(lhs_ty, rhs_inferred); + + // Auto-unwrap optional operands for arithmetic/comparison + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .optional) { + ty = info.optional.child; + lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty); + } + } + const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs); + if (!rhs_ty.isBuiltin()) { + const rhs_info = self.module.types.get(rhs_ty); + if (rhs_info == .optional) { + rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child); + } + } + + // String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison + if (ty == .string and (bop.op == .eq or bop.op == .neq)) { + return if (bop.op == .eq) + self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool) + else + self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool); + } + + // Tuple operators + if (!ty.isBuiltin()) { + const lhs_info = self.module.types.get(ty); + if (lhs_info == .tuple) { + return self.lowerTupleOp(bop, lhs, rhs, ty); + } + } + // Tuple membership: value in (tuple) + if (bop.op == .in_op) { + const rhs_ty_raw = self.inferExprType(bop.rhs); + if (!rhs_ty_raw.isBuiltin()) { + const rhs_info_raw = self.module.types.get(rhs_ty_raw); + if (rhs_info_raw == .tuple) { + return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple); + } + } + } + + // Reject scalar ops on incompatible operand types (e.g. + // `s64 + string`, `s64 < string`, `s64 & string`). The result type + // `ty` is derived from the LHS, so without this the op lowers as + // ` : ` and either reinterprets the RHS bytes (arithmetic + // / bitwise → garbage) or feeds mismatched LLVM types to `icmp` + // (ordering → verifier failure). + { + const group: enum { none, arith, ordering, bitwise } = switch (bop.op) { + .add, .sub, .mul, .div, .mod => .arith, + .lt, .lte, .gt, .gte => .ordering, + .bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise, + else => .none, + }; + if (group != .none) { + const eff_rhs_ty = blk: { + if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs); + if (!rhs_ty.isBuiltin()) { + const ri = self.module.types.get(rhs_ty); + if (ri == .optional) break :blk ri.optional.child; + } + break :blk rhs_ty; + }; + const ok = switch (group) { + .arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty), + .ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty), + .bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty), + .none => true, + }; + if (!ok) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{ + binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty), + }); + } + return self.emitPlaceholder("operand-type-mismatch"); + } + } + } + + return switch (bop.op) { + .add => self.builder.add(lhs, rhs, ty), + .sub => self.builder.sub(lhs, rhs, ty), + .mul => self.builder.mul(lhs, rhs, ty), + .div => self.builder.div(lhs, rhs, ty), + .mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty), + .eq => self.builder.cmpEq(lhs, rhs), + .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .lt => self.builder.cmpLt(lhs, rhs), + .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .gt => self.builder.cmpGt(lhs, rhs), + .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty), + .bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty), + .bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), + .shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), + .shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), + .in_op => self.emitError("in_op", bop.lhs.span), + }; +} + +/// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=) +pub fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref { + const lhs_info = self.module.types.get(lhs_ty); + const lhs_fields = lhs_info.tuple.fields; + + switch (bop.op) { + .add => { + // Tuple concatenation: (a, b) + (c, d) → (a, b, c, d) + const rhs_ty = self.inferExprType(bop.rhs); + const rhs_fields = if (!rhs_ty.isBuiltin()) blk: { + const ri = self.module.types.get(rhs_ty); + break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{}; + } else &[_]TypeId{}; + + var all_fields = std.ArrayList(TypeId).empty; + defer all_fields.deinit(self.alloc); + var all_vals = std.ArrayList(Ref).empty; + defer all_vals.deinit(self.alloc); + + for (lhs_fields, 0..) |f, i| { + all_fields.append(self.alloc, f) catch unreachable; + all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; + } + for (rhs_fields, 0..) |f, i| { + all_fields.append(self.alloc, f) catch unreachable; + all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable; + } + + const result_ty = self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, + .names = null, + } }); + const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); + }, + .mul => { + // Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b) + const count: usize = switch (bop.rhs.data) { + .int_literal => |il| @intCast(@as(u64, @bitCast(il.value))), + else => 1, + }; + + var all_fields = std.ArrayList(TypeId).empty; + defer all_fields.deinit(self.alloc); + var all_vals = std.ArrayList(Ref).empty; + defer all_vals.deinit(self.alloc); + + for (0..count) |_| { + for (lhs_fields, 0..) |f, i| { + all_fields.append(self.alloc, f) catch unreachable; + all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable; + } + } + + const result_ty = self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable, + .names = null, + } }); + const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable; + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty); + }, + .eq, .neq => { + // Element-wise equality (or single-element tuple vs scalar) + const rhs_is_tuple = blk: { + const rt = self.inferExprType(bop.rhs); + if (!rt.isBuiltin()) { + break :blk self.module.types.get(rt) == .tuple; + } + break :blk false; + }; + if (!rhs_is_tuple and lhs_fields.len == 1) { + // Single-element tuple vs scalar: unwrap and compare + const lf = self.builder.structGet(lhs, 0, lhs_fields[0]); + const eq = self.builder.cmpEq(lf, rhs); + return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq; + } + var result = self.builder.constBool(true); + for (lhs_fields, 0..) |f, i| { + const lf = self.builder.structGet(lhs, @intCast(i), f); + const rf = self.builder.structGet(rhs, @intCast(i), f); + const eq = self.builder.cmpEq(lf, rf); + result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool); + } + return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result; + }, + .lt, .lte, .gt, .gte => { + // Lexicographic comparison + return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields); + }, + else => return self.builder.constInt(0, .s64), + } +} + +pub fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref { + // Lexicographic comparison using boolean logic. + // (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1) + // (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1) + if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte); + + const n = fields.len; + // Start with the last field using the actual op + const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]); + const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]); + var result = switch (op) { + .lt => self.builder.cmpLt(lf_last, rf_last), + .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), + .gt => self.builder.cmpGt(lf_last, rf_last), + .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool), + else => unreachable, + }; + + // Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result) + if (n > 1) { + var i: usize = n - 1; + while (i > 0) { + i -= 1; + const lf = self.builder.structGet(lhs, @intCast(i), fields[i]); + const rf = self.builder.structGet(rhs, @intCast(i), fields[i]); + const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf); + const eq = self.builder.cmpEq(lf, rf); + const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool); + result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool); + } + } + return result; +} + +pub fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref { + // value in (a, b, c) → value == a || value == b || value == c + var result = self.builder.constBool(false); + for (tuple_info.fields, 0..) |f, i| { + const elem = self.builder.structGet(tuple, @intCast(i), f); + const eq = self.builder.cmpEq(value, elem); + result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool); + } + return result; +} + +// ── Control flow ──────────────────────────────────────────────── + +pub fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref { + // a < b < c → (a < b) and (b < c) + // Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice. + if (cc.operands.len < 2 or cc.ops.len == 0) { + return self.builder.constBool(true); + } + + var refs = std.ArrayList(Ref).empty; + defer refs.deinit(self.alloc); + for (cc.operands) |op| { + refs.append(self.alloc, self.lowerExpr(op)) catch unreachable; + } + + var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]); + + var i: usize = 1; + while (i < cc.ops.len) : (i += 1) { + const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]); + result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool); + } + + return result; +} + +pub fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref { + return switch (op) { + .eq => self.builder.cmpEq(lhs, rhs), + .neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .lt => self.builder.cmpLt(lhs, rhs), + .lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool), + .gt => self.builder.cmpGt(lhs, rhs), + .gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool), + else => self.builder.constBool(false), + }; +} + +// ── Defer/Push/MultiAssign ──────────────────────────────────────