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:
57
examples/0642-comptime-value-param-generic-method.sx
Normal file
57
examples/0642-comptime-value-param-generic-method.sx
Normal file
@@ -0,0 +1,57 @@
|
||||
// Comptime VALUE params bind on methods of GENERIC structs.
|
||||
//
|
||||
// A free function's `$o: Ord` comptime value param binds via
|
||||
// lowerComptimeCall → bindComptimeValueParams (see 0627 / 0640). The
|
||||
// generic-struct-instance method path (`b.pick(.b)`) takes a different
|
||||
// dispatch route: the struct monomorphizes for `T`, but the method's `$o`
|
||||
// must STILL bind by inlining the body — a plain call to the monomorphized
|
||||
// FuncId would leave `o` unresolved. This locks the composition: `T` is bound
|
||||
// from the struct instantiation, `$o` from the comptime value arg, and a
|
||||
// `self.field` read through the pointer receiver works inside the inlined body.
|
||||
//
|
||||
// Regression: composition gap — comptime value params on generic-struct methods.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Ord :: enum { a; b; c; }
|
||||
|
||||
Box :: struct ($T: Type) {
|
||||
value: i64;
|
||||
// Enum comptime value param: `if o == .x` lowers as a tag comparison; the
|
||||
// receiver `self` is bound so `self.value` reads correctly.
|
||||
pick :: (self: *Box(T), $o: Ord) -> i64 {
|
||||
if o == .a { return self.value + 1; }
|
||||
if o == .b { return self.value + 2; }
|
||||
return self.value + 3;
|
||||
}
|
||||
// The bound tag is also readable in a TYPE position ([o]i64), via
|
||||
// comptimeIntNamed — the same accessor the atomics stream uses for
|
||||
// `Atomic($T).load($o)`.
|
||||
size_for :: (self: *Box(T), $o: Ord) -> i64 {
|
||||
arr : [o]i64 = ---;
|
||||
return arr.len;
|
||||
}
|
||||
}
|
||||
|
||||
Shape :: enum { circle: f64; point; }
|
||||
|
||||
Drawer :: struct ($T: Type) {
|
||||
// Tagged-union comptime value param on a generic-struct method.
|
||||
area :: (self: *Drawer(T), $s: Shape) -> i64 {
|
||||
if s == .circle { return 1; }
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
main :: () {
|
||||
b := Box(i64).{ value = 10 };
|
||||
print("{}\n", b.pick(.a)); // 11
|
||||
print("{}\n", b.pick(.b)); // 12
|
||||
print("{}\n", b.pick(.c)); // 13
|
||||
print("{}\n", b.size_for(.a)); // tag 0 → [0]i64 → 0
|
||||
print("{}\n", b.size_for(.b)); // tag 1 → [1]i64 → 1
|
||||
print("{}\n", b.size_for(.c)); // tag 2 → [2]i64 → 2
|
||||
|
||||
d := Drawer(i64).{};
|
||||
print("{}\n", d.area(.circle(2.0))); // 1
|
||||
print("{}\n", d.area(.point)); // 0
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
11
|
||||
12
|
||||
13
|
||||
0
|
||||
1
|
||||
2
|
||||
1
|
||||
0
|
||||
@@ -1581,6 +1581,8 @@ pub const Lowering = struct {
|
||||
pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal;
|
||||
pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect;
|
||||
pub const lowerComptimeCall = lower_comptime.lowerComptimeCall;
|
||||
pub const lowerComptimeCallArgs = lower_comptime.lowerComptimeCallArgs;
|
||||
pub const lowerComptimeCallArgsSkip = lower_comptime.lowerComptimeCallArgsSkip;
|
||||
pub const bindComptimeValueParams = lower_comptime.bindComptimeValueParams;
|
||||
pub const recordComptimeTag = lower_comptime.recordComptimeTag;
|
||||
pub const recordComptimeValueRef = lower_comptime.recordComptimeValueRef;
|
||||
@@ -1894,6 +1896,7 @@ pub const Lowering = struct {
|
||||
pub const returnExprMintsType = lower_generic.returnExprMintsType;
|
||||
pub const genericInstanceMethod = lower_generic.genericInstanceMethod;
|
||||
pub const ensureGenericInstanceMethodLowered = lower_generic.ensureGenericInstanceMethodLowered;
|
||||
pub const lowerComptimeGenericInstanceMethod = lower_generic.lowerComptimeGenericInstanceMethod;
|
||||
pub const assertInstanceMapsCoincide = lower_generic.assertInstanceMapsCoincide;
|
||||
pub const isStaticTypeArg = lower_generic.isStaticTypeArg;
|
||||
pub const isStaticTypeRef = lower_generic.isStaticTypeRef;
|
||||
|
||||
@@ -907,6 +907,14 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
// the one authored alongside this instance's layout — never the
|
||||
// global last-wins `fn_ast_map["Template.method"]`.
|
||||
if (self.genericInstanceMethod(sname, fa.field)) |gm| {
|
||||
// A comptime method (`pick :: (self: *Box(T), $o: Ord)`) must
|
||||
// INLINE so its `$o` binds — a plain `call` to a monomorphized
|
||||
// FuncId would leave `o` unresolved. The struct is already
|
||||
// monomorphized for `T`; this composes that with the
|
||||
// comptime-value-param binding (`bindComptimeValueParams`).
|
||||
if (hasComptimeParams(gm.fd)) {
|
||||
return self.lowerComptimeGenericInstanceMethod(gm, effective_obj_node, obj, obj_ty, c.args);
|
||||
}
|
||||
if (self.ensureGenericInstanceMethodLowered(gm)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
|
||||
@@ -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(¶m);
|
||||
const slot = self.builder.alloca(pty);
|
||||
self.builder.store(slot, arg_val);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user