const std = @import("std"); const ast = @import("../../ast.zig"); const Node = ast.Node; const types = @import("../types.zig"); const inst_mod = @import("../inst.zig"); const errors = @import("../../errors.zig"); const TypeId = types.TypeId; const Ref = inst_mod.Ref; const BlockId = inst_mod.BlockId; 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 i64, 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; const old_defer_base = self.loop_defer_base; self.break_target = exit_bb; self.continue_target = header_bb; self.loop_defer_base = self.defer_stack.items.len; defer { self.break_target = old_break; self.continue_target = old_continue; self.loop_defer_base = old_defer_base; } 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.? } }, .i64), }; } /// Lowered prep for one position of a multi-iterable `for` header. Every /// position gets its own i64 cursor slot (ranges start at their `start`, /// collections at 0); all cursors advance by 1 per iteration, and ONLY the /// first position's bound terminates the loop (first-iterable-wins). const IterPrep = struct { is_range: bool, slot: Ref, // Collection-only fields: data: Ref = Ref.none, data_ty: TypeId = .unresolved, elem_ty: TypeId = .unresolved, is_array: bool = false, storage: ?Ref = null, // array's own alloca when addressable (not deref'd) }; /// `for it1, it2, ... (c1, c2, ...) { }` — parallel iteration. The first /// iterable's length/bound drives the loop; the others follow by position. /// Consequences of first-iterable-wins: a non-first range's end is never /// lowered (its side effects do not run), and a shorter non-first collection /// is read past its length on mismatch — the first iterable is the /// authoritative one. pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { if (fe.is_inline) return self.lowerInlineRangeFor(fe); // A pack has no runtime value to iterate (Decision 1) — point the user // at `inline for`. for (fe.iterables) |it| { if (!it.is_range and it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) { return self.diagPackAsValue(it.expr.data.identifier.name, it.expr.span, .runtime_iter); } } var preps = std.ArrayList(IterPrep).empty; defer preps.deinit(self.alloc); var limit: Ref = Ref.none; // exclusive bound of position 0 for (fe.iterables, 0..) |it, i| { if (it.is_range) { var start_ref = self.lowerExpr(it.expr); if (it.start_exclusive) start_ref = self.builder.add(start_ref, self.builder.constInt(1, .i64), .i64); const slot = self.builder.alloca(.i64); self.builder.store(slot, start_ref); if (i == 0) { // Parser guarantees the first iterable is bounded. var end_ref = self.lowerExpr(it.range_end.?); if (it.end_inclusive) end_ref = self.builder.add(end_ref, self.builder.constInt(1, .i64), .i64); limit = end_ref; } preps.append(self.alloc, .{ .is_range = true, .slot = slot }) catch unreachable; } else { var data = self.lowerExpr(it.expr); var data_ty = self.inferExprType(it.expr); // `*List` / `*[]T` etc. — deref to the collection value. Tracked // because a deref'd iterable's identifier binding holds the // POINTER, so its alloca is not the collection's storage. var was_deref = false; const ptr_info = if (data_ty.isBuiltin()) null else self.module.types.get(data_ty); if (ptr_info != null and ptr_info.? == .pointer) { data = self.builder.load(data, ptr_info.?.pointer.pointee); data_ty = ptr_info.?.pointer.pointee; was_deref = true; } // A `List(T)`-like struct iterates its `items[0..len]`; // arrays/slices use their intrinsic length. var len: Ref = Ref.none; if (self.listView(data, data_ty)) |lv| { data = lv.data; data_ty = lv.data_ty; len = lv.len; } else if (i == 0) { len = self.builder.emit(.{ .length = .{ .operand = data } }, .i64); } const elem_ty = self.getElementType(data_ty); if (elem_ty == .unresolved) { // Not a collection. The common trip: `for f(n) { }` — the // trailing parens are the CAPTURE, so the iterable is `f`. if (self.diagnostics) |d| { if (data_ty == .unresolved) { d.addFmt(.err, it.expr.span, "cannot iterate this expression — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{}); } else { d.addFmt(.err, it.expr.span, "cannot iterate a value of type '{s}' — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{self.module.types.typeName(data_ty)}); } } return self.builder.constInt(0, .void); } const is_array = !data_ty.isBuiltin() and self.module.types.get(data_ty) == .array; const storage = if (is_array and !was_deref) self.getExprAlloca(it.expr) else null; const slot = self.builder.alloca(.i64); self.builder.store(slot, self.builder.constInt(0, .i64)); if (i == 0) limit = len; preps.append(self.alloc, .{ .is_range = false, .slot = slot, .data = data, .data_ty = data_ty, .elem_ty = elem_ty, .is_array = is_array, .storage = storage, }) catch unreachable; } } 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: first cursor against the first bound. self.builder.switchToBlock(header_bb); const cur0 = self.builder.load(preps.items[0].slot, .i64); const cmp = self.builder.cmpLt(cur0, limit); self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{}); // Body: bind one capture per position (when captures are present). self.builder.switchToBlock(body_bb); var body_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &body_scope; for (fe.captures, 0..) |cap, i| { const prep = preps.items[i]; const cur = if (i == 0) cur0 else self.builder.load(prep.slot, .i64); if (prep.is_range) { body_scope.put(cap.name, .{ .ref = cur, .ty = .i64, .is_alloca = false }); continue; } const bind_ty = if (cap.by_ref) self.module.types.ptrTo(prep.elem_ty) else prep.elem_ty; const elem = if (cap.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 base = if (prep.is_array) (prep.storage orelse prep.data) else prep.data; break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = cur } }, bind_ty); } else blk: { // By-value over an array with addressable storage: GEP + load ONE // element. `index_get` on the array VALUE spills the whole array // to a temp on every iteration — O(N²) bytes copied per loop. if (prep.storage) |storage| { const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = storage, .rhs = cur } }, self.module.types.ptrTo(prep.elem_ty)); break :blk self.builder.load(elem_ptr, prep.elem_ty); } break :blk self.builder.emit(.{ .index_get = .{ .lhs = prep.data, .rhs = cur } }, bind_ty); }; body_scope.put(cap.name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = cap.by_ref }); } // Save and set loop targets const old_break = self.break_target; const old_continue = self.continue_target; const old_defer_base = self.loop_defer_base; self.break_target = exit_bb; self.continue_target = inc_bb; // continue → increment, not header self.loop_defer_base = self.defer_stack.items.len; self.lowerBlock(fe.body); self.break_target = old_break; self.continue_target = old_continue; self.loop_defer_base = old_defer_base; self.scope = old_scope; body_scope.deinit(); // Fall through to increment block if (!self.currentBlockHasTerminator()) { self.builder.br(inc_bb, &.{}); } // Increment block: advance every cursor and jump back to header. self.builder.switchToBlock(inc_bb); { const one = self.builder.constInt(1, .i64); for (preps.items) |prep| { const cur = self.builder.load(prep.slot, .i64); const next = self.builder.add(cur, one, .i64); self.builder.store(prep.slot, next); } self.builder.br(header_bb, &.{}); } // Continue at exit self.builder.switchToBlock(exit_bb); return self.builder.constInt(0, .void); } /// Comptime-unrolled `inline for`. Iterables are comptime ranges and/or /// PACKS, mirroring the runtime multi-iterable contract: position 0 drives /// the iteration count (a pack's arity, or a bounded range's span) and /// trailing range bounds are ignored. Per iteration the body is lowered /// once; a range capture binds as an `int_val` comptime constant (so /// `xs[i]` substitutes the concrete per-position argument), and a pack /// capture binds as an AST alias for the synthesized `xs[]` /// (`Binding.pack_elem`), inheriting full pack-element semantics — /// substitution, typing, and the interface-only constraint check. /// /// inline for 0..xs.len (i) { xs[i].show(); } // index form /// inline for xs (x) { x.show(); } // element form /// inline for xs, 0.. (x, i) { ... } // element + index pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr) Ref { const IterClass = union(enum) { range: i64, // comptime start value pack: []const u8, // pack name }; var classes = std.ArrayList(IterClass).empty; defer classes.deinit(self.alloc); var count: i64 = 0; for (fe.iterables, 0..) |it, idx| { if (it.is_range) { var start = self.evalComptimeInt(it.expr) orelse { if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: range start is not a compile-time integer", .{}); return self.builder.constInt(0, .void); }; if (it.start_exclusive) start += 1; if (idx == 0) { const end_node = it.range_end orelse { if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: the first range must be bounded — `inline for 0..N (i) {{ }}`", .{}); return self.builder.constInt(0, .void); }; var 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); }; if (it.end_inclusive) end += 1; count = end - start; } classes.append(self.alloc, .{ .range = start }) catch unreachable; } else if (it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) { const name = it.expr.data.identifier.name; const len: i64 = if (self.pack_param_count) |ppc| @intCast(ppc.get(name) orelse 0) else 0; if (idx == 0) { count = len; } else if (len < count) { if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: pack '{s}' has {} element{s} but the unroll is {} iterations", .{ name, len, if (len == 1) @as([]const u8, "") else @as([]const u8, "s"), count, }); return self.builder.constInt(0, .void); } classes.append(self.alloc, .{ .pack = name }) catch unreachable; } else { if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) {{ }}` / `inline for xs (x) {{ }}`", .{}); return self.builder.constInt(0, .void); } } // `(*x)` on a pack element: there is no storage to borrow — an element // is an AST-substituted call argument. for (fe.captures, 0..) |cap, ci| { if (cap.by_ref and ci < classes.items.len and classes.items[ci] == .pack) { const sp = cap.span orelse fe.iterables[ci].expr.span; if (self.diagnostics) |d| d.addFmt(.err, sp, "a pack element cannot be captured by reference", .{}); return self.builder.constInt(0, .void); } } const CursorSave = struct { name: []const u8, had_prev: bool, prev: ComptimeValue }; var i: i64 = 0; while (i < count) : (i += 1) { var body_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &body_scope; var saves = std.ArrayList(CursorSave).empty; defer saves.deinit(self.alloc); for (fe.captures, 0..) |cap, ci| { if (cap.name.len == 0) continue; switch (classes.items[ci]) { .range => |start| { // Bind the cursor both as a runtime value (constInt, for // uses like `print(i)`) and as a comptime constant (for // `xs[i]` substitution). const v = start + i; body_scope.put(cap.name, .{ .ref = self.builder.constInt(v, .i64), .ty = .i64, .is_alloca = false }); var save = CursorSave{ .name = cap.name, .had_prev = false, .prev = undefined }; if (self.comptime_constants.get(cap.name)) |p| { save.had_prev = true; save.prev = p; } saves.append(self.alloc, save) catch {}; self.comptime_constants.put(cap.name, .{ .int_val = v }) catch {}; }, .pack => |pack_name| { const span = fe.iterables[ci].expr.span; const id_node = self.alloc.create(Node) catch break; id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } }; const idx_node = self.alloc.create(Node) catch break; idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = i } } }; const elem_node = self.alloc.create(Node) catch break; elem_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; const elem_ty = self.inferExprType(elem_node); body_scope.put(cap.name, .{ .ref = Ref.none, .ty = elem_ty, .is_alloca = false, .pack_elem = elem_node }); }, } } self.lowerBlock(fe.body); for (saves.items) |save| { if (save.had_prev) { self.comptime_constants.put(save.name, save.prev) catch {}; } else { _ = self.comptime_constants.remove(save.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 (i64) 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 } }, .i64); } 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 .i32; }; 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 .i64; 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`. 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 . 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, span: ast.Span) Ref { if (self.break_target) |target| { // Leaving the loop body's scope: run the defers registered since the // loop began (LIFO) before the jump — same as the fall-through exit. self.emitLoopExitDefers(); self.builder.br(target, &.{}); } else if (self.diagnostics) |d| { d.addFmt(.err, span, "`break` outside a loop", .{}); } return Ref.none; } pub fn lowerContinue(self: *Lowering, span: ast.Span) Ref { if (self.continue_target) |target| { self.emitLoopExitDefers(); self.builder.br(target, &.{}); } else if (self.diagnostics) |d| { d.addFmt(.err, span, "`continue` outside a loop", .{}); } return Ref.none; } // ── Block plumbing ────────────────────────────────────────────── 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; } 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); } }