Files
sx/issues/0151-ufcs-closure-return-pack-generic-unresolved.md
agra 0ab26c8a40 fibers B1.2: record review findings — async surface blocked on 0151 (widened)
Adversarial review of 45d869d: the Io infrastructure (both materializers,
push-inherit, 37 .ir regens, !-lint) is correct + landed; but await/cancel
(*Future($R)) are uncallable in EVERY form because sx can't infer a generic
$T from a pointer-wrapped arg. Widened issue 0151 to that root (repro:
unbox(b: *Box($T)) -> $T). Checkpoint: B1.2 partially landed; next = fix 0151
generic inference -> make await/cancel callable -> add 1805/1806 -> B1.3.
2026-06-21 00:43:09 +03:00

126 lines
5.5 KiB
Markdown

# 0151 — generic type-var not inferred through a pointer / via UFCS (LLVM SIGTRAP / "cannot infer")
## 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,
no UFCS / no pack / no closure involved:
```sx
Box :: struct ($T: Type) { v: T; }
unbox :: (b: *Box($T)) -> $T { return b.v; }
// unbox(@b) → error: cannot infer generic type parameter 'T' for 'unbox'
```
This blocks `await`/`cancel` in `library/modules/std/io.sx` (both take `*Future($R)`) —
they are **uncallable in EVERY form** (explicit `await(@f)` → "cannot infer 'R'"; UFCS
`f.await()` → SIGTRAP). So B1.2's async layer can CREATE a Future (`async(...)` works) but
cannot AWAIT it. Fix scope is the generic-inference engine (infer `$T` from `*T`-wrapped
params, and from closure-return-via-pack, and through UFCS dot-dispatch) — not the Io lib.
The two symptoms below + the `*Box($T)` repro above are the acceptance cases.
## 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`.
```