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).
This commit is contained in:
agra
2026-06-21 09:11:06 +03:00
parent ad45ae07ef
commit 333f57026c
8 changed files with 81 additions and 4 deletions

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
own EventErr.Boom

View File

@@ -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

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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),
}
}

View File

@@ -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