diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 91f590d..a05eca4 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -36,6 +36,7 @@ const semantic_diagnostics = @import("semantic_diagnostics.zig"); const lower_error = @import("lower/error.zig"); const lower_comptime = @import("lower/comptime.zig"); const lower_stmt = @import("lower/stmt.zig"); +const lower_control_flow = @import("lower/control_flow.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -3673,201 +3674,6 @@ pub const Lowering = struct { // ── Control flow ──────────────────────────────────────────────── - fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { - // inline if: evaluate condition at compile time, only lower taken branch - if (ie.is_comptime) { - if (self.evalComptimeCondition(ie.condition)) |is_true| { - if (is_true) { - return self.lowerInlineBranch(ie.then_branch); - } else if (ie.else_branch) |eb| { - return self.lowerInlineBranch(eb); - } - return self.builder.constInt(0, .void); - } - // Condition couldn't be evaluated — fall through to runtime - } - - // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors - if (self.tryConstBoolCondition(ie.condition)) |is_true| { - if (is_true) { - // Condition always true: only lower then-branch - if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) { - return self.lowerExpr(ie.then_branch); - } - self.lowerBlock(ie.then_branch); - // If then-branch terminated (return/break), mark block as dead - if (self.currentBlockHasTerminator()) { - self.block_terminated = true; - return .none; - } - return self.builder.constInt(0, .void); - } else { - // Condition always false: only lower else-branch (if any) - if (ie.else_branch) |eb| { - if (ie.is_inline or self.force_block_value) { - return self.lowerExpr(eb); - } - self.lowerBlock(eb); - if (self.currentBlockHasTerminator()) { - self.block_terminated = true; - return .none; - } - } - return self.builder.constInt(0, .void); - } - } - - // Optional binding: `if val := expr { ... }` - // Clear target_type so the ternary's result type doesn't leak into the condition - // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32) - const saved_cond_target = self.target_type; - self.target_type = null; - const opt_val = self.lowerExpr(ie.condition); - self.target_type = saved_cond_target; - const cond = if (ie.binding_name != null) blk: { - // The condition is an optional — emit has_value check - break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); - } else opt_val; - const has_else = ie.else_branch != null; - // If-else produces a value when inline OR when in value position (force_block_value) - var is_value = (ie.is_inline or self.force_block_value) and has_else; - - // Infer result type from then branch for value if-exprs - // If then_branch is null/void, try else_branch (e.g., `if cond then null else val`) - var result_type: TypeId = if (is_value) blk: { - var t = self.inferExprType(ie.then_branch); - if ((t == .void or t == .unresolved) and ie.else_branch != null) { - t = self.inferExprType(ie.else_branch.?); - } - // Branch type not statically inferable (e.g. `null` / a bare enum - // literal) — use the contextually expected type rather than a guess. - if (t == .unresolved) { - if (self.target_type) |tt| t = tt; - } - break :blk t; - } else .void; - - // A value-position if/else whose branches yield no value (both are - // `;`-terminated / void blocks) is really a statement-if — lowering it - // as a value would build a `phi void`. Demote it. - if (is_value and result_type == .void) { - is_value = false; - result_type = .void; - } - - const then_bb = self.freshBlock("if.then"); - const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null; - const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{}; - const merge_bb = self.freshBlockWithParams("if.merge", merge_params); - - // Conditional branch - self.builder.condBr( - cond, - then_bb, - &.{}, - if (else_bb) |eb| eb else merge_bb, - &.{}, - ); - - // Then branch - self.builder.switchToBlock(then_bb); - // If binding: unwrap the optional and bind to the name - if (ie.binding_name) |bind_name| { - const opt_ty = self.inferExprType(ie.condition); - const inner_ty = if (!opt_ty.isBuiltin()) blk: { - const info = self.module.types.get(opt_ty); - break :blk if (info == .optional) info.optional.child else opt_ty; - } else opt_ty; - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty); - const slot = self.builder.alloca(inner_ty); - self.builder.store(slot, unwrapped); - if (self.scope) |scope| { - scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true }); - } - } - // Set target_type so null/undef in branches get the right type - const saved_target = self.target_type; - if (is_value and result_type != .void) self.target_type = result_type; - if (is_value) { - var v = self.lowerExpr(ie.then_branch); - if (!self.currentBlockHasTerminator()) { - const v_ty = self.builder.getRefType(v); - if (v_ty != result_type and v_ty != .void and result_type != .void) { - v = self.coerceToType(v, v_ty, result_type); - } - self.builder.br(merge_bb, &.{v}); - } - } else { - self.lowerBlock(ie.then_branch); - if (!self.currentBlockHasTerminator()) { - self.builder.br(merge_bb, &.{}); - } - } - - // Else branch - if (has_else) { - self.builder.switchToBlock(else_bb.?); - if (is_value) { - var v = self.lowerExpr(ie.else_branch.?); - if (!self.currentBlockHasTerminator()) { - const v_ty = self.builder.getRefType(v); - if (v_ty != result_type and v_ty != .void and result_type != .void) { - v = self.coerceToType(v, v_ty, result_type); - } - self.builder.br(merge_bb, &.{v}); - } - } else { - self.lowerBlock(ie.else_branch.?); - if (!self.currentBlockHasTerminator()) { - self.builder.br(merge_bb, &.{}); - } - } - } - self.target_type = saved_target; - - // Continue at merge - self.builder.switchToBlock(merge_bb); - if (is_value) { - return self.builder.blockParam(merge_bb, 0, result_type); - } - return self.builder.constInt(0, .void); - } - - /// Try to evaluate an AST condition as a compile-time constant bool. - /// Returns true/false if the condition is known at compile time, null otherwise. - fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool { - switch (node.data) { - .bool_literal => |bl| return bl.value, - .call => |c| { - if (c.callee.data == .identifier) { - const cname = c.callee.data.identifier.name; - if (std.mem.eql(u8, cname, "is_flags")) { - // Resolve the type arg to check if it's actually a flags enum - if (c.args.len > 0) { - const ty = self.resolveTypeArg(c.args[0]); - if (!ty.isBuiltin()) { - const info = self.module.types.get(ty); - if (info == .@"enum") return info.@"enum".is_flags; - } - } - return false; - } - if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) { - const a = self.resolveTypeArg(c.args[0]); - const b = self.resolveTypeArg(c.args[1]); - return a == b; - } - if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) { - const ty = self.resolveTypeArg(c.args[1]); - return self.computeHasImpl(c.args[0], ty); - } - } - }, - else => {}, - } - return null; - } - /// Shared implementation for the `has_impl(P, T)` builtin and its /// `tryConstBoolCondition` arm. The protocol expression is either: /// - Plain `Hash` (identifier / type_expr) → walks @@ -3876,7 +3682,7 @@ pub const Lowering = struct { /// keyed by `"

\x00\x00"`. /// Returns false on any malformed protocol-arg shape (caller /// reports a diagnostic if it wants). - fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool { + pub fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool { switch (proto_node.data) { .identifier => |id| return self.protocolResolver().hasImplPlain(id.name, ty), .type_expr => |te| return self.protocolResolver().hasImplPlain(te.name, ty), @@ -3906,673 +3712,6 @@ pub const Lowering = struct { } } - fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { - const header_bb = self.freshBlock("while.hdr"); - const body_bb = self.freshBlock("while.body"); - const exit_bb = self.freshBlock("while.exit"); - - // Branch to header - self.builder.br(header_bb, &.{}); - - // Header: evaluate condition - self.builder.switchToBlock(header_bb); - const cond = self.lowerExpr(we.condition); - self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{}); - - // Body - self.builder.switchToBlock(body_bb); - - // Save and set loop targets - const old_break = self.break_target; - const old_continue = self.continue_target; - self.break_target = exit_bb; - self.continue_target = header_bb; - defer { - self.break_target = old_break; - self.continue_target = old_continue; - } - - self.lowerBlock(we.body); - if (!self.currentBlockHasTerminator()) { - self.builder.br(header_bb, &.{}); - } - - // Continue at exit - self.builder.switchToBlock(exit_bb); - return self.builder.constInt(0, .void); - } - - /// View a `List(T)`-like struct (`{ items: [*]T, len, … }`) as its backing - /// `items` pointer + element type + `len`, so `for list: (x)` iterates the - /// elements. Null for anything that isn't such a struct. - fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, data_ty: TypeId, len: Ref } { - if (ty.isBuiltin()) return null; - const info = self.module.types.get(ty); - if (info != .@"struct") return null; - const items_id = self.module.types.internString("items"); - const len_id = self.module.types.internString("len"); - var items_idx: ?u32 = null; - var items_ty: TypeId = .unresolved; - var len_idx: ?u32 = null; - for (info.@"struct".fields, 0..) |f, i| { - if (f.name == items_id and !f.ty.isBuiltin() and self.module.types.get(f.ty) == .many_pointer) { - items_idx = @intCast(i); - items_ty = f.ty; - } else if (f.name == len_id) { - len_idx = @intCast(i); - } - } - if (items_idx == null or len_idx == null) return null; - return .{ - .data = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = items_idx.? } }, items_ty), - .data_ty = items_ty, - .len = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = len_idx.? } }, .s64), - }; - } - - fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { - if (fe.range_end) |end_node| { - if (fe.is_inline) return self.lowerInlineRangeFor(fe, end_node); - return self.lowerRuntimeRangeFor(fe, end_node); - } - // Collection-form `for xs : (x)` over a pack: a pack has no runtime - // value to iterate (Decision 1) — point the user at `inline for`. - if (fe.iterable.data == .identifier and self.isPackName(fe.iterable.data.identifier.name)) { - return self.diagPackAsValue(fe.iterable.data.identifier.name, fe.iterable.span, .runtime_iter); - } - - // Lower iterable + resolve its static type. - var iterable = self.lowerExpr(fe.iterable); - var iterable_ty = self.inferExprType(fe.iterable); - - // `*List` / `*[]T` etc. — deref to the collection value. - const ptr_info = if (iterable_ty.isBuiltin()) null else self.module.types.get(iterable_ty); - if (ptr_info != null and ptr_info.? == .pointer) { - iterable = self.builder.load(iterable, ptr_info.?.pointer.pointee); - iterable_ty = ptr_info.?.pointer.pointee; - } - - // A `List(T)`-like struct iterates its `items[0..len]`; arrays/slices - // use their intrinsic length. - var len: Ref = undefined; - if (self.listView(iterable, iterable_ty)) |lv| { - iterable = lv.data; - iterable_ty = lv.data_ty; - len = lv.len; - } else { - len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64); - } - - // Create index variable - const idx_slot = self.builder.alloca(.s64); - const zero = self.builder.constInt(0, .s64); - self.builder.store(idx_slot, zero); - - const header_bb = self.freshBlock("for.hdr"); - const body_bb = self.freshBlock("for.body"); - const inc_bb = self.freshBlock("for.inc"); - const exit_bb = self.freshBlock("for.exit"); - - self.builder.br(header_bb, &.{}); - - // Header: compare index < length - self.builder.switchToBlock(header_bb); - const idx_val = self.builder.load(idx_slot, .s64); - const cmp = self.builder.cmpLt(idx_val, len); - self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); - - // Body - self.builder.switchToBlock(body_bb); - - // Bind element — resolve element type from iterable. `for xs: (*x)` - // binds a pointer into the collection (no per-element copy); `(x)` - // binds a value copy. - const elem_ty = self.getElementType(iterable_ty); - const bind_ty = if (fe.capture_by_ref) self.module.types.ptrTo(elem_ty) else elem_ty; - const elem = if (fe.capture_by_ref) blk: { - // A slice value carries its backing pointer, so GEP on it writes - // through. An array is a value — GEP needs its storage (alloca) or - // mutations would hit a copy. - const is_array = !iterable_ty.isBuiltin() and self.module.types.get(iterable_ty) == .array; - const base = if (is_array) (self.getExprAlloca(fe.iterable) orelse iterable) else iterable; - break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx_val } }, bind_ty); - } else self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, bind_ty); - - var body_scope = Scope.init(self.alloc, self.scope); - const old_scope = self.scope; - self.scope = &body_scope; - - body_scope.put(fe.capture_name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = fe.capture_by_ref }); - - // Bind index if requested - if (fe.index_name) |iname| { - body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); - } - - // Save and set loop targets - const old_break = self.break_target; - const old_continue = self.continue_target; - self.break_target = exit_bb; - self.continue_target = inc_bb; // continue → increment, not header - - self.lowerBlock(fe.body); - - self.break_target = old_break; - self.continue_target = old_continue; - self.scope = old_scope; - body_scope.deinit(); - - // Fall through to increment block - if (!self.currentBlockHasTerminator()) { - self.builder.br(inc_bb, &.{}); - } - - // Increment block: increment index and jump back to header - self.builder.switchToBlock(inc_bb); - { - const cur_idx = self.builder.load(idx_slot, .s64); - const one = self.builder.constInt(1, .s64); - const next_idx = self.builder.add(cur_idx, one, .s64); - self.builder.store(idx_slot, next_idx); - self.builder.br(header_bb, &.{}); - } - - // Continue at exit - self.builder.switchToBlock(exit_bb); - return self.builder.constInt(0, .void); - } - - /// Runtime counting loop `for start..end (i) { }` — `i` (optional) is the - /// cursor, `end` is exclusive. Lowers to the same header/inc/exit shape as - /// the collection form, minus the element fetch. - fn lowerRuntimeRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref { - const start = self.lowerExpr(fe.iterable); - const end = self.lowerExpr(end_node); - - const idx_slot = self.builder.alloca(.s64); - self.builder.store(idx_slot, start); - - const header_bb = self.freshBlock("for.hdr"); - const body_bb = self.freshBlock("for.body"); - const inc_bb = self.freshBlock("for.inc"); - const exit_bb = self.freshBlock("for.exit"); - - self.builder.br(header_bb, &.{}); - - self.builder.switchToBlock(header_bb); - const idx_val = self.builder.load(idx_slot, .s64); - const cmp = self.builder.cmpLt(idx_val, end); - self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); - - self.builder.switchToBlock(body_bb); - var body_scope = Scope.init(self.alloc, self.scope); - const old_scope = self.scope; - self.scope = &body_scope; - if (fe.capture_name.len > 0) { - body_scope.put(fe.capture_name, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); - } - - const old_break = self.break_target; - const old_continue = self.continue_target; - self.break_target = exit_bb; - self.continue_target = inc_bb; - - self.lowerBlock(fe.body); - - self.break_target = old_break; - self.continue_target = old_continue; - self.scope = old_scope; - body_scope.deinit(); - - if (!self.currentBlockHasTerminator()) { - self.builder.br(inc_bb, &.{}); - } - - self.builder.switchToBlock(inc_bb); - { - const cur_idx = self.builder.load(idx_slot, .s64); - const one = self.builder.constInt(1, .s64); - const next_idx = self.builder.add(cur_idx, one, .s64); - self.builder.store(idx_slot, next_idx); - self.builder.br(header_bb, &.{}); - } - - self.builder.switchToBlock(exit_bb); - return self.builder.constInt(0, .void); - } - - /// Comptime-unrolled `inline for start..end (i) { }`. `start`/`end` must be - /// comptime-known. The body is lowered `end - start` times with the cursor - /// bound as an `int_val` comptime constant, so `xs[i]` over a pack - /// substitutes the concrete per-position argument each iteration. - fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref { - const start = self.evalComptimeInt(fe.iterable) orelse { - if (self.diagnostics) |d| d.addFmt(.err, fe.iterable.span, "inline for: range start is not a compile-time integer", .{}); - return self.builder.constInt(0, .void); - }; - const end = self.evalComptimeInt(end_node) orelse { - if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{}); - return self.builder.constInt(0, .void); - }; - - var i: i64 = start; - while (i < end) : (i += 1) { - var body_scope = Scope.init(self.alloc, self.scope); - const old_scope = self.scope; - self.scope = &body_scope; - - // Bind the cursor both as a runtime value (constInt, for uses like - // `print(i)`) and as a comptime constant (for `xs[i]` substitution). - var had_prev = false; - var prev: ComptimeValue = undefined; - if (fe.capture_name.len > 0) { - body_scope.put(fe.capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false }); - if (self.comptime_constants.get(fe.capture_name)) |p| { - had_prev = true; - prev = p; - } - self.comptime_constants.put(fe.capture_name, .{ .int_val = i }) catch {}; - } - - self.lowerBlock(fe.body); - - if (fe.capture_name.len > 0) { - if (had_prev) { - self.comptime_constants.put(fe.capture_name, prev) catch {}; - } else { - _ = self.comptime_constants.remove(fe.capture_name); - } - } - - self.scope = old_scope; - body_scope.deinit(); - - if (self.currentBlockHasTerminator()) break; - } - - return self.builder.constInt(0, .void); - } - - fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { - // inline if match: evaluate at compile time, only lower the matching arm - if (me.is_comptime) { - if (self.evalComptimeMatch(me)) |arm_body| { - return self.lowerInlineBranch(arm_body); - } - // Couldn't evaluate — fall through to runtime - } - - const is_type_match = isTypeCategoryMatch(me); - var subject = self.lowerExpr(me.subject); - var subject_ty = self.inferExprType(me.subject); - // A pointer subject (e.g. a `for xs: (*x)` element capture) — deref to - // the pointed-to union/enum so tag/payload extraction works. - if (!subject_ty.isBuiltin()) { - const sinfo = self.module.types.get(subject_ty); - if (sinfo == .pointer and !sinfo.pointer.pointee.isBuiltin()) { - const pinfo = self.module.types.get(sinfo.pointer.pointee); - if (pinfo == .tagged_union or pinfo == .@"enum") { - subject = self.builder.load(subject, sinfo.pointer.pointee); - subject_ty = sinfo.pointer.pointee; - } - } - } - const is_optional_match = blk: { - if (!subject_ty.isBuiltin()) { - const info = self.module.types.get(subject_ty); - break :blk info == .optional; - } - break :blk false; - }; - // An error-set subject (`catch e == { case .X: ... }` / `if e == { ... }`): - // the value IS its u32 tag id, and `case .X` matches the global tag id - // of `X`. Used by ERR E1.5's catch match-body form. - const is_error_set_match = blk: { - if (!subject_ty.isBuiltin()) { - break :blk self.module.types.get(subject_ty) == .error_set; - } - break :blk false; - }; - - // Determine if the match produces a value (has non-void arms) - // For type-category matches (inside any_to_string), only produce value when force_block_value - // For regular enum/optional matches, always produce value if arms are non-void - var inferred_result = self.inferMatchResultType(me); - // Arms not statically inferable (bare enum literals etc.): only a - // value-position match (`force_block_value`) needs a concrete result — - // use the contextually expected type. A statement match with non-value - // arms is a side-effect (void); don't let a leaked `target_type` turn - // it into a value match. - if (inferred_result == .unresolved) { - inferred_result = if (self.force_block_value) (self.target_type orelse .unresolved) else .void; - } - const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved)); - const result_type: TypeId = if (is_value) inferred_result else .void; - // A fully-diverging match (`result_type == .noreturn` — every arm - // `return`s / `raise`s / etc.) produces no value, so it builds no - // merge phi; its arms terminate and the merge block is unreachable. - const has_value_merge = is_value and result_type != .void and result_type != .noreturn; - const merge_params: []const TypeId = if (has_value_merge) &.{result_type} else &.{}; - const merge_bb = self.freshBlockWithParams("match.merge", merge_params); - - // Build arm blocks - var default_bb: ?BlockId = null; - var arm_blocks = std.ArrayList(BlockId).empty; - defer arm_blocks.deinit(self.alloc); - for (me.arms) |_| { - arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable; - } - - // Build case list and pre-collect type tags per arm - var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; - defer cases.deinit(self.alloc); - var arm_tag_values = std.ArrayList([]const u64).empty; - defer arm_tag_values.deinit(self.alloc); - - for (me.arms, 0..) |arm, i| { - if (arm.pattern == null) { - default_bb = arm_blocks.items[i]; - arm_tag_values.append(self.alloc, &.{}) catch unreachable; - continue; - } - const pat = arm.pattern.?; - - if (is_type_match) { - // Type-category match: resolve category name to tag values - const name = switch (pat.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => "", - }; - // E4 single-hop visibility + ambiguity gate: a SPECIFIC 2-flat-hop - // type name in a type-match arm (`case COnly:`) is not bare-visible - // (consistent with annotations / 0763); ≥2 direct flat same-name - // authors are ambiguous (loud diagnostic, 0755/0767). A category - // keyword (`int`, `struct`, …) is not a type author anywhere → the - // gate is a no-op (`.proceed`) and `resolveTypeCategoryTags` expands - // it. A source-keyed specific TYPE author — including the querying - // source's OWN author over a same-name flat import (own-wins, 0754) — - // matches on ITS TypeId, NOT whichever same-name author a global - // `findByName` (inside `resolveTypeCategoryTags`) would pick. - const tag_values = switch (self.headTypeGate(name, pat.span)) { - .ambiguous, .not_visible => { - arm_tag_values.append(self.alloc, &.{}) catch unreachable; - continue; - }, - .resolved => |tid| blk_tv: { - const tv = self.alloc.alloc(u64, 1) catch unreachable; - tv[0] = tid.index(); - break :blk_tv tv; - }, - .proceed => self.resolveTypeCategoryTags(name), - }; - arm_tag_values.append(self.alloc, tag_values) catch unreachable; - for (tag_values) |tag| { - cases.append(self.alloc, .{ - .value = @intCast(tag), - .target = arm_blocks.items[i], - .args = &.{}, - }) catch unreachable; - } - } else if (is_optional_match) { - // Optional match: .some → 1 (has_value=true), .none → 0 - arm_tag_values.append(self.alloc, &.{}) catch unreachable; - const pat_name = switch (pat.data) { - .enum_literal => |el| el.name, - .identifier => |id| id.name, - else => "", - }; - const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0; - cases.append(self.alloc, .{ - .value = @intCast(case_val), - .target = arm_blocks.items[i], - .args = &.{}, - }) catch unreachable; - } else { - // Enum/value match: resolve variant name to actual tag value - arm_tag_values.append(self.alloc, &.{}) catch unreachable; - const case_val: u64 = blk: { - const pat_name = switch (pat.data) { - .enum_literal => |el| el.name, - .identifier => |id| id.name, - .int_literal => |il| break :blk @intCast(il.value), - .bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0), - else => break :blk @as(u64, @intCast(i)), - }; - // Look up variant value in the subject's type - if (!subject_ty.isBuiltin()) { - const ty_info = self.module.types.get(subject_ty); - if (ty_info == .tagged_union) { - for (ty_info.tagged_union.fields, 0..) |f, vi| { - const vname = self.module.types.strings.get(f.name); - if (std.mem.eql(u8, vname, pat_name)) { - if (ty_info.tagged_union.explicit_tag_values) |vals| { - if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); - } - break :blk @intCast(vi); - } - } - if (self.diagnostics) |diags| { - const ty_name = self.formatTypeName(subject_ty); - diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); - } - } else if (ty_info == .@"enum") { - for (ty_info.@"enum".variants, 0..) |v, vi| { - const vname = self.module.types.strings.get(v); - if (std.mem.eql(u8, vname, pat_name)) { - if (ty_info.@"enum".explicit_values) |vals| { - if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); - } - break :blk @intCast(vi); - } - } - if (self.diagnostics) |diags| { - const ty_name = self.formatTypeName(subject_ty); - diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); - } - } else if (ty_info == .error_set) { - // `case .X` matches the global tag id of `X`. - break :blk @intCast(self.module.types.internTag(pat_name)); - } - } - break :blk @intCast(i); - }; - cases.append(self.alloc, .{ - .value = @intCast(case_val), - .target = arm_blocks.items[i], - .args = &.{}, - }) catch unreachable; - } - } - - // If no default arm, create an unreachable default - if (default_bb == null) { - default_bb = self.freshBlock("match.unr"); - } - - // Switch on the subject (for type match, subject is either a - // bare TypeId (s64) or an Any-shaped Type value — unbox in the - // latter case so the switch sees the i64 type id). - const tag = if (is_type_match) tag_blk: { - if (subject_ty == .any) { - break :tag_blk self.builder.emit(.{ .unbox_any = .{ .operand = subject } }, .s64); - } - break :tag_blk subject; - } else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else if (is_error_set_match) subject else blk: { - // Determine actual tag type from union info (e.g. u32 for SDL_Event) - const tag_ty: TypeId = tt: { - if (!subject_ty.isBuiltin()) { - const ty_info = self.module.types.get(subject_ty); - if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type; - } - break :tt .s32; - }; - break :blk self.builder.enumTag(subject, tag_ty); - }; - self.builder.switchBr(tag, cases.items, default_bb.?, &.{}); - - // Lower each arm's body - for (me.arms, 0..) |arm, i| { - self.builder.switchToBlock(arm_blocks.items[i]); - - // For type-match arms with empty tag lists, the arm is unreachable - // (no switch case targets it). Skip lowering to avoid invalid IR - // from runtime cast/dispatch with no matching types. - if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) { - self.builder.emitUnreachable(); - continue; - } - - var arm_scope = Scope.init(self.alloc, self.scope); - const old_scope = self.scope; - self.scope = &arm_scope; - - if (arm.capture) |capture_name| { - if (is_optional_match) { - // For optional match, unwrap the optional value - const opt_info = self.module.types.get(subject_ty); - const child_ty = if (opt_info == .optional) opt_info.optional.child else .s64; - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty); - arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false }); - } else { - // Resolve actual variant index and payload type from the subject's type - var variant_idx: u32 = @intCast(i); - var payload_ty: TypeId = .unresolved; - if (arm.pattern) |arm_pat| { - const pat_name = switch (arm_pat.data) { - .enum_literal => |el| el.name, - .identifier => |id| id.name, - else => "", - }; - if (!subject_ty.isBuiltin()) { - const ty_info = self.module.types.get(subject_ty); - if (ty_info == .tagged_union) { - for (ty_info.tagged_union.fields, 0..) |f, vi| { - const vname = self.module.types.strings.get(f.name); - if (std.mem.eql(u8, vname, pat_name)) { - variant_idx = @intCast(vi); - payload_ty = f.ty; - break; - } - } - } - } - } - const payload = self.builder.emit(.{ .enum_payload = .{ - .base = subject, - .field_index = variant_idx, - } }, payload_ty); - arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false }); - } - } - - // Set match arm context for runtime type dispatch - const saved_match_tags = self.current_match_tags; - if (is_type_match) { - self.current_match_tags = arm_tag_values.items[i]; - } - - if (has_value_merge) { - // Lower the arm body against the merge's result type so literals - // (and negated literals) in the arm pick the right width — the - // phi operands must all match `result_type` (issue 0066). - const saved_arm_target = self.target_type; - self.target_type = result_type; - const maybe_v = self.lowerBlockValue(arm.body); - self.target_type = saved_arm_target; - self.current_match_tags = saved_match_tags; - self.scope = old_scope; - arm_scope.deinit(); - // Only materialize a value + branch to the merge when the arm - // body did NOT diverge. A diverging arm (e.g. `return x`) has - // already terminated its block; emitting the fallback const - // here would land AFTER the terminator (the issue-0057 bug). - if (!self.currentBlockHasTerminator()) { - var v = maybe_v orelse if (result_type == .string or !result_type.isBuiltin()) - self.builder.constUndef(result_type) - else - self.builder.constInt(0, result_type); - const v_ty = self.builder.getRefType(v); - v = self.coerceToType(v, v_ty, result_type); - self.builder.br(merge_bb, &.{v}); - } - } else { - self.lowerBlock(arm.body); - self.current_match_tags = saved_match_tags; - self.scope = old_scope; - arm_scope.deinit(); - if (!self.currentBlockHasTerminator()) { - self.builder.br(merge_bb, &.{}); - } - } - } - - // Emit default block if no explicit else arm - if (default_bb != null) { - var found_default = false; - for (me.arms) |arm| { - if (arm.pattern == null) { found_default = true; break; } - } - if (!found_default) { - self.builder.switchToBlock(default_bb.?); - if (is_type_match) { - // For type-category matches, unrecognized tags should skip to merge - // (e.g., optional types not covered by any_to_string categories) - if (has_value_merge) { - const default_val = self.builder.constUndef(result_type); - self.builder.br(merge_bb, &.{default_val}); - } else { - self.builder.br(merge_bb, &.{}); - } - } else { - // For non-exhaustive matches (union/enum with unhandled variants), - // fall through to merge instead of unreachable - const is_exhaustive = blk: { - if (!subject_ty.isBuiltin()) { - const ty_info = self.module.types.get(subject_ty); - if (ty_info == .tagged_union) { - break :blk cases.items.len >= ty_info.tagged_union.fields.len; - } else if (ty_info == .@"enum") { - break :blk cases.items.len >= ty_info.@"enum".variants.len; - } - } - break :blk false; - }; - if (is_exhaustive) { - self.builder.emitUnreachable(); - } else if (has_value_merge) { - const default_val = self.builder.constUndef(result_type); - self.builder.br(merge_bb, &.{default_val}); - } else { - self.builder.br(merge_bb, &.{}); - } - } - } - } - - self.builder.switchToBlock(merge_bb); - if (has_value_merge) { - return self.builder.blockParam(merge_bb, 0, result_type); - } - return self.builder.constInt(0, .void); - } - - fn lowerBreak(self: *Lowering) Ref { - if (self.break_target) |target| { - self.builder.br(target, &.{}); - } - return Ref.none; - } - - fn lowerContinue(self: *Lowering) Ref { - if (self.continue_target) |target| { - self.builder.br(target, &.{}); - } - return Ref.none; - } - - // ── Struct/enum/union ops ─────────────────────────────────────── - 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 @@ -10892,7 +10031,7 @@ pub const Lowering = struct { /// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values. /// Returns a list of TypeId index values that match the category. - fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { + pub fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 { var tags = std.ArrayList(u64).empty; // Fixed builtin categories @@ -10983,7 +10122,7 @@ pub const Lowering = struct { } /// Check if a match expression is a type-category match (patterns are type/category names). - fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { + pub fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId { // Infer result type from the first non-null arm body. // If we skip null_literal arms and find a concrete type T, and there // were null arms, the result is ?T (optional). @@ -11030,7 +10169,7 @@ pub const Lowering = struct { return .void; } - fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { + pub fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { for (me.arms) |arm| { if (arm.pattern) |pat| { const name = switch (pat.data) { @@ -11422,35 +10561,6 @@ pub const Lowering = struct { return p.is_variadic and (p.is_comptime or p.is_pack); } - pub fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { - return self.freshBlockWithParams(prefix, &.{}); - } - - pub fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { - var buf: [64]u8 = undefined; - const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix; - self.block_counter += 1; - const name_id = self.module.types.internString(name); - return self.builder.appendBlock(name_id, params); - } - - pub fn currentBlockHasTerminator(self: *Lowering) bool { - const func = self.builder.module.getFunctionMut(self.builder.func.?); - const block_idx = self.builder.current_block orelse return true; - const block = &func.blocks.items[block_idx.index()]; - if (block.insts.items.len > 0) { - const last_op = block.insts.items[block.insts.items.len - 1].op; - return switch (last_op) { - .ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true, - else => false, - }; - } - return false; - } - - // ── Type resolution ───────────────────────────────────────────── - // Delegates to type_bridge for full AST type node resolution. - pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); @@ -12064,7 +11174,7 @@ pub const Lowering = struct { ambiguous, not_visible, }; - fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { + pub fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate { if (self.emitting_default_context) return .proceed; if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed; const from = self.current_source_file orelse return .proceed; @@ -15434,25 +14544,6 @@ pub const Lowering = struct { } } - pub fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { - if (self.currentBlockHasTerminator()) return; - if (ret_ty == .noreturn) { - // A `-> noreturn` function never returns; if control reaches the - // end of the body it's genuinely unreachable (the body is expected - // to diverge — call another noreturn, loop forever, etc.). - self.builder.emitUnreachable(); - } else if (ret_ty == .void) { - self.builder.retVoid(); - } else { - // Use const_undef for complex types (string, struct, etc.) - const default_val = if (ret_ty == .string or !ret_ty.isBuiltin()) - self.builder.constUndef(ret_ty) - else - self.builder.constInt(0, ret_ty); - self.builder.ret(default_val, ret_ty); - } - } - /// Emit a C-ABI exported function for every bodied method on a /// `#jni_main #jni_class("...")` declaration. The symbol name follows /// JNI's name-mangling convention so Android's JNI runtime can resolve @@ -16981,6 +16072,22 @@ pub const Lowering = struct { pub const emitBlockDefers = lower_stmt.emitBlockDefers; pub const lowerCleanupBody = lower_stmt.lowerCleanupBody; pub const emitErrorCleanup = lower_stmt.emitErrorCleanup; + + // --- moved to lower/control_flow.zig (lower_control_flow) --- + pub const lowerIfExpr = lower_control_flow.lowerIfExpr; + pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition; + pub const lowerWhile = lower_control_flow.lowerWhile; + pub const listView = lower_control_flow.listView; + pub const lowerFor = lower_control_flow.lowerFor; + pub const lowerRuntimeRangeFor = lower_control_flow.lowerRuntimeRangeFor; + pub const lowerInlineRangeFor = lower_control_flow.lowerInlineRangeFor; + pub const lowerMatch = lower_control_flow.lowerMatch; + pub const lowerBreak = lower_control_flow.lowerBreak; + pub const lowerContinue = lower_control_flow.lowerContinue; + pub const freshBlock = lower_control_flow.freshBlock; + pub const freshBlockWithParams = lower_control_flow.freshBlockWithParams; + pub const currentBlockHasTerminator = lower_control_flow.currentBlockHasTerminator; + pub const ensureTerminator = lower_control_flow.ensureTerminator; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/control_flow.zig b/src/ir/lower/control_flow.zig new file mode 100644 index 0000000..822353f --- /dev/null +++ b/src/ir/lower/control_flow.zig @@ -0,0 +1,960 @@ +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 ComptimeValue = Lowering.ComptimeValue; +const isTypeCategoryMatch = Lowering.isTypeCategoryMatch; + +pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { + // inline if: evaluate condition at compile time, only lower taken branch + if (ie.is_comptime) { + if (self.evalComptimeCondition(ie.condition)) |is_true| { + if (is_true) { + return self.lowerInlineBranch(ie.then_branch); + } else if (ie.else_branch) |eb| { + return self.lowerInlineBranch(eb); + } + return self.builder.constInt(0, .void); + } + // Condition couldn't be evaluated — fall through to runtime + } + + // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors + if (self.tryConstBoolCondition(ie.condition)) |is_true| { + if (is_true) { + // Condition always true: only lower then-branch + if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) { + return self.lowerExpr(ie.then_branch); + } + self.lowerBlock(ie.then_branch); + // If then-branch terminated (return/break), mark block as dead + if (self.currentBlockHasTerminator()) { + self.block_terminated = true; + return .none; + } + return self.builder.constInt(0, .void); + } else { + // Condition always false: only lower else-branch (if any) + if (ie.else_branch) |eb| { + if (ie.is_inline or self.force_block_value) { + return self.lowerExpr(eb); + } + self.lowerBlock(eb); + if (self.currentBlockHasTerminator()) { + self.block_terminated = true; + return .none; + } + } + return self.builder.constInt(0, .void); + } + } + + // Optional binding: `if val := expr { ... }` + // Clear target_type so the ternary's result type doesn't leak into the condition + // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32) + const saved_cond_target = self.target_type; + self.target_type = null; + const opt_val = self.lowerExpr(ie.condition); + self.target_type = saved_cond_target; + const cond = if (ie.binding_name != null) blk: { + // The condition is an optional — emit has_value check + break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); + } else opt_val; + const has_else = ie.else_branch != null; + // If-else produces a value when inline OR when in value position (force_block_value) + var is_value = (ie.is_inline or self.force_block_value) and has_else; + + // Infer result type from then branch for value if-exprs + // If then_branch is null/void, try else_branch (e.g., `if cond then null else val`) + var result_type: TypeId = if (is_value) blk: { + var t = self.inferExprType(ie.then_branch); + if ((t == .void or t == .unresolved) and ie.else_branch != null) { + t = self.inferExprType(ie.else_branch.?); + } + // Branch type not statically inferable (e.g. `null` / a bare enum + // literal) — use the contextually expected type rather than a guess. + if (t == .unresolved) { + if (self.target_type) |tt| t = tt; + } + break :blk t; + } else .void; + + // A value-position if/else whose branches yield no value (both are + // `;`-terminated / void blocks) is really a statement-if — lowering it + // as a value would build a `phi void`. Demote it. + if (is_value and result_type == .void) { + is_value = false; + result_type = .void; + } + + const then_bb = self.freshBlock("if.then"); + const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null; + const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{}; + const merge_bb = self.freshBlockWithParams("if.merge", merge_params); + + // Conditional branch + self.builder.condBr( + cond, + then_bb, + &.{}, + if (else_bb) |eb| eb else merge_bb, + &.{}, + ); + + // Then branch + self.builder.switchToBlock(then_bb); + // If binding: unwrap the optional and bind to the name + if (ie.binding_name) |bind_name| { + const opt_ty = self.inferExprType(ie.condition); + const inner_ty = if (!opt_ty.isBuiltin()) blk: { + const info = self.module.types.get(opt_ty); + break :blk if (info == .optional) info.optional.child else opt_ty; + } else opt_ty; + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty); + const slot = self.builder.alloca(inner_ty); + self.builder.store(slot, unwrapped); + if (self.scope) |scope| { + scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true }); + } + } + // Set target_type so null/undef in branches get the right type + const saved_target = self.target_type; + if (is_value and result_type != .void) self.target_type = result_type; + if (is_value) { + var v = self.lowerExpr(ie.then_branch); + if (!self.currentBlockHasTerminator()) { + const v_ty = self.builder.getRefType(v); + if (v_ty != result_type and v_ty != .void and result_type != .void) { + v = self.coerceToType(v, v_ty, result_type); + } + self.builder.br(merge_bb, &.{v}); + } + } else { + self.lowerBlock(ie.then_branch); + if (!self.currentBlockHasTerminator()) { + self.builder.br(merge_bb, &.{}); + } + } + + // Else branch + if (has_else) { + self.builder.switchToBlock(else_bb.?); + if (is_value) { + var v = self.lowerExpr(ie.else_branch.?); + if (!self.currentBlockHasTerminator()) { + const v_ty = self.builder.getRefType(v); + if (v_ty != result_type and v_ty != .void and result_type != .void) { + v = self.coerceToType(v, v_ty, result_type); + } + self.builder.br(merge_bb, &.{v}); + } + } else { + self.lowerBlock(ie.else_branch.?); + if (!self.currentBlockHasTerminator()) { + self.builder.br(merge_bb, &.{}); + } + } + } + self.target_type = saved_target; + + // Continue at merge + self.builder.switchToBlock(merge_bb); + if (is_value) { + return self.builder.blockParam(merge_bb, 0, result_type); + } + return self.builder.constInt(0, .void); +} + +/// Try to evaluate an AST condition as a compile-time constant bool. +/// Returns true/false if the condition is known at compile time, null otherwise. +pub fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool { + switch (node.data) { + .bool_literal => |bl| return bl.value, + .call => |c| { + if (c.callee.data == .identifier) { + const cname = c.callee.data.identifier.name; + if (std.mem.eql(u8, cname, "is_flags")) { + // Resolve the type arg to check if it's actually a flags enum + if (c.args.len > 0) { + const ty = self.resolveTypeArg(c.args[0]); + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .@"enum") return info.@"enum".is_flags; + } + } + return false; + } + if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) { + const a = self.resolveTypeArg(c.args[0]); + const b = self.resolveTypeArg(c.args[1]); + return a == b; + } + if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) { + const ty = self.resolveTypeArg(c.args[1]); + return self.computeHasImpl(c.args[0], ty); + } + } + }, + else => {}, + } + return null; +} + +pub fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref { + const header_bb = self.freshBlock("while.hdr"); + const body_bb = self.freshBlock("while.body"); + const exit_bb = self.freshBlock("while.exit"); + + // Branch to header + self.builder.br(header_bb, &.{}); + + // Header: evaluate condition + self.builder.switchToBlock(header_bb); + const cond = self.lowerExpr(we.condition); + self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{}); + + // Body + self.builder.switchToBlock(body_bb); + + // Save and set loop targets + const old_break = self.break_target; + const old_continue = self.continue_target; + self.break_target = exit_bb; + self.continue_target = header_bb; + defer { + self.break_target = old_break; + self.continue_target = old_continue; + } + + self.lowerBlock(we.body); + if (!self.currentBlockHasTerminator()) { + self.builder.br(header_bb, &.{}); + } + + // Continue at exit + self.builder.switchToBlock(exit_bb); + return self.builder.constInt(0, .void); +} + +/// View a `List(T)`-like struct (`{ items: [*]T, len, … }`) as its backing +/// `items` pointer + element type + `len`, so `for list: (x)` iterates the +/// elements. Null for anything that isn't such a struct. +pub fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, data_ty: TypeId, len: Ref } { + if (ty.isBuiltin()) return null; + const info = self.module.types.get(ty); + if (info != .@"struct") return null; + const items_id = self.module.types.internString("items"); + const len_id = self.module.types.internString("len"); + var items_idx: ?u32 = null; + var items_ty: TypeId = .unresolved; + var len_idx: ?u32 = null; + for (info.@"struct".fields, 0..) |f, i| { + if (f.name == items_id and !f.ty.isBuiltin() and self.module.types.get(f.ty) == .many_pointer) { + items_idx = @intCast(i); + items_ty = f.ty; + } else if (f.name == len_id) { + len_idx = @intCast(i); + } + } + if (items_idx == null or len_idx == null) return null; + return .{ + .data = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = items_idx.? } }, items_ty), + .data_ty = items_ty, + .len = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = len_idx.? } }, .s64), + }; +} + +pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { + if (fe.range_end) |end_node| { + if (fe.is_inline) return self.lowerInlineRangeFor(fe, end_node); + return self.lowerRuntimeRangeFor(fe, end_node); + } + // Collection-form `for xs : (x)` over a pack: a pack has no runtime + // value to iterate (Decision 1) — point the user at `inline for`. + if (fe.iterable.data == .identifier and self.isPackName(fe.iterable.data.identifier.name)) { + return self.diagPackAsValue(fe.iterable.data.identifier.name, fe.iterable.span, .runtime_iter); + } + + // Lower iterable + resolve its static type. + var iterable = self.lowerExpr(fe.iterable); + var iterable_ty = self.inferExprType(fe.iterable); + + // `*List` / `*[]T` etc. — deref to the collection value. + const ptr_info = if (iterable_ty.isBuiltin()) null else self.module.types.get(iterable_ty); + if (ptr_info != null and ptr_info.? == .pointer) { + iterable = self.builder.load(iterable, ptr_info.?.pointer.pointee); + iterable_ty = ptr_info.?.pointer.pointee; + } + + // A `List(T)`-like struct iterates its `items[0..len]`; arrays/slices + // use their intrinsic length. + var len: Ref = undefined; + if (self.listView(iterable, iterable_ty)) |lv| { + iterable = lv.data; + iterable_ty = lv.data_ty; + len = lv.len; + } else { + len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64); + } + + // Create index variable + const idx_slot = self.builder.alloca(.s64); + const zero = self.builder.constInt(0, .s64); + self.builder.store(idx_slot, zero); + + const header_bb = self.freshBlock("for.hdr"); + const body_bb = self.freshBlock("for.body"); + const inc_bb = self.freshBlock("for.inc"); + const exit_bb = self.freshBlock("for.exit"); + + self.builder.br(header_bb, &.{}); + + // Header: compare index < length + self.builder.switchToBlock(header_bb); + const idx_val = self.builder.load(idx_slot, .s64); + const cmp = self.builder.cmpLt(idx_val, len); + self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); + + // Body + self.builder.switchToBlock(body_bb); + + // Bind element — resolve element type from iterable. `for xs: (*x)` + // binds a pointer into the collection (no per-element copy); `(x)` + // binds a value copy. + const elem_ty = self.getElementType(iterable_ty); + const bind_ty = if (fe.capture_by_ref) self.module.types.ptrTo(elem_ty) else elem_ty; + const elem = if (fe.capture_by_ref) blk: { + // A slice value carries its backing pointer, so GEP on it writes + // through. An array is a value — GEP needs its storage (alloca) or + // mutations would hit a copy. + const is_array = !iterable_ty.isBuiltin() and self.module.types.get(iterable_ty) == .array; + const base = if (is_array) (self.getExprAlloca(fe.iterable) orelse iterable) else iterable; + break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx_val } }, bind_ty); + } else self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, bind_ty); + + var body_scope = Scope.init(self.alloc, self.scope); + const old_scope = self.scope; + self.scope = &body_scope; + + body_scope.put(fe.capture_name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = fe.capture_by_ref }); + + // Bind index if requested + if (fe.index_name) |iname| { + body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); + } + + // Save and set loop targets + const old_break = self.break_target; + const old_continue = self.continue_target; + self.break_target = exit_bb; + self.continue_target = inc_bb; // continue → increment, not header + + self.lowerBlock(fe.body); + + self.break_target = old_break; + self.continue_target = old_continue; + self.scope = old_scope; + body_scope.deinit(); + + // Fall through to increment block + if (!self.currentBlockHasTerminator()) { + self.builder.br(inc_bb, &.{}); + } + + // Increment block: increment index and jump back to header + self.builder.switchToBlock(inc_bb); + { + const cur_idx = self.builder.load(idx_slot, .s64); + const one = self.builder.constInt(1, .s64); + const next_idx = self.builder.add(cur_idx, one, .s64); + self.builder.store(idx_slot, next_idx); + self.builder.br(header_bb, &.{}); + } + + // Continue at exit + self.builder.switchToBlock(exit_bb); + return self.builder.constInt(0, .void); +} + +/// Runtime counting loop `for start..end (i) { }` — `i` (optional) is the +/// cursor, `end` is exclusive. Lowers to the same header/inc/exit shape as +/// the collection form, minus the element fetch. +pub fn lowerRuntimeRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref { + const start = self.lowerExpr(fe.iterable); + const end = self.lowerExpr(end_node); + + const idx_slot = self.builder.alloca(.s64); + self.builder.store(idx_slot, start); + + const header_bb = self.freshBlock("for.hdr"); + const body_bb = self.freshBlock("for.body"); + const inc_bb = self.freshBlock("for.inc"); + const exit_bb = self.freshBlock("for.exit"); + + self.builder.br(header_bb, &.{}); + + self.builder.switchToBlock(header_bb); + const idx_val = self.builder.load(idx_slot, .s64); + const cmp = self.builder.cmpLt(idx_val, end); + self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); + + self.builder.switchToBlock(body_bb); + var body_scope = Scope.init(self.alloc, self.scope); + const old_scope = self.scope; + self.scope = &body_scope; + if (fe.capture_name.len > 0) { + body_scope.put(fe.capture_name, .{ .ref = idx_val, .ty = .s64, .is_alloca = false }); + } + + const old_break = self.break_target; + const old_continue = self.continue_target; + self.break_target = exit_bb; + self.continue_target = inc_bb; + + self.lowerBlock(fe.body); + + self.break_target = old_break; + self.continue_target = old_continue; + self.scope = old_scope; + body_scope.deinit(); + + if (!self.currentBlockHasTerminator()) { + self.builder.br(inc_bb, &.{}); + } + + self.builder.switchToBlock(inc_bb); + { + const cur_idx = self.builder.load(idx_slot, .s64); + const one = self.builder.constInt(1, .s64); + const next_idx = self.builder.add(cur_idx, one, .s64); + self.builder.store(idx_slot, next_idx); + self.builder.br(header_bb, &.{}); + } + + self.builder.switchToBlock(exit_bb); + return self.builder.constInt(0, .void); +} + +/// Comptime-unrolled `inline for start..end (i) { }`. `start`/`end` must be +/// comptime-known. The body is lowered `end - start` times with the cursor +/// bound as an `int_val` comptime constant, so `xs[i]` over a pack +/// substitutes the concrete per-position argument each iteration. +pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref { + const start = self.evalComptimeInt(fe.iterable) orelse { + if (self.diagnostics) |d| d.addFmt(.err, fe.iterable.span, "inline for: range start is not a compile-time integer", .{}); + return self.builder.constInt(0, .void); + }; + const end = self.evalComptimeInt(end_node) orelse { + if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{}); + return self.builder.constInt(0, .void); + }; + + var i: i64 = start; + while (i < end) : (i += 1) { + var body_scope = Scope.init(self.alloc, self.scope); + const old_scope = self.scope; + self.scope = &body_scope; + + // Bind the cursor both as a runtime value (constInt, for uses like + // `print(i)`) and as a comptime constant (for `xs[i]` substitution). + var had_prev = false; + var prev: ComptimeValue = undefined; + if (fe.capture_name.len > 0) { + body_scope.put(fe.capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false }); + if (self.comptime_constants.get(fe.capture_name)) |p| { + had_prev = true; + prev = p; + } + self.comptime_constants.put(fe.capture_name, .{ .int_val = i }) catch {}; + } + + self.lowerBlock(fe.body); + + if (fe.capture_name.len > 0) { + if (had_prev) { + self.comptime_constants.put(fe.capture_name, prev) catch {}; + } else { + _ = self.comptime_constants.remove(fe.capture_name); + } + } + + self.scope = old_scope; + body_scope.deinit(); + + if (self.currentBlockHasTerminator()) break; + } + + return self.builder.constInt(0, .void); +} + +pub fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref { + // inline if match: evaluate at compile time, only lower the matching arm + if (me.is_comptime) { + if (self.evalComptimeMatch(me)) |arm_body| { + return self.lowerInlineBranch(arm_body); + } + // Couldn't evaluate — fall through to runtime + } + + const is_type_match = isTypeCategoryMatch(me); + var subject = self.lowerExpr(me.subject); + var subject_ty = self.inferExprType(me.subject); + // A pointer subject (e.g. a `for xs: (*x)` element capture) — deref to + // the pointed-to union/enum so tag/payload extraction works. + if (!subject_ty.isBuiltin()) { + const sinfo = self.module.types.get(subject_ty); + if (sinfo == .pointer and !sinfo.pointer.pointee.isBuiltin()) { + const pinfo = self.module.types.get(sinfo.pointer.pointee); + if (pinfo == .tagged_union or pinfo == .@"enum") { + subject = self.builder.load(subject, sinfo.pointer.pointee); + subject_ty = sinfo.pointer.pointee; + } + } + } + const is_optional_match = blk: { + if (!subject_ty.isBuiltin()) { + const info = self.module.types.get(subject_ty); + break :blk info == .optional; + } + break :blk false; + }; + // An error-set subject (`catch e == { case .X: ... }` / `if e == { ... }`): + // the value IS its u32 tag id, and `case .X` matches the global tag id + // of `X`. Used by ERR E1.5's catch match-body form. + const is_error_set_match = blk: { + if (!subject_ty.isBuiltin()) { + break :blk self.module.types.get(subject_ty) == .error_set; + } + break :blk false; + }; + + // Determine if the match produces a value (has non-void arms) + // For type-category matches (inside any_to_string), only produce value when force_block_value + // For regular enum/optional matches, always produce value if arms are non-void + var inferred_result = self.inferMatchResultType(me); + // Arms not statically inferable (bare enum literals etc.): only a + // value-position match (`force_block_value`) needs a concrete result — + // use the contextually expected type. A statement match with non-value + // arms is a side-effect (void); don't let a leaked `target_type` turn + // it into a value match. + if (inferred_result == .unresolved) { + inferred_result = if (self.force_block_value) (self.target_type orelse .unresolved) else .void; + } + const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved)); + const result_type: TypeId = if (is_value) inferred_result else .void; + // A fully-diverging match (`result_type == .noreturn` — every arm + // `return`s / `raise`s / etc.) produces no value, so it builds no + // merge phi; its arms terminate and the merge block is unreachable. + const has_value_merge = is_value and result_type != .void and result_type != .noreturn; + const merge_params: []const TypeId = if (has_value_merge) &.{result_type} else &.{}; + const merge_bb = self.freshBlockWithParams("match.merge", merge_params); + + // Build arm blocks + var default_bb: ?BlockId = null; + var arm_blocks = std.ArrayList(BlockId).empty; + defer arm_blocks.deinit(self.alloc); + for (me.arms) |_| { + arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable; + } + + // Build case list and pre-collect type tags per arm + var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty; + defer cases.deinit(self.alloc); + var arm_tag_values = std.ArrayList([]const u64).empty; + defer arm_tag_values.deinit(self.alloc); + + for (me.arms, 0..) |arm, i| { + if (arm.pattern == null) { + default_bb = arm_blocks.items[i]; + arm_tag_values.append(self.alloc, &.{}) catch unreachable; + continue; + } + const pat = arm.pattern.?; + + if (is_type_match) { + // Type-category match: resolve category name to tag values + const name = switch (pat.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => "", + }; + // E4 single-hop visibility + ambiguity gate: a SPECIFIC 2-flat-hop + // type name in a type-match arm (`case COnly:`) is not bare-visible + // (consistent with annotations / 0763); ≥2 direct flat same-name + // authors are ambiguous (loud diagnostic, 0755/0767). A category + // keyword (`int`, `struct`, …) is not a type author anywhere → the + // gate is a no-op (`.proceed`) and `resolveTypeCategoryTags` expands + // it. A source-keyed specific TYPE author — including the querying + // source's OWN author over a same-name flat import (own-wins, 0754) — + // matches on ITS TypeId, NOT whichever same-name author a global + // `findByName` (inside `resolveTypeCategoryTags`) would pick. + const tag_values = switch (self.headTypeGate(name, pat.span)) { + .ambiguous, .not_visible => { + arm_tag_values.append(self.alloc, &.{}) catch unreachable; + continue; + }, + .resolved => |tid| blk_tv: { + const tv = self.alloc.alloc(u64, 1) catch unreachable; + tv[0] = tid.index(); + break :blk_tv tv; + }, + .proceed => self.resolveTypeCategoryTags(name), + }; + arm_tag_values.append(self.alloc, tag_values) catch unreachable; + for (tag_values) |tag| { + cases.append(self.alloc, .{ + .value = @intCast(tag), + .target = arm_blocks.items[i], + .args = &.{}, + }) catch unreachable; + } + } else if (is_optional_match) { + // Optional match: .some → 1 (has_value=true), .none → 0 + arm_tag_values.append(self.alloc, &.{}) catch unreachable; + const pat_name = switch (pat.data) { + .enum_literal => |el| el.name, + .identifier => |id| id.name, + else => "", + }; + const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0; + cases.append(self.alloc, .{ + .value = @intCast(case_val), + .target = arm_blocks.items[i], + .args = &.{}, + }) catch unreachable; + } else { + // Enum/value match: resolve variant name to actual tag value + arm_tag_values.append(self.alloc, &.{}) catch unreachable; + const case_val: u64 = blk: { + const pat_name = switch (pat.data) { + .enum_literal => |el| el.name, + .identifier => |id| id.name, + .int_literal => |il| break :blk @intCast(il.value), + .bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0), + else => break :blk @as(u64, @intCast(i)), + }; + // Look up variant value in the subject's type + if (!subject_ty.isBuiltin()) { + const ty_info = self.module.types.get(subject_ty); + if (ty_info == .tagged_union) { + for (ty_info.tagged_union.fields, 0..) |f, vi| { + const vname = self.module.types.strings.get(f.name); + if (std.mem.eql(u8, vname, pat_name)) { + if (ty_info.tagged_union.explicit_tag_values) |vals| { + if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); + } + break :blk @intCast(vi); + } + } + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(subject_ty); + diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); + } + } else if (ty_info == .@"enum") { + for (ty_info.@"enum".variants, 0..) |v, vi| { + const vname = self.module.types.strings.get(v); + if (std.mem.eql(u8, vname, pat_name)) { + if (ty_info.@"enum".explicit_values) |vals| { + if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi]))); + } + break :blk @intCast(vi); + } + } + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(subject_ty); + diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name }); + } + } else if (ty_info == .error_set) { + // `case .X` matches the global tag id of `X`. + break :blk @intCast(self.module.types.internTag(pat_name)); + } + } + break :blk @intCast(i); + }; + cases.append(self.alloc, .{ + .value = @intCast(case_val), + .target = arm_blocks.items[i], + .args = &.{}, + }) catch unreachable; + } + } + + // If no default arm, create an unreachable default + if (default_bb == null) { + default_bb = self.freshBlock("match.unr"); + } + + // Switch on the subject (for type match, subject is either a + // bare TypeId (s64) or an Any-shaped Type value — unbox in the + // latter case so the switch sees the i64 type id). + const tag = if (is_type_match) tag_blk: { + if (subject_ty == .any) { + break :tag_blk self.builder.emit(.{ .unbox_any = .{ .operand = subject } }, .s64); + } + break :tag_blk subject; + } else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else if (is_error_set_match) subject else blk: { + // Determine actual tag type from union info (e.g. u32 for SDL_Event) + const tag_ty: TypeId = tt: { + if (!subject_ty.isBuiltin()) { + const ty_info = self.module.types.get(subject_ty); + if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type; + } + break :tt .s32; + }; + break :blk self.builder.enumTag(subject, tag_ty); + }; + self.builder.switchBr(tag, cases.items, default_bb.?, &.{}); + + // Lower each arm's body + for (me.arms, 0..) |arm, i| { + self.builder.switchToBlock(arm_blocks.items[i]); + + // For type-match arms with empty tag lists, the arm is unreachable + // (no switch case targets it). Skip lowering to avoid invalid IR + // from runtime cast/dispatch with no matching types. + if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) { + self.builder.emitUnreachable(); + continue; + } + + var arm_scope = Scope.init(self.alloc, self.scope); + const old_scope = self.scope; + self.scope = &arm_scope; + + if (arm.capture) |capture_name| { + if (is_optional_match) { + // For optional match, unwrap the optional value + const opt_info = self.module.types.get(subject_ty); + const child_ty = if (opt_info == .optional) opt_info.optional.child else .s64; + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty); + arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false }); + } else { + // Resolve actual variant index and payload type from the subject's type + var variant_idx: u32 = @intCast(i); + var payload_ty: TypeId = .unresolved; + if (arm.pattern) |arm_pat| { + const pat_name = switch (arm_pat.data) { + .enum_literal => |el| el.name, + .identifier => |id| id.name, + else => "", + }; + if (!subject_ty.isBuiltin()) { + const ty_info = self.module.types.get(subject_ty); + if (ty_info == .tagged_union) { + for (ty_info.tagged_union.fields, 0..) |f, vi| { + const vname = self.module.types.strings.get(f.name); + if (std.mem.eql(u8, vname, pat_name)) { + variant_idx = @intCast(vi); + payload_ty = f.ty; + break; + } + } + } + } + } + const payload = self.builder.emit(.{ .enum_payload = .{ + .base = subject, + .field_index = variant_idx, + } }, payload_ty); + arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false }); + } + } + + // Set match arm context for runtime type dispatch + const saved_match_tags = self.current_match_tags; + if (is_type_match) { + self.current_match_tags = arm_tag_values.items[i]; + } + + if (has_value_merge) { + // Lower the arm body against the merge's result type so literals + // (and negated literals) in the arm pick the right width — the + // phi operands must all match `result_type` (issue 0066). + const saved_arm_target = self.target_type; + self.target_type = result_type; + const maybe_v = self.lowerBlockValue(arm.body); + self.target_type = saved_arm_target; + self.current_match_tags = saved_match_tags; + self.scope = old_scope; + arm_scope.deinit(); + // Only materialize a value + branch to the merge when the arm + // body did NOT diverge. A diverging arm (e.g. `return x`) has + // already terminated its block; emitting the fallback const + // here would land AFTER the terminator (the issue-0057 bug). + if (!self.currentBlockHasTerminator()) { + var v = maybe_v orelse if (result_type == .string or !result_type.isBuiltin()) + self.builder.constUndef(result_type) + else + self.builder.constInt(0, result_type); + const v_ty = self.builder.getRefType(v); + v = self.coerceToType(v, v_ty, result_type); + self.builder.br(merge_bb, &.{v}); + } + } else { + self.lowerBlock(arm.body); + self.current_match_tags = saved_match_tags; + self.scope = old_scope; + arm_scope.deinit(); + if (!self.currentBlockHasTerminator()) { + self.builder.br(merge_bb, &.{}); + } + } + } + + // Emit default block if no explicit else arm + if (default_bb != null) { + var found_default = false; + for (me.arms) |arm| { + if (arm.pattern == null) { found_default = true; break; } + } + if (!found_default) { + self.builder.switchToBlock(default_bb.?); + if (is_type_match) { + // For type-category matches, unrecognized tags should skip to merge + // (e.g., optional types not covered by any_to_string categories) + if (has_value_merge) { + const default_val = self.builder.constUndef(result_type); + self.builder.br(merge_bb, &.{default_val}); + } else { + self.builder.br(merge_bb, &.{}); + } + } else { + // For non-exhaustive matches (union/enum with unhandled variants), + // fall through to merge instead of unreachable + const is_exhaustive = blk: { + if (!subject_ty.isBuiltin()) { + const ty_info = self.module.types.get(subject_ty); + if (ty_info == .tagged_union) { + break :blk cases.items.len >= ty_info.tagged_union.fields.len; + } else if (ty_info == .@"enum") { + break :blk cases.items.len >= ty_info.@"enum".variants.len; + } + } + break :blk false; + }; + if (is_exhaustive) { + self.builder.emitUnreachable(); + } else if (has_value_merge) { + const default_val = self.builder.constUndef(result_type); + self.builder.br(merge_bb, &.{default_val}); + } else { + self.builder.br(merge_bb, &.{}); + } + } + } + } + + self.builder.switchToBlock(merge_bb); + if (has_value_merge) { + return self.builder.blockParam(merge_bb, 0, result_type); + } + return self.builder.constInt(0, .void); +} + +pub fn lowerBreak(self: *Lowering) Ref { + if (self.break_target) |target| { + self.builder.br(target, &.{}); + } + return Ref.none; +} + +pub fn lowerContinue(self: *Lowering) Ref { + if (self.continue_target) |target| { + self.builder.br(target, &.{}); + } + return Ref.none; +} + +// ── Struct/enum/union ops ─────────────────────────────────────── + +pub fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { + return self.freshBlockWithParams(prefix, &.{}); +} + +pub fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { + var buf: [64]u8 = undefined; + const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix; + self.block_counter += 1; + const name_id = self.module.types.internString(name); + return self.builder.appendBlock(name_id, params); +} + +pub fn currentBlockHasTerminator(self: *Lowering) bool { + const func = self.builder.module.getFunctionMut(self.builder.func.?); + const block_idx = self.builder.current_block orelse return true; + const block = &func.blocks.items[block_idx.index()]; + if (block.insts.items.len > 0) { + const last_op = block.insts.items[block.insts.items.len - 1].op; + return switch (last_op) { + .ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true, + else => false, + }; + } + return false; +} + +// ── Type resolution ───────────────────────────────────────────── +// Delegates to type_bridge for full AST type node resolution. + +pub fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void { + if (self.currentBlockHasTerminator()) return; + if (ret_ty == .noreturn) { + // A `-> noreturn` function never returns; if control reaches the + // end of the body it's genuinely unreachable (the body is expected + // to diverge — call another noreturn, loop forever, etc.). + self.builder.emitUnreachable(); + } else if (ret_ty == .void) { + self.builder.retVoid(); + } else { + // Use const_undef for complex types (string, struct, etc.) + const default_val = if (ret_ty == .string or !ret_ty.isBuiltin()) + self.builder.constUndef(ret_ty) + else + self.builder.constInt(0, ret_ty); + self.builder.ret(default_val, ret_ty); + } +}