comptime value params: bind on generic-struct methods

A free function's $o comptime value param binds via lowerComptimeCall →
bindComptimeValueParams. The generic-struct-instance method path
(b.pick(.b)) took a different dispatch route: genericInstanceMethod →
ensureGenericInstanceMethodLowered emitted a plain call to the
monomorphized FuncId, never checking hasComptimeParams — so the method's
$o was never bound and lowered to 'unresolved o'.

Fix: when the selected generic-instance method declares comptime params,
route through the new lowerComptimeGenericInstanceMethod, which composes
the two mechanisms — installs the struct instance's type_bindings (so T /
*Box(T) resolve), pre-binds the receiver self as a normal pointer-param
alloca (so self.field reads work in the inlined body), then routes the
remaining ($) params through lowerComptimeCallArgsSkip(skip_params=1).
That reuses bindComptimeValueParams, so comptimeIntNamed /
comptimeValueRefNamed resolve the value param inside the method body,
identically to the free-function path.

lowerComptimeCall is refactored into lowerComptimeCallArgs(Skip) cores
parameterized over the effective arg-node slice + a leading skip count;
the original free-call entry point is unchanged behaviorally.

Loud-diagnostic behavior preserved: a non-constant / unknown-variant arg
still emits the value-param diagnostic, never a silent default. Int value
params ($n: i64) remain unbound — a pre-existing limitation shared with
free functions, orthogonal to this fix.

Locks examples/0642 (enum + tagged-union comptime value params on a
generic-struct method, incl. self.field read and comptimeIntNamed via a
type-position [o]i64).
This commit is contained in:
agra
2026-06-20 09:57:15 +03:00
parent d7a6857ee1
commit d95ba0a937
8 changed files with 180 additions and 9 deletions

View File

@@ -670,6 +670,28 @@ pub fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.Stri
/// Lower a call to a function with comptime params by inlining its body.
/// Comptime params are substituted, `#insert` expressions are evaluated.
pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref {
return self.lowerComptimeCallArgs(fd, call_node.args);
}
/// Core of `lowerComptimeCall`, parameterized over the EFFECTIVE call-site arg
/// nodes. A free call passes `call_node.args`; a generic-struct method passes
/// the receiver node prepended ahead of `call_node.args`, so `fd.params[0]`
/// (`self`) binds from the receiver and the method's `$o` / comptime params bind
/// from the rest. The caller is responsible for installing any type bindings
/// (e.g. `type_bindings` for a generic struct's `T`) before invoking this — the
/// body lowers with whatever bindings are active.
pub fn lowerComptimeCallArgs(self: *Lowering, fd: *const ast.FnDecl, call_args: []const *Node) Ref {
return self.lowerComptimeCallArgsSkip(fd, call_args, 0);
}
/// Generalization of `lowerComptimeCallArgs` that skips the FIRST `skip_params`
/// formal params — they are PRE-BOUND in the current scope by the caller before
/// invoking (the generic-struct-method path binds `self` via the normal
/// receiver-fixup so a `self: *Box(T)` pointer param is correct, then routes the
/// method's `$o`/comptime params through here). `call_args` corresponds to
/// `fd.params[skip_params..]`. With `skip_params == 0` this is the ordinary
/// free-call inline.
pub fn lowerComptimeCallArgsSkip(self: *Lowering, fd: *const ast.FnDecl, call_args: []const *Node, skip_params: usize) Ref {
// Build comptime param substitution map: param_name → call_site AST node
var cpn = std.StringHashMap(*const Node).init(self.alloc);
var call_arg_idx: usize = 0;
@@ -681,17 +703,17 @@ pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *con
var pack_arg_name: ?[]const u8 = null;
var pack_arg_slice: []const *const Node = &.{};
for (fd.params) |param| {
for (fd.params[skip_params..]) |param| {
if (param.is_variadic) {
// Variadic param: pack remaining call args into []Any slice
self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx);
self.lowerVariadicArgs(param.name, call_args, call_arg_idx);
// Only heterogeneous pack form `..$args` (is_comptime AND
// is_variadic) registers for typed indexing. Plain
// `args: ..Any` keeps the existing []Any path so stdlib's
// `format`/`print` continue boxing through Any.
if (param.is_comptime and call_arg_idx <= call_node.args.len) {
if (param.is_comptime and call_arg_idx <= call_args.len) {
pack_arg_name = param.name;
pack_arg_slice = call_node.args[call_arg_idx..];
pack_arg_slice = call_args[call_arg_idx..];
// Stamp each pack arg with the caller's source so the
// body's typed `args[i]` substitution (via packArgNodeAt,
// lowered under the defining-module pin set below) resolves
@@ -700,19 +722,19 @@ pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *con
// Without it a caller-owned helper passed to an imported
// metaprogram (`std.print("{}", caller_fn())`) resolves
// under the callee's module and is reported "not visible".
for (call_node.args[call_arg_idx..]) |pack_arg| {
for (call_args[call_arg_idx..]) |pack_arg| {
self.stampCallerSource(pack_arg);
}
}
break; // variadic is always the last param
}
if (call_arg_idx >= call_node.args.len) break;
if (call_arg_idx >= call_args.len) break;
if (param.is_comptime) {
self.stampCallerSource(call_node.args[call_arg_idx]);
cpn.put(param.name, call_node.args[call_arg_idx]) catch {};
self.stampCallerSource(call_args[call_arg_idx]);
cpn.put(param.name, call_args[call_arg_idx]) catch {};
call_arg_idx += 1;
} else {
const arg_val = self.lowerExpr(call_node.args[call_arg_idx]);
const arg_val = self.lowerExpr(call_args[call_arg_idx]);
const pty = self.resolveParamType(&param);
const slot = self.builder.alloca(pty);
self.builder.store(slot, arg_val);