Files
sx/src/ir/lower/pack.zig
agra 2b8041a828 cleanup: drop resolved-issue citations from src comments
Sweep all src/**.zig comments that cite resolved issues (issue NNNN /
fix-NNNN / KB-N): the invariant or mechanism each comment states is
kept; the historical citation is dropped, per the no-conclusion-comments
rule. Pure-history parentheticals are removed outright. References to
the 16 still-open issues (0030, 0041-0056) are untouched, as are test
NAMES carrying regression provenance (matching the sanctioned
"Regression (issue NNNN)" example-header convention).

Also removes the issues/0019-import-non-transitive-c-scope/ fixture dir
— the issue is superseded and its behavior is covered by
examples/0706-modules-import-non-transitive.sx (the .md writeup stays).
issues/0030's repro .sx stays: that issue is an open feature request.

Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
2026-06-10 16:34:17 +03:00

1102 lines
53 KiB
Zig

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.<method>`: call the (zero-arg)
/// protocol method on each element and collect the results into a tuple
/// `(xs[0].<method>(), …, xs[N-1].<method>())`. 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 `<pack_name>[<int_literal>]` 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
/// `<pack_name>[<comptime_int_literal>]` 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 <pack>` 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;
}
// ── 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 `$<pack>` 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[<lit>]` 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[<lit>]` 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 <expr>` 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: `<fn_name>__pack__<arg_types>` with comptime values
// (if any) folded into a `__ct_<value>` 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_<name>_<i>`) plus any fixed-prefix non-pack params from
/// the original declaration. The body lowers normally — real
/// `return X;` emits real `ret X`; `args[<lit>]` 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[<lit>]` 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[<runtime_int>]` (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.<name>` 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
};