diff --git a/examples/expected/1059-errors-same-name-error-set-own-wins.exit b/examples/expected/1059-errors-same-name-error-set-own-wins.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1059-errors-same-name-error-set-own-wins.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1059-errors-same-name-error-set-own-wins.stderr b/examples/expected/1059-errors-same-name-error-set-own-wins.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1059-errors-same-name-error-set-own-wins.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1059-errors-same-name-error-set-own-wins.stdout b/examples/expected/1059-errors-same-name-error-set-own-wins.stdout new file mode 100644 index 00000000..e80bfbe7 --- /dev/null +++ b/examples/expected/1059-errors-same-name-error-set-own-wins.stdout @@ -0,0 +1 @@ +own EventErr.Boom diff --git a/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md b/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md index 7e3fa1a0..b43e30f6 100644 --- a/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md +++ b/issues/0134-error-set-no-per-decl-nominal-identity-same-name-collapse.md @@ -1,5 +1,16 @@ # 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 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f75f6630..1a20cece 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1745,6 +1745,7 @@ pub const Lowering = struct { pub const reserveShadowStructSlot = lower_nominal.reserveShadowStructSlot; pub const reserveShadowEnumSlot = lower_nominal.reserveShadowEnumSlot; pub const reserveShadowUnionSlot = lower_nominal.reserveShadowUnionSlot; + pub const reserveShadowErrorSetSlot = lower_nominal.reserveShadowErrorSetSlot; pub const topLevelTypeDecl = lower_nominal.topLevelTypeDecl; pub const reserveShadowSlot = lower_nominal.reserveShadowSlot; pub const internNamedTypeDecl = lower_nominal.internNamedTypeDecl; diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index dc4943dd..9cf2c9be 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -1939,7 +1939,13 @@ pub fn namedRefTid(self: *Lowering, ref: resolver_mod.RawDeclRef, name: []const .struct_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), .enum_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), .union_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), - .error_set_decl, .protocol_decl, .runtime_class_decl => table.findByName(table.internString(name)), + // Error sets now carry per-decl nominal identity (issue 0134), so prefer + // the own author's reserved TypeId over the name-keyed first-author + // `findByName` — mirroring the struct/enum/union arms above. A set that + // was not decl-registered (no `type_decl_tids` entry) falls back to the + // name lookup, byte-identical to pre-0134. + .error_set_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), + .protocol_decl, .runtime_class_decl => table.findByName(table.internString(name)), .fn_decl, .const_decl, .var_decl, .namespace_decl => null, }; } diff --git a/src/ir/lower/nominal.zig b/src/ir/lower/nominal.zig index d527c84e..7253bb14 100644 --- a/src/ir/lower/nominal.zig +++ b/src/ir/lower/nominal.zig @@ -31,7 +31,20 @@ pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { } return; } - _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + // Per-decl nominal identity (E6a) — the error-set twin of `registerEnumDecl`. + // A GENUINE same-name shadow already reserved its DISTINCT slot up-front in + // `scanDecls` (the first at id 0, the rest at nonzero ids): reuse that id. A + // single-author name keeps id 0 and the legacy registration. The body is built + // by the shared `type_bridge.buildErrorSetInfo`; `internNamedTypeDecl` interns + // it under the computed nominal id and records `decl_key → TypeId` so a local + // `Foo :: error { Boom }` no longer collapses onto a same-name imported set + // (issue 0134). + const table = &self.module.types; + const name_id = table.internString(esd.name); + const decl_key: *const anyopaque = @ptrCast(&node.data.error_set_decl); + const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); + const info = type_bridge.buildErrorSetInfo(&node.data.error_set_decl, table); + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); } /// The `nominal_id` stamped on a nominal `TypeInfo` (0 for non-nominal / @@ -121,6 +134,23 @@ pub fn reserveShadowUnionSlot(self: *Lowering, ud: *const ast.UnionDecl) void { table.type_decl_tids.put(decl_key, reserved) catch {}; } +/// Reserve a GENUINE same-name ERROR-SET shadow author's DISTINCT nominal slot +/// up-front — the error-set twin of `reserveShadowStructSlot` (E6a). The reserved +/// slot is an empty `.error_set` (its body — the tag id list — is not part of the +/// intern key, only name + nominal id), so `internNamedTypeDecl` later fills the +/// real tags via `updatePreservingKey`. Without this, a local `Foo :: error { ... }` +/// declared after a same-name imported set would collapse onto the imported +/// TypeId via the `findByName` first-author fallback (issue 0134). +pub fn reserveShadowErrorSetSlot(self: *Lowering, esd: *const ast.ErrorSetDecl) void { + const table = &self.module.types; + const decl_key: *const anyopaque = @ptrCast(esd); + if (table.type_decl_tids.contains(decl_key)) return; + const name_id = table.internString(esd.name); + const nominal_id = self.shadowNominalId(name_id); + const reserved = table.internNominal(.{ .error_set = .{ .name = name_id, .tags = &.{} } }, nominal_id); + table.type_decl_tids.put(decl_key, reserved) catch {}; +} + /// A top-level NAMED type decl the genuine-shadow scan tracks, KIND-tagged so /// same-name authors of DIFFERENT kinds (a `struct Foo` and an `enum Foo`) are /// NOT mistaken for one shadow group. Carries the stable decl pointer (the @@ -130,6 +160,7 @@ const ShadowTypeDecl = union(enum) { @"struct": *const ast.StructDecl, @"enum": *const ast.EnumDecl, @"union": *const ast.UnionDecl, + @"error_set": *const ast.ErrorSetDecl, pub fn key(self: ShadowTypeDecl) *const anyopaque { return switch (self) { @@ -159,10 +190,12 @@ pub fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl { .struct_decl => .{ .@"struct" = &decl.data.struct_decl }, .enum_decl => .{ .@"enum" = &decl.data.enum_decl }, .union_decl => .{ .@"union" = &decl.data.union_decl }, + .error_set_decl => .{ .@"error_set" = &decl.data.error_set_decl }, .const_decl => |cd| switch (cd.value.data) { .struct_decl => .{ .@"struct" = &cd.value.data.struct_decl }, .enum_decl => .{ .@"enum" = &cd.value.data.enum_decl }, .union_decl => .{ .@"union" = &cd.value.data.union_decl }, + .error_set_decl => .{ .@"error_set" = &cd.value.data.error_set_decl }, else => null, }, else => null, @@ -175,6 +208,7 @@ pub fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void { .@"struct" => |sd| self.reserveShadowStructSlot(sd), .@"enum" => |ed| self.reserveShadowEnumSlot(ed), .@"union" => |ud| self.reserveShadowUnionSlot(ud), + .@"error_set" => |esd| self.reserveShadowErrorSetSlot(esd), } } diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 10b49e0a..c5643ebd 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -591,18 +591,40 @@ pub fn buildUnionInfo(ud: *const ast.UnionDecl, table: *TypeTable, inner: anytyp /// interned into the global tag pool; the set stores their (sorted) ids. The /// caller (lowering) is responsible for rejecting an empty set, so this only /// sees non-empty declarations. +/// +/// INLINE / structural path ONLY: keeps the `findByName` short-circuit so an +/// anonymous / re-resolved set re-uses an existing same-name slot. The +/// declaration-side per-decl nominal path (`Lowering.registerErrorSetDecl`) +/// builds the body via `buildErrorSetInfo` and interns under its own nominal id +/// instead — see issue 0134. fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId { - const alloc = table.alloc; const name_id = table.internString(esd.name); if (table.findByName(name_id)) |existing| return existing; + const info = buildErrorSetInfo(esd, table); + return table.intern(info); +} + +/// Build the `.error_set` `TypeInfo` body for an error-set decl WITHOUT +/// interning a top-level slot — the shared body-BUILDER behind both the +/// structural inline path (`resolveInlineErrorSet`) and the stateful per-decl +/// registration (`Lowering.registerErrorSetDecl`, which interns it under a +/// per-decl nominal identity so two same-name top-level sets get DISTINCT +/// TypeIds). Tags are interned into the global pool and stored sorted in the +/// slice arena (mirrors `errorSetType`'s canonicalization). +pub fn buildErrorSetInfo(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeInfo { + const alloc = table.alloc; + const name_id = table.internString(esd.name); + var tag_ids = std.ArrayList(u32).empty; defer tag_ids.deinit(alloc); for (esd.tag_names) |tn| { tag_ids.append(alloc, table.internTag(tn)) catch unreachable; } - return table.errorSetType(name_id, tag_ids.items); + const owned = table.slice_arena.allocator().dupe(u32, tag_ids.items) catch unreachable; + std.mem.sort(u32, owned, {}, std.sort.asc(u32)); + return .{ .error_set = .{ .name = name_id, .tags = owned } }; } /// The error channel of a failable signature: `!Named` → the declared error