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.
7.6 KiB
0157 — UFCS generic method whose name collides with a stdlib re-export leaves $R unresolved → LLVM panic
RESOLVED. Root cause: a bare-ufcs call
(recv).name(args)resolvednamevia a single last-winsfn_ast_map[name]with NO receiver-type filtering — a usercancel :: ufcs (t: *Task($R))colliding with the stdlib re-exportcancel :: ufcs (f: *Future($R))picked the wrong one,$Rnever bound, and.unresolvedreached LLVM → panic. Fix (src/ir/lower/call.zig): for every generic-ufcs dispatch,selectUfcsGenericByReceiverenumerates ALL module authors of the name (program_index.module_decls— covers namespaced-imported modules, not just flat-visible ones), keeps those whose receiver binds all type-params, and picks the most receiver-SPECIFIC one (concrete*Task($R)over a bare(x: $T)), deduping re-exports by fd identity; two distinct equally-specific binders → a deterministic "ambiguous, qualify the call" diagnostic; none bind → "cannot infer" (never an.unresolvedinto codegen). Regression test:examples/generics/0217-generics-ufcs-method-name-collides-stdlib.sx(usercancel(*Box($R))vs stdlibcancel→ resolves to the user method).Residual (acceptable, no worse than pre-fix): when the receiver-matching author isn't in
module_decls(synthetic), the call falls back to the last-winsfd0if it binds. Determinism is guaranteed for all enumerable authors. The same missing-unbound-param guard also covers the qualified struct-method path (call.zig ~960) — left as-is (separate, not hit here).
Symptom
One-line: a user-defined generic UFCS method whose name collides with a
stdlib re-exported generic UFCS (cancel, re-exported by std.sx from
io.sx), called via UFCS on a different generic struct, leaves that struct's
type parameter $R unresolved; the .unresolved TypeId then reaches LLVM
emission and panics.
- Observed: compiler panic during codegen —
thread … panic: unresolved type reached LLVM emission — a type resolution failure was not diagnosed/abortedatsrc/backend/llvm/types.zig:196(.unresolved => @panic(...)), reached viafieldLLVMType→toLLVMTypeInfo→declareFunction. - Expected: the UFCS call resolves to the user's
cancel(receiver type*Box($R)≠ the stdlibcancel's*Future($R)),$Rbinds toi64, the program prints1. If the call were genuinely ambiguous/unresolvable, a diagnostic must be emitted (e.g. "cannot infer generic type parameter 'R'", which the non-UFCS formcancel(c)already produces) — never a raw codegen panic.
Reproduction
issues/0157-ufcs-generic-method-name-collides-stdlib-unresolved.sx:
#import "modules/std.sx";
Box :: struct ($R: Type) { value: R; flag: i64; }
// Name collides with std.sx's re-exported `cancel :: io_mod.cancel`
// (generic ufcs over `*Future($R)`).
cancel :: ufcs (b: *Box($R)) { b.flag = 1; }
main :: () -> i64 {
x : Box(i64) = ---; x.value = 7; x.flag = 0;
(@x).cancel(); // expected: prints 1
print("{}\n", x.flag); // actual: $R unresolved -> LLVM panic
return 0;
}
Run: ./zig-out/bin/sx run issues/0157-...sx → panics.
Isolation already done (the trigger is the NAME, nothing else)
Bisected from the B1.4a async-task work (a user cancel :: ufcs (t: *Task($R))
on std/sched.sx). All of these are IRRELEVANT to the crash — it reproduces or
not based solely on whether the method name collides with a std.sx re-export:
- Renaming
cancelto any name NOT exported bystd.sx(drop,m,zz_cancel99, …) → compiles & runs, prints1. This is the whole bug: same body, same struct, same call site — only the name differs. - Body is irrelevant:
{ b.flag = 1; }(ignores$R),{ b.value = b.value; }(touches$R), and-> $R { return b.value; }all crash under the namecancel. - Struct shape is irrelevant: single field
{ value: R; }and two fields{ value: R; flag: i64; }both crash; field order doesn't matter. - Construction is irrelevant: an explicit
x : Box(i64) = ---local crashes just as a heap*Box(i64)returned from another generic ufcs does. No closures / allocator / fibers needed. - The sibling stdlib name
wait(also re-exported bystd.sxfromio.sx, generic ufcs over*Future($R)returning$R) does NOT crash when user-redefined over*Box($R)— it resolves and runs. So only some colliding names trip it;cancel(a void-returning generic ufcs) does. - The non-UFCS spelling
cancel(c)instead ofc.cancel()produces a clean diagnostic —error: cannot infer generic type parameter 'R' for 'cancel' from this call's arguments— rather than the panic. So the UFCS path is silently skipping the inference-failure diagnostic the non-UFCS path emits, and falling through to codegen with$R=.unresolved.
std.sx re-exports the colliding name at line ~101:
cancel :: io_mod.cancel; (and io.sx:127 cancel :: ufcs (f: *Future($R))).
Investigation prompt (paste into a fresh session)
Fix issue 0157. A user-defined generic UFCS method whose name collides with a stdlib re-exported generic UFCS (
std.sxre-exportscancel :: io_mod.cancel, a genericufcs (f: *Future($R))fromio.sx) is mis-resolved when called via UFCS on a different generic struct. Repro:./zig-out/bin/sx run issues/0157-ufcs-generic-method-name-collides-stdlib-unresolved.sx→panic: unresolved type reached LLVM emissionatsrc/backend/llvm/types.zig:196. Renaming the user method to a non-colliding name makes it work, and the non-UFCS call form (cancel(c)) already emits the correct diagnosticcannot infer generic type parameter 'R' for 'cancel'.Suspected area: UFCS method/overload resolution + generic-arg inference (look in
src/ir/lower.zig/ the call-lowering + UFCS-candidate-selection path, and the generic-instantiation inference that binds$Rfrom the receiver argument — grep for where UFCS rewritesrecv.f(args)into the candidate set and where a generic callee's type params are inferred from actual arg types). The bug: when an overload set for the UFCS name contains BOTH the stdlibcancel(*Future($R))and the usercancel(*Box($R)), the resolver appears to bind$Ragainst the wrong candidate (or fails to bind it and proceeds anyway) for the receiver*Box(i64), leavingBox's$R=.unresolved. The fix likely needs to either (a) pick the candidate whose receiver type unifies with the actual receiver (*Box(i64)→ usercancel) BEFORE inferring type params, or (b) when inference fails for the chosen candidate, emit the SAME "cannot infer generic type parameter 'R'" diagnostic the non-UFCS path emits — never fall through to codegen with an.unresolvedfield type.Verification: the repro must now print
1(the usercancelruns) — OR, if the overload truly is meant to be ambiguous, must emit a clean diagnostic instead of the LLVM panic. Then move the repro into the feature suite per CLAUDE.md (examples/generics/...or wherever name-collision UFCS belongs) and re-runzig build test. Also re-enable the BLOCKED B1.4a work: the suspending fiber-task layer (go/wait/cancel) is already implemented inlibrary/modules/std/sched.sx; its exampleexamples/concurrency/1813-concurrency-fiber-async-suspend.sx(acancelUFCS over*Task($R)) is what surfaced this — once 0157 is fixed, seedexamples/concurrency/expected/1813-...exit, capture goldens with-Dname=...1813...sx -Dupdate-goldens, and verify the full suite.