Files
sx/issues/0120-generic-struct-alias-head-unresolved-panic.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

6.7 KiB
Raw Permalink Blame History

0120 — aliasing a GENERIC struct head: silent .unresolved, backend panic

RESOLVED (2026-06-11, same session — Agra-directed fix). Root cause: a const alias of a generic struct head was registered nowhere (type_alias_map holds TypeIds, struct_template_map only direct struct decls), and the head selector's miss fell through as .not_generic; the .call-node type resolver then returned .unresolved SILENTLY (its parameterized sibling diagnosed; it didn't). Fix, option 1 (support): selectGenericStructHead now follows const-alias decls (aliasedStructTemplate in src/ir/lower/nominal.zig) — own-wins / single-flat author, each hop resolved from the ALIAS AUTHOR's source (namespaceAliasVerdictFrom for ns.X RHS), depth-capped against cycles, checked BEFORE the template map so a facade's same-name re-export beats an invisible global template. Plus the missing diagnostic: an unknown .call type head now errors "unknown type 'X'" instead of silently poisoning (resolveTypeCallWithBindings). Alias-vs-alias flat collisions stay loud (not-visible diagnostic). Still unsupported, by scope: ns.AliasName(..) qualified heads (namespace member that is itself an alias). Regression test: examples/0211-generics-struct-alias-head.sx (+ -rich.sx / -facade.sx companions; pins same-file alias, method, chain, annotation, and the cross-module facade re-export). Gates: zig build test 426/426 (incl. fixing the PRE-EXISTING stale calls.test.zig UFCS plan test that predated 0119's opt-in model), suite 587/587.

Symptom

Alias :: Box; where Box is a generic struct (struct ($T: Type)) lowers without any diagnostic, and instantiating through the alias (Alias(i64).{ ... }) reaches LLVM emission with an .unresolved type — the backend tripwire panics:

panic: unresolved type reached LLVM emission — a type resolution
failure was not diagnosed/aborted
  src/backend/llvm/types.zig:175 toLLVMTypeInfo
  src/backend/llvm/ops.zig:1204 emitStructInit

Observed (one probe family, three manifestations of the same root):

  • field access through the aliased instantiation → backend panic (no front-end diagnostic at all);
  • method call through the aliased instantiation (b.get()) → misleading unresolved 'get' (the receiver's type never resolved);
  • cross-module re-export (facade.sx: Box :: r.Box;, consumer flat-imports facade) → consumer gets type 'Box' is not visible; #import the module that declares it even though the alias is the facade's OWN declaration.

Expected: one of the two, decided explicitly —

  1. Support it (desirable): a const decl whose RHS names a generic struct head (bare Box or qualified r.Box) binds the alias to the SAME template; instantiation, methods, and one-level flat-import carry behave exactly as the non-generic struct alias already does.
  2. Reject it loudly: a decl-site diagnostic ("cannot alias a generic struct head" or similar) at Alias :: Box;.

Silently lowering and panicking in the backend is neither — it is the REJECTED-PATTERNS "silent unresolved" shape.

For contrast, both of these alias re-exports already WORK across one flat-import hop (own-decl visibility): helper :: r.helper; (plain fn) and Thing :: r.Thing; (non-generic struct, including its static init). Only the generic head breaks. A fix must not regress these.

Reproduction

Backend panic (primary):

#import "modules/std.sx";

Box :: struct ($T: Type) {
    item: T;
}

BoxAlias :: Box;

main :: () {
    b := BoxAlias(i64).{ item = 3 };
    print("{}\n", b.item);
}

Method-call variant (front-end unresolved 'get', same root):

#import "modules/std.sx";

Box :: struct ($T: Type) {
    item: T;
    get :: (b: *Box(T)) -> T { b.item }
}

BoxAlias :: Box;

main :: () {
    b := BoxAlias(i64).{ item = 3 };
    print("{}\n", b.get());
}

Cross-module variant (rich.sx declares Box; facade.sx has r :: #import "rich.sx"; Box :: r.Box;; a consumer flat-importing facade.sx gets type 'Box' is not visible at Box(i64).{ ... }).

Investigation prompt

Generic structs live as TEMPLATES in src/ir/program_index.zigstruct_template_map (StringHashMap(StructTemplate), registered by registerStructDecl; a parallel struct_template_by_decl exists but isn't read for selection yet). Instantiation resolves the head name against that map in src/ir/lower/nominal.zig (see the qualified-head comments around nominal.zig:357382) and monomorphizes via lower_generic.instantiateGenericStruct (re-exported at src/ir/lower.zig:1820).

BoxAlias :: Box; is a const decl whose RHS identifier names a template, not a value or a concrete Type — const-decl lowering neither registers BoxAlias as a template alias nor rejects the decl. The instantiation head lookup for BoxAlias then misses, and the Name(args).{ ... } path continues with an .unresolved struct type instead of diagnosing the miss — that silent continuation is the bug underneath all three manifestations, and fixing it is step one regardless of the language decision: a struct_init whose head fails to resolve must produce a hard diagnostic, never reach emission.

Then the language decision (confirm with Agra if option 2 is ever preferred; the motivating use case wants option 1): when a const decl's RHS resolves to a generic struct head — bare identifier or ns.X through a namespace alias — register the alias name in the template registry bound to the same StructTemplate, scoped to the declaring module with ordinary own-decl visibility so one-level flat-import carry works (mirror whatever makes Thing :: r.Thing; re-export correctly today). Mind collision semantics (own-wins / ambiguity) and that the alias must also work as a plain type head in annotations (x: BoxAlias(i64)), nested generics (List(BoxAlias(i64)) if applicable), and method/UFCS dispatch on instantiations through the alias.

Motivating context: the std.sx-as-pure-re-exports restructure wants List :: list.List; in modules/std.sx (with list :: #import "modules/std/list.sx";) so List stays bare-visible to std.sx's flat importers. Plain fns and plain structs already re-export this way; generic heads are the missing piece.

Verification:

  1. Primary repro: prints 3, exit 0 (option 1) — or a clean decl-site diagnostic, no panic (option 2).
  2. Matrix: method-call variant runs (b.get() → 3); cross-module variant runs through the facade; helper :: r.helper; and Thing :: r.Thing; re-exports unchanged; two facades carrying the same alias name still diagnose ambiguity.
  3. bash tests/run_examples.sh — full suite ok, zero failures.
  4. Pin the repro as a regression example per CLAUDE.md.