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.
4.5 KiB
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$nameexpression ascomptime_pack_ref, so a single-type binding ($RfromClosure(..$args) -> $R) used asBox($R)/size_of(Box($R))reachedresolveTypeWithBindings(the resolverinstantiateGenericStructruns each type-arg through) as acomptime_pack_refit had no arm for → catch-allelse→.unresolved→src/backend/llvm/types.zig:196panic. Fix: mirrorresolveTypeArg'scomptime_pack_refarm inresolveTypeWithBindings(src/ir/lower.zig) — look uptype_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-expandsargs[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.unresolvedrather 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)
#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.