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