refactor(B3.2): move control flow to lower/control_flow.zig
Verbatim relocation of the 14-method control-flow cluster (if/while/ for/match lowering incl. comptime-inline variants, break/continue, block plumbing: freshBlock, freshBlockWithParams, currentBlockHasTerminator, ensureTerminator) into src/ir/lower/control_flow.zig. 14 aliases on Lowering keep all call sites unchanged. Method pub-flips: computeHasImpl, headTypeGate, inferMatchResultType, resolveTypeCategoryTags, isTypeCategoryMatch. Unqualified references in moved bodies (ComptimeValue nested type, isTypeCategoryMatch static) resolved via file-scope alias consts — bodies stay verbatim. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn.
This commit is contained in:
937
src/ir/lower.zig
937
src/ir/lower.zig
File diff suppressed because it is too large
Load Diff
960
src/ir/lower/control_flow.zig
Normal file
960
src/ir/lower/control_flow.zig
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user