- 0132: rewrite to the verified root cause -- protocol method signature
registration resolves type names via flat findByName and picks the wrong
same-name author. Original payload-field hypothesis kept as superseded;
repro switched to canonical `impl ... for` syntax. Still open (the
protocol path is unchanged).
- 0133: assigning a struct literal to a union member panics ("unresolved
type reached LLVM emission"); pre-existing, surfaced while testing.
- 0134: a same-name `error` set collapses into a namespaced import's set --
error-set declarations lack per-decl nominal identity (E6a gap); this is
what keeps the 0132-class error-ref resolution dormant.
7.3 KiB
0134 — a same-name error set collapses into a namespaced import's set (error sets lack per-decl nominal identity)
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.