ffi M5.A.next.2b.fu1.B: mixed comptime+pack — mono with comptime values folded into mangle
Fixes follow-up #1 from step 2b. Pack-fns can now mix non-pack comptime params with the trailing pack: tagged :: ($tag: s32, ..$args) -> s64 { return tag * 100 + args.len; } `isPackFn` relaxed to "exactly one trailing pack + any number of non-pack comptime params". The mono path takes over. Plumbing in src/ir/lower.zig: - `lowerPackFnCall` walks fd.params + call_node.args in lockstep: comptime non-pack args fold into the mangle (`__ct_<value>` segments); non-comptime non-pack args contribute to the runtime arg-type list; remaining call args populate the pack expansion. - `appendComptimeValueMangle` mangles int / bool / float / string literals stably. Strings hash to keep the symbol short. Distinct comptime values get distinct monos. - `monomorphizePackFn` takes `call_node` so it can read comptime call args. Skips comptime non-pack params when building the runtime IR signature. Binds each comptime non-pack param both as a `comptime_param_nodes` entry (for `#insert`) AND as a runtime local via alloca+store (for bare-name body access). `examples/164-pack-mixed-comptime.sx` flips from "unresolved 'tag'" to `703` / `900`. Two calls of `tagged` with different comptime tags get distinct monos (`tagged__ct_7__pack_...` and `tagged__ct_9__pack`). This is the load-bearing prerequisite for step 6 of the plan (stdlib `print` / `format` refactor to `(\$fmt, ..\$args)`). Out of scope: - Non-literal comptime args. `appendComptimeValueMangle` degrades them to `?` (so two distinct non-literal expressions in the same call slot would collide). Acceptable since literal args are the only common case; non-literal would need comptime evaluation to determine the value. 203/203 example tests + `zig build test` green.
This commit is contained in:
174
src/ir/lower.zig
174
src/ir/lower.zig
@@ -8183,37 +8183,80 @@ pub const Lowering = struct {
|
||||
/// params with concrete types; the body's `args[<lit>]` and
|
||||
/// `args.len` resolve to those params via the pack bindings.
|
||||
fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref {
|
||||
var arg_types_list = std.ArrayList(TypeId).empty;
|
||||
defer arg_types_list.deinit(self.alloc);
|
||||
for (call_node.args) |a| {
|
||||
arg_types_list.append(self.alloc, self.inferExprType(a)) catch return self.builder.constInt(0, .void);
|
||||
// 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 runtime_arg_types = std.ArrayList(TypeId).empty;
|
||||
defer runtime_arg_types.deinit(self.alloc);
|
||||
var pack_arg_types = std.ArrayList(TypeId).empty;
|
||||
defer pack_arg_types.deinit(self.alloc);
|
||||
var pack_start: usize = call_node.args.len;
|
||||
var fi: usize = 0;
|
||||
for (fd.params) |p| {
|
||||
if (p.is_variadic and p.is_comptime) {
|
||||
pack_start = fi;
|
||||
break;
|
||||
}
|
||||
if (fi >= call_node.args.len) break;
|
||||
if (!p.is_comptime) {
|
||||
runtime_arg_types.append(self.alloc, self.inferExprType(call_node.args[fi])) catch return self.builder.constInt(0, .void);
|
||||
}
|
||||
// Comptime non-pack: consumed but not added to runtime types.
|
||||
fi += 1;
|
||||
}
|
||||
if (pack_start <= call_node.args.len) {
|
||||
for (call_node.args[pack_start..]) |a| {
|
||||
pack_arg_types.append(self.alloc, self.inferExprType(a)) catch return self.builder.constInt(0, .void);
|
||||
}
|
||||
}
|
||||
const arg_types = arg_types_list.items;
|
||||
|
||||
// Mangle: `<fn_name>__pack__<arg_types>`. Distinct call shapes
|
||||
// get distinct symbols; the same shape called repeatedly
|
||||
// shares one mono.
|
||||
// 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 (p.is_variadic and p.is_comptime) 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.appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]);
|
||||
}
|
||||
ct_fi += 1;
|
||||
}
|
||||
name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void);
|
||||
for (arg_types) |t| {
|
||||
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, arg_types);
|
||||
self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node);
|
||||
}
|
||||
|
||||
// Lower args BEFORE re-fetching the func pointer — lowering
|
||||
// call-site args can trigger more module functions to be
|
||||
// appended, which reallocates `module.functions.items` and
|
||||
// invalidates any `&self.module.functions.items[i]` pointer.
|
||||
// Lower ONLY runtime args (skip comptime non-pack args; their
|
||||
// values are folded into the mangle, not passed at runtime).
|
||||
var args = std.ArrayList(Ref).empty;
|
||||
defer args.deinit(self.alloc);
|
||||
for (call_node.args) |a| {
|
||||
var ri: usize = 0;
|
||||
for (fd.params) |p| {
|
||||
if (p.is_variadic and p.is_comptime) break;
|
||||
if (ri >= call_node.args.len) break;
|
||||
if (!p.is_comptime) {
|
||||
args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void);
|
||||
}
|
||||
ri += 1;
|
||||
}
|
||||
for (call_node.args[pack_start..]) |a| {
|
||||
args.append(self.alloc, self.lowerExpr(a)) catch return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
@@ -8226,13 +8269,54 @@ pub const Lowering = struct {
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
}
|
||||
|
||||
/// Append a stable mangle segment for a comptime call-arg literal.
|
||||
/// Supports int / bool / float / string literals; non-literals
|
||||
/// degrade to "?" (the mono is still cached but two different
|
||||
/// non-literal expressions sharing one call site would collide,
|
||||
/// which is acceptable since they'd lower the same body anyway).
|
||||
fn appendComptimeValueMangle(self: *Lowering, buf: *std.ArrayList(u8), node: *const Node) void {
|
||||
switch (node.data) {
|
||||
.int_literal => |lit| {
|
||||
var tmp: [32]u8 = undefined;
|
||||
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
|
||||
buf.appendSlice(self.alloc, written) catch return;
|
||||
},
|
||||
.bool_literal => |lit| {
|
||||
buf.appendSlice(self.alloc, if (lit.value) "true" else "false") catch return;
|
||||
},
|
||||
.float_literal => |lit| {
|
||||
var tmp: [64]u8 = undefined;
|
||||
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
|
||||
for (written) |c| {
|
||||
buf.append(self.alloc, if (c == '.') '_' else if (c == '-') 'n' else c) catch return;
|
||||
}
|
||||
},
|
||||
.string_literal => |lit| {
|
||||
// Hash the string to a fixed-length tag — keeps the
|
||||
// mangle short and stable for arbitrary content.
|
||||
var h = std.hash.Wyhash.init(0);
|
||||
h.update(lit.raw);
|
||||
var tmp: [32]u8 = undefined;
|
||||
const written = std.fmt.bufPrint(&tmp, "s{x}", .{h.final()}) catch return;
|
||||
buf.appendSlice(self.alloc, written) catch return;
|
||||
},
|
||||
else => buf.append(self.alloc, '?') catch return,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
fn monomorphizePackFn(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, arg_types: []const TypeId) void {
|
||||
fn monomorphizePackFn(
|
||||
self: *Lowering,
|
||||
fd: *const ast.FnDecl,
|
||||
mangled_name: []const u8,
|
||||
arg_types: []const TypeId,
|
||||
call_node: *const ast.Call,
|
||||
) void {
|
||||
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
||||
self.lowered_functions.put(owned_name, {}) catch {};
|
||||
|
||||
@@ -8333,10 +8417,11 @@ pub const Lowering = struct {
|
||||
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`.
|
||||
// Freeing here would leave the function holding a freed slice.
|
||||
// (Matches the leak convention in `monomorphizeFunction`.)
|
||||
var params = std.ArrayList(Function.Param).empty;
|
||||
if (wants_ctx) {
|
||||
params.append(self.alloc, .{
|
||||
@@ -8346,6 +8431,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
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),
|
||||
@@ -8372,15 +8458,44 @@ pub const Lowering = struct {
|
||||
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) continue;
|
||||
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];
|
||||
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;
|
||||
}
|
||||
const pty = self.resolveParamType(&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| {
|
||||
@@ -9413,22 +9528,17 @@ pub const Lowering = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Pure pack-fn: the ONLY comptime param is a trailing heterogeneous
|
||||
/// pack (`is_variadic AND is_comptime`). Detected at call sites that
|
||||
/// today route to `lowerComptimeCall`; siphoned off to
|
||||
/// `lowerPackFnCall` for per-call-shape monomorphisation. Mixed
|
||||
/// `($fmt, ..$args)` stays on the inline path for now — different
|
||||
/// substitution mechanism for the comptime non-pack param.
|
||||
/// 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).
|
||||
fn isPackFn(fd: *const ast.FnDecl) bool {
|
||||
var seen_pack = false;
|
||||
for (fd.params) |p| {
|
||||
if (p.is_comptime and p.is_variadic) {
|
||||
seen_pack = true;
|
||||
} else if (p.is_comptime) {
|
||||
return false; // mixed — defer to lowerComptimeCall
|
||||
}
|
||||
if (p.is_comptime and p.is_variadic) return true;
|
||||
}
|
||||
return seen_pack;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Creates a temporary function marked `is_comptime = true` that wraps
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
/Users/agra/projects/sx/examples/164-pack-mixed-comptime.sx:22:12: error: unresolved 'tag' (in /Users/agra/projects/sx/examples/164-pack-mixed-comptime.sx fn main)
|
||||
703
|
||||
900
|
||||
|
||||
Reference in New Issue
Block a user