diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 503c5b8..a01fe7c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -501,927 +501,7 @@ 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 lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref { + 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"; @@ -2115,44 +1195,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); @@ -2752,7 +1794,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; @@ -2767,7 +1809,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; @@ -2781,7 +1823,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; @@ -2816,7 +1858,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 => "-", @@ -3280,4 +2322,14 @@ pub const Lowering = struct { 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; }; diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 76663a9..5fd11db 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -47,6 +47,9 @@ 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; @@ -1422,3 +1425,961 @@ pub fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId { } // ── 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 ──────────────────────────────────────