lang: generic struct head aliases bind the template (fix 0120) — alias-follow from each author's source in head selection; loud unknown-type on the .call type tail
BoxAlias :: Box; / Box :: r.Box; now resolve instantiation, methods,
annotations, and chains through the aliased template, and re-export one
flat-import level as ordinary own decls (the facade shape the std.sx
restructure needs). selectGenericStructHead consults aliasedStructTemplate
(nominal.zig) before the global template map — own-wins/single-flat alias
author, each hop pinned to the alias author's source, ns.X RHS through
namespaceAliasVerdictFrom, depth-capped. resolveTypeCallWithBindings'
silent .unresolved tail (panicked in LLVM emission) now diagnoses
"unknown type". Also aligns the stale pre-existing calls.test.zig UFCS
plan test with the opt-in model (a47ea14). Regression: examples/0211
(+rich/+facade). Gates: zig build test 426/426, suite 587/587.
This commit is contained in:
161
issues/0120-generic-struct-alias-head-unresolved-panic.md
Normal file
161
issues/0120-generic-struct-alias-head-unresolved-panic.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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(s64).{ ... }`) 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):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct ($T: Type) {
|
||||
item: T;
|
||||
}
|
||||
|
||||
BoxAlias :: Box;
|
||||
|
||||
main :: () {
|
||||
b := BoxAlias(s64).{ item = 3 };
|
||||
print("{}\n", b.item);
|
||||
}
|
||||
```
|
||||
|
||||
Method-call variant (front-end `unresolved 'get'`, same root):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct ($T: Type) {
|
||||
item: T;
|
||||
get :: (b: *Box(T)) -> T { b.item }
|
||||
}
|
||||
|
||||
BoxAlias :: Box;
|
||||
|
||||
main :: () {
|
||||
b := BoxAlias(s64).{ 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(s64).{ ... }`).
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
Generic structs live as TEMPLATES in
|
||||
`src/ir/program_index.zig` — `struct_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:357–382) 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(s64)`), nested generics
|
||||
(`List(BoxAlias(s64))` 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.
|
||||
Reference in New Issue
Block a user