issue 0151 RESOLVED: infer generic $T through generic-struct / pointer / UFCS-pack params
The generic-inference engine could not bind a $T from a generic-struct argument head. Four gaps, all on the inference + UFCS dispatch path: - extractTypeParam / matchTypeParam(Static) gained a parameterized_type_expr arm: recover the arg instance's recorded per-param bindings (struct_instance_bindings + the template's ordered type_params via struct_instance_author) and recurse positionally, so $T binds from Box($T) <=> Box(i64) like it does from []$T <=> []i64. This also fixes the pointer case — *Box($T) recurses into its Box($T) pointee. - The pointer_type_expr arm now falls through to match the pointee against a non-pointer arg (auto-address-of: a *Box($T) param accepts a by-value Box($T), e.g. the UFCS receiver b.m()). - ExprTyper.inferType gained a .lambda arm building the closure type from the lambda's annotations, so the UFCS binder (which types args from the raw AST before they are lowered) can bind a Closure(..) -> $R from the worker's declared return type. - A pack UFCS target (worker: Closure(..) -> $R, ..$args) now routes through the same lowerPackFnCall the direct call uses, with the receiver spliced in as args[0] (lowerPackFnCall reads only call_node.args, never the callee). Regression tests: examples/0214 (direct + UFCS closure-return pack) and examples/0215 (by-value / pointer / multi-param / nested / UFCS-auto-ref generic-struct-head inference). Suite green 728/0.
This commit is contained in:
26
examples/0214-generics-ufcs-closure-return-pack.sx
Normal file
26
examples/0214-generics-ufcs-closure-return-pack.sx
Normal file
@@ -0,0 +1,26 @@
|
||||
// Generic inference where `$R` comes from a worker closure's RETURN type
|
||||
// through a variadic `..$args` pack — both the DIRECT spelling
|
||||
// `mymk(bx, worker, 40, 2)` and the UFCS dot-call `bx.mymk(worker, 40, 2)`
|
||||
// resolve `$R = i64` identically and build `Wrap($R)` correctly.
|
||||
// Regression (issue 0151): the UFCS path used to splice the receiver as
|
||||
// arg 0 without running the direct path's pack/closure-return binding, so
|
||||
// `$R` stayed `.unresolved` and SIGTRAPped at LLVM emission.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct { n: i64; }
|
||||
Wrap :: struct ($R: Type) { value: R; }
|
||||
|
||||
mymk :: ufcs (b: Box, worker: Closure(..$args) -> $R, ..$args) -> Wrap($R) {
|
||||
f : Wrap($R) = ---;
|
||||
f.value = worker(..args);
|
||||
return f;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
bx : Box = .{ n = 1 };
|
||||
direct := mymk(bx, (a: i64, b: i64) -> i64 => a + b, 40, 2);
|
||||
ufcs := bx.mymk((a: i64, b: i64) -> i64 => a + b, 40, 2);
|
||||
print("direct={}\n", direct.value);
|
||||
print("ufcs={}\n", ufcs.value);
|
||||
return 0;
|
||||
}
|
||||
29
examples/0215-generics-infer-through-pointer.sx
Normal file
29
examples/0215-generics-infer-through-pointer.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Generic `$T` inferred through a generic-struct argument head — both
|
||||
// by-value (`Box($T)`) and pointer-wrapped (`*Box($T)`), the latter also
|
||||
// via a UFCS dot-call (auto-address-of receiver). Multi-param heads
|
||||
// (`Pair($A, $B)`) and nested heads (`Box(Box($T))`) bind positionally.
|
||||
// Regression (issue 0151, widened): `extractTypeParam` had no
|
||||
// `parameterized_type_expr` arm, so `$T` never bound from a generic-struct
|
||||
// param — the call failed with "cannot infer generic type parameter 'T'".
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct ($T: Type) { v: T; }
|
||||
Pair :: struct ($A: Type, $B: Type) { a: A; b: B; }
|
||||
|
||||
unbox :: (b: *Box($T)) -> $T { return b.v; } // infer through `*`
|
||||
byval :: (b: Box($T)) -> $T { return b.v; } // infer through head
|
||||
second :: (p: Pair($A, $B)) -> $B { return p.b; } // 2nd of two params
|
||||
nested :: (b: Box(Box($T))) -> $T { return b.v.v; } // nested head
|
||||
get :: ufcs (b: *Box($T)) -> $T { return b.v; } // UFCS auto-ref
|
||||
|
||||
main :: () -> i32 {
|
||||
b : Box(i64) = .{ v = 42 };
|
||||
p : Pair(i64, f64) = .{ a = 1, b = 2.5 };
|
||||
nb : Box(Box(i64)) = .{ v = .{ v = 9 } };
|
||||
print("unbox={}\n", unbox(@b));
|
||||
print("byval={}\n", byval(b));
|
||||
print("second={}\n", second(p));
|
||||
print("nested={}\n", nested(nb));
|
||||
print("ufcs={}\n", b.get());
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
direct=42
|
||||
ufcs=42
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
unbox=42
|
||||
byval=42
|
||||
second=2.500000
|
||||
nested=9
|
||||
ufcs=42
|
||||
@@ -1,5 +1,47 @@
|
||||
# 0151 — generic type-var not inferred through a pointer / via UFCS (LLVM SIGTRAP / "cannot infer")
|
||||
|
||||
## ✅ RESOLVED (2026-06-21)
|
||||
|
||||
**Root cause** — the generic-inference engine had no path to bind a `$T`
|
||||
from a generic-struct argument head. Three gaps, all in
|
||||
`src/ir/lower/generic.zig` + the UFCS dispatch:
|
||||
|
||||
1. `extractTypeParam` / `matchTypeParam` / `matchTypeParamStatic` lacked a
|
||||
`.parameterized_type_expr` arm — so `Box($T)` (and, recursively, the
|
||||
pointee of `*Box($T)`) never matched a type-param. Added an arm that
|
||||
recovers the arg instance's recorded per-param bindings
|
||||
(`struct_instance_bindings` + the template's ordered `type_params` via
|
||||
`struct_instance_author`) and recurses positionally.
|
||||
2. The `pointer_type_expr` arm bailed when the arg wasn't itself a pointer.
|
||||
A UFCS receiver (`b.m()`) / a value passed to a `*T` param is auto-
|
||||
address-of'd, so the arg type is the *value* `Box($T)`. Added a fall-
|
||||
through that matches the pointee against the non-pointer arg.
|
||||
3. `ExprTyper.inferType` had no `.lambda` arm (returned `.unresolved`), so
|
||||
the UFCS binder — which types args from the raw AST *before* they're
|
||||
lowered — couldn't read a lambda's declared return type to bind a
|
||||
`Closure(..) -> $R`. Added an arm that builds the closure type from the
|
||||
lambda's annotations.
|
||||
4. A pack UFCS target (`worker: Closure(..) -> $R, ..$args`) was dispatched
|
||||
through the non-pack generic path, which can't expand the pack. Routed
|
||||
it through the SAME `lowerPackFnCall` the direct call uses, with the
|
||||
receiver spliced in as `args[0]` (a synthetic call — `lowerPackFnCall`
|
||||
reads only `call_node.args`, never the callee).
|
||||
|
||||
**Fix verified** — the repro prints `value=42` (both spellings). Regression
|
||||
tests: `examples/0214-generics-ufcs-closure-return-pack.sx` (direct + UFCS
|
||||
closure-return pack) and `examples/0215-generics-infer-through-pointer.sx`
|
||||
(by-value / pointer / multi-param / nested / UFCS-auto-ref struct-head
|
||||
inference). Full suite green (726/0).
|
||||
|
||||
**Downstream (NOT this bug):** with `await`/`cancel` now callable, the
|
||||
B1.2 async examples surface a SEPARATE codegen bug — `Atomic(bool)` emits a
|
||||
sub-byte (i1) atomic load/store that fails LLVM verification (filed as a new
|
||||
issue). The `Future.canceled: Atomic(bool)` field hits it, so `1805`/`1806`
|
||||
stay blocked on that, not on 0151.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## WIDENED (adversarial review of B1.2, 2026-06-21)
|
||||
The UFCS-closure-return-pack case below is one symptom of a BROADER generic-inference
|
||||
gap: **sx cannot infer a generic `$T` from a POINTER-wrapped argument.** Minimal repro,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Repro for issue 0151 — UFCS dot-call where `$R` is inferred from a
|
||||
// worker closure's RETURN type through a variadic `..$args` pack leaves
|
||||
// `$R` unresolved (SIGTRAP at LLVM emission). The DIRECT spelling
|
||||
// `mymk(bx, worker, 40, 2)` resolves `$R = i64` and works; the UFCS
|
||||
// spelling `bx.mymk(worker, 40, 2)` does not. Depends on no project
|
||||
// symbols beyond modules/std.sx.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct { n: i64; }
|
||||
Wrap :: struct ($R: Type) { value: R; }
|
||||
|
||||
mymk :: ufcs (b: Box, worker: Closure(..$args) -> $R, ..$args) -> Wrap($R) {
|
||||
f : Wrap($R) = ---;
|
||||
f.value = worker(..args);
|
||||
return f;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
bx : Box = .{ n = 1 };
|
||||
g := bx.mymk((a: i64, b: i64) -> i64 => a + b, 40, 2);
|
||||
print("value={}\n", g.value);
|
||||
return 0;
|
||||
}
|
||||
@@ -401,6 +401,29 @@ pub const ExprTyper = struct {
|
||||
}
|
||||
break :blk self.l.inferExprType(nc.rhs);
|
||||
},
|
||||
// A lambda literal's type is the closure it denotes, recovered from
|
||||
// its annotations. The generic-call binder types args from the raw
|
||||
// AST (notably the UFCS path, before args are lowered), so without
|
||||
// this a `Closure(..) -> $R` worker couldn't bind `$R` from the
|
||||
// lambda's declared return type (issue 0151). An unannotated param /
|
||||
// body-inferred return stays `.unresolved` here — that arg simply
|
||||
// doesn't contribute a binding, exactly as before.
|
||||
.lambda => |lam| blk: {
|
||||
var pbuf = std.ArrayList(TypeId).empty;
|
||||
defer pbuf.deinit(self.l.alloc);
|
||||
for (lam.params) |p| {
|
||||
const pty: TypeId = if (p.type_expr.data == .inferred_type)
|
||||
.unresolved
|
||||
else
|
||||
self.l.resolveTypeWithBindings(p.type_expr);
|
||||
pbuf.append(self.l.alloc, pty) catch {};
|
||||
}
|
||||
const ret: TypeId = if (lam.return_type) |rt|
|
||||
self.l.resolveTypeWithBindings(rt)
|
||||
else
|
||||
.unresolved;
|
||||
break :blk self.l.module.types.closureType(pbuf.items, ret);
|
||||
},
|
||||
// Inline asm result type (0→void, 1→T, N→named tuple) — the single
|
||||
// owner is `Lowering.asmResultType`, shared with `lowerAsmExpr` so a
|
||||
// `return asm`, a `x := asm`, and a `q, r := asm` destructure all
|
||||
|
||||
@@ -1033,6 +1033,25 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
||||
return Ref.none;
|
||||
}
|
||||
// A pack ufcs target (`worker: Closure(..) -> $R, ..$args`):
|
||||
// route through the SAME pack-call path the direct call uses,
|
||||
// with the receiver spliced in as the first arg so the pack
|
||||
// boundary, the `$R` closure-return binding, and the pack
|
||||
// expansion all line up with `fd.params[0]` (issue 0151).
|
||||
// `lowerPackFnCall` reads only `call_node.args` (never the
|
||||
// callee), so a synthetic spliced-args call is sufficient.
|
||||
if (ufcs_fd) |fd| {
|
||||
if (isPackFn(fd)) {
|
||||
// `lowerPackFnCall` only READS these nodes; the const-cast
|
||||
// back to `*Node` (Call.args' element type) is sound.
|
||||
var syn_args = std.ArrayList(*Node).empty;
|
||||
defer syn_args.deinit(self.alloc);
|
||||
syn_args.append(self.alloc, @constCast(effective_obj_node)) catch unreachable;
|
||||
for (c.args) |a| syn_args.append(self.alloc, a) catch unreachable;
|
||||
const syn_call = ast.Call{ .callee = c.callee, .args = syn_args.items };
|
||||
return self.lowerPackFnCall(fd, &syn_call);
|
||||
}
|
||||
}
|
||||
// Generic ufcs target: monomorphize with the receiver's AST
|
||||
// node prepended so bindings align with fd.params[0].
|
||||
if (ufcs_fd) |fd| {
|
||||
|
||||
@@ -537,6 +537,10 @@ pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8)
|
||||
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
|
||||
break :blk false;
|
||||
},
|
||||
.parameterized_type_expr => |pt| blk: {
|
||||
for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true;
|
||||
break :blk false;
|
||||
},
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -555,6 +559,10 @@ pub fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool {
|
||||
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
|
||||
break :blk false;
|
||||
},
|
||||
.parameterized_type_expr => |pt| blk: {
|
||||
for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true;
|
||||
break :blk false;
|
||||
},
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -583,7 +591,11 @@ pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId,
|
||||
const info = self.module.types.get(arg_ty);
|
||||
break :blk switch (info) {
|
||||
.pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name),
|
||||
else => null,
|
||||
// Auto-address-of: a `*Box($T)` param accepts a by-value
|
||||
// `Box($T)` arg (the UFCS receiver `b.m()` / a value passed to a
|
||||
// pointer param). Match the pointee against the value arg so the
|
||||
// type-var still binds (issue 0151).
|
||||
else => self.extractTypeParam(pt.pointee_type, arg_ty, tp_name),
|
||||
};
|
||||
},
|
||||
.many_pointer_type_expr => |mp| blk: {
|
||||
@@ -628,6 +640,33 @@ pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId,
|
||||
}
|
||||
break :blk null;
|
||||
},
|
||||
.parameterized_type_expr => |pt| blk: {
|
||||
// A generic-struct param head (`Box($T)`, also reached recursively
|
||||
// for a pointer-wrapped `*Box($T)`): the arg is a monomorphized
|
||||
// instance whose per-param bindings were recorded at instantiation
|
||||
// (`struct_instance_bindings`). Recover the concrete type the i-th
|
||||
// template param bound and recurse against the i-th param-head arg,
|
||||
// so `$T` is inferred from `Box($T)` ⇔ `Box(i64)` exactly as it is
|
||||
// from `[]$T` ⇔ `[]i64` (issue 0151).
|
||||
if (arg_ty.isBuiltin()) break :blk null;
|
||||
const info = self.module.types.get(arg_ty);
|
||||
if (info != .@"struct") break :blk null;
|
||||
const inst_name = self.module.types.getString(info.@"struct".name);
|
||||
const binds = self.struct_instance_bindings.getPtr(inst_name) orelse break :blk null;
|
||||
// The param head must name the same template the arg instance was
|
||||
// stamped from, so the positional args line up with the params.
|
||||
const tmpl_name = self.struct_instance_template.get(inst_name) orelse break :blk null;
|
||||
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
|
||||
if (!std.mem.eql(u8, base_name, tmpl_name)) break :blk null;
|
||||
const author = self.struct_instance_author.get(inst_name) orelse break :blk null;
|
||||
for (author.type_params, 0..) |atp, i| {
|
||||
if (i >= pt.args.len) break;
|
||||
if (atp.is_variadic) break; // type-pack params not inferred here
|
||||
const concrete = binds.get(atp.name) orelse continue;
|
||||
if (self.extractTypeParam(pt.args[i], concrete, tp_name)) |ety| break :blk ety;
|
||||
}
|
||||
break :blk null;
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user