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:
agra
2026-05-27 16:47:52 +03:00
parent fc8a8c3f2e
commit 159f898ffe
3 changed files with 145 additions and 34 deletions

View File

@@ -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

View File

@@ -1 +1 @@
1
0

View File

@@ -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