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

@@ -1441,6 +1441,77 @@ pub fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMetho
return self.resolveFuncByName(mangled);
}
/// Dispatch a generic-struct-instance method that declares COMPTIME params
/// (`pick :: (self: *Box(T), $o: Ord) -> i64`). The struct already monomorphized
/// for `T` (its bindings live in `m.bindings`), but a comptime value param like
/// `$o` must still bind by INLINING the body the same way a free comptime call
/// does — a plain `call` to a monomorphized FuncId would leave `$o` unresolved.
///
/// Composition: install the struct's type bindings (so `T` / `*Box(T)` resolve
/// in the body), pre-bind the receiver `self` in scope exactly as normal
/// param-lowering does (alloca of the pointer param type, holding the receiver's
/// address), then route the remaining (comptime) params through
/// `lowerComptimeCallArgsSkip` with `skip_params = 1`. That reuses
/// `bindComptimeValueParams` — so `comptimeIntNamed` / `comptimeValueRefNamed`
/// resolve `$o` inside the body, identically to the free-function path.
pub fn lowerComptimeGenericInstanceMethod(
self: *Lowering,
m: GenericStructMethod,
recv_node: *const Node,
recv_val: Ref,
recv_ty: TypeId,
call_args: []const *Node,
) Ref {
// Install the struct instance's type bindings for the duration of the inline
// body (mirrors `monomorphizeFunction`), so `self: *Box(T)` and any `T` in
// the body / return type resolve to the concrete instantiation.
const saved_bindings = self.type_bindings;
self.type_bindings = m.bindings.*;
defer self.type_bindings = saved_bindings;
const fd = m.fd;
// hasComptimeParams was true to route here, so the body declares at least one
// `$` param. A receiver `self` is param[0] for any instance method.
if (fd.params.len == 0) return self.lowerComptimeCallArgsSkip(fd, call_args, 0);
// Pre-bind the receiver `self` into scope — same shape normal param lowering
// uses: an alloca of the (resolved) param type holding the receiver. For a
// pointer receiver (`self: *Box(T)`) the stored value is the receiver's
// ADDRESS, so body `self.field` reads load-the-pointer-then-deref correctly.
const self_param = &fd.params[0];
const self_pty = self.resolveParamType(self_param);
const recv_ref: Ref = blk: {
if (!self_pty.isBuiltin() and self.module.types.get(self_pty) == .pointer) {
// Param wants `*T`. If the receiver is already a pointer, pass it; else
// take its address (identifier-with-alloca → addr_of the alloca; any
// other lvalue → lowerExprAsPtr).
if (!recv_ty.isBuiltin() and self.module.types.get(recv_ty) == .pointer) break :blk recv_val;
if (recv_node.data == .identifier) {
if (self.scope) |scope| {
if (scope.lookup(recv_node.data.identifier.name)) |b| {
if (b.is_alloca) {
const ptr_ty = self.module.types.ptrTo(b.ty);
break :blk self.builder.emit(.{ .addr_of = .{ .operand = b.ref } }, ptr_ty);
}
}
}
}
break :blk self.lowerExprAsPtr(recv_node);
}
// Value receiver param (`self: Box(T)`): if we have a pointer, deref it.
if (!recv_ty.isBuiltin() and self.module.types.get(recv_ty) == .pointer)
break :blk self.builder.load(recv_val, self_pty);
break :blk recv_val;
};
const slot = self.builder.alloca(self_pty);
self.builder.store(slot, recv_ref);
if (self.scope) |scope| scope.put(self_param.name, .{ .ref = slot, .ty = self_pty, .is_alloca = true });
// The remaining params (`$o`, ...) bind from the call-site args; the receiver
// is already bound, so skip param[0].
return self.lowerComptimeCallArgsSkip(fd, call_args, 1);
}
/// Debug invariant (CP coverage lock): the two generic-instance maps written
/// in lockstep at the SAME two writers (instantiation + alias copy) —
/// `struct_instance_template` and `struct_instance_author` — must have