Files
sx/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md
agra 333f57026c fix: give error-set decls per-decl nominal identity (issue 0134)
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).
2026-06-21 09:11:06 +03:00

177 lines
8.1 KiB
Markdown

# 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. `registerErrorSetDecl` builds the
> `.error_set` `TypeInfo` (via a new `buildErrorSetInfo` helper factored out of
> `resolveInlineErrorSet`) and interns it through `internNamedTypeDecl` with a
> `shadowNominalId`; a `reserveShadowErrorSetSlot` reserves a distinct slot in
> `scanDecls`, and `namedRefTid`'s `.error_set_decl` arm consults the per-decl
> `type_decl_tids` before falling back to `findByName` — so a local set no
> longer collapses onto a same-name imported one. The inline/anonymous
> `findByName` short-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'` on `raise error.Boom` (and on `r == error.Boom`), where
`EventErr :: error { Boom }` is declared locally but
`#import "modules/std.sx"` also carries `event.EventErr`
(tags `Init` / `Register` / `Wait`). The membership check sees the
IMPORTED set, which has no `Boom`.
- **Expected:** the local `EventErr { Boom }` is its OWN type; `Boom` is
a member; the program prints `own 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:
```sx
#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 FLAT `type_bridge.resolveAstType(node, …)` →
`resolveInlineErrorSet` (`src/ir/type_bridge.zig`), whose first line is
`if (table.findByName(name_id)) |existing| return existing;` — so the
SECOND author of a name (here the local `EventErr`, 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 through `internNamedTypeDecl(decl_key, name_id, info,
nominal_id)` with `nominal_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`) is
`union(enum) { @"struct", @"enum", @"union" }`, `topLevelTypeDecl`
maps only those, and there is `reserveShadow{Struct,Enum,Union}Slot`
but no error-set equivalent. So a same-name error-set shadow is never
reserved up-front.
- The plumbing is half-there: `nominalIdOf` / `stampNominalId` already
handle the `.error_set` arm — 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 print
> `own 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 flat
> `type_bridge.resolveAstType` → `resolveInlineErrorSet`
> (`src/ir/type_bridge.zig`), which short-circuits on
> `findByName(name)` and returns the first same-name author's TypeId —
> instead of interning under a per-decl nominal id like
> `registerEnumDecl` does via `internNamedTypeDecl` +
> `shadowNominalId`.
>
> Fix direction (mirror E6a for error sets):
> 1. Add an `@"error_set"` variant to `ShadowTypeDecl`, an arm in
> `topLevelTypeDecl`, and a `reserveShadowErrorSetSlot` (mirroring
> `reserveShadowEnumSlot` — reserve a `.error_set` placeholder under
> the computed `shadowNominalId`).
> 2. Rewrite `registerErrorSetDecl` to build the `.error_set` `TypeInfo`
> (intern the tag ids — factor the body out of `resolveInlineErrorSet`
> if helpful, like `buildEnumInfo`) and intern it via
> `internNamedTypeDecl(decl_key, name_id, info, nominal_id)` with
> `nominal_id` from the reserved slot / `shadowNominalId`, instead of
> the flat `resolveAstType`.
> 3. The reference side is ALREADY visibility-aware (issue 0132's broader
> fix): `resolveErrorType` (`src/ir/type_bridge.zig`) resolves a named
> set through `inner.resolveName`, which for `*Lowering` is
> `resolveNominalLeaf` (own-wins). Once the declaration has its own
> TypeId, the named reference `!EventErr` will 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.Boom` exit 0; then
> `zig build && zig build test` green. When resolved, promote the repro
> to `examples/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.zig` `registerErrorSetDecl`
(flat registration, no nominal id) + the `ShadowTypeDecl` /
`topLevelTypeDecl` / `reserveShadow*Slot` set (error sets excluded);
`src/ir/type_bridge.zig` `resolveInlineErrorSet` (the `findByName`
short-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_expr` fix stays in place and
activates once this lands.