From 3eeb9659250fc2daeffb1c1cf364500d6202c21c Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 22:21:38 +0300 Subject: [PATCH] issue 0151: UFCS dot-call leaves $R inferred from a closure return type via a pack unresolved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A generic free fn whose `$R` is inferred from a worker `Closure(..$args) -> $R` (+ trailing `..$args`) and which returns a type built from `$R` (`-> Wrap($R)`) monomorphizes correctly when called directly (`f(recv, worker, ..)`) but leaves `$R` UNRESOLVED when called via UFCS dot syntax (`recv.f(worker, ..)`) — the unresolved type reaches LLVM emission and trips the `.unresolved` tripwire (SIGTRAP). Distinct from RESOLVED issue 0119 (UFCS `$T` from receiver/slice). Blocks the B1.2 user-facing async idiom `context.io.async((a,b) -> R => ..., x, y)` (a UFCS call inferring $R from the worker closure's return type). The Io/async library + compiler plumbing are in place and correct (landed in the prior commit); only the UFCS call form hits this inference gap. Repro depends on no project symbols beyond modules/std.sx; unpinned (no expected/ marker) so it does not run in the corpus. --- ...-closure-return-pack-generic-unresolved.md | 109 ++++++++++++++++++ ...-closure-return-pack-generic-unresolved.sx | 23 ++++ 2 files changed, 132 insertions(+) create mode 100644 issues/0151-ufcs-closure-return-pack-generic-unresolved.md create mode 100644 issues/0151-ufcs-closure-return-pack-generic-unresolved.sx diff --git a/issues/0151-ufcs-closure-return-pack-generic-unresolved.md b/issues/0151-ufcs-closure-return-pack-generic-unresolved.md new file mode 100644 index 00000000..69969871 --- /dev/null +++ b/issues/0151-ufcs-closure-return-pack-generic-unresolved.md @@ -0,0 +1,109 @@ +# 0151 — UFCS dot-call: `$R` inferred from a closure's RETURN type via a variadic pack is left unresolved (LLVM SIGTRAP) + +## Symptom + +A generic free fn whose generic param `$R` is inferred from a worker +**closure's return type** — `worker: Closure(..$args) -> $R` plus a +trailing `..$args` pack — and which RETURNS a type built from `$R` +(`-> Wrap($R)`), monomorphizes correctly when called **directly** +(`mymk(recv, worker, ..)`) but leaves `$R` **unresolved** when called via +**UFCS dot syntax** (`recv.mymk(worker, ..)`). The unresolved `Wrap($R)` +reaches LLVM emission and trips the tripwire: + +``` +thread … panic: unresolved type reached LLVM emission — a type +resolution failure was not diagnosed/aborted + src/backend/llvm/types.zig:180 .unresolved => @panic(...) + … toLLVMTypeInfo (struct field) → toLLVMType + … emit_llvm.zig:1262 declareFunction const raw_ret_ty = self.toLLVMType(func.ret) +``` + +- `mymk(bx, worker, 40, 2)` (direct) → **works** (prints 42) +- `bx.mymk(worker, 40, 2)` (UFCS) → **SIGTRAP** (unresolved `$R`) + +This is distinct from RESOLVED issue 0119 (UFCS generic free-fn where +`$T` is inferred from the **receiver / a slice param**). Here the +receiver does NOT carry `$R`; `$R` comes only from the **closure +return type**, and the variadic `..$args` pack is also present. The +UFCS dispatch path infers `$R` differently from (and incorrectly vs) +the direct-call path: direct resolves `$R = i64`, UFCS leaves it +`.unresolved`. + +## Reproduction + +Standalone — depends on no project symbols beyond `modules/std.sx`: + +```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 }; + // Direct call — works (prints 42): + // g := mymk(bx, (a: i64, b: i64) -> i64 => a + b, 40, 2); + // UFCS dot-call — SIGTRAPs with "unresolved type reached LLVM emission": + g := bx.mymk((a: i64, b: i64) -> i64 => a + b, 40, 2); + print("value={}\n", g.value); + return 0; +} +``` + +Expected: the UFCS spelling resolves `$R = i64` identically to the +direct spelling and prints `value=42`. + +## Impact + +Blocks Stream B1 step B1.2's verified async idiom: the user-facing +call site is `context.io.async((a,b) -> R => ..., x, y)` — a UFCS +dot-call whose `$R` is inferred from the worker closure's return type +through the `..$args` pack. The async/await LIBRARY layer (Io protocol, +`Future($R)`, blocking impl, `async`/`async_void`/`await`/`cancel`) +and the compiler plumbing (Context.io field, both `__sx_default_context` +materializers, push-inherit) are all in place and correct — the +`async` body itself works when invoked with the receiver passed +explicitly (`async(context.io, worker, ..)`). Only the UFCS dot form +hits this inference gap. + +## Investigation prompt + +The UFCS-vs-direct generic inference divergence lives in the UFCS +rewrite + generic-binding path. issue 0119's fix routed +`inferGenericReturnType` through the same `buildTypeBindings` the +monomorphizer uses, but that fix established bindings from the +**receiver / structured params** (`[]$T`); it does NOT cover `$R` +inferred from a **closure argument's return type** combined with a +trailing **variadic pack** on the UFCS path. + +Suspect area: +- `src/ir/calls.zig` — UFCS dot-call resolution / receiver binding. +- `src/ir/lower/call.zig` — generic argument inference + (`buildTypeBindings` / the closure-arg → `$R` binding; the + variadic-pack `..$args` binding). +- Compare the binding map produced for the DIRECT call + `mymk(bx, worker, 40, 2)` (resolves `$R = i64`) against the one for + the UFCS call `bx.mymk(worker, 40, 2)` — the UFCS path must be + dropping/mis-indexing the closure-arg's return-type contribution to + `$R` (likely because the receiver is spliced in as arg 0 and the + closure/pack arg indices shift, so the closure-return → `$R` + inference rule no longer fires). + +The fix likely needs: when rewriting `recv.f(args)` → `f(recv, args)`, +run the SAME closure-return-type → generic-param inference (and the +same variadic-pack binding) the direct path runs, against the +spliced-receiver argument list — so `$R` binds from the closure's +return type regardless of dot vs direct spelling. + +Verification: run the repro above (`issues/0151-...sx`); expect +`value=42` (was SIGTRAP). Then restore example +`examples/1805-concurrency-io-blocking-async.sx` to the UFCS +`context.io.async(...)` form (see Stream B1 / CHECKPOINT-FIBERS) and +confirm it prints `double: 42` / `sum: 42` / `clock ok`. +``` diff --git a/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx b/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx new file mode 100644 index 00000000..c9f16be3 --- /dev/null +++ b/issues/0151-ufcs-closure-return-pack-generic-unresolved.sx @@ -0,0 +1,23 @@ +// 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; +}