From 159f898ffef9fb4110d1e5792877d0a17f22eac4 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 16:47:52 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20M5.A.next.2b.fu1.B:=20mixed=20comptime+pa?= =?UTF-8?q?ck=20=E2=80=94=20mono=20with=20comptime=20values=20folded=20int?= =?UTF-8?q?o=20mangle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_` 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. --- src/ir/lower.zig | 174 ++++++++++++++++---- tests/expected/164-pack-mixed-comptime.exit | 2 +- tests/expected/164-pack-mixed-comptime.txt | 3 +- 3 files changed, 145 insertions(+), 34 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0bf9ed1..4b9f8ed 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -8183,37 +8183,80 @@ pub const Lowering = struct { /// params with concrete types; the body's `args[]` 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: `__pack__`. Distinct call shapes - // get distinct symbols; the same shape called repeatedly - // shares one mono. + // Mangle: `__pack__` with comptime values + // (if any) folded into a `__ct_` segment per non-pack + // comptime param. Distinct call shapes — including different + // comptime VALUES — get distinct symbols. var name_buf = std.ArrayList(u8).empty; defer name_buf.deinit(self.alloc); name_buf.appendSlice(self.alloc, fd.name) catch return self.builder.constInt(0, .void); + // Comptime values first (deterministic by fd.params order). + var ct_fi: usize = 0; + for (fd.params) |p| { + if (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__`) plus any fixed-prefix non-pack params from /// the original declaration. The body lowers normally — real /// `return X;` emits real `ret X`; `args[]` substitutes via /// `pack_arg_nodes`; `args.len` resolves via `pack_param_count`. - 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 diff --git a/tests/expected/164-pack-mixed-comptime.exit b/tests/expected/164-pack-mixed-comptime.exit index d00491f..573541a 100644 --- a/tests/expected/164-pack-mixed-comptime.exit +++ b/tests/expected/164-pack-mixed-comptime.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/164-pack-mixed-comptime.txt b/tests/expected/164-pack-mixed-comptime.txt index 6c400b9..1c874af 100644 --- a/tests/expected/164-pack-mixed-comptime.txt +++ b/tests/expected/164-pack-mixed-comptime.txt @@ -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