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.
1102 lines
53 KiB
Zig
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
|
|
};
|