From 6b07346d98f8a057ecbebf00b145d269d226f6ec Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 14:30:34 +0300 Subject: [PATCH] refactor(B7.2): move pack/variadic lowering to lower/pack.zig Verbatim relocation of the 19-method pack cluster (pack element/value projection, spread refs, pack indexing + diagnostics, pack-to-slice, variadic arg lowering/packing, pack-slice materialisation, pack-fn calls + monomorphisation, pack projection resolution, isPackFn/ isPackParam predicates) into src/ir/lower/pack.zig. 19 aliases on Lowering keep all call sites unchanged. ProjectionPosition and PackProjection move from lower/protocol.zig to lower/pack.zig (their domain home, next to resolvePackProjection); the Lowering type aliases are repointed, so external references (lower.test.zig) are unchanged. Method pub-flip: findReturnValueType. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 1108 +----------------------------------- src/ir/lower/pack.zig | 1136 +++++++++++++++++++++++++++++++++++++ src/ir/lower/protocol.zig | 8 - 3 files changed, 1161 insertions(+), 1091 deletions(-) create mode 100644 src/ir/lower/pack.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 80f82a7..d6e1961 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -44,6 +44,7 @@ const lower_coerce = @import("lower/coerce.zig"); const lower_ffi = @import("lower/ffi.zig"); const lower_objc_class = @import("lower/objc_class.zig"); const lower_call = @import("lower/call.zig"); +const lower_pack = @import("lower/pack.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -2015,71 +2016,6 @@ pub const Lowering = struct { return self.emitPlaceholder(fa.field); } - /// Lower each pack element to a Ref: `pack_name[i]` when `method` is null, - /// or `pack_name[i].method()` when given. Synthesizes the index/field/call - /// AST per element and lowers it (substitution turns `xs[i]` into the - /// concrete arg; UFCS dispatches the method). Caller owns the returned slice. - fn lowerPackElems(self: *Lowering, pack_name: []const u8, method: ?[]const u8, span: ast.Span) []Ref { - const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0; - var refs = std.ArrayList(Ref).empty; - var i: u32 = 0; - while (i < n) : (i += 1) { - 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 = @intCast(i) } } }; - const index_node = self.alloc.create(Node) catch break; - index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; - var elem_node = index_node; - if (method) |m| { - const fa_node = self.alloc.create(Node) catch break; - fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = m } } }; - const call_node = self.alloc.create(Node) catch break; - call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } }; - elem_node = call_node; - } - refs.append(self.alloc, self.lowerExpr(elem_node)) catch break; - } - return refs.toOwnedSlice(self.alloc) catch &.{}; - } - - /// Value-position pack projection `xs.`: call the (zero-arg) - /// protocol method on each element and collect the results into a tuple - /// `(xs[0].(), …, xs[N-1].())`. N=0 yields the empty tuple. - fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref { - const refs = self.lowerPackElems(pack_name, method, span); - defer self.alloc.free(refs); - var tys = std.ArrayList(TypeId).empty; - defer tys.deinit(self.alloc); - for (refs) |r| tys.append(self.alloc, self.builder.getRefType(r)) catch {}; - const tuple_ty = self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void), - .names = null, - } }); - const owned = self.alloc.dupe(Ref, refs) catch return self.builder.constInt(0, .void); - return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); - } - - /// If `operand` is a pack spread — `..xs` (bare pack) or `..xs.method` - /// (per-element projection) — return the per-element Refs to splice into a - /// call's positional args. Null when it's not a pack spread (e.g. a runtime - /// slice `..arr`, handled by the slice-variadic path). Caller owns the slice. - pub fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref { - const ppc = self.pack_param_count orelse return null; - switch (operand.data) { - .identifier => |id| { - if (ppc.contains(id.name)) return self.lowerPackElems(id.name, null, span); - }, - .field_access => |fa| { - if (fa.object.data == .identifier and ppc.contains(fa.object.data.identifier.name)) { - return self.lowerPackElems(fa.object.data.identifier.name, fa.field, span); - } - }, - else => {}, - } - return null; - } - /// Lower a struct-level constant value (e.g., Phys.GRAVITY). fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref { const val_node = info.value; @@ -2662,130 +2598,6 @@ pub const Lowering = struct { return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); } - /// Detect `[]` where the literal exceeds - /// the pack arity (or is negative). Emits a diagnostic and - /// returns true; caller skips the standard indexing path and - /// returns a placeholder Ref. Returns false for non-pack bases, - /// non-literal indices, or in-range indices. - fn diagPackIndexOOB(self: *Lowering, ie: *const ast.IndexExpr) bool { - const ppc = self.pack_param_count orelse return false; - if (ie.object.data != .identifier) return false; - const pack_name = ie.object.data.identifier.name; - const n = ppc.get(pack_name) orelse return false; - // Any comptime index (int literal or a comptime-constant cursor) that's - // out of range — runtime indices are handled by the caller's - // must-be-comptime check. - const raw: i64 = self.comptimeIndexOf(ie.index) orelse return false; - if (raw >= 0 and @as(u32, @intCast(raw)) < n) return false; - if (self.diagnostics) |diags| { - diags.addFmt(.err, ie.index.span, "pack index {} out of bounds: '{s}' has {} element{s}", .{ - raw, pack_name, n, if (n == 1) @as([]const u8, "") else @as([]const u8, "s"), - }); - } - return true; - } - - /// Returns the call-site arg AST node when `ie` matches - /// `[]` with the pack name bound - /// in the active `pack_arg_nodes` map and the index in range. - /// Otherwise null — caller falls back to standard slice indexing. - pub fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node { - const pan = self.pack_arg_nodes orelse return null; - if (ie.object.data != .identifier) return null; - const arg_nodes = pan.get(ie.object.data.identifier.name) orelse return null; - const raw: i64 = self.comptimeIndexOf(ie.index) orelse return null; - if (raw < 0) return null; - const i: usize = @intCast(raw); - if (i >= arg_nodes.len) return null; - return arg_nodes[i]; - } - - /// Resolve an index expression to a comptime-known integer: a literal, - /// or an identifier bound to an `int_val` in `comptime_constants` (e.g. - /// the cursor of an `inline for 0..N (i)` unroll). Otherwise null. - pub fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 { - switch (index.data) { - .int_literal => |lit| return lit.value, - .identifier => |id| { - if (self.comptime_constants.get(id.name)) |cv| { - switch (cv) { - .int_val => |iv| return iv, - else => return null, - } - } - return null; - }, - else => return null, - } - } - - const PackValueKind = enum { storage, call_arg, return_value, runtime_iter, generic }; - - /// `xs` is a pack name used where a runtime value is required. A pack is - /// comptime-only (Decision 1), so this is an error — with a context-tailored - /// suggestion for how to express the intent instead. - pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref { - if (self.diagnostics) |d| { - const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name}); - switch (kind) { - .storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}), - .call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}), - .return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}), - .runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }), - .generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }), - } - } - return self.emitPlaceholder(name); - } - - /// True when `name` is a pack parameter bound in the current mono body. - pub fn isPackName(self: *Lowering, name: []const u8) bool { - const ppc = self.pack_param_count orelse return false; - return ppc.contains(name); - } - - /// `xx ` with a slice target: materialize the comptime pack into a - /// runtime `[]elem` by lowering each element node and boxing (`[]Any`) or - /// `xx`-erasing (`[]P`) it into a stack `[N]elem`, then return the slice. - /// This is the explicit pack→slice bridge (issue 0053). - fn lowerPackToSlice(self: *Lowering, pack_name: []const u8, slice_ty: TypeId) Ref { - const arg_nodes = (self.pack_arg_nodes orelse return self.builder.constInt(0, .unresolved)).get(pack_name) orelse - return self.builder.constInt(0, .unresolved); - const elem_ty = self.module.types.get(slice_ty).slice.element; - const is_any = elem_ty == .any; - const elem_is_protocol = blk: { - if (elem_ty.isBuiltin()) break :blk false; - const ei = self.module.types.get(elem_ty); - break :blk ei == .@"struct" and ei.@"struct".is_protocol; - }; - const slice_slot = self.builder.alloca(slice_ty); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); - if (arg_nodes.len == 0) { - self.builder.store(ptr_gep, self.builder.constNull(self.module.types.ptrTo(elem_ty))); - self.builder.store(len_gep, self.builder.constInt(0, .s64)); - return self.builder.load(slice_slot, slice_ty); - } - const array_ty = self.module.types.arrayOf(elem_ty, @intCast(arg_nodes.len)); - const array_slot = self.builder.alloca(array_ty); - for (arg_nodes, 0..) |arg, i| { - var val = self.lowerExpr(arg); - var source_ty = self.inferExprType(arg); - if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); - if (is_any) { - if (source_ty != .any) val = self.builder.boxAny(val, source_ty); - } else if (elem_is_protocol) { - if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg, source_ty, elem_ty); - } - const ep = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(@intCast(i), .s64) } }, self.module.types.ptrTo(elem_ty)); - self.builder.store(ep, val); - } - const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(0, .s64) } }, self.module.types.ptrTo(elem_ty)); - self.builder.store(ptr_gep, data_ptr); - self.builder.store(len_gep, self.builder.constInt(@intCast(arg_nodes.len), .s64)); - return self.builder.load(slice_slot, slice_ty); - } - fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { const obj = self.lowerExpr(se.object); const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); @@ -3712,854 +3524,6 @@ pub const Lowering = struct { // ── Defer/Push/MultiAssign ────────────────────────────────────── - /// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, - /// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. - pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { - const any_slice_ty = self.module.types.sliceOf(.any); - const n = if (call_args.len > start_idx) call_args.len - start_idx else 0; - - if (n == 0) { - // Empty slice: {null, 0} - const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any)); - const zero_len = self.builder.constInt(0, .s64); - const slice_slot = self.builder.alloca(any_slice_ty); - // Store ptr (field 0) and len (field 1) into the slice alloca - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); - self.builder.store(ptr_gep, null_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, zero_len); - if (self.scope) |scope| { - scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); - } - return; - } - - // Allocate stack array [N x Any] - const array_ty = self.module.types.arrayOf(.any, @intCast(n)); - const array_slot = self.builder.alloca(array_ty); - - // Box each arg and store into array - for (call_args[start_idx..], 0..) |arg, i| { - var val = self.lowerExpr(arg); - var source_ty = self.inferExprType(arg); - // If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that - if (source_ty == .unresolved) { - const ref_ty = self.builder.getRefType(val); - if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) { - source_ty = ref_ty; - } else if (!ref_ty.isBuiltin()) { - const ri = self.module.types.get(ref_ty); - if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) { - source_ty = ref_ty; - } - } - } - // Auto-unwrap optionals: box inner value if present, else box string "null" - if (!source_ty.isBuiltin()) { - const opt_info = self.module.types.get(source_ty); - if (opt_info == .optional) { - const child_ty = opt_info.optional.child; - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); - const some_bb = self.freshBlock("opt.some"); - const none_bb = self.freshBlock("opt.none"); - const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); - self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); - self.builder.switchToBlock(some_bb); - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); - const boxed_inner = self.builder.boxAny(unwrapped, child_ty); - self.builder.br(merge_bb, &.{boxed_inner}); - self.builder.switchToBlock(none_bb); - const null_str_id = self.module.types.internString("null"); - const null_str = self.builder.constString(null_str_id); - const boxed_null = self.builder.boxAny(null_str, .string); - self.builder.br(merge_bb, &.{boxed_null}); - self.builder.switchToBlock(merge_bb); - val = self.builder.blockParam(merge_bb, 0, TypeId.any); - source_ty = .any; - } - } - const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty); - // GEP to array[i] and store - const idx_ref = self.builder.constInt(@intCast(i), .s64); - const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any)); - self.builder.store(elem_ptr, boxed); - } - - // Build slice {ptr_to_first_element, len} - const slice_slot = self.builder.alloca(any_slice_ty); - // Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any) - const zero = self.builder.constInt(0, .s64); - const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any)); - const len_ref = self.builder.constInt(@intCast(n), .s64); - // Store into slice fields - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); - self.builder.store(ptr_gep, data_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, len_ref); - - if (self.scope) |scope| { - scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); - } - } - - /// Pack variadic args into a slice for regular function calls. - /// Detects variadic params in the function decl, packs remaining args into a typed slice, - /// and replaces the args list with [fixed_args..., slice_ref]. - pub fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { - // `#foreign` variadic uses the C calling convention's `...` tail — - // extras are passed through directly with default argument promotion - // (handled at the call site), not packed into an sx slice. - if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { - return; - } - // Find variadic param index. The two surface forms differ in - // what `p.type_expr` resolves to: legacy `name: ..T` declares T - // (element type), new `..name: []T` declares []T (already a - // slice). Unwrap the latter so the per-element packing below - // sees T in both cases. - var variadic_idx: ?usize = null; - var elem_ty: TypeId = .any; - for (fd.params, 0..) |p, i| { - if (p.is_variadic) { - variadic_idx = i; - const declared = self.resolveTypeWithBindings(p.type_expr); - elem_ty = declared; - if (!declared.isBuiltin()) { - const info = self.module.types.get(declared); - if (info == .slice) elem_ty = info.slice.element; - } - break; - } - } - const vi = variadic_idx orelse return; // no variadic param - - // Number of non-variadic args - const fixed_count = vi; - const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0; - const slice_ty = self.module.types.sliceOf(elem_ty); - - // Check for spread operator: sum(..arr) — single spread arg becomes the slice directly - if (variadic_count == 1 and fixed_count < c.args.len) { - const arg_node = c.args[fixed_count]; - if (arg_node.data == .spread_expr) { - const spread = arg_node.data.spread_expr; - const arr_val = self.lowerExpr(spread.operand); - const arr_ty = self.inferExprType(spread.operand); - const arr_info = self.module.types.get(arr_ty); - // Convert array to slice - const slice_val = switch (arr_info) { - .array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty), - .slice => arr_val, - else => arr_val, - }; - args.shrinkRetainingCapacity(fixed_count); - args.append(self.alloc, slice_val) catch unreachable; - return; - } - } - - if (variadic_count == 0) { - // Empty slice - const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty)); - const zero_len = self.builder.constInt(0, .s64); - const slice_slot = self.builder.alloca(slice_ty); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); - self.builder.store(ptr_gep, null_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); - self.builder.store(len_gep, zero_len); - const slice_val = self.builder.load(slice_slot, slice_ty); - // Replace args: keep fixed args, append slice - args.shrinkRetainingCapacity(fixed_count); - args.append(self.alloc, slice_val) catch unreachable; - return; - } - - // Determine if we need to box as Any (for ..Any params) or use raw type - const is_any = (elem_ty == .any); - // `..xs: []P` (slice of a protocol): each concrete arg must be erased to - // a protocol value {ctx, vtable}, not stored raw (which would be a - // size/type mismatch — a heap of garbage vtables → crash on dispatch). - const elem_is_protocol = blk: { - if (elem_ty.isBuiltin()) break :blk false; - const ei = self.module.types.get(elem_ty); - break :blk ei == .@"struct" and ei.@"struct".is_protocol; - }; - - // Allocate stack array [N x ElemType] - const array_elem = if (is_any) TypeId.any else elem_ty; - const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count)); - const array_slot = self.builder.alloca(array_ty); - - // Store each variadic arg into array - for (0..variadic_count) |i| { - var val = args.items[fixed_count + i]; - if (is_any) { - var source_ty = self.inferExprType(c.args[fixed_count + i]); - // If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that - if (source_ty == .unresolved) { - const ref_ty = self.builder.getRefType(val); - if (ref_ty != .unresolved and ref_ty != .void) source_ty = ref_ty; - } - // Auto-unwrap optionals: box inner value if present, else box string "null" - if (!source_ty.isBuiltin()) { - const opt_info = self.module.types.get(source_ty); - if (opt_info == .optional) { - const child_ty = opt_info.optional.child; - // Branch: has_value? → box inner : box "null" - const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); - const some_bb = self.freshBlock("opt.some"); - const none_bb = self.freshBlock("opt.none"); - const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); - self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); - // Some: unwrap and box inner value - self.builder.switchToBlock(some_bb); - const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); - const boxed_inner = self.builder.boxAny(unwrapped, child_ty); - self.builder.br(merge_bb, &.{boxed_inner}); - // None: box the string "null" - self.builder.switchToBlock(none_bb); - const null_str_id = self.module.types.internString("null"); - const null_str = self.builder.constString(null_str_id); - const boxed_null = self.builder.boxAny(null_str, .string); - self.builder.br(merge_bb, &.{boxed_null}); - // Merge - self.builder.switchToBlock(merge_bb); - val = self.builder.blockParam(merge_bb, 0, TypeId.any); - source_ty = .any; // already boxed - } - } - if (source_ty != .any) { - val = self.builder.boxAny(val, source_ty); - } - } else if (elem_is_protocol) { - // Erase each concrete arg to the protocol value via the same - // impl-driven `xx` machinery, so the runtime `[]P` holds real - // {ctx, vtable} values and `xs[i].method()` dispatches. - const arg_node = c.args[fixed_count + i]; - var source_ty = self.inferExprType(arg_node); - if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); - if (source_ty != elem_ty) { - val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty); - } - } - const idx_ref = self.builder.constInt(@intCast(i), .s64); - const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem)); - self.builder.store(elem_ptr, val); - } - - // Build slice {ptr, len} - const slice_slot = self.builder.alloca(slice_ty); - const zero = self.builder.constInt(0, .s64); - const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem)); - const len_ref = self.builder.constInt(@intCast(variadic_count), .s64); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty); - self.builder.store(ptr_gep, data_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); - self.builder.store(len_gep, len_ref); - const slice_val = self.builder.load(slice_slot, slice_ty); - - // Replace args: keep fixed args, append slice - args.shrinkRetainingCapacity(fixed_count); - args.append(self.alloc, slice_val) catch unreachable; - } - - // ── Generic monomorphization ────────────────────────────────── - - /// Build an `[]Any` slice value from the mono's pack params and - /// bind it to the pack name in scope. Each pack-param slot is - /// loaded, boxed via `boxAny`, and stored into a stack [N x Any] - /// array; the slice {data_ptr, len} is then bound. Used by - /// `monomorphizePackFn` so bodies that reference `args` bare or - /// index it with a runtime int resolve through the slice (with - /// element type `Any`). Literal-indexed accesses keep the - /// concrete per-position types via `packArgNodeAt`. - /// Build a `[]Type` slice VALUE for a bare `$` reference. - /// Differs from `materialisePackSlice` (which boxes each pack - /// element as Any so the body's `args[i]` reads an Any) — this - /// helper stores raw `.type_tag` Values via `const_type`, so the - /// slice is a list-of-Types that builder fns walk at interp time. - /// Slice IR type is `[]Any` (since `Type → .any`); the interp - /// stores whichever Value the elements actually carry. - fn buildPackSliceValue(self: *Lowering, arg_types: []const TypeId) Ref { - const any_slice_ty = self.module.types.sliceOf(.any); - const any_ptr_ty = self.module.types.ptrTo(.any); - - if (arg_types.len == 0) { - const null_ptr = self.builder.constNull(any_ptr_ty); - const zero_len = self.builder.constInt(0, .s64); - const slice_slot = self.builder.alloca(any_slice_ty); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); - self.builder.store(ptr_gep, null_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, zero_len); - return self.builder.load(slice_slot, any_slice_ty); - } - - const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); - const array_slot = self.builder.alloca(array_ty); - - for (arg_types, 0..) |ty, i| { - // `const_type` produces an `.any`-typed Type value - // (`{tag=.any, value=tid}`) — already the canonical Any - // shape, so no re-box needed. - const type_val = self.builder.constType(ty); - const idx_ref = self.builder.constInt(@intCast(i), .s64); - const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); - self.builder.store(elem_ptr, type_val); - } - - const slice_slot = self.builder.alloca(any_slice_ty); - const zero = self.builder.constInt(0, .s64); - const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); - const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); - self.builder.store(ptr_gep, data_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, len_ref); - return self.builder.load(slice_slot, any_slice_ty); - } - - fn materialisePackSlice( - self: *Lowering, - scope: *Scope, - pack_name: []const u8, - slot_refs: []const Ref, - arg_types: []const TypeId, - ) void { - const any_slice_ty = self.module.types.sliceOf(.any); - const any_ptr_ty = self.module.types.ptrTo(.any); - - if (arg_types.len == 0) { - const null_ptr = self.builder.constNull(any_ptr_ty); - const zero_len = self.builder.constInt(0, .s64); - const slice_slot = self.builder.alloca(any_slice_ty); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); - self.builder.store(ptr_gep, null_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, zero_len); - scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); - return; - } - - const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); - const array_slot = self.builder.alloca(array_ty); - - for (slot_refs, arg_types, 0..) |slot, ty, i| { - const val = self.builder.load(slot, ty); - const boxed = if (ty == .any) val else self.builder.boxAny(val, ty); - const idx_ref = self.builder.constInt(@intCast(i), .s64); - const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); - self.builder.store(elem_ptr, boxed); - } - - const slice_slot = self.builder.alloca(any_slice_ty); - const zero = self.builder.constInt(0, .s64); - const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); - const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); - const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); - self.builder.store(ptr_gep, data_ptr); - const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); - self.builder.store(len_gep, len_ref); - scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); - } - - /// Infer the return type of a pack-fn body for the generic-`$R` - /// case. Walks the body looking for the first concrete return - /// type: a `return X;` statement's value type, or — failing that — - /// the tail expression of an arrow-form body. Caller must have - /// `pack_arg_nodes` installed so `args[]` substitutes during - /// inference. Falls back to `.s64` if nothing concrete is found - /// (matches the broader "default to .s64" convention elsewhere). - fn inferPackBodyReturnType(self: *Lowering, body: *const Node) TypeId { - // First try explicit `return X;` — walks past structured - // control flow but stops at nested fn / lambda bodies. - if (self.findReturnValueType(body)) |ty| return ty; - // Arrow-form / tail-expression body: the body IS the value. - // For block bodies whose last stmt is an expression, walk down. - if (body.data == .block) { - const stmts = body.data.block.stmts; - if (stmts.len == 0) return .void; - return self.inferExprType(stmts[stmts.len - 1]); - } - return self.inferExprType(body); - } - - /// Per-call-shape monomorphisation entry for pack-fns - /// (`isPackFn(fd) == true`). Computes a mangled name from the - /// call-site arg types, builds the mono if it's not cached, and - /// emits a direct call. Pack params expand into N positional IR - /// params with concrete types; the body's `args[]` and - /// `args.len` resolve to those params via the pack bindings. - pub fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { - // Split call args along the fd.params boundary: - // - non-comptime non-pack params → consume one call arg as a - // runtime IR param. - // - comptime non-pack params → consume one call arg, fold its - // value into the mangle (NOT a runtime IR param). - // - pack param (always last) → consume the remaining call args - // as the pack expansion. - var pack_arg_types = std.ArrayList(TypeId).empty; - defer pack_arg_types.deinit(self.alloc); - var pack_start: usize = call_node.args.len; - // Constraint protocol of the pack param (`..xs: P`), if any. The - // comptime type-pack `..$args` has no constraint to check. - var pack_protocol: ?[]const u8 = null; - var pack_is_comptime = false; - var pack_name: []const u8 = ""; - { - var fi: usize = 0; - for (fd.params) |p| { - if (isPackParam(p)) { - pack_start = fi; - pack_is_comptime = p.is_comptime; - pack_name = p.name; - if (p.is_pack and p.type_expr.data == .type_expr) { - pack_protocol = p.type_expr.data.type_expr.name; - } - break; - } - if (fi >= call_node.args.len) break; - fi += 1; - } - } - - // Lower the PACK args first, taking each type from the lowered value - // (`getRefType`) — never a pre-lowering `inferExprType` guess. Knowing - // the pack element types up front lets the prefix args (e.g. - // `mapper: Closure(..sources.T) -> $R`) resolve against them, so a - // lambda arg types its params from the projected closure signature. - // (A comptime `..$args` pack keeps `inferExprType` — its args may be - // type-position.) - // A pack arg is independently typed — it takes its natural type and - // (for a comptime `..$args` pack) auto-boxes to `Any` at the call - // boundary. It is NEVER coerced to a leftover outer `target_type`, so - // clear it: otherwise an `xx ` pack arg (whose result type IS - // `target_type`) would cast to the stale target — e.g. `format("…", xx i)` - // inside a `-> string` fn mis-typed the arg as `string`, monomorphizing - // `__pack_string` and ABI-coercing the 4-byte int as a 16-byte fat - // pointer → memory corruption (issue 0057). - const saved_pack_tt = self.target_type; - self.target_type = null; - var pack_refs = std.ArrayList(Ref).empty; - defer pack_refs.deinit(self.alloc); - for (call_node.args[pack_start..]) |a| { - const r = self.lowerExpr(a); - pack_refs.append(self.alloc, r) catch return self.builder.constInt(0, .void); - if (pack_is_comptime) { - const it = self.inferExprType(a); - pack_arg_types.append(self.alloc, if (it == .unresolved) self.builder.getRefType(r) else it) catch return self.builder.constInt(0, .void); - } else { - pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); - } - } - self.target_type = saved_pack_tt; - - // Install the pack's element types + constraint so prefix-arg param - // types like `Closure(..sources.T)` resolve while lowering the prefix. - var pat_map = std.StringHashMap([]const TypeId).init(self.alloc); - defer pat_map.deinit(); - pat_map.put(pack_name, pack_arg_types.items) catch {}; - var pcon_map = std.StringHashMap([]const u8).init(self.alloc); - defer pcon_map.deinit(); - if (pack_protocol) |proto| pcon_map.put(pack_name, proto) catch {}; - const saved_pat = self.pack_arg_types; - const saved_pcon = self.pack_constraint; - self.pack_arg_types = pat_map; - if (pack_protocol != null) self.pack_constraint = pcon_map; - - var args = std.ArrayList(Ref).empty; - defer args.deinit(self.alloc); - { - var ri: usize = 0; - for (fd.params) |p| { - if (isPackParam(p)) break; - if (ri >= call_node.args.len) break; - if (!p.is_comptime) { - // Contextually type the arg from the param (so a lambda arg - // `(x) => …` takes its param types from a `Closure(...)` param). - // The param type is resolved under the pack fn's OWN source - // (E4): a fixed-prefix type bare-visible only in the defining - // module must resolve there, not the caller's. The arg itself - // is lowered AFTER, in the caller's context. - const saved_tt = self.target_type; - const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); - if (pty != .unresolved) self.target_type = pty; - args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void); - self.target_type = saved_tt; - } - ri += 1; - } - } - self.pack_arg_types = saved_pat; - self.pack_constraint = saved_pcon; - - // Infer type-param bindings (e.g. `$R` in `mapper: Closure(..) -> $R`) - // from the lowered prefix args. `args.items` holds the non-comptime - // prefix refs in declaration order; match each prefix param's declared - // type against its arg's concrete type to bind the function's - // type-params. These flow into the mangle and the mono's - // `self.type_bindings` so `-> VL($R)` / `Combined($R, ..)` resolve. - var tparam_bindings = std.StringHashMap(TypeId).init(self.alloc); - defer tparam_bindings.deinit(); - if (fd.type_params.len > 0) { - var pref_ref_idx: usize = 0; - for (fd.params) |p| { - if (isPackParam(p)) break; - if (p.is_comptime) continue; - if (pref_ref_idx >= args.items.len) break; - const arg_ty = self.builder.getRefType(args.items[pref_ref_idx]); - for (fd.type_params) |tp| { - if (tparam_bindings.contains(tp.name)) continue; - if (self.extractTypeParam(p.type_expr, arg_ty, tp.name)) |ety| { - if (ety != .unresolved) tparam_bindings.put(tp.name, ety) catch {}; - } - } - pref_ref_idx += 1; - } - } - - // Append the (already-lowered) pack args after the prefix args. - for (pack_refs.items) |r| args.append(self.alloc, r) catch return self.builder.constInt(0, .void); - - // Per-position conformance: each pack arg must impl the constraint - // protocol. Only enforced for a known protocol constraint — an unknown - // name (e.g. a plain type used as a pack constraint) is left alone. - if (pack_protocol) |proto| { - if (self.program_index.protocol_ast_map.contains(proto)) { - for (call_node.args[pack_start..], pack_arg_types.items) |arg_node, arg_ty| { - if (!self.protocolResolver().packArgConformsTo(proto, arg_ty)) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, arg_node.span, "pack argument of type '{s}' does not conform to protocol '{s}'", .{ self.formatTypeName(arg_ty), proto }); - } - } - } - } - } - - // Mangle: `__pack__` with comptime values - // (if any) folded into a `__ct_` segment per non-pack - // comptime param. Distinct call shapes — including different - // comptime VALUES — get distinct symbols. - var name_buf = std.ArrayList(u8).empty; - defer name_buf.deinit(self.alloc); - name_buf.appendSlice(self.alloc, fd.name) catch return self.builder.constInt(0, .void); - // Comptime values first (deterministic by fd.params order). - var ct_fi: usize = 0; - for (fd.params) |p| { - if (isPackParam(p)) break; - if (ct_fi >= call_node.args.len) break; - if (p.is_comptime) { - name_buf.appendSlice(self.alloc, "__ct_") catch return self.builder.constInt(0, .void); - self.genericResolver().appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]); - } - ct_fi += 1; - } - // Inferred type-param bindings (deterministic by fd.type_params order). - for (fd.type_params) |tp| { - if (tparam_bindings.get(tp.name)) |ty| { - name_buf.appendSlice(self.alloc, "__tp_") catch return self.builder.constInt(0, .void); - name_buf.appendSlice(self.alloc, self.mangleTypeName(ty)) catch return self.builder.constInt(0, .void); - } - } - name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void); - for (pack_arg_types.items) |t| { - name_buf.append(self.alloc, '_') catch return self.builder.constInt(0, .void); - name_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return self.builder.constInt(0, .void); - } - const mangled = name_buf.items; - - if (!self.lowered_functions.contains(mangled)) { - self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node, &tparam_bindings); - } - - const fid = self.resolveFuncByName(mangled) orelse return self.builder.constInt(0, .void); - const func = &self.module.functions.items[@intFromEnum(fid)]; - const ret_ty = func.ret; - const params = func.params; - const final_args = self.prependCtxIfNeeded(func, args.items); - self.coerceCallArgs(final_args, params); - return self.builder.call(fid, final_args, ret_ty); - } - - - /// Build a single mono fn for the given pack-fn + concrete arg types. - /// The mono carries N positional pack-params (synthesised names - /// `__pack__`) plus any fixed-prefix non-pack params from - /// the original declaration. The body lowers normally — real - /// `return X;` emits real `ret X`; `args[]` substitutes via - /// `pack_arg_nodes`; `args.len` resolves via `pack_param_count`. - fn monomorphizePackFn( - self: *Lowering, - fd: *const ast.FnDecl, - mangled_name: []const u8, - arg_types: []const TypeId, - call_node: *const ast.Call, - type_bindings: *const std.StringHashMap(TypeId), - ) void { - const owned_name = self.alloc.dupe(u8, mangled_name) catch return; - self.lowered_functions.put(owned_name, {}) catch {}; - - // Find the pack param's name and position in fd.params, plus its - // constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none). - var pack_name: []const u8 = ""; - var pack_param_idx: usize = std.math.maxInt(usize); - var pack_proto: ?[]const u8 = null; - for (fd.params, 0..) |p, i| { - if (isPackParam(p)) { - pack_name = p.name; - pack_param_idx = i; - if (p.is_pack and p.type_expr.data == .type_expr) { - pack_proto = p.type_expr.data.type_expr.name; - } - break; - } - } - if (pack_param_idx == std.math.maxInt(usize)) return; - - // Save state — mirrors monomorphizeFunction but also captures - // pack/inline-return state since the mono body must NOT route - // returns through any caller's inline slot. - const saved_func = self.builder.func; - const saved_block = self.builder.current_block; - const saved_counter = self.builder.inst_counter; - const saved_scope = self.scope; - const saved_defer_base = self.func_defer_base; - const saved_block_terminated = self.block_terminated; - const saved_target = self.target_type; - const saved_pan = self.pack_arg_nodes; - const saved_ppc = self.pack_param_count; - const saved_pat = self.pack_arg_types; - const saved_pcon = self.pack_constraint; - const saved_iri = self.inline_return_target; - const saved_ctx_ref = self.current_ctx_ref; - const saved_type_bindings = self.type_bindings; - self.func_defer_base = self.defer_stack.items.len; - self.block_terminated = false; - self.inline_return_target = null; - // Generic type-params inferred at the call site (e.g. `$R` from the - // mapper's closure return). Installed for the whole mono so - // return-type resolution and body lowering substitute them. - self.type_bindings = type_bindings.*; - defer { - self.type_bindings = saved_type_bindings; - self.scope = saved_scope; - self.func_defer_base = saved_defer_base; - self.block_terminated = saved_block_terminated; - self.target_type = saved_target; - self.pack_arg_nodes = saved_pan; - self.pack_param_count = saved_ppc; - self.pack_arg_types = saved_pat; - self.pack_constraint = saved_pcon; - self.inline_return_target = saved_iri; - self.current_ctx_ref = saved_ctx_ref; - self.builder.func = saved_func; - self.builder.current_block = saved_block; - self.builder.inst_counter = saved_counter; - } - - const wants_ctx = self.funcWantsImplicitCtx(fd); - - // Synthesise pack-param names + AST ident nodes used to bind - // `args[]` substitutions during body lowering. Built - // BEFORE return-type resolution so the generic-`$R` path can - // pre-install the binding for type inference. - var pack_synth_names = std.ArrayList([]const u8).empty; - defer pack_synth_names.deinit(self.alloc); - var pack_arg_idents = std.ArrayList(*const Node).empty; - defer pack_arg_idents.deinit(self.alloc); - for (arg_types, 0..) |_, i| { - const synth_name = std.fmt.allocPrint(self.alloc, "__pack_{s}_{d}", .{ pack_name, i }) catch return; - pack_synth_names.append(self.alloc, synth_name) catch return; - const ident_node = self.alloc.create(Node) catch return; - ident_node.* = .{ - .span = fd.body.span, - .data = .{ .identifier = .{ .name = synth_name } }, - }; - pack_arg_idents.append(self.alloc, ident_node) catch return; - } - - // Resolve return type. When the declared type is a generic - // name (e.g. `(..$args) -> $R`), `resolveReturnType` would - // return an opaque struct TypeId and the mono's signature - // would be wrong. Pre-install the pack bindings + infer the - // ret type from the body's tail expression / first explicit - // `return X;` instead. - var pre_pan = std.StringHashMap([]const *const Node).init(self.alloc); - defer pre_pan.deinit(); - pre_pan.put(pack_name, pack_arg_idents.items) catch return; - var pre_ppc = std.StringHashMap(u32).init(self.alloc); - defer pre_ppc.deinit(); - pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return; - var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc); - defer pre_pat.deinit(); - pre_pat.put(pack_name, arg_types) catch return; - var pre_pcon = std.StringHashMap([]const u8).init(self.alloc); - defer pre_pcon.deinit(); - if (pack_proto) |proto| pre_pcon.put(pack_name, proto) catch return; - self.pack_arg_nodes = pre_pan; - self.pack_param_count = pre_ppc; - self.pack_arg_types = pre_pat; - self.pack_constraint = if (pack_proto != null) pre_pcon else null; - - // Resolve the declared return + fixed-prefix param types in the pack fn's - // OWN module (E4), so a 2-flat-hop library type named in the signature is - // bare-visible — mirrors the body pin further down and the - // `monomorphizeFunction` pin. The comptime call-site args below are - // lowered AFTER this restore, in the caller's context (issue 0106). - const saved_sig_src = self.current_source_file; - if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - - const declared_is_generic_ret = blk: { - const rt = fd.return_type orelse break :blk false; - if (rt.data != .type_expr) break :blk false; - break :blk rt.data.type_expr.is_generic; - }; - const ret_ty: TypeId = if (declared_is_generic_ret) - self.inferPackBodyReturnType(fd.body) - else - self.resolveReturnType(fd); - self.target_type = ret_ty; - - // Param list: ctx (if needed) + fixed prefix + N pack params. - // Comptime non-pack params are NOT in the runtime signature — - // their values are folded into the mangle and substituted via - // `comptime_param_nodes` / bound as runtime locals in scope. - // NOT deinit'd — `params.items` is stored by reference in - // `Function.init` and read back later via `func.params`. - var params = std.ArrayList(Function.Param).empty; - if (wants_ctx) { - params.append(self.alloc, .{ - .name = self.module.types.internString("__sx_ctx"), - .ty = self.module.types.ptrTo(.void), - }) catch return; - } - for (fd.params, 0..) |p, i| { - if (i == pack_param_idx) continue; - if (p.is_comptime) continue; // folded into mangle, not in IR - const pty = self.resolveParamType(&p); - params.append(self.alloc, .{ - .name = self.module.types.internString(p.name), - .ty = pty, - }) catch return; - } - for (arg_types, 0..) |ty, i| { - params.append(self.alloc, .{ - .name = self.module.types.internString(pack_synth_names.items[i]), - .ty = ty, - }) catch return; - } - self.setCurrentSourceFile(saved_sig_src); - - const name_id = self.module.types.internString(owned_name); - _ = self.builder.beginFunction(name_id, params.items, ret_ty); - self.builder.currentFunc().has_implicit_ctx = wants_ctx; - - const entry_name = self.module.types.internString("entry"); - const entry = self.builder.appendBlock(entry_name, &.{}); - self.builder.switchToBlock(entry); - if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); - - var scope = Scope.init(self.alloc, null); - defer scope.deinit(); - self.scope = &scope; - - // Bind non-pack params. Walk fd.params + call_node.args - // together; comptime non-pack params bind both as runtime - // locals (so bare-name body access works) AND as - // comptime_param_nodes entries (so `#insert` substitution - // works). Non-comptime non-pack params consume IR param - // slots in order. - var cpn = std.StringHashMap(*const Node).init(self.alloc); - defer cpn.deinit(); - var param_idx: u32 = if (wants_ctx) 1 else 0; - var ct_arg_idx: usize = 0; - for (fd.params, 0..) |p, i| { - if (i == pack_param_idx) break; - if (p.is_comptime) { - if (ct_arg_idx < call_node.args.len) { - const call_arg = call_node.args[ct_arg_idx]; - self.stampCallerSource(call_arg); - cpn.put(p.name, call_arg) catch return; - // Bind as a runtime local for bare-name access. - // Lower the call arg as a value, then alloca + store. - const val = self.lowerExpr(call_arg); - const val_ty = self.builder.getRefType(val); - const slot = self.builder.alloca(val_ty); - self.builder.store(slot, val); - scope.put(p.name, .{ .ref = slot, .ty = val_ty, .is_alloca = true }); - } - ct_arg_idx += 1; - continue; - } - // Pin to the pack fn's OWN module (E4): a fixed-prefix param whose - // type is bare-visible only in the defining module must resolve - // there, not in the caller's restored context. Mirrors the - // signature build above and `resolveParamTypeInSource` at the - // cross-module call-arg typing sites. - const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); - const slot = self.builder.alloca(pty); - self.builder.store(slot, Ref.fromIndex(param_idx)); - scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); - param_idx += 1; - ct_arg_idx += 1; - } - // Install comptime_param_nodes for the body lowering. - const saved_cpn = self.comptime_param_nodes; - self.comptime_param_nodes = cpn; - defer self.comptime_param_nodes = saved_cpn; - var pack_param_slots = std.ArrayList(Ref).empty; - defer pack_param_slots.deinit(self.alloc); - for (arg_types, 0..) |ty, i| { - const synth_name = pack_synth_names.items[i]; - const slot = self.builder.alloca(ty); - self.builder.store(slot, Ref.fromIndex(param_idx)); - scope.put(synth_name, .{ .ref = slot, .ty = ty, .is_alloca = true }); - pack_param_slots.append(self.alloc, slot) catch return; - param_idx += 1; - } - - // Pack bindings remain installed from the pre-resolution - // (generic-`$R`) inference step above. No need to reinstall. - - // Materialise an `[]Any` slice value for the pack name so - // bare `args` (forwarding) and `args[]` (loops) - // resolve at runtime. Per-position type info is lost via - // Any boxing — that's the inherent cost of treating a - // heterogeneous pack as a uniform value. Literal-indexed - // access still goes through `packArgNodeAt` and keeps the - // concrete per-position types. - self.materialisePackSlice(&scope, pack_name, pack_param_slots.items, arg_types); - - // Pin to the metaprogram's OWN module for the BODY lowering only, so its - // bare names (and anything it `#insert`s — e.g. `build_format` / `out` / - // `emit` inside `std.print`) resolve in the defining module's visibility - // context, not the call site's (issue 0106). The comptime-param call-site - // args above were deliberately lowered FIRST, in the caller's context. - // Mirrors `lowerFunctionBodyInto`, which switches to `func.source_file`; - // the defining path is stamped on the body node by `resolveImports`. A - // synthesized/sourceless body keeps the caller's context. - const saved_source = self.current_source_file; - defer self.setCurrentSourceFile(saved_source); - if (fd.body.source_file) |src| self.setCurrentSourceFile(src); - - if (ret_ty != .void) { - const body_val = self.lowerBlockValue(fd.body); - if (!self.currentBlockHasTerminator()) { - if (body_val) |val| { - const val_ty = self.builder.getRefType(val); - const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; - self.builder.ret(coerced, ret_ty); - } else { - self.ensureTerminator(ret_ty); - } - } - } else { - self.lowerBlock(fd.body); - self.ensureTerminator(ret_ty); - } - self.builder.finalize(); - } - pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void { // Mark as lowered before lowering (prevents infinite recursion) // Need to dupe the name since mangled_name may be stack-allocated @@ -5352,27 +4316,6 @@ pub const Lowering = struct { }; } - /// Pack-fn: has a trailing heterogeneous pack param (`is_variadic - /// AND is_comptime`). Mixed shapes — non-pack comptime params - /// before the pack — are also accepted; the mono folds those - /// comptime VALUES into the mangled name and binds them as both - /// comptime substitutions (for #insert) and runtime locals (for - /// bare-name body references). - pub fn isPackFn(fd: *const ast.FnDecl) bool { - for (fd.params) |p| { - if (isPackParam(p)) return true; - } - return false; - } - - /// A trailing pack parameter: the comptime type-pack `..$args` - /// (`is_comptime`) or the protocol-constrained pack `..xs: P` (`is_pack`). - /// Both monomorphize per call shape via `lowerPackFnCall`; the slice - /// variadic (`..xs: []T`) is neither and stays a runtime slice. - fn isPackParam(p: ast.Param) bool { - return p.is_variadic and (p.is_comptime or p.is_pack); - } - pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { return self.resolveTypeWithBindings(rt); @@ -5416,7 +4359,7 @@ pub const Lowering = struct { /// Walk a function body and return the type of the first `return ;` /// statement encountered. Does not descend into nested function or lambda /// declarations (those have their own return types). - fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId { + pub fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId { return switch (node.data) { .return_stmt => |rs| if (rs.value) |v| self.inferExprType(v) else null, .block => |blk| blk: { @@ -6740,28 +5683,6 @@ pub const Lowering = struct { // ── Type registration ─────────────────────────────────────────── - /// Resolve `..pack.` against `protocol_name` by position (Decision 4). - /// No cross-namespace fallback: a value-position name that exists only as a - /// type-arg (or vice versa) is `.not_found`, letting the caller emit a - /// position-specific diagnostic (G3, Step 2.7). - pub fn resolvePackProjection( - self: *Lowering, - protocol_name: []const u8, - name: []const u8, - pos: ProjectionPosition, - ) PackProjection { - return switch (pos) { - .type_position => if (self.lookupProtocolArg(protocol_name, name)) |i| - .{ .type_arg = i } - else - .not_found, - .value_position => if (self.lookupProtocolField(protocol_name, name)) |i| - .{ .method = i } - else - .not_found, - }; - } - /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { @@ -7284,8 +6205,8 @@ pub const Lowering = struct { pub const registerGenericStructAlias = lower_nominal.registerGenericStructAlias; // --- moved to lower/protocol.zig (lower_protocol) --- - pub const ProjectionPosition = lower_protocol.ProjectionPosition; - pub const PackProjection = lower_protocol.PackProjection; + pub const ProjectionPosition = lower_pack.ProjectionPosition; + pub const PackProjection = lower_pack.PackProjection; pub const registerProtocolDecl = lower_protocol.registerProtocolDecl; pub const instantiateParamProtocol = lower_protocol.instantiateParamProtocol; pub const lookupProtocolArg = lower_protocol.lookupProtocolArg; @@ -7392,4 +6313,25 @@ pub const Lowering = struct { pub const expandCallDefaults = lower_call.expandCallDefaults; pub const userParamTypes = lower_call.userParamTypes; pub const resolveCallParamTypes = lower_call.resolveCallParamTypes; + + // --- moved to lower/pack.zig (lower_pack) --- + pub const lowerPackElems = lower_pack.lowerPackElems; + pub const lowerPackValueProjection = lower_pack.lowerPackValueProjection; + pub const packSpreadRefs = lower_pack.packSpreadRefs; + pub const diagPackIndexOOB = lower_pack.diagPackIndexOOB; + pub const packArgNodeAt = lower_pack.packArgNodeAt; + pub const comptimeIndexOf = lower_pack.comptimeIndexOf; + pub const diagPackAsValue = lower_pack.diagPackAsValue; + pub const isPackName = lower_pack.isPackName; + pub const lowerPackToSlice = lower_pack.lowerPackToSlice; + pub const lowerVariadicArgs = lower_pack.lowerVariadicArgs; + pub const packVariadicCallArgs = lower_pack.packVariadicCallArgs; + pub const buildPackSliceValue = lower_pack.buildPackSliceValue; + pub const materialisePackSlice = lower_pack.materialisePackSlice; + pub const inferPackBodyReturnType = lower_pack.inferPackBodyReturnType; + pub const lowerPackFnCall = lower_pack.lowerPackFnCall; + pub const monomorphizePackFn = lower_pack.monomorphizePackFn; + pub const resolvePackProjection = lower_pack.resolvePackProjection; + pub const isPackFn = lower_pack.isPackFn; + pub const isPackParam = lower_pack.isPackParam; }; diff --git a/src/ir/lower/pack.zig b/src/ir/lower/pack.zig new file mode 100644 index 0000000..70e4262 --- /dev/null +++ b/src/ir/lower/pack.zig @@ -0,0 +1,1136 @@ +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; + +/// Lower each pack element to a Ref: `pack_name[i]` when `method` is null, +/// or `pack_name[i].method()` when given. Synthesizes the index/field/call +/// AST per element and lowers it (substitution turns `xs[i]` into the +/// concrete arg; UFCS dispatches the method). Caller owns the returned slice. +pub fn lowerPackElems(self: *Lowering, pack_name: []const u8, method: ?[]const u8, span: ast.Span) []Ref { + const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0; + var refs = std.ArrayList(Ref).empty; + var i: u32 = 0; + while (i < n) : (i += 1) { + 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 = @intCast(i) } } }; + const index_node = self.alloc.create(Node) catch break; + index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; + var elem_node = index_node; + if (method) |m| { + const fa_node = self.alloc.create(Node) catch break; + fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = m } } }; + const call_node = self.alloc.create(Node) catch break; + call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } }; + elem_node = call_node; + } + refs.append(self.alloc, self.lowerExpr(elem_node)) catch break; + } + return refs.toOwnedSlice(self.alloc) catch &.{}; +} + +/// Value-position pack projection `xs.`: call the (zero-arg) +/// protocol method on each element and collect the results into a tuple +/// `(xs[0].(), …, xs[N-1].())`. N=0 yields the empty tuple. +pub fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref { + const refs = self.lowerPackElems(pack_name, method, span); + defer self.alloc.free(refs); + var tys = std.ArrayList(TypeId).empty; + defer tys.deinit(self.alloc); + for (refs) |r| tys.append(self.alloc, self.builder.getRefType(r)) catch {}; + const tuple_ty = self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void), + .names = null, + } }); + const owned = self.alloc.dupe(Ref, refs) catch return self.builder.constInt(0, .void); + return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty); +} + +/// If `operand` is a pack spread — `..xs` (bare pack) or `..xs.method` +/// (per-element projection) — return the per-element Refs to splice into a +/// call's positional args. Null when it's not a pack spread (e.g. a runtime +/// slice `..arr`, handled by the slice-variadic path). Caller owns the slice. +pub fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref { + const ppc = self.pack_param_count orelse return null; + switch (operand.data) { + .identifier => |id| { + if (ppc.contains(id.name)) return self.lowerPackElems(id.name, null, span); + }, + .field_access => |fa| { + if (fa.object.data == .identifier and ppc.contains(fa.object.data.identifier.name)) { + return self.lowerPackElems(fa.object.data.identifier.name, fa.field, span); + } + }, + else => {}, + } + return null; +} + +/// Detect `[]` where the literal exceeds +/// the pack arity (or is negative). Emits a diagnostic and +/// returns true; caller skips the standard indexing path and +/// returns a placeholder Ref. Returns false for non-pack bases, +/// non-literal indices, or in-range indices. +pub fn diagPackIndexOOB(self: *Lowering, ie: *const ast.IndexExpr) bool { + const ppc = self.pack_param_count orelse return false; + if (ie.object.data != .identifier) return false; + const pack_name = ie.object.data.identifier.name; + const n = ppc.get(pack_name) orelse return false; + // Any comptime index (int literal or a comptime-constant cursor) that's + // out of range — runtime indices are handled by the caller's + // must-be-comptime check. + const raw: i64 = self.comptimeIndexOf(ie.index) orelse return false; + if (raw >= 0 and @as(u32, @intCast(raw)) < n) return false; + if (self.diagnostics) |diags| { + diags.addFmt(.err, ie.index.span, "pack index {} out of bounds: '{s}' has {} element{s}", .{ + raw, pack_name, n, if (n == 1) @as([]const u8, "") else @as([]const u8, "s"), + }); + } + return true; +} + +/// Returns the call-site arg AST node when `ie` matches +/// `[]` with the pack name bound +/// in the active `pack_arg_nodes` map and the index in range. +/// Otherwise null — caller falls back to standard slice indexing. +pub fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node { + const pan = self.pack_arg_nodes orelse return null; + if (ie.object.data != .identifier) return null; + const arg_nodes = pan.get(ie.object.data.identifier.name) orelse return null; + const raw: i64 = self.comptimeIndexOf(ie.index) orelse return null; + if (raw < 0) return null; + const i: usize = @intCast(raw); + if (i >= arg_nodes.len) return null; + return arg_nodes[i]; +} + +/// Resolve an index expression to a comptime-known integer: a literal, +/// or an identifier bound to an `int_val` in `comptime_constants` (e.g. +/// the cursor of an `inline for 0..N (i)` unroll). Otherwise null. +pub fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 { + switch (index.data) { + .int_literal => |lit| return lit.value, + .identifier => |id| { + if (self.comptime_constants.get(id.name)) |cv| { + switch (cv) { + .int_val => |iv| return iv, + else => return null, + } + } + return null; + }, + else => return null, + } +} + +const PackValueKind = enum { storage, call_arg, return_value, runtime_iter, generic }; + +/// `xs` is a pack name used where a runtime value is required. A pack is +/// comptime-only (Decision 1), so this is an error — with a context-tailored +/// suggestion for how to express the intent instead. +pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref { + if (self.diagnostics) |d| { + const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name}); + switch (kind) { + .storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}), + .call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}), + .return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}), + .runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }), + .generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }), + } + } + return self.emitPlaceholder(name); +} + +/// True when `name` is a pack parameter bound in the current mono body. +pub fn isPackName(self: *Lowering, name: []const u8) bool { + const ppc = self.pack_param_count orelse return false; + return ppc.contains(name); +} + +/// `xx ` with a slice target: materialize the comptime pack into a +/// runtime `[]elem` by lowering each element node and boxing (`[]Any`) or +/// `xx`-erasing (`[]P`) it into a stack `[N]elem`, then return the slice. +/// This is the explicit pack→slice bridge (issue 0053). +pub fn lowerPackToSlice(self: *Lowering, pack_name: []const u8, slice_ty: TypeId) Ref { + const arg_nodes = (self.pack_arg_nodes orelse return self.builder.constInt(0, .unresolved)).get(pack_name) orelse + return self.builder.constInt(0, .unresolved); + const elem_ty = self.module.types.get(slice_ty).slice.element; + const is_any = elem_ty == .any; + const elem_is_protocol = blk: { + if (elem_ty.isBuiltin()) break :blk false; + const ei = self.module.types.get(elem_ty); + break :blk ei == .@"struct" and ei.@"struct".is_protocol; + }; + const slice_slot = self.builder.alloca(slice_ty); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); + if (arg_nodes.len == 0) { + self.builder.store(ptr_gep, self.builder.constNull(self.module.types.ptrTo(elem_ty))); + self.builder.store(len_gep, self.builder.constInt(0, .s64)); + return self.builder.load(slice_slot, slice_ty); + } + const array_ty = self.module.types.arrayOf(elem_ty, @intCast(arg_nodes.len)); + const array_slot = self.builder.alloca(array_ty); + for (arg_nodes, 0..) |arg, i| { + var val = self.lowerExpr(arg); + var source_ty = self.inferExprType(arg); + if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); + if (is_any) { + if (source_ty != .any) val = self.builder.boxAny(val, source_ty); + } else if (elem_is_protocol) { + if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg, source_ty, elem_ty); + } + const ep = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(@intCast(i), .s64) } }, self.module.types.ptrTo(elem_ty)); + self.builder.store(ep, val); + } + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(0, .s64) } }, self.module.types.ptrTo(elem_ty)); + self.builder.store(ptr_gep, data_ptr); + self.builder.store(len_gep, self.builder.constInt(@intCast(arg_nodes.len), .s64)); + return self.builder.load(slice_slot, slice_ty); +} + +/// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value}, +/// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name. +pub fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void { + const any_slice_ty = self.module.types.sliceOf(.any); + const n = if (call_args.len > start_idx) call_args.len - start_idx else 0; + + if (n == 0) { + // Empty slice: {null, 0} + const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any)); + const zero_len = self.builder.constInt(0, .s64); + const slice_slot = self.builder.alloca(any_slice_ty); + // Store ptr (field 0) and len (field 1) into the slice alloca + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); + self.builder.store(ptr_gep, null_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, zero_len); + if (self.scope) |scope| { + scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); + } + return; + } + + // Allocate stack array [N x Any] + const array_ty = self.module.types.arrayOf(.any, @intCast(n)); + const array_slot = self.builder.alloca(array_ty); + + // Box each arg and store into array + for (call_args[start_idx..], 0..) |arg, i| { + var val = self.lowerExpr(arg); + var source_ty = self.inferExprType(arg); + // If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that + if (source_ty == .unresolved) { + const ref_ty = self.builder.getRefType(val); + if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) { + source_ty = ref_ty; + } else if (!ref_ty.isBuiltin()) { + const ri = self.module.types.get(ref_ty); + if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) { + source_ty = ref_ty; + } + } + } + // Auto-unwrap optionals: box inner value if present, else box string "null" + if (!source_ty.isBuiltin()) { + const opt_info = self.module.types.get(source_ty); + if (opt_info == .optional) { + const child_ty = opt_info.optional.child; + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); + const some_bb = self.freshBlock("opt.some"); + const none_bb = self.freshBlock("opt.none"); + const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); + self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); + self.builder.switchToBlock(some_bb); + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); + const boxed_inner = self.builder.boxAny(unwrapped, child_ty); + self.builder.br(merge_bb, &.{boxed_inner}); + self.builder.switchToBlock(none_bb); + const null_str_id = self.module.types.internString("null"); + const null_str = self.builder.constString(null_str_id); + const boxed_null = self.builder.boxAny(null_str, .string); + self.builder.br(merge_bb, &.{boxed_null}); + self.builder.switchToBlock(merge_bb); + val = self.builder.blockParam(merge_bb, 0, TypeId.any); + source_ty = .any; + } + } + const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty); + // GEP to array[i] and store + const idx_ref = self.builder.constInt(@intCast(i), .s64); + const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any)); + self.builder.store(elem_ptr, boxed); + } + + // Build slice {ptr_to_first_element, len} + const slice_slot = self.builder.alloca(any_slice_ty); + // Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any) + const zero = self.builder.constInt(0, .s64); + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any)); + const len_ref = self.builder.constInt(@intCast(n), .s64); + // Store into slice fields + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty); + self.builder.store(ptr_gep, data_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, len_ref); + + if (self.scope) |scope| { + scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); + } +} + +/// Pack variadic args into a slice for regular function calls. +/// Detects variadic params in the function decl, packs remaining args into a typed slice, +/// and replaces the args list with [fixed_args..., slice_ref]. +pub fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void { + // `#foreign` variadic uses the C calling convention's `...` tail — + // extras are passed through directly with default argument promotion + // (handled at the call site), not packed into an sx slice. + if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) { + return; + } + // Find variadic param index. The two surface forms differ in + // what `p.type_expr` resolves to: legacy `name: ..T` declares T + // (element type), new `..name: []T` declares []T (already a + // slice). Unwrap the latter so the per-element packing below + // sees T in both cases. + var variadic_idx: ?usize = null; + var elem_ty: TypeId = .any; + for (fd.params, 0..) |p, i| { + if (p.is_variadic) { + variadic_idx = i; + const declared = self.resolveTypeWithBindings(p.type_expr); + elem_ty = declared; + if (!declared.isBuiltin()) { + const info = self.module.types.get(declared); + if (info == .slice) elem_ty = info.slice.element; + } + break; + } + } + const vi = variadic_idx orelse return; // no variadic param + + // Number of non-variadic args + const fixed_count = vi; + const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0; + const slice_ty = self.module.types.sliceOf(elem_ty); + + // Check for spread operator: sum(..arr) — single spread arg becomes the slice directly + if (variadic_count == 1 and fixed_count < c.args.len) { + const arg_node = c.args[fixed_count]; + if (arg_node.data == .spread_expr) { + const spread = arg_node.data.spread_expr; + const arr_val = self.lowerExpr(spread.operand); + const arr_ty = self.inferExprType(spread.operand); + const arr_info = self.module.types.get(arr_ty); + // Convert array to slice + const slice_val = switch (arr_info) { + .array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty), + .slice => arr_val, + else => arr_val, + }; + args.shrinkRetainingCapacity(fixed_count); + args.append(self.alloc, slice_val) catch unreachable; + return; + } + } + + if (variadic_count == 0) { + // Empty slice + const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty)); + const zero_len = self.builder.constInt(0, .s64); + const slice_slot = self.builder.alloca(slice_ty); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); + self.builder.store(ptr_gep, null_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); + self.builder.store(len_gep, zero_len); + const slice_val = self.builder.load(slice_slot, slice_ty); + // Replace args: keep fixed args, append slice + args.shrinkRetainingCapacity(fixed_count); + args.append(self.alloc, slice_val) catch unreachable; + return; + } + + // Determine if we need to box as Any (for ..Any params) or use raw type + const is_any = (elem_ty == .any); + // `..xs: []P` (slice of a protocol): each concrete arg must be erased to + // a protocol value {ctx, vtable}, not stored raw (which would be a + // size/type mismatch — a heap of garbage vtables → crash on dispatch). + const elem_is_protocol = blk: { + if (elem_ty.isBuiltin()) break :blk false; + const ei = self.module.types.get(elem_ty); + break :blk ei == .@"struct" and ei.@"struct".is_protocol; + }; + + // Allocate stack array [N x ElemType] + const array_elem = if (is_any) TypeId.any else elem_ty; + const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count)); + const array_slot = self.builder.alloca(array_ty); + + // Store each variadic arg into array + for (0..variadic_count) |i| { + var val = args.items[fixed_count + i]; + if (is_any) { + var source_ty = self.inferExprType(c.args[fixed_count + i]); + // If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that + if (source_ty == .unresolved) { + const ref_ty = self.builder.getRefType(val); + if (ref_ty != .unresolved and ref_ty != .void) source_ty = ref_ty; + } + // Auto-unwrap optionals: box inner value if present, else box string "null" + if (!source_ty.isBuiltin()) { + const opt_info = self.module.types.get(source_ty); + if (opt_info == .optional) { + const child_ty = opt_info.optional.child; + // Branch: has_value? → box inner : box "null" + const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool); + const some_bb = self.freshBlock("opt.some"); + const none_bb = self.freshBlock("opt.none"); + const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any}); + self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{}); + // Some: unwrap and box inner value + self.builder.switchToBlock(some_bb); + const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty); + const boxed_inner = self.builder.boxAny(unwrapped, child_ty); + self.builder.br(merge_bb, &.{boxed_inner}); + // None: box the string "null" + self.builder.switchToBlock(none_bb); + const null_str_id = self.module.types.internString("null"); + const null_str = self.builder.constString(null_str_id); + const boxed_null = self.builder.boxAny(null_str, .string); + self.builder.br(merge_bb, &.{boxed_null}); + // Merge + self.builder.switchToBlock(merge_bb); + val = self.builder.blockParam(merge_bb, 0, TypeId.any); + source_ty = .any; // already boxed + } + } + if (source_ty != .any) { + val = self.builder.boxAny(val, source_ty); + } + } else if (elem_is_protocol) { + // Erase each concrete arg to the protocol value via the same + // impl-driven `xx` machinery, so the runtime `[]P` holds real + // {ctx, vtable} values and `xs[i].method()` dispatches. + const arg_node = c.args[fixed_count + i]; + var source_ty = self.inferExprType(arg_node); + if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); + if (source_ty != elem_ty) { + val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty); + } + } + const idx_ref = self.builder.constInt(@intCast(i), .s64); + const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem)); + self.builder.store(elem_ptr, val); + } + + // Build slice {ptr, len} + const slice_slot = self.builder.alloca(slice_ty); + const zero = self.builder.constInt(0, .s64); + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem)); + const len_ref = self.builder.constInt(@intCast(variadic_count), .s64); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty); + self.builder.store(ptr_gep, data_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); + self.builder.store(len_gep, len_ref); + const slice_val = self.builder.load(slice_slot, slice_ty); + + // Replace args: keep fixed args, append slice + args.shrinkRetainingCapacity(fixed_count); + args.append(self.alloc, slice_val) catch unreachable; +} + +// ── Generic monomorphization ────────────────────────────────── + +/// Build an `[]Any` slice value from the mono's pack params and +/// bind it to the pack name in scope. Each pack-param slot is +/// loaded, boxed via `boxAny`, and stored into a stack [N x Any] +/// array; the slice {data_ptr, len} is then bound. Used by +/// `monomorphizePackFn` so bodies that reference `args` bare or +/// index it with a runtime int resolve through the slice (with +/// element type `Any`). Literal-indexed accesses keep the +/// concrete per-position types via `packArgNodeAt`. +/// Build a `[]Type` slice VALUE for a bare `$` reference. +/// Differs from `materialisePackSlice` (which boxes each pack +/// element as Any so the body's `args[i]` reads an Any) — this +/// helper stores raw `.type_tag` Values via `const_type`, so the +/// slice is a list-of-Types that builder fns walk at interp time. +/// Slice IR type is `[]Any` (since `Type → .any`); the interp +/// stores whichever Value the elements actually carry. +pub fn buildPackSliceValue(self: *Lowering, arg_types: []const TypeId) Ref { + const any_slice_ty = self.module.types.sliceOf(.any); + const any_ptr_ty = self.module.types.ptrTo(.any); + + if (arg_types.len == 0) { + const null_ptr = self.builder.constNull(any_ptr_ty); + const zero_len = self.builder.constInt(0, .s64); + const slice_slot = self.builder.alloca(any_slice_ty); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); + self.builder.store(ptr_gep, null_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, zero_len); + return self.builder.load(slice_slot, any_slice_ty); + } + + const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); + const array_slot = self.builder.alloca(array_ty); + + for (arg_types, 0..) |ty, i| { + // `const_type` produces an `.any`-typed Type value + // (`{tag=.any, value=tid}`) — already the canonical Any + // shape, so no re-box needed. + const type_val = self.builder.constType(ty); + const idx_ref = self.builder.constInt(@intCast(i), .s64); + const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); + self.builder.store(elem_ptr, type_val); + } + + const slice_slot = self.builder.alloca(any_slice_ty); + const zero = self.builder.constInt(0, .s64); + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); + const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); + self.builder.store(ptr_gep, data_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, len_ref); + return self.builder.load(slice_slot, any_slice_ty); +} + +pub fn materialisePackSlice( + self: *Lowering, + scope: *Scope, + pack_name: []const u8, + slot_refs: []const Ref, + arg_types: []const TypeId, +) void { + const any_slice_ty = self.module.types.sliceOf(.any); + const any_ptr_ty = self.module.types.ptrTo(.any); + + if (arg_types.len == 0) { + const null_ptr = self.builder.constNull(any_ptr_ty); + const zero_len = self.builder.constInt(0, .s64); + const slice_slot = self.builder.alloca(any_slice_ty); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); + self.builder.store(ptr_gep, null_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, zero_len); + scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); + return; + } + + const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len)); + const array_slot = self.builder.alloca(array_ty); + + for (slot_refs, arg_types, 0..) |slot, ty, i| { + const val = self.builder.load(slot, ty); + const boxed = if (ty == .any) val else self.builder.boxAny(val, ty); + const idx_ref = self.builder.constInt(@intCast(i), .s64); + const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty); + self.builder.store(elem_ptr, boxed); + } + + const slice_slot = self.builder.alloca(any_slice_ty); + const zero = self.builder.constInt(0, .s64); + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty); + const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty); + self.builder.store(ptr_gep, data_ptr); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty); + self.builder.store(len_gep, len_ref); + scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true }); +} + +/// Infer the return type of a pack-fn body for the generic-`$R` +/// case. Walks the body looking for the first concrete return +/// type: a `return X;` statement's value type, or — failing that — +/// the tail expression of an arrow-form body. Caller must have +/// `pack_arg_nodes` installed so `args[]` substitutes during +/// inference. Falls back to `.s64` if nothing concrete is found +/// (matches the broader "default to .s64" convention elsewhere). +pub fn inferPackBodyReturnType(self: *Lowering, body: *const Node) TypeId { + // First try explicit `return X;` — walks past structured + // control flow but stops at nested fn / lambda bodies. + if (self.findReturnValueType(body)) |ty| return ty; + // Arrow-form / tail-expression body: the body IS the value. + // For block bodies whose last stmt is an expression, walk down. + if (body.data == .block) { + const stmts = body.data.block.stmts; + if (stmts.len == 0) return .void; + return self.inferExprType(stmts[stmts.len - 1]); + } + return self.inferExprType(body); +} + +/// Per-call-shape monomorphisation entry for pack-fns +/// (`isPackFn(fd) == true`). Computes a mangled name from the +/// call-site arg types, builds the mono if it's not cached, and +/// emits a direct call. Pack params expand into N positional IR +/// params with concrete types; the body's `args[]` and +/// `args.len` resolve to those params via the pack bindings. +pub fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref { + // Split call args along the fd.params boundary: + // - non-comptime non-pack params → consume one call arg as a + // runtime IR param. + // - comptime non-pack params → consume one call arg, fold its + // value into the mangle (NOT a runtime IR param). + // - pack param (always last) → consume the remaining call args + // as the pack expansion. + var pack_arg_types = std.ArrayList(TypeId).empty; + defer pack_arg_types.deinit(self.alloc); + var pack_start: usize = call_node.args.len; + // Constraint protocol of the pack param (`..xs: P`), if any. The + // comptime type-pack `..$args` has no constraint to check. + var pack_protocol: ?[]const u8 = null; + var pack_is_comptime = false; + var pack_name: []const u8 = ""; + { + var fi: usize = 0; + for (fd.params) |p| { + if (isPackParam(p)) { + pack_start = fi; + pack_is_comptime = p.is_comptime; + pack_name = p.name; + if (p.is_pack and p.type_expr.data == .type_expr) { + pack_protocol = p.type_expr.data.type_expr.name; + } + break; + } + if (fi >= call_node.args.len) break; + fi += 1; + } + } + + // Lower the PACK args first, taking each type from the lowered value + // (`getRefType`) — never a pre-lowering `inferExprType` guess. Knowing + // the pack element types up front lets the prefix args (e.g. + // `mapper: Closure(..sources.T) -> $R`) resolve against them, so a + // lambda arg types its params from the projected closure signature. + // (A comptime `..$args` pack keeps `inferExprType` — its args may be + // type-position.) + // A pack arg is independently typed — it takes its natural type and + // (for a comptime `..$args` pack) auto-boxes to `Any` at the call + // boundary. It is NEVER coerced to a leftover outer `target_type`, so + // clear it: otherwise an `xx ` pack arg (whose result type IS + // `target_type`) would cast to the stale target — e.g. `format("…", xx i)` + // inside a `-> string` fn mis-typed the arg as `string`, monomorphizing + // `__pack_string` and ABI-coercing the 4-byte int as a 16-byte fat + // pointer → memory corruption (issue 0057). + const saved_pack_tt = self.target_type; + self.target_type = null; + var pack_refs = std.ArrayList(Ref).empty; + defer pack_refs.deinit(self.alloc); + for (call_node.args[pack_start..]) |a| { + const r = self.lowerExpr(a); + pack_refs.append(self.alloc, r) catch return self.builder.constInt(0, .void); + if (pack_is_comptime) { + const it = self.inferExprType(a); + pack_arg_types.append(self.alloc, if (it == .unresolved) self.builder.getRefType(r) else it) catch return self.builder.constInt(0, .void); + } else { + pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void); + } + } + self.target_type = saved_pack_tt; + + // Install the pack's element types + constraint so prefix-arg param + // types like `Closure(..sources.T)` resolve while lowering the prefix. + var pat_map = std.StringHashMap([]const TypeId).init(self.alloc); + defer pat_map.deinit(); + pat_map.put(pack_name, pack_arg_types.items) catch {}; + var pcon_map = std.StringHashMap([]const u8).init(self.alloc); + defer pcon_map.deinit(); + if (pack_protocol) |proto| pcon_map.put(pack_name, proto) catch {}; + const saved_pat = self.pack_arg_types; + const saved_pcon = self.pack_constraint; + self.pack_arg_types = pat_map; + if (pack_protocol != null) self.pack_constraint = pcon_map; + + var args = std.ArrayList(Ref).empty; + defer args.deinit(self.alloc); + { + var ri: usize = 0; + for (fd.params) |p| { + if (isPackParam(p)) break; + if (ri >= call_node.args.len) break; + if (!p.is_comptime) { + // Contextually type the arg from the param (so a lambda arg + // `(x) => …` takes its param types from a `Closure(...)` param). + // The param type is resolved under the pack fn's OWN source + // (E4): a fixed-prefix type bare-visible only in the defining + // module must resolve there, not the caller's. The arg itself + // is lowered AFTER, in the caller's context. + const saved_tt = self.target_type; + const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); + if (pty != .unresolved) self.target_type = pty; + args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void); + self.target_type = saved_tt; + } + ri += 1; + } + } + self.pack_arg_types = saved_pat; + self.pack_constraint = saved_pcon; + + // Infer type-param bindings (e.g. `$R` in `mapper: Closure(..) -> $R`) + // from the lowered prefix args. `args.items` holds the non-comptime + // prefix refs in declaration order; match each prefix param's declared + // type against its arg's concrete type to bind the function's + // type-params. These flow into the mangle and the mono's + // `self.type_bindings` so `-> VL($R)` / `Combined($R, ..)` resolve. + var tparam_bindings = std.StringHashMap(TypeId).init(self.alloc); + defer tparam_bindings.deinit(); + if (fd.type_params.len > 0) { + var pref_ref_idx: usize = 0; + for (fd.params) |p| { + if (isPackParam(p)) break; + if (p.is_comptime) continue; + if (pref_ref_idx >= args.items.len) break; + const arg_ty = self.builder.getRefType(args.items[pref_ref_idx]); + for (fd.type_params) |tp| { + if (tparam_bindings.contains(tp.name)) continue; + if (self.extractTypeParam(p.type_expr, arg_ty, tp.name)) |ety| { + if (ety != .unresolved) tparam_bindings.put(tp.name, ety) catch {}; + } + } + pref_ref_idx += 1; + } + } + + // Append the (already-lowered) pack args after the prefix args. + for (pack_refs.items) |r| args.append(self.alloc, r) catch return self.builder.constInt(0, .void); + + // Per-position conformance: each pack arg must impl the constraint + // protocol. Only enforced for a known protocol constraint — an unknown + // name (e.g. a plain type used as a pack constraint) is left alone. + if (pack_protocol) |proto| { + if (self.program_index.protocol_ast_map.contains(proto)) { + for (call_node.args[pack_start..], pack_arg_types.items) |arg_node, arg_ty| { + if (!self.protocolResolver().packArgConformsTo(proto, arg_ty)) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, arg_node.span, "pack argument of type '{s}' does not conform to protocol '{s}'", .{ self.formatTypeName(arg_ty), proto }); + } + } + } + } + } + + // Mangle: `__pack__` with comptime values + // (if any) folded into a `__ct_` segment per non-pack + // comptime param. Distinct call shapes — including different + // comptime VALUES — get distinct symbols. + var name_buf = std.ArrayList(u8).empty; + defer name_buf.deinit(self.alloc); + name_buf.appendSlice(self.alloc, fd.name) catch return self.builder.constInt(0, .void); + // Comptime values first (deterministic by fd.params order). + var ct_fi: usize = 0; + for (fd.params) |p| { + if (isPackParam(p)) break; + if (ct_fi >= call_node.args.len) break; + if (p.is_comptime) { + name_buf.appendSlice(self.alloc, "__ct_") catch return self.builder.constInt(0, .void); + self.genericResolver().appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]); + } + ct_fi += 1; + } + // Inferred type-param bindings (deterministic by fd.type_params order). + for (fd.type_params) |tp| { + if (tparam_bindings.get(tp.name)) |ty| { + name_buf.appendSlice(self.alloc, "__tp_") catch return self.builder.constInt(0, .void); + name_buf.appendSlice(self.alloc, self.mangleTypeName(ty)) catch return self.builder.constInt(0, .void); + } + } + name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void); + for (pack_arg_types.items) |t| { + name_buf.append(self.alloc, '_') catch return self.builder.constInt(0, .void); + name_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return self.builder.constInt(0, .void); + } + const mangled = name_buf.items; + + if (!self.lowered_functions.contains(mangled)) { + self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node, &tparam_bindings); + } + + const fid = self.resolveFuncByName(mangled) orelse return self.builder.constInt(0, .void); + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); +} + + +/// Build a single mono fn for the given pack-fn + concrete arg types. +/// The mono carries N positional pack-params (synthesised names +/// `__pack__`) plus any fixed-prefix non-pack params from +/// the original declaration. The body lowers normally — real +/// `return X;` emits real `ret X`; `args[]` substitutes via +/// `pack_arg_nodes`; `args.len` resolves via `pack_param_count`. +pub fn monomorphizePackFn( + self: *Lowering, + fd: *const ast.FnDecl, + mangled_name: []const u8, + arg_types: []const TypeId, + call_node: *const ast.Call, + type_bindings: *const std.StringHashMap(TypeId), +) void { + const owned_name = self.alloc.dupe(u8, mangled_name) catch return; + self.lowered_functions.put(owned_name, {}) catch {}; + + // Find the pack param's name and position in fd.params, plus its + // constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none). + var pack_name: []const u8 = ""; + var pack_param_idx: usize = std.math.maxInt(usize); + var pack_proto: ?[]const u8 = null; + for (fd.params, 0..) |p, i| { + if (isPackParam(p)) { + pack_name = p.name; + pack_param_idx = i; + if (p.is_pack and p.type_expr.data == .type_expr) { + pack_proto = p.type_expr.data.type_expr.name; + } + break; + } + } + if (pack_param_idx == std.math.maxInt(usize)) return; + + // Save state — mirrors monomorphizeFunction but also captures + // pack/inline-return state since the mono body must NOT route + // returns through any caller's inline slot. + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + const saved_scope = self.scope; + const saved_defer_base = self.func_defer_base; + const saved_block_terminated = self.block_terminated; + const saved_target = self.target_type; + const saved_pan = self.pack_arg_nodes; + const saved_ppc = self.pack_param_count; + const saved_pat = self.pack_arg_types; + const saved_pcon = self.pack_constraint; + const saved_iri = self.inline_return_target; + const saved_ctx_ref = self.current_ctx_ref; + const saved_type_bindings = self.type_bindings; + self.func_defer_base = self.defer_stack.items.len; + self.block_terminated = false; + self.inline_return_target = null; + // Generic type-params inferred at the call site (e.g. `$R` from the + // mapper's closure return). Installed for the whole mono so + // return-type resolution and body lowering substitute them. + self.type_bindings = type_bindings.*; + defer { + self.type_bindings = saved_type_bindings; + self.scope = saved_scope; + self.func_defer_base = saved_defer_base; + self.block_terminated = saved_block_terminated; + self.target_type = saved_target; + self.pack_arg_nodes = saved_pan; + self.pack_param_count = saved_ppc; + self.pack_arg_types = saved_pat; + self.pack_constraint = saved_pcon; + self.inline_return_target = saved_iri; + self.current_ctx_ref = saved_ctx_ref; + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + } + + const wants_ctx = self.funcWantsImplicitCtx(fd); + + // Synthesise pack-param names + AST ident nodes used to bind + // `args[]` substitutions during body lowering. Built + // BEFORE return-type resolution so the generic-`$R` path can + // pre-install the binding for type inference. + var pack_synth_names = std.ArrayList([]const u8).empty; + defer pack_synth_names.deinit(self.alloc); + var pack_arg_idents = std.ArrayList(*const Node).empty; + defer pack_arg_idents.deinit(self.alloc); + for (arg_types, 0..) |_, i| { + const synth_name = std.fmt.allocPrint(self.alloc, "__pack_{s}_{d}", .{ pack_name, i }) catch return; + pack_synth_names.append(self.alloc, synth_name) catch return; + const ident_node = self.alloc.create(Node) catch return; + ident_node.* = .{ + .span = fd.body.span, + .data = .{ .identifier = .{ .name = synth_name } }, + }; + pack_arg_idents.append(self.alloc, ident_node) catch return; + } + + // Resolve return type. When the declared type is a generic + // name (e.g. `(..$args) -> $R`), `resolveReturnType` would + // return an opaque struct TypeId and the mono's signature + // would be wrong. Pre-install the pack bindings + infer the + // ret type from the body's tail expression / first explicit + // `return X;` instead. + var pre_pan = std.StringHashMap([]const *const Node).init(self.alloc); + defer pre_pan.deinit(); + pre_pan.put(pack_name, pack_arg_idents.items) catch return; + var pre_ppc = std.StringHashMap(u32).init(self.alloc); + defer pre_ppc.deinit(); + pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return; + var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc); + defer pre_pat.deinit(); + pre_pat.put(pack_name, arg_types) catch return; + var pre_pcon = std.StringHashMap([]const u8).init(self.alloc); + defer pre_pcon.deinit(); + if (pack_proto) |proto| pre_pcon.put(pack_name, proto) catch return; + self.pack_arg_nodes = pre_pan; + self.pack_param_count = pre_ppc; + self.pack_arg_types = pre_pat; + self.pack_constraint = if (pack_proto != null) pre_pcon else null; + + // Resolve the declared return + fixed-prefix param types in the pack fn's + // OWN module (E4), so a 2-flat-hop library type named in the signature is + // bare-visible — mirrors the body pin further down and the + // `monomorphizeFunction` pin. The comptime call-site args below are + // lowered AFTER this restore, in the caller's context (issue 0106). + const saved_sig_src = self.current_source_file; + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + + const declared_is_generic_ret = blk: { + const rt = fd.return_type orelse break :blk false; + if (rt.data != .type_expr) break :blk false; + break :blk rt.data.type_expr.is_generic; + }; + const ret_ty: TypeId = if (declared_is_generic_ret) + self.inferPackBodyReturnType(fd.body) + else + self.resolveReturnType(fd); + self.target_type = ret_ty; + + // Param list: ctx (if needed) + fixed prefix + N pack params. + // Comptime non-pack params are NOT in the runtime signature — + // their values are folded into the mangle and substituted via + // `comptime_param_nodes` / bound as runtime locals in scope. + // NOT deinit'd — `params.items` is stored by reference in + // `Function.init` and read back later via `func.params`. + var params = std.ArrayList(Function.Param).empty; + if (wants_ctx) { + params.append(self.alloc, .{ + .name = self.module.types.internString("__sx_ctx"), + .ty = self.module.types.ptrTo(.void), + }) catch return; + } + for (fd.params, 0..) |p, i| { + if (i == pack_param_idx) continue; + if (p.is_comptime) continue; // folded into mangle, not in IR + const pty = self.resolveParamType(&p); + params.append(self.alloc, .{ + .name = self.module.types.internString(p.name), + .ty = pty, + }) catch return; + } + for (arg_types, 0..) |ty, i| { + params.append(self.alloc, .{ + .name = self.module.types.internString(pack_synth_names.items[i]), + .ty = ty, + }) catch return; + } + self.setCurrentSourceFile(saved_sig_src); + + const name_id = self.module.types.internString(owned_name); + _ = self.builder.beginFunction(name_id, params.items, ret_ty); + self.builder.currentFunc().has_implicit_ctx = wants_ctx; + + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0); + + var scope = Scope.init(self.alloc, null); + defer scope.deinit(); + self.scope = &scope; + + // Bind non-pack params. Walk fd.params + call_node.args + // together; comptime non-pack params bind both as runtime + // locals (so bare-name body access works) AND as + // comptime_param_nodes entries (so `#insert` substitution + // works). Non-comptime non-pack params consume IR param + // slots in order. + var cpn = std.StringHashMap(*const Node).init(self.alloc); + defer cpn.deinit(); + var param_idx: u32 = if (wants_ctx) 1 else 0; + var ct_arg_idx: usize = 0; + for (fd.params, 0..) |p, i| { + if (i == pack_param_idx) break; + if (p.is_comptime) { + if (ct_arg_idx < call_node.args.len) { + const call_arg = call_node.args[ct_arg_idx]; + self.stampCallerSource(call_arg); + cpn.put(p.name, call_arg) catch return; + // Bind as a runtime local for bare-name access. + // Lower the call arg as a value, then alloca + store. + const val = self.lowerExpr(call_arg); + const val_ty = self.builder.getRefType(val); + const slot = self.builder.alloca(val_ty); + self.builder.store(slot, val); + scope.put(p.name, .{ .ref = slot, .ty = val_ty, .is_alloca = true }); + } + ct_arg_idx += 1; + continue; + } + // Pin to the pack fn's OWN module (E4): a fixed-prefix param whose + // type is bare-visible only in the defining module must resolve + // there, not in the caller's restored context. Mirrors the + // signature build above and `resolveParamTypeInSource` at the + // cross-module call-arg typing sites. + const pty = self.resolveParamTypeInSource(fd.body.source_file, &p); + const slot = self.builder.alloca(pty); + self.builder.store(slot, Ref.fromIndex(param_idx)); + scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); + param_idx += 1; + ct_arg_idx += 1; + } + // Install comptime_param_nodes for the body lowering. + const saved_cpn = self.comptime_param_nodes; + self.comptime_param_nodes = cpn; + defer self.comptime_param_nodes = saved_cpn; + var pack_param_slots = std.ArrayList(Ref).empty; + defer pack_param_slots.deinit(self.alloc); + for (arg_types, 0..) |ty, i| { + const synth_name = pack_synth_names.items[i]; + const slot = self.builder.alloca(ty); + self.builder.store(slot, Ref.fromIndex(param_idx)); + scope.put(synth_name, .{ .ref = slot, .ty = ty, .is_alloca = true }); + pack_param_slots.append(self.alloc, slot) catch return; + param_idx += 1; + } + + // Pack bindings remain installed from the pre-resolution + // (generic-`$R`) inference step above. No need to reinstall. + + // Materialise an `[]Any` slice value for the pack name so + // bare `args` (forwarding) and `args[]` (loops) + // resolve at runtime. Per-position type info is lost via + // Any boxing — that's the inherent cost of treating a + // heterogeneous pack as a uniform value. Literal-indexed + // access still goes through `packArgNodeAt` and keeps the + // concrete per-position types. + self.materialisePackSlice(&scope, pack_name, pack_param_slots.items, arg_types); + + // Pin to the metaprogram's OWN module for the BODY lowering only, so its + // bare names (and anything it `#insert`s — e.g. `build_format` / `out` / + // `emit` inside `std.print`) resolve in the defining module's visibility + // context, not the call site's (issue 0106). The comptime-param call-site + // args above were deliberately lowered FIRST, in the caller's context. + // Mirrors `lowerFunctionBodyInto`, which switches to `func.source_file`; + // the defining path is stamped on the body node by `resolveImports`. A + // synthesized/sourceless body keeps the caller's context. + const saved_source = self.current_source_file; + defer self.setCurrentSourceFile(saved_source); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + + if (ret_ty != .void) { + const body_val = self.lowerBlockValue(fd.body); + if (!self.currentBlockHasTerminator()) { + if (body_val) |val| { + const val_ty = self.builder.getRefType(val); + const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val; + self.builder.ret(coerced, ret_ty); + } else { + self.ensureTerminator(ret_ty); + } + } + } else { + self.lowerBlock(fd.body); + self.ensureTerminator(ret_ty); + } + self.builder.finalize(); +} + +/// Pack-fn: has a trailing heterogeneous pack param (`is_variadic +/// AND is_comptime`). Mixed shapes — non-pack comptime params +/// before the pack — are also accepted; the mono folds those +/// comptime VALUES into the mangled name and binds them as both +/// comptime substitutions (for #insert) and runtime locals (for +/// bare-name body references). +pub fn isPackFn(fd: *const ast.FnDecl) bool { + for (fd.params) |p| { + if (isPackParam(p)) return true; + } + return false; +} + +/// A trailing pack parameter: the comptime type-pack `..$args` +/// (`is_comptime`) or the protocol-constrained pack `..xs: P` (`is_pack`). +/// Both monomorphize per call shape via `lowerPackFnCall`; the slice +/// variadic (`..xs: []T`) is neither and stays a runtime slice. +pub fn isPackParam(p: ast.Param) bool { + return p.is_variadic and (p.is_comptime or p.is_pack); +} + +/// Resolve `..pack.` against `protocol_name` by position (Decision 4). +/// No cross-namespace fallback: a value-position name that exists only as a +/// type-arg (or vice versa) is `.not_found`, letting the caller emit a +/// position-specific diagnostic (G3, Step 2.7). +pub fn resolvePackProjection( + self: *Lowering, + protocol_name: []const u8, + name: []const u8, + pos: ProjectionPosition, +) PackProjection { + return switch (pos) { + .type_position => if (self.lookupProtocolArg(protocol_name, name)) |i| + .{ .type_arg = i } + else + .not_found, + .value_position => if (self.lookupProtocolField(protocol_name, name)) |i| + .{ .method = i } + else + .not_found, + }; +} + +pub const ProjectionPosition = enum { type_position, value_position }; +pub const PackProjection = union(enum) { + type_arg: u32, // index into the protocol's `type_params` + method: u32, // index into the protocol's `methods` + not_found, // `name` absent from the position-selected namespace +}; diff --git a/src/ir/lower/protocol.zig b/src/ir/lower/protocol.zig index fad30da..6f16db5 100644 --- a/src/ir/lower/protocol.zig +++ b/src/ir/lower/protocol.zig @@ -43,7 +43,6 @@ 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; @@ -204,13 +203,6 @@ pub fn instantiateParamProtocol(self: *Lowering, pd: *const ast.ProtocolDecl, ar // consults type-args, value position consults methods, with NO // cross-namespace fallback. -pub const ProjectionPosition = enum { type_position, value_position }; - -pub const PackProjection = union(enum) { - type_arg: u32, // index into the protocol's `type_params` - method: u32, // index into the protocol's `methods` - not_found, // `name` absent from the position-selected namespace -}; /// Find `name` in `protocol_name`'s type-arg namespace (`protocol($T,...)`). /// Returns the `type_params` index, or null (also for unknown protocols).