A local 'error { ... }' set with the same name as an imported one collapsed
onto the import, losing its own tags, because registerErrorSetDecl deduped via
the flat findByName path while struct/enum/union use E6a per-decl identity.
Build the .error_set TypeInfo (new buildErrorSetInfo helper factored from
resolveInlineErrorSet) and intern via internNamedTypeDecl with shadowNominalId;
reserve a distinct shadow slot in scanDecls; consult per-decl type_decl_tids in
namedRefTid before findByName. The inline/anonymous findByName short-circuit is
preserved.
Regression: examples/1059-errors-same-name-error-set-own-wins.sx (moved from
issues/0134).
8.1 KiB
0134 — a same-name error set collapses into a namespaced import's set (error sets lack per-decl nominal identity)
RESOLVED. Error-set declarations now get the same per-decl nominal identity (E6a) as struct/enum/union.
registerErrorSetDeclbuilds the.error_setTypeInfo(via a newbuildErrorSetInfohelper factored out ofresolveInlineErrorSet) and interns it throughinternNamedTypeDeclwith ashadowNominalId; areserveShadowErrorSetSlotreserves a distinct slot inscanDecls, andnamedRefTid's.error_set_declarm consults the per-decltype_decl_tidsbefore falling back tofindByName— so a local set no longer collapses onto a same-name imported one. The inline/anonymousfindByNameshort-circuit is preserved. Regression test:examples/1059-errors-same-name-error-set-own-wins.sx.
Symptom
One-line: a top-level error { ... } whose NAME matches an error set
reachable through a (namespaced) import collapses into the imported
set at registration — losing its own tags — because error-set
declarations are NOT given per-decl nominal identity the way
struct / enum / union are (E6a). So a local set's tags become
"unknown".
- Observed:
error: error tag 'error.Boom' is not in error set 'EventErr'onraise error.Boom(and onr == error.Boom), whereEventErr :: error { Boom }is declared locally but#import "modules/std.sx"also carriesevent.EventErr(tagsInit/Register/Wait). The membership check sees the IMPORTED set, which has noBoom. - Expected: the local
EventErr { Boom }is its OWN type;Boomis a member; the program printsown EventErr.Boom, exit 0 — exactly as a uniquely-named local error set already does.
This is the declaration-side twin of issue 0132's class. The
reference-side is already visibility-aware: error_type_expr
(!EventErr) resolves its name through Lowering.resolveName →
resolveNominalLeaf (own-author-wins). But that fix is dormant for
error sets: because the local declaration never gets its own TypeId
(it collapses into the import's), there is only ONE EventErr in the
type table for the reference to find. Fixing THIS issue is what makes
the reference-side resolution observable.
Reproduction
Minimal, standalone (only modules/std.sx). The trigger is the name
EventErr colliding with std/event.sx's EventErr error set:
#import "modules/std.sx";
EventErr :: error { Boom } // collides with std/event.sx `EventErr { Init, Register, Wait }`
fail :: () -> !EventErr {
raise error.Boom; // Boom IS a member of the local set
}
main :: () -> i32 {
r := fail();
if r == error.Boom {
print("own EventErr.Boom\n");
return 0;
}
print("wrong set\n");
return 1;
}
Run: ./zig-out/bin/sx run issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.sx
Actual (today):
error: error tag 'error.Boom' is not in error set 'EventErr'
--> ...:NN:NN
|
| fail :: () -> !EventErr { raise error.Boom; }
| ^^^^^^^^^^
(and again on r == error.Boom). The fix should make it print
own EventErr.Boom, exit 0.
Decisive bisection (verified)
| Variant | Result |
|---|---|
Local EventErr (name collides with std/event.sx) |
FAILS — membership checked against the imported set |
Rename the local set MyErr :: error { Boom } (no collision) |
OK — prints own EventErr.Boom-equivalent |
So the trigger is purely the same-name collision; the local set's body
({ Boom }) is correct — it's simply never registered under its own
identity.
Root cause
Error sets are excluded from the per-decl nominal identity system (E6a) that struct / enum / union use:
Lowering.registerErrorSetDecl(src/ir/lower/nominal.zig) registers via the FLATtype_bridge.resolveAstType(node, …)→resolveInlineErrorSet(src/ir/type_bridge.zig), whose first line isif (table.findByName(name_id)) |existing| return existing;— so the SECOND author of a name (here the localEventErr, registered after the imported one) just gets the first author's TypeId. No distinct nominal slot, no own tags.- Contrast
registerEnumDecl/registerStructDecl/registerUnionDecl, which intern throughinternNamedTypeDecl(decl_key, name_id, info, nominal_id)withnominal_id = shadowNominalId(name_id)— each author gets a distinct TypeId. - The E6a shadow-reservation scan only enumerates struct / enum / union:
ShadowTypeDecl(src/ir/lower/nominal.zig) isunion(enum) { @"struct", @"enum", @"union" },topLevelTypeDeclmaps only those, and there isreserveShadow{Struct,Enum,Union}Slotbut no error-set equivalent. So a same-name error-set shadow is never reserved up-front. - The plumbing is half-there:
nominalIdOf/stampNominalIdalready handle the.error_setarm — registration just never sets a nominal id.
Investigation prompt
A top-level
error { ... }whose name collides with a same-name error set from a namespaced import collapses into the imported set, so its own tags are lost ("error tag 'X' is not in error set 'Name'"). Repro:issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.sx(expect it to FAIL today; the fix should make it printown EventErr.Boom, exit 0).Root cause: error sets are excluded from the per-decl nominal identity system (E6a).
Lowering.registerErrorSetDecl(src/ir/lower/nominal.zig) registers through the flattype_bridge.resolveAstType→resolveInlineErrorSet(src/ir/type_bridge.zig), which short-circuits onfindByName(name)and returns the first same-name author's TypeId — instead of interning under a per-decl nominal id likeregisterEnumDecldoes viainternNamedTypeDecl+shadowNominalId.Fix direction (mirror E6a for error sets):
- Add an
@"error_set"variant toShadowTypeDecl, an arm intopLevelTypeDecl, and areserveShadowErrorSetSlot(mirroringreserveShadowEnumSlot— reserve a.error_setplaceholder under the computedshadowNominalId).- Rewrite
registerErrorSetDeclto build the.error_setTypeInfo(intern the tag ids — factor the body out ofresolveInlineErrorSetif helpful, likebuildEnumInfo) and intern it viainternNamedTypeDecl(decl_key, name_id, info, nominal_id)withnominal_idfrom the reserved slot /shadowNominalId, instead of the flatresolveAstType.- The reference side is ALREADY visibility-aware (issue 0132's broader fix):
resolveErrorType(src/ir/type_bridge.zig) resolves a named set throughinner.resolveName, which for*LoweringisresolveNominalLeaf(own-wins). Once the declaration has its own TypeId, the named reference!EventErrwill resolve to it automatically — no further reference-side change needed.Per CLAUDE.md "Silent fallback defaults": don't paper over with a findByName default — give error-set declarations real per-decl identity so the wrong-author resolution stops at the source.
Verification: the repro prints
own EventErr.Boomexit 0; thenzig build && zig build testgreen. When resolved, promote the repro toexamples/10xx-errors-same-name-error-set-own-wins.sx(the example was drafted during the 0132 broader-latent work and removed because it could not pass until this lands).
Notes
- Membership-check diagnostic site (where the symptom surfaces, not the
root cause):
src/ir/lower/expr.zig("error tag '...' is not in error set '...'"). - Root-cause sites:
src/ir/lower/nominal.zigregisterErrorSetDecl(flat registration, no nominal id) + theShadowTypeDecl/topLevelTypeDecl/reserveShadow*Slotset (error sets excluded);src/ir/type_bridge.zigresolveInlineErrorSet(thefindByNameshort-circuit). - Related: issue 0132 (same class, reference + payload/field side, fixed
for struct/enum/union). This issue is the error-set declaration side;
the 0132 reference-side
error_type_exprfix stays in place and activates once this lands.