From 362674f04d5fdd8e3e78967fd55824f845789384 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 21 Jun 2026 05:25:39 +0300 Subject: [PATCH] issue 0151 RESOLVED: infer generic $T through generic-struct / pointer / UFCS-pack params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../0214-generics-ufcs-closure-return-pack.sx | 26 ++++++++++++ .../0215-generics-infer-through-pointer.sx | 29 +++++++++++++ ...214-generics-ufcs-closure-return-pack.exit | 1 + ...4-generics-ufcs-closure-return-pack.stderr | 1 + ...4-generics-ufcs-closure-return-pack.stdout | 2 + .../0215-generics-infer-through-pointer.exit | 1 + ...0215-generics-infer-through-pointer.stderr | 1 + ...0215-generics-infer-through-pointer.stdout | 5 +++ ...-closure-return-pack-generic-unresolved.md | 42 +++++++++++++++++++ ...-closure-return-pack-generic-unresolved.sx | 23 ---------- src/ir/expr_typer.zig | 23 ++++++++++ src/ir/lower/call.zig | 19 +++++++++ src/ir/lower/generic.zig | 41 +++++++++++++++++- 13 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 examples/0214-generics-ufcs-closure-return-pack.sx create mode 100644 examples/0215-generics-infer-through-pointer.sx create mode 100644 examples/expected/0214-generics-ufcs-closure-return-pack.exit create mode 100644 examples/expected/0214-generics-ufcs-closure-return-pack.stderr create mode 100644 examples/expected/0214-generics-ufcs-closure-return-pack.stdout create mode 100644 examples/expected/0215-generics-infer-through-pointer.exit create mode 100644 examples/expected/0215-generics-infer-through-pointer.stderr create mode 100644 examples/expected/0215-generics-infer-through-pointer.stdout delete mode 100644 issues/0151-ufcs-closure-return-pack-generic-unresolved.sx diff --git a/examples/0214-generics-ufcs-closure-return-pack.sx b/examples/0214-generics-ufcs-closure-return-pack.sx new file mode 100644 index 00000000..f6573eb6 --- /dev/null +++ b/examples/0214-generics-ufcs-closure-return-pack.sx @@ -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; +} diff --git a/examples/0215-generics-infer-through-pointer.sx b/examples/0215-generics-infer-through-pointer.sx new file mode 100644 index 00000000..b1b2fd3e --- /dev/null +++ b/examples/0215-generics-infer-through-pointer.sx @@ -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; +} diff --git a/examples/expected/0214-generics-ufcs-closure-return-pack.exit b/examples/expected/0214-generics-ufcs-closure-return-pack.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0214-generics-ufcs-closure-return-pack.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0214-generics-ufcs-closure-return-pack.stderr b/examples/expected/0214-generics-ufcs-closure-return-pack.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0214-generics-ufcs-closure-return-pack.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0214-generics-ufcs-closure-return-pack.stdout b/examples/expected/0214-generics-ufcs-closure-return-pack.stdout new file mode 100644 index 00000000..16f485d3 --- /dev/null +++ b/examples/expected/0214-generics-ufcs-closure-return-pack.stdout @@ -0,0 +1,2 @@ +direct=42 +ufcs=42 diff --git a/examples/expected/0215-generics-infer-through-pointer.exit b/examples/expected/0215-generics-infer-through-pointer.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0215-generics-infer-through-pointer.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0215-generics-infer-through-pointer.stderr b/examples/expected/0215-generics-infer-through-pointer.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0215-generics-infer-through-pointer.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0215-generics-infer-through-pointer.stdout b/examples/expected/0215-generics-infer-through-pointer.stdout new file mode 100644 index 00000000..bb06ca85 --- /dev/null +++ b/examples/expected/0215-generics-infer-through-pointer.stdout @@ -0,0 +1,5 @@ +unbox=42 +byval=42 +second=2.500000 +nested=9 +ufcs=42 diff --git a/issues/0151-ufcs-closure-return-pack-generic-unresolved.md b/issues/0151-ufcs-closure-return-pack-generic-unresolved.md index c93e47f7..a7e31c64 100644 --- a/issues/0151-ufcs-closure-return-pack-generic-unresolved.md +++ b/issues/0151-ufcs-closure-return-pack-generic-unresolved.md @@ -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, diff --git a/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx b/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx deleted file mode 100644 index c9f16be3..00000000 --- a/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx +++ /dev/null @@ -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; -} diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 67edf7fd..b8782766 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -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 diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index bbef375a..a1f8640e 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -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| { diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index c18d471c..216119f2 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -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, }; }