diff --git a/examples/0642-comptime-value-param-generic-method.sx b/examples/0642-comptime-value-param-generic-method.sx new file mode 100644 index 00000000..652d7ff1 --- /dev/null +++ b/examples/0642-comptime-value-param-generic-method.sx @@ -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 +} diff --git a/examples/expected/0642-comptime-value-param-generic-method.exit b/examples/expected/0642-comptime-value-param-generic-method.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0642-comptime-value-param-generic-method.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0642-comptime-value-param-generic-method.stderr b/examples/expected/0642-comptime-value-param-generic-method.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0642-comptime-value-param-generic-method.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0642-comptime-value-param-generic-method.stdout b/examples/expected/0642-comptime-value-param-generic-method.stdout new file mode 100644 index 00000000..c8956975 --- /dev/null +++ b/examples/expected/0642-comptime-value-param-generic-method.stdout @@ -0,0 +1,8 @@ +11 +12 +13 +0 +1 +2 +1 +0 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f91e525b..374bc14c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 40bae749..c56a2ccf 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -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; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index c7fb9ba9..d8e465bb 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -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); diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 6476f13a..4aa4ab55 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -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