Files
sx/issues/0151-ufcs-closure-return-pack-generic-unresolved.md
agra 362674f04d issue 0151 RESOLVED: infer generic $T through generic-struct / pointer / UFCS-pack params
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.
2026-06-21 05:25:39 +03:00

7.7 KiB

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, no UFCS / no pack / no closure involved:

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 typeworker: 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 → $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.