Files
sx/issues/0156-comptime-pack-captured-into-closure.md
agra d3944570b9 lang: generic $R type-arg resolution + receiver-driven ufcs overload (issues 0156, 0157)
0156 Part 1: a single-type generic $R (parsed as comptime_pack_ref)
used as a type-arg in a pack-fn body (Box($R), size_of(Box($R))) hit a
missing arm in resolveTypeWithBindings -> .unresolved -> LLVM panic.
Fix: mirror resolveTypeArg's comptime_pack_ref arm (look up
type_bindings, else a loud diagnostic). Regression: examples/generics/0216.
(Part 2 -- deferred .. spread crashes -- reframed OPEN/non-blocking.)

0157: a user generic ufcs method whose name collides with a stdlib
re-export resolved via last-wins fn_ast_map with no receiver filtering,
so the wrong overload won, $R never bound, and .unresolved reached LLVM.
Fix: selectUfcsGenericByReceiver enumerates all module authors, keeps
the receiver-binding ones, picks the most receiver-specific (concrete >
bare $T), dedups re-exports, and flags a genuine tie as a deterministic
'ambiguous -- qualify' diagnostic. Regression: examples/generics/0217.
2026-06-21 18:43:49 +03:00

86 lines
4.5 KiB
Markdown

# 0156 — deferred `..` spread (pack captured into a closure / tuple spread) crashes the backend
> **Two bugs were conflated under this number.** Investigation split them:
>
> **Part 1 — `$R` (single-type generic) in a type-arg slot inside a pack-fn body
> → LLVM panic — ✅ FIXED.** The parser tags every `$name` expression as
> `comptime_pack_ref`, so a single-type binding (`$R` from `Closure(..$args) ->
> $R`) used as `Box($R)` / `size_of(Box($R))` reached `resolveTypeWithBindings`
> (the resolver `instantiateGenericStruct` runs each type-arg through) as a
> `comptime_pack_ref` it had no arm for → catch-all `else` → `.unresolved` →
> `src/backend/llvm/types.zig:196` panic. Fix: mirror `resolveTypeArg`'s
> `comptime_pack_ref` arm in `resolveTypeWithBindings` (`src/ir/lower.zig`) —
> look up `type_bindings`, else emit a loud "pack used where a single type is
> required" diagnostic (never a silent default type). Regression test:
> `examples/generics/0216-generics-typearg-in-pack-fn-body.sx` (`size_of(Box($R))`
> in a pack-fn → `r: 42`).
>
> **Part 2 — deferred `..` spread crashes — OPEN, NON-BLOCKING (below).**
## Part 2 — Symptom (OPEN)
A comptime variadic pack is **comptime state**, not a runtime value: a spread
`f(..args)` is expanded at the spread site from `pack_arg_nodes` (the original
call-site arg AST, referencing the *caller's* locals). Trying to make a `..`
spread cross a **deferred / value boundary** crashes instead of either working
or diagnosing:
- **pack captured into a closure** then spread later — `() => { ... worker(..args) ... }`
**SEGFAULTs at runtime** (the deferred body re-expands `args[i]` from the
spawner's locals, which are gone by the time the closure runs on another
stack), or panics in the backend when types don't resolve.
- **spreading a concrete TUPLE** — `t := .{40, 2}; w(..t)`**panics**
(`unresolved type reached LLVM emission`): `..` only accepts a comptime pack,
not a runtime aggregate, and the unsupported case degrades to `.unresolved`
rather than a diagnostic.
Expected: either (a) a `..` spread of a concrete tuple/array is a real feature
that lowers to N positional args, and capturing a pack into a closure
materializes it; or (b) both are rejected with a clean diagnostic at the spread
site. Never a segfault / `.unresolved`-reaches-backend.
## Reproduction (Part 2)
```sx
#import "modules/std.sx";
main :: () {
w := (a: i64, b: i64) -> i64 => a + b;
t := .{40, 2};
out : i64 = 0; po := @out;
captured :: () => { po.* = w(..t); }; // tuple spread inside a closure
captured();
print("out: {}\n", out); // panics: unresolved type reached LLVM emission
}
```
(Pack-into-closure variant — segfault: see the original repro shape in this
issue's history; `runner :: ufcs (io, worker: Closure(..$args)->i64, ..$args)`
with `captured :: () => { po.* = worker(..args); }` segfaults at runtime.)
## Why it is NON-BLOCKING for the fiber async work (B1.4a)
The fiber `async`/`await` layer does NOT need a `..` spread to cross the fiber
boundary. Deferred async is expressed as a **nullary thunk** that captures its
inputs at the call site (where they are live) — `async(io, work: Closure() ->
$R)`, used `context.io.async(() => a + b)`. The user's lambda captures `a`/`b`;
`async` spawns the already-bound nullary closure as a fiber. No pack crosses the
deferral. This is the idiomatic deferred-async shape (cf. `go func(){...}()`),
proven end-to-end (`.sx-tmp/pnullary.sx``log: 1 2 3 42 100`). So Part 2 is
filed for its own session, not a B1.4a blocker.
## Investigation prompt (Part 2)
Decide the intended semantics of `..` on a concrete value first (consult
`specs.md` §packs). If a `..` spread of a runtime tuple/array SHOULD lower to N
positional args: implement it in the pack-spread call lowering (`src/ir/lower/pack.zig`
`lowerPackElems` / the `.spread_expr` handling) for a concrete-aggregate operand
(emit a GEP+load per element), and make closure capture of a pack materialize
the pack's monomorphized element values into the env. If `..` is intentionally
comptime-pack-only: emit a diagnostic at the spread site when the operand is a
runtime value or a captured pack ("cannot spread a runtime value / a captured
pack; `..` applies to a comptime pack only"), and ensure the capture-analysis
pass rejects a `comptime_pack_ref` capture cleanly — never let `.unresolved`
reach the backend (the segfault path must become a diagnostic). Verify: the
Part-2 repro above either prints `out: 42` or emits one clean diagnostic — never
a segfault / panic.