issue 0151: UFCS dot-call leaves $R inferred from a closure return type via a pack unresolved
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.
This commit is contained in:
109
issues/0151-ufcs-closure-return-pack-generic-unresolved.md
Normal file
109
issues/0151-ufcs-closure-return-pack-generic-unresolved.md
Normal file
@@ -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`.
|
||||
```
|
||||
23
issues/0151-ufcs-closure-return-pack-generic-unresolved.sx
Normal file
23
issues/0151-ufcs-closure-return-pack-generic-unresolved.sx
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user