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.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# 0132 — protocol method return/param type resolves to the WRONG same-name type (visibility-unaware registration)
|
||||
|
||||
> **ROOT CAUSE CORRECTED (2026-06-13).** The original write-up (kept in
|
||||
> "Original hypothesis" below) guessed this was about an inferred
|
||||
> protocol-return enum TypeId "not carrying payload struct field types".
|
||||
> That is **not** the cause. A ground-truth trace (instrumented build)
|
||||
> shows the real bug: **`registerProtocolDecl` resolves method
|
||||
> parameter/return type NAMES through a flat, visibility-UNAWARE lookup**
|
||||
> (`type_bridge.resolveAstType` → `resolveNamed` → global `findByName`),
|
||||
> so when the named type has a same-name shadow (another module also
|
||||
> declares that name), it picks the WRONG one. In the repro the user's
|
||||
> `Event` enum collides with the stdlib `library/modules/std/event.sx`
|
||||
> `Event :: struct` (pulled in by `#import "modules/std.sx"`, std.sx:101,
|
||||
> namespaced as `event`). The protocol grabs the stdlib struct; the
|
||||
> annotation path grabs the user enum. Hence inferred fails, annotated
|
||||
> works.
|
||||
|
||||
## Symptom
|
||||
|
||||
One-line: a protocol (dynamic-dispatch) method whose declared parameter
|
||||
or return type NAME also exists in another module resolves to the wrong
|
||||
type, because protocol signature registration is not visibility-aware.
|
||||
|
||||
- **Observed (repro):** `error: enum literal '.escape' has no destination
|
||||
type to resolve against` on `if e.key == .escape { ... }`, where `e` is
|
||||
the payload bound by `case .key_up: (e)` on a value whose type was
|
||||
inferred from a protocol method returning `Event`.
|
||||
- **Why that error:** the protocol method's cached `ret_type` is the
|
||||
stdlib `Event` **struct** (empty of the user's variants), not the
|
||||
user's `Event` **tagged_union**. So `ev := g_plat.one_event()` types
|
||||
`ev` as a plain struct; the `case .key_up:(e)` match finds no
|
||||
tagged-union variant, binds `e` to `.unresolved`; `e.key` on an
|
||||
`.unresolved` object silently returns a placeholder (the cascade guard
|
||||
in `lower.zig:emitFieldError` suppresses the field error on
|
||||
`.unresolved`); so `.escape` then has an `.unresolved` destination and
|
||||
emits the reported diagnostic.
|
||||
- **Expected:** bare `Event` inside the protocol resolves to the user's
|
||||
own `Event` (the visibility-correct author), exactly as an explicit
|
||||
`ev : Event = …` annotation already does. The repro then prints
|
||||
`escape!`, exit 0.
|
||||
|
||||
Surfaced building the downstream `m3te` app at `main.sx:222` —
|
||||
`for g_plat.poll_events() (*ev) { … case .key_up: (e) { if e.key == .escape … } }`,
|
||||
where `g_plat : Platform` is a `modules/platform/api.sx` protocol and
|
||||
`poll_events :: () -> []Event` returns `ui.Event`. m3te imports `std`
|
||||
(which carries the namespaced `event.Event` struct) AND has its own
|
||||
`ui.Event`, so the protocol's flat lookup picks the wrong `Event` — the
|
||||
same collision as the minimal repro.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Minimal, standalone (only depends on `modules/std.sx`). The trigger is
|
||||
the type NAME `Event` colliding with `std/event.sx`'s `Event` struct:
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Keycode :: enum { unknown; escape; enter; }
|
||||
KeyData :: struct { key: Keycode; }
|
||||
Event :: enum { none; key_up: KeyData; } // <-- name collides with std/event.sx `Event :: struct`
|
||||
|
||||
Plat :: protocol { one_event :: () -> Event; }
|
||||
|
||||
Impl :: struct { dummy: i64; }
|
||||
impl Plat for Impl {
|
||||
one_event :: (self: *Impl) -> Event { return .key_up(.{ key = .escape }); }
|
||||
}
|
||||
|
||||
main :: () {
|
||||
impl : Impl = .{ dummy = 0 };
|
||||
g_plat : Plat = xx @impl;
|
||||
ev := g_plat.one_event(); // type INFERRED from protocol return
|
||||
if ev == {
|
||||
case .key_up: (e) {
|
||||
if e.key == .escape { print("escape!\n"); } // <-- errors here
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run: `./zig-out/bin/sx run issues/0132-protocol-return-enum-case-payload-field-unresolved.sx`
|
||||
|
||||
Actual:
|
||||
```
|
||||
error: enum literal '.escape' has no destination type to resolve against
|
||||
--> ...:NN:NN
|
||||
|
|
||||
| if e.key == .escape { print("escape!\n"); }
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
### Decisive bisection (verified)
|
||||
|
||||
| Variant | Result | Why |
|
||||
|---|---|---|
|
||||
| Repro as above (name `Event`, inferred) | **FAILS** | protocol flat-resolves `Event` → stdlib `event.Event` struct (104) |
|
||||
| Rename the user type `Event` → `Evt` everywhere | **OK** (`escape!`) | no same-name shadow → flat lookup gets the only `Evt` |
|
||||
| Keep `Event` but annotate `ev : Event = g_plat.one_event()` | **OK** | annotation uses the visibility-aware `resolveNominalLeaf` → user enum (152) |
|
||||
| Concrete fn (non-protocol) returns `Event`, same body | **OK** | concrete fn signatures already resolve via `self.resolveType` (visibility-aware) |
|
||||
| Protocol returns a plain struct / a plain enum named `Event` | varies | same root cause: flat lookup picks the colliding author |
|
||||
|
||||
Ground-truth TypeIds (from an instrumented build): the protocol method's
|
||||
`ret_type` = **104** (`tag=struct name=Event`, the stdlib placeholder);
|
||||
the annotation resolves `Event` = **152** (`tag=tagged_union name=Event`,
|
||||
the user type with the `key_up → KeyData` payload). Two distinct authors
|
||||
of the name `Event`; the flat path picks 104, the visibility-aware path
|
||||
picks 152.
|
||||
|
||||
## Fix
|
||||
|
||||
Make protocol method signature registration visibility-aware, mirroring
|
||||
what concrete functions and `registerStructDecl` already do.
|
||||
|
||||
In `src/ir/protocols.zig` `registerProtocolDecl` (~lines 289–316), pin
|
||||
the visibility context to the protocol's declaring module and resolve
|
||||
through the source-aware helpers instead of the flat resolver:
|
||||
|
||||
- param types: `self.l.resolveParamTypeInSource(pd.source_file, p)`
|
||||
(keep the `Self → *void` special-case)
|
||||
- return type: `self.l.resolveTypeInSource(pd.source_file, rt)`
|
||||
(keep the `Self → *void` special-case)
|
||||
|
||||
Both helpers already exist (`src/ir/lower.zig:670` / `:684`) and are the
|
||||
exact tool for "resolve a type in its DEFINING module's visibility
|
||||
context". `ProtocolDecl.source_file` is already stamped by
|
||||
`resolveImports` for this purpose (`src/ast.zig:817`). The
|
||||
**parameterized**-protocol path (`instantiateParamProtocol`,
|
||||
`src/ir/lower/protocol.zig:119`) ALREADY does exactly this (pins
|
||||
`current_source_file = pd.source_file` and resolves via
|
||||
`resolveTypeWithBindings`); this change brings the NON-parameterized path
|
||||
to parity.
|
||||
|
||||
No silent default is introduced: the visibility-aware path emits real
|
||||
diagnostics for genuinely not-visible / ambiguous names and poisons with
|
||||
`.unresolved` (per CLAUDE.md "Silent fallback defaults" rules).
|
||||
|
||||
## Broader latent risk (same class — track separately)
|
||||
|
||||
The same visibility-unaware flat resolution at REGISTRATION time also
|
||||
affects **enum payloads** and **union field types** (CONFIRMED failing),
|
||||
because `registerEnumDecl` / `registerUnionDecl` build their bodies via
|
||||
the stateless `type_bridge.buildEnumInfo` / `buildUnionInfo`, which
|
||||
flat-resolve type names. Repro shape (confirmed):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Event :: struct { code: i64; } // collides with std/event.sx Event
|
||||
Wrap :: enum { none; got: Event; } // payload type Event → flat-resolves to the WRONG Event
|
||||
main :: () {
|
||||
w : Wrap = .{}; w = .got(.{ code = 7 });
|
||||
if w == { case .got: (e) { print("{}\n", e.code); } } // error: field 'code' not found on type 'Event'
|
||||
}
|
||||
```
|
||||
|
||||
(Structs are already SAFE — `registerStructDecl` resolves fields via the
|
||||
visibility-aware `self.resolveType`, `src/ir/lower/nominal.zig:637`.)
|
||||
|
||||
Suggested broader fix: inject a resolver into `buildEnumInfo` /
|
||||
`buildUnionInfo` (an `anytype` adapter with a `resolve(node) → TypeId`
|
||||
method) so the stateless inline callers keep the flat resolver while the
|
||||
stateful `registerEnumDecl` / `registerUnionDecl` pass a
|
||||
`self.resolveType`-backed (visibility-aware) one — single source of truth
|
||||
for the body shape, two resolution strategies. Also switch the struct-
|
||||
constant annotation resolve (`src/ir/lower/nominal.zig:706`) to
|
||||
`self.resolveType`. See the session notes for the full design.
|
||||
|
||||
## Verification
|
||||
|
||||
`./zig-out/bin/sx run issues/0132-…sx` prints `escape!` exit 0; then
|
||||
`zig build && zig build test` and `bash tests/run_examples.sh` all green.
|
||||
When resolved, promote the repro to
|
||||
`examples/04xx-protocols-protocol-return-name-collision.sx` per the
|
||||
"Resolving an open issue" procedure.
|
||||
|
||||
## Notes
|
||||
|
||||
- Diagnostic site (where the symptom surfaces, NOT the root cause):
|
||||
`src/ir/lower/expr.zig:920` (`lowerEnumLiteral`, `target == .unresolved`
|
||||
branch).
|
||||
- Root cause site: `src/ir/protocols.zig:299,309`
|
||||
(`registerProtocolDecl`, flat `type_bridge.resolveAstType` for
|
||||
param/return types).
|
||||
- The minimal repro previously used a legacy `Impl_methods :: { … }`
|
||||
block; that compiles but crashes at runtime independently. The repro
|
||||
here uses the canonical `impl Plat for Impl { … }` so that, post-fix,
|
||||
it actually runs and prints `escape!`.
|
||||
- Workaround in downstream code (annotate the binding, or rename the
|
||||
type to avoid the std collision) is NOT applied in m3te per the
|
||||
IMPASSABLE RULES — the fix belongs in the compiler.
|
||||
|
||||
---
|
||||
|
||||
## Original hypothesis (SUPERSEDED — kept for provenance)
|
||||
|
||||
The first write-up framed this as: "when an enum value's type is inferred
|
||||
from a protocol method's declared return, a `case`-payload binding loses
|
||||
its struct-field types", and pointed the fix at the call-result TypeId in
|
||||
`src/ir/calls.zig` / `src/ir/conversions.zig` "not carrying the variant
|
||||
payload struct's field types". The instrumented trace disproved this: the
|
||||
inferred and annotated `Event` are two DIFFERENT registered types (a
|
||||
same-name shadow), and the divergence is purely that protocol signature
|
||||
registration uses a flat, visibility-unaware lookup. The payload-field
|
||||
machinery is fine once the correct `Event` reaches the binding.
|
||||
@@ -0,0 +1,44 @@
|
||||
// issue 0132 — protocol method return/param type resolves to the WRONG
|
||||
// same-name type (visibility-unaware registration).
|
||||
//
|
||||
// ROOT CAUSE (corrected — see the .md): `registerProtocolDecl` resolves
|
||||
// the method return type `Event` through a flat, visibility-UNAWARE lookup
|
||||
// (type_bridge.resolveAstType → findByName). The user's `Event` enum
|
||||
// collides by NAME with the stdlib `std/event.sx` `Event :: struct`
|
||||
// (pulled in by `#import "modules/std.sx"`, namespaced as `event`). The
|
||||
// flat lookup picks the stdlib struct, so `ev := g_plat.one_event()` is
|
||||
// typed as a fieldless struct; the `case .key_up:(e)` payload then binds
|
||||
// `.unresolved`, and `.escape` has no destination type.
|
||||
//
|
||||
// EXPECT (today): build FAILS —
|
||||
// error: enum literal '.escape' has no destination type to resolve against
|
||||
// EXPECT (after fix): prints `escape!`, exit 0.
|
||||
//
|
||||
// Proof it's a name collision: rename `Event` -> `Evt` everywhere and the
|
||||
// inferred form compiles and prints `escape!`. Annotating
|
||||
// `ev : Event = g_plat.one_event();` also sidesteps it (the annotation
|
||||
// path is visibility-aware). See the .md for the full bisection.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Keycode :: enum { unknown; escape; enter; }
|
||||
KeyData :: struct { key: Keycode; }
|
||||
Event :: enum { none; key_up: KeyData; }
|
||||
|
||||
Plat :: protocol { one_event :: () -> Event; }
|
||||
|
||||
Impl :: struct { dummy: i64; }
|
||||
impl Plat for Impl {
|
||||
one_event :: (self: *Impl) -> Event { return .key_up(.{ key = .escape }); }
|
||||
}
|
||||
|
||||
main :: () {
|
||||
impl : Impl = .{ dummy = 0 };
|
||||
g_plat : Plat = xx @impl;
|
||||
ev := g_plat.one_event(); // type INFERRED from protocol return
|
||||
if ev == {
|
||||
case .key_up: (e) {
|
||||
if e.key == .escape { print("escape!\n"); } // <-- errors here
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# 0133 — assigning a struct LITERAL to a union member panics ("unresolved type reached LLVM emission")
|
||||
|
||||
## Symptom
|
||||
|
||||
One-line: `u.b = .{ ... }` where `b` is a NAMED-struct member of a plain
|
||||
`union` compiles to an `.unresolved`-typed `struct_init` and trips the
|
||||
LLVM-emission tripwire. The RHS struct literal never receives its target
|
||||
type (the union member's type), so it lowers as `.unresolved`.
|
||||
|
||||
- **Observed:** `thread … panic: unresolved type reached LLVM emission —
|
||||
a type resolution failure was not diagnosed/aborted`
|
||||
(`src/backend/llvm/types.zig:176`), reached from
|
||||
`emitStructInit` (`src/backend/llvm/ops.zig:1211`) because the
|
||||
`struct_init` instruction's `ty` is `.unresolved`.
|
||||
- **Expected:** the literal types itself as the union member's struct type
|
||||
(here `S`) and stores into the member — exactly as it already does when
|
||||
the left-hand side is a STRUCT field.
|
||||
|
||||
This is PRE-EXISTING (reproduces on `master` / before any issue-0132
|
||||
work) and ORTHOGONAL to type-name resolution: it reproduces with a
|
||||
unique, non-colliding type name. Surfaced while testing issue 0132's
|
||||
broader-latent fix (making enum/union payload registration
|
||||
visibility-aware) — that fix makes a *colliding*-name union member
|
||||
resolve to the correct type, at which point this separate codegen bug is
|
||||
what blocks the end-to-end union case.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Minimal, standalone (only `modules/std.sx`):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
S :: struct { code: i64; }
|
||||
U :: union { a: i64; b: S; }
|
||||
|
||||
main :: () {
|
||||
u : U = ---;
|
||||
u.b = .{ code = 9 }; // <-- panics: struct literal has no target type
|
||||
print("code={}\n", u.b.code);
|
||||
}
|
||||
```
|
||||
|
||||
Run: `./zig-out/bin/sx run issues/0133-union-member-struct-literal-assign-unresolved-panic.sx`
|
||||
→ panics today; the fix should make it print `code=9`, exit 0.
|
||||
|
||||
### Bisection (what does / does not trigger it)
|
||||
|
||||
| Variant | Result |
|
||||
|---|---|
|
||||
| `u.b = .{ code = 9 }` (union member ← struct LITERAL) | **PANICS** |
|
||||
| `o.b = .{ code = 9 }` where `o : Outer = struct { a; b: S }` (STRUCT member ← literal) | **OK** |
|
||||
| `s : S = .{ code = 9 }; u.b = s` (union member ← pre-made value) | **OK** |
|
||||
| `u : U = ---` then only read (no literal assign) | **OK** |
|
||||
|
||||
So the trigger is exactly the conjunction **(LHS is a union member) AND
|
||||
(RHS is a struct literal)**. A struct-field LHS propagates the target
|
||||
type to the literal; a pre-made value needs no target type. Only the
|
||||
union-member-lvalue + literal-RHS combination drops it.
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
> Assigning a struct literal to a NAMED-struct member of a plain `union`
|
||||
> panics with "unresolved type reached LLVM emission". Repro:
|
||||
> `issues/0133-union-member-struct-literal-assign-unresolved-panic.sx`
|
||||
> (expect a panic today; the fix should make it print `code=9`, exit 0).
|
||||
>
|
||||
> The `struct_init` instruction for the RHS literal `.{ code = 9 }` has
|
||||
> `ty == .unresolved` — the literal was lowered without a target type, so
|
||||
> it could not resolve to the union member's struct type `S`. The panic
|
||||
> is the codegen tripwire in `src/backend/llvm/types.zig:176`
|
||||
> (`toLLVMTypeInfo`), reached from `emitStructInit`
|
||||
> (`src/backend/llvm/ops.zig:1211`).
|
||||
>
|
||||
> Root area: assignment lowering in `src/ir/lower.zig` —
|
||||
> `lowerAssignment`'s `.field_access` target path. Issue 0094 already
|
||||
> routes the lvalue POINTER through the shared `fieldLvaluePtr` (which
|
||||
> correctly resolves union/tagged-union direct members — that's why a
|
||||
> pre-made value stores fine). The gap is the RHS TARGET TYPE: for a
|
||||
> STRUCT-field LHS the code sets `self.target_type` to the field's type
|
||||
> before lowering the RHS (so a struct literal types itself), but for a
|
||||
> UNION-member LHS that target-type propagation is missing, so the
|
||||
> literal lowers under a null/unresolved target → `struct_init.ty ==
|
||||
> .unresolved`.
|
||||
>
|
||||
> Suspected fix: before lowering the RHS expression in
|
||||
> `lowerAssignment`'s field-access path, compute the LHS member's type
|
||||
> for union / tagged-union members too (reuse the same member-type lookup
|
||||
> `fieldLvaluePtr` already performs — ideally have it RETURN the resolved
|
||||
> field type, or factor a `fieldLvalueType` helper, so the lvalue-pointer
|
||||
> path and the target-type path cannot diverge — the two-resolver defect
|
||||
> class this codebase keeps burning on) and set `self.target_type` to it
|
||||
> for the RHS lowering. Do NOT paper over with an `.unresolved`→default;
|
||||
> per CLAUDE.md, resolve the real member type or emit a diagnostic.
|
||||
>
|
||||
> Verification: the repro prints `code=9` exit 0; then `zig build &&
|
||||
> zig build test` green. Add positive coverage (a union member written
|
||||
> via struct literal, then read back) — extend
|
||||
> `examples/0166-types-union-promoted-member-lvalue.sx` or add a new
|
||||
> `examples/01xx-types-union-member-struct-literal-assign.sx`. When
|
||||
> resolved, also note in issue 0132 that the broader-latent union case is
|
||||
> now demonstrable end-to-end.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tripwire site (symptom): `src/backend/llvm/types.zig:176`
|
||||
(`toLLVMTypeInfo`, `.unresolved` arm) via `emitStructInit`
|
||||
(`src/backend/llvm/ops.zig:1211`).
|
||||
- Root area (cause): `Lowering.lowerAssignment` `.field_access` target
|
||||
path in `src/ir/lower.zig` — RHS target-type not set for union/
|
||||
tagged-union members.
|
||||
- Related but distinct: issue 0094 (RESOLVED) fixed the lvalue-POINTER
|
||||
field resolution (missing-field panic + `.i64`/field-0 defaults). This
|
||||
issue is the RHS-literal TARGET-TYPE path, which 0094 did not touch.
|
||||
@@ -0,0 +1,23 @@
|
||||
// issue 0133 — assigning a struct LITERAL to a union member panics
|
||||
// ("unresolved type reached LLVM emission").
|
||||
//
|
||||
// `u.b = .{ code = 9 }` where `b` is a named-struct member of a plain
|
||||
// `union`: the RHS struct literal never receives its target type (the
|
||||
// member's type `S`), so it lowers as `.unresolved` and trips the LLVM
|
||||
// tripwire in emitStructInit. A STRUCT-field LHS propagates the target
|
||||
// type fine; a pre-made value needs none — only the union-member-lvalue
|
||||
// + struct-literal-RHS combination drops it. PRE-EXISTING, orthogonal to
|
||||
// name resolution (reproduces with this unique, non-colliding name).
|
||||
//
|
||||
// EXPECT (today): panic. EXPECT (after fix): prints `code=9`, exit 0.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
S :: struct { code: i64; }
|
||||
U :: union { a: i64; b: S; }
|
||||
|
||||
main :: () {
|
||||
u : U = ---;
|
||||
u.b = .{ code = 9 };
|
||||
print("code={}\n", u.b.code);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,39 @@
|
||||
// issue 0134 — a same-name `error` set collapses into a namespaced import's
|
||||
// set (error sets lack per-decl nominal identity).
|
||||
//
|
||||
// `EventErr` is declared locally as `error { Boom }`, but
|
||||
// `#import "modules/std.sx"` also carries `event.EventErr` (an error set with
|
||||
// tags Init/Register/Wait). Because error-set DECLARATIONS are not given
|
||||
// per-decl nominal identity (unlike struct/enum/union under E6a) —
|
||||
// `registerErrorSetDecl` registers via the flat `findByName`-dedup path — the
|
||||
// local `EventErr` collapses into the imported one, losing its own `Boom` tag.
|
||||
//
|
||||
// So `raise error.Boom` / `r == error.Boom` are checked against the IMPORTED
|
||||
// set, which has no `Boom`.
|
||||
//
|
||||
// EXPECT (today): build FAILS —
|
||||
// error: error tag 'error.Boom' is not in error set 'EventErr'
|
||||
// EXPECT (after fix): prints `own EventErr.Boom`, exit 0.
|
||||
//
|
||||
// Proof it's the collision: rename `EventErr` -> `MyErr` and it compiles and
|
||||
// prints. The reference side (`!EventErr` → resolveNominalLeaf) is already
|
||||
// visibility-aware from issue 0132's broader fix, but it is dormant until the
|
||||
// local declaration gets its own TypeId. See the .md.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
EventErr :: error { Boom }
|
||||
|
||||
fail :: () -> !EventErr {
|
||||
raise error.Boom;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
r := fail();
|
||||
if r == error.Boom {
|
||||
print("own EventErr.Boom\n");
|
||||
return 0;
|
||||
}
|
||||
print("wrong set\n");
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user