Files
sx/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md
agra 45befed698 docs(issues): correct 0132 root cause; file 0133 and 0134
- 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.
2026-06-13 13:41:30 +03:00

166 lines
7.3 KiB
Markdown

# 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'` 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.