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.
168 lines
7.7 KiB
Markdown
168 lines
7.7 KiB
Markdown
# 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:
|
|
```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`.
|
|
```
|