- 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.
166 lines
7.3 KiB
Markdown
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.
|