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.
4.6 KiB
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:
#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 →$Rbinding; the variadic-pack..$argsbinding).- Compare the binding map produced for the DIRECT call
mymk(bx, worker, 40, 2)(resolves$R = i64) against the one for the UFCS callbx.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 →$Rinference 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.