const std = @import("std"); const ast = @import("../../ast.zig"); const Node = ast.Node; const types = @import("../types.zig"); const inst_mod = @import("../inst.zig"); const TypeId = types.TypeId; const Ref = inst_mod.Ref; const Function = inst_mod.Function; 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 {s} (x)` (or `inline for 0..{s}.len (i)` for the index); for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, 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, .i64, 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, .i64)); 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), .i64) } }, 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, .i64) } }, 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), .i64)); 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, .i64); 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, .i64, 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 .i64 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), .i64); 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, .i64); 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), .i64); // 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, .i64, 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, .i64); 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, .i64, 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 .i64 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), .i64); 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, .i64); 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), .i64); 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, .i64, 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; } // ── Pack-fn calls & 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, .i64); 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, .i64, 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), .i64); 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, .i64); 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), .i64); 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, .i64, 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, .i64); 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, .i64, 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), .i64); 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, .i64); 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), .i64); 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, .i64, 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 `.i64` if nothing concrete is found /// (matches the broader "default to .i64" 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. 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. 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. 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 };