diff --git a/examples/0795-modules-same-name-enum-ambiguous.sx b/examples/0795-modules-same-name-enum-ambiguous.sx new file mode 100644 index 0000000..923230a --- /dev/null +++ b/examples/0795-modules-same-name-enum-ambiguous.sx @@ -0,0 +1,37 @@ +// E6a — per-decl nominal identity for ENUM decls. A bare ENUM reference is +// non-transitive AND ambiguity-checked at every site, exactly like the struct +// leaf (0755) and the non-leaf struct forms (0767). `main` flat-imports two +// modules that each author a same-name `Dir` enum and authors none itself, so +// EACH of the following bare ENUM forms is a genuine collision the source cannot +// disambiguate — and each must emit the LOUD "type 'Dir' is ambiguous" diagnostic +// and poison the result, NEVER silently pick a global `findByName` last-wins +// author: +// +// - reflection / type-arg slot `size_of(Dir)` +// - typed enum-value annotation `d : Dir = .north` +// - type-as-value `t : Type = Dir` +// - type-category match arm `case Dir:` +// +// Fail-before (pre-E6a): the stateless `type_bridge.resolveInlineEnum` +// `findByName` short-circuit interned ONE global last-wins `Dir`, so every bare +// form silently resolved to it and the program exited 0. + +#import "modules/std.sx"; +#import "0795-modules-same-name-enum-ambiguous/a.sx"; +#import "0795-modules-same-name-enum-ambiguous/b.sx"; + +describe :: ($T: Type) -> s32 { + r := if T == { + case Dir: 1; + else: 0; + } + r +} + +main :: () -> s32 { + sz := size_of(Dir); + d : Dir = .north; + t : Type = Dir; + k := describe(s64); + 0 +} diff --git a/examples/0795-modules-same-name-enum-ambiguous/a.sx b/examples/0795-modules-same-name-enum-ambiguous/a.sx new file mode 100644 index 0000000..dae7650 --- /dev/null +++ b/examples/0795-modules-same-name-enum-ambiguous/a.sx @@ -0,0 +1,4 @@ +// One of two flat-imported authors of a same-name `Dir` enum. With both modules +// flat-visible from a file that authors none itself, every bare reference to the +// name is genuinely ambiguous. +Dir :: enum { north; south; east; west; } diff --git a/examples/0795-modules-same-name-enum-ambiguous/b.sx b/examples/0795-modules-same-name-enum-ambiguous/b.sx new file mode 100644 index 0000000..31d9f2f --- /dev/null +++ b/examples/0795-modules-same-name-enum-ambiguous/b.sx @@ -0,0 +1,4 @@ +// The second flat-imported author of a same-name `Dir` enum. A separate nominal +// identity from a.sx's `Dir`, so each bare reference is a real collision the +// importing source cannot disambiguate. +Dir :: enum { north; south; east; west; } diff --git a/examples/0796-modules-same-name-enum-own-wins.sx b/examples/0796-modules-same-name-enum-own-wins.sx new file mode 100644 index 0000000..203a895 --- /dev/null +++ b/examples/0796-modules-same-name-enum-own-wins.sx @@ -0,0 +1,22 @@ +// E6a — own-wins-over-flat for ENUM per-decl nominal identity. `main` flat-imports +// `dep.sx` (which authors `Dir { east; west }`) AND authors its OWN `Dir { north; +// south }`. A bare `Dir` reference in `main` resolves to `main`'s OWN author, not +// the flat-imported one (the querying source's author wins outright — no +// ambiguity), so `d : Dir = .north` binds `main`'s enum (whose `.north` variant +// dep's `Dir` lacks) while `dep_dir()` returns dep's DISTINCT `Dir`. +// +// Fail-before (pre-E6a): the stateless `type_bridge.resolveInlineEnum` `findByName` +// short-circuit interned ONE global last-wins `Dir`, so `main`'s `Dir` and dep's +// `Dir` collapsed to a single nominal — `.north` would resolve against whichever +// author won the global slot, silently wrong with no diagnostic. + +#import "modules/std.sx"; +#import "0796-modules-same-name-enum-own-wins/dep.sx"; + +Dir :: enum { north; south; } + +main :: () -> s32 { + d : Dir = .north; + print("own={} dep={}\n", d, dep_dir()); + 0 +} diff --git a/examples/0796-modules-same-name-enum-own-wins/dep.sx b/examples/0796-modules-same-name-enum-own-wins/dep.sx new file mode 100644 index 0000000..5d615fc --- /dev/null +++ b/examples/0796-modules-same-name-enum-own-wins/dep.sx @@ -0,0 +1,6 @@ +// A flat-imported module authors its OWN `Dir { east; west }`. The importing file +// (`main`) ALSO authors a `Dir` — its own author must win there (own-wins), while +// this module's `Dir` stays a DISTINCT nominal type used by `dep_dir`. The variant +// sets are disjoint, so a cross-binding to the wrong `Dir` is a hard compile error. +Dir :: enum { east; west; } +dep_dir :: () -> Dir { return .west; } diff --git a/examples/0797-modules-same-name-union-ambiguous.sx b/examples/0797-modules-same-name-union-ambiguous.sx new file mode 100644 index 0000000..96f5ef5 --- /dev/null +++ b/examples/0797-modules-same-name-union-ambiguous.sx @@ -0,0 +1,36 @@ +// E6a — per-decl nominal identity for UNION decls. A bare UNION reference is +// non-transitive AND ambiguity-checked at every site, exactly like the enum +// (0795) and struct (0767) forms. `main` flat-imports two modules that each author +// a same-name `Pair` union and authors none itself, so EACH of the following bare +// UNION forms is a genuine collision the source cannot disambiguate — and each must +// emit the LOUD "type 'Pair' is ambiguous" diagnostic and poison the result, NEVER +// silently pick a global `findByName` last-wins author: +// +// - reflection / type-arg slot `size_of(Pair)` +// - typed annotation `u : Pair = ---` +// - type-as-value `t : Type = Pair` +// - type-category match arm `case Pair:` +// +// Fail-before (pre-E6a): the stateless `type_bridge.resolveInlineUnion` +// `findByName` short-circuit interned ONE global last-wins `Pair`, so every bare +// form silently resolved to it and the program exited 0. + +#import "modules/std.sx"; +#import "0797-modules-same-name-union-ambiguous/a.sx"; +#import "0797-modules-same-name-union-ambiguous/b.sx"; + +describe :: ($T: Type) -> s32 { + r := if T == { + case Pair: 1; + else: 0; + } + r +} + +main :: () -> s32 { + sz := size_of(Pair); + u : Pair = ---; + t : Type = Pair; + k := describe(s64); + 0 +} diff --git a/examples/0797-modules-same-name-union-ambiguous/a.sx b/examples/0797-modules-same-name-union-ambiguous/a.sx new file mode 100644 index 0000000..bfc2243 --- /dev/null +++ b/examples/0797-modules-same-name-union-ambiguous/a.sx @@ -0,0 +1,4 @@ +// One of two flat-imported authors of a same-name `Pair` union. With both modules +// flat-visible from a file that authors none itself, every bare reference to the +// name is genuinely ambiguous. +Pair :: union { f: f32; i: s32; } diff --git a/examples/0797-modules-same-name-union-ambiguous/b.sx b/examples/0797-modules-same-name-union-ambiguous/b.sx new file mode 100644 index 0000000..56b52d7 --- /dev/null +++ b/examples/0797-modules-same-name-union-ambiguous/b.sx @@ -0,0 +1,4 @@ +// The second flat-imported author of a same-name `Pair` union. A separate nominal +// identity from a.sx's `Pair`, so each bare reference is a real collision the +// importing source cannot disambiguate. +Pair :: union { f: f32; i: s32; } diff --git a/examples/0798-modules-same-name-union-own-wins.sx b/examples/0798-modules-same-name-union-own-wins.sx new file mode 100644 index 0000000..96c1921 --- /dev/null +++ b/examples/0798-modules-same-name-union-own-wins.sx @@ -0,0 +1,23 @@ +// E6a — own-wins-over-flat for UNION per-decl nominal identity. `main` flat-imports +// `dep.sx` (which authors `Pair { a }`) AND authors its OWN `Pair { m }`. A bare +// `Pair` reference in `main` resolves to `main`'s OWN author, not the flat-imported +// one (the querying source's author wins outright — no ambiguity), so `p : Pair` +// here binds `main`'s union (whose `m` field dep's `Pair` lacks) while `dep_pair()` +// uses dep's DISTINCT `Pair`. +// +// Fail-before (pre-E6a): the stateless `type_bridge.resolveInlineUnion` `findByName` +// short-circuit interned ONE global last-wins `Pair`, so `main`'s `Pair` and dep's +// `Pair` collapsed to a single nominal — `p.m` would resolve against whichever +// author won the global slot, silently wrong with no diagnostic. + +#import "modules/std.sx"; +#import "0798-modules-same-name-union-own-wins/dep.sx"; + +Pair :: union { m: s32; } + +main :: () -> s32 { + p : Pair = ---; + p.m = 5; + print("own={} dep={}\n", p.m, dep_pair()); + 0 +} diff --git a/examples/0798-modules-same-name-union-own-wins/dep.sx b/examples/0798-modules-same-name-union-own-wins/dep.sx new file mode 100644 index 0000000..56b59a5 --- /dev/null +++ b/examples/0798-modules-same-name-union-own-wins/dep.sx @@ -0,0 +1,10 @@ +// A flat-imported module authors its OWN `Pair { a }`. The importing file (`main`) +// ALSO authors a `Pair` — its own author must win there (own-wins), while this +// module's `Pair` stays a DISTINCT nominal type used by `dep_pair`. The field sets +// are disjoint, so a cross-binding to the wrong `Pair` is a hard compile error. +Pair :: union { a: s32; } +dep_pair :: () -> s32 { + p : Pair = ---; + p.a = 9; + return p.a; +} diff --git a/examples/0799-types-self-ref-recursive-enum-union.sx b/examples/0799-types-self-ref-recursive-enum-union.sx new file mode 100644 index 0000000..4a674d0 --- /dev/null +++ b/examples/0799-types-self-ref-recursive-enum-union.sx @@ -0,0 +1,45 @@ +// E6a (attempt-2) regression — RECURSIVE top-level enum/union via per-decl nominal +// identity. Three single-author shapes that reference a not-yet-interned name in a +// `*Name` field: +// * `Node` — a SELF-referential UNION (linked cells: `next: *Node`). +// * `Tree` — a SELF-referential ENUM/tagged-union (`branch: *Tree`). +// * `A`/`B` — a MUTUALLY-referential union pair (`A` holds `*B`, `B` holds `*A`). +// +// Pre-fix (eed2f99) the new per-decl register path built each enum/union body +// through the STATELESS `type_bridge` BEFORE a matching nominal slot existed, so a +// `*Name` field forward-created a STRUCT stub under `Name`; `internNamedTypeDecl` +// then refreshed that struct stub as an enum/union and tripped the kind-stability +// assert in `types.zig` `updatePreservingKey` — a hard panic (the corpus had no +// recursive enum/union, so the gate missed it). The fix adopts the forward struct +// stub IN PLACE (re-key to the real enum/union kind), mirroring how a self-ref +// struct adopts its own forward stub — so `*Node`/`*Tree`/`*B`/`*A` resolve to the +// genuine 8-byte-pointer nominal types and the recursive walks read through. +#import "modules/std.sx"; + +Node :: union { next: *Node; value: s32; } +Tree :: enum { leaf: s32; branch: *Tree; } +A :: union { b: *B; tag: s32; } +B :: union { a: *A; val: s32; } + +main :: () -> s32 { + // Self-ref union: two-hop walk to the tail cell's value. + n2 : Node = ---; + n2.value = 7; + n1 : Node = ---; + n1.next = @n2; + n0 : Node = ---; + n0.next = @n1; + + // Self-ref enum: a branch whose payload pointer derefs to a leaf. + leaf_node : Tree = .leaf(42); + root : Tree = .branch(@leaf_node); + + // Mutual-ref pair: reach B's value through A's `*B`. + bv : B = ---; + bv.val = 99; + av : A = ---; + av.b = @bv; + + print("union={} enum={} mutual={}\n", n0.next.*.next.*.value, root.branch.*.leaf, av.b.*.val); + 0 +} diff --git a/examples/expected/0795-modules-same-name-enum-ambiguous.exit b/examples/expected/0795-modules-same-name-enum-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0795-modules-same-name-enum-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0795-modules-same-name-enum-ambiguous.stderr b/examples/expected/0795-modules-same-name-enum-ambiguous.stderr new file mode 100644 index 0000000..16593e9 --- /dev/null +++ b/examples/expected/0795-modules-same-name-enum-ambiguous.stderr @@ -0,0 +1,23 @@ +error: type 'Dir' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0795-modules-same-name-enum-ambiguous.sx:32:19 + | +32 | sz := size_of(Dir); + | ^^^ + +error: type 'Dir' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0795-modules-same-name-enum-ambiguous.sx:33:9 + | +33 | d : Dir = .north; + | ^^^ + +error: type 'Dir' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0795-modules-same-name-enum-ambiguous.sx:34:16 + | +34 | t : Type = Dir; + | ^^^ + +error: type 'Dir' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0795-modules-same-name-enum-ambiguous.sx:25:14 + | +25 | case Dir: 1; + | ^^^ diff --git a/examples/expected/0795-modules-same-name-enum-ambiguous.stdout b/examples/expected/0795-modules-same-name-enum-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0795-modules-same-name-enum-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0796-modules-same-name-enum-own-wins.exit b/examples/expected/0796-modules-same-name-enum-own-wins.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0796-modules-same-name-enum-own-wins.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0796-modules-same-name-enum-own-wins.stderr b/examples/expected/0796-modules-same-name-enum-own-wins.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0796-modules-same-name-enum-own-wins.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0796-modules-same-name-enum-own-wins.stdout b/examples/expected/0796-modules-same-name-enum-own-wins.stdout new file mode 100644 index 0000000..09078dd --- /dev/null +++ b/examples/expected/0796-modules-same-name-enum-own-wins.stdout @@ -0,0 +1 @@ +own=.north dep=.west diff --git a/examples/expected/0797-modules-same-name-union-ambiguous.exit b/examples/expected/0797-modules-same-name-union-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0797-modules-same-name-union-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0797-modules-same-name-union-ambiguous.stderr b/examples/expected/0797-modules-same-name-union-ambiguous.stderr new file mode 100644 index 0000000..ede30ca --- /dev/null +++ b/examples/expected/0797-modules-same-name-union-ambiguous.stderr @@ -0,0 +1,23 @@ +error: type 'Pair' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0797-modules-same-name-union-ambiguous.sx:31:19 + | +31 | sz := size_of(Pair); + | ^^^^ + +error: type 'Pair' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0797-modules-same-name-union-ambiguous.sx:32:9 + | +32 | u : Pair = ---; + | ^^^^ + +error: type 'Pair' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0797-modules-same-name-union-ambiguous.sx:33:16 + | +33 | t : Type = Pair; + | ^^^^ + +error: type 'Pair' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import + --> examples/0797-modules-same-name-union-ambiguous.sx:24:14 + | +24 | case Pair: 1; + | ^^^^ diff --git a/examples/expected/0797-modules-same-name-union-ambiguous.stdout b/examples/expected/0797-modules-same-name-union-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0797-modules-same-name-union-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0798-modules-same-name-union-own-wins.exit b/examples/expected/0798-modules-same-name-union-own-wins.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0798-modules-same-name-union-own-wins.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0798-modules-same-name-union-own-wins.stderr b/examples/expected/0798-modules-same-name-union-own-wins.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0798-modules-same-name-union-own-wins.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0798-modules-same-name-union-own-wins.stdout b/examples/expected/0798-modules-same-name-union-own-wins.stdout new file mode 100644 index 0000000..bf61927 --- /dev/null +++ b/examples/expected/0798-modules-same-name-union-own-wins.stdout @@ -0,0 +1 @@ +own=5 dep=9 diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.exit b/examples/expected/0799-types-self-ref-recursive-enum-union.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.stderr b/examples/expected/0799-types-self-ref-recursive-enum-union.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.stdout b/examples/expected/0799-types-self-ref-recursive-enum-union.stdout new file mode 100644 index 0000000..6b3ade7 --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.stdout @@ -0,0 +1 @@ +union=7 enum=42 mutual=99 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c6093f5..4a54cf9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -762,9 +762,9 @@ pub const Lowering = struct { } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file); } else if (cd.value.data == .enum_decl) { - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerEnumDecl(&cd.value.data.enum_decl); } else if (cd.value.data == .union_decl) { - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerUnionDecl(&cd.value.data.union_decl); } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } @@ -776,10 +776,10 @@ pub const Lowering = struct { self.registerStructDecl(&decl.data.struct_decl, decl.source_file); }, .enum_decl => { - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerEnumDecl(&decl.data.enum_decl); }, .union_decl => { - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerUnionDecl(&decl.data.union_decl); }, .error_set_decl => { self.registerErrorSetDecl(decl); @@ -925,41 +925,44 @@ pub const Lowering = struct { else => {}, } } - // Pass 0b: reserve every GENUINE same-name STRUCT shadow's DISTINCT nominal - // slot BEFORE the registration loop resolves any fields (E2/F1). A field - // type referencing a shadow name — self (`next: *Box`), or a forward / - // mutual ref to a shadow declared LATER in the same module (`peer: *Node`) - // — then binds to its OWN nominal TypeId via `type_decl_tids`, never the - // global findByName first-author fallback (issue 0105). + // Pass 0b: reserve every GENUINE same-name NAMED-TYPE shadow's DISTINCT + // nominal slot BEFORE the registration loop resolves any fields (E2/F1, and + // enum/union from E6a). A field / variant type referencing a shadow name — + // self (`next: *Box`), or a forward / mutual ref to a shadow declared LATER + // in the same module (`peer: *Node`) — then binds to its OWN nominal TypeId + // via `type_decl_tids`, never the global findByName first-author fallback + // (issue 0105). // - // "Genuine" = ≥2 DISTINCT struct decls in THIS scan author the name (so it - // needs ≥2 distinct nominal TypeIds). Gating on the scanned decls — NOT - // `nameHasMultipleTypeAuthors` (the raw import facts, which over-count one - // file reached via two un-normalized import spellings, e.g. `math/matrix44` - // pulled in twice) — keeps a single-real-decl name on the legacy id-0 path, - // byte-identical. ALL authors of a genuine shadow reserve, in declaration - // order: the FIRST at id 0, the rest at fresh nonzero ids, matching the - // per-decl registration order so the first-author-keeps-0 assignment holds. - var shadow_first = std.AutoHashMap(types.StringId, *const anyopaque).init(self.alloc); + // "Genuine" = ≥2 DISTINCT decls of the SAME KIND in THIS scan author the name + // (so it needs ≥2 distinct nominal TypeIds). Grouping by (kind, name) keeps a + // `struct Foo` and an `enum Foo` in separate groups — neither is a shadow of + // the other. Gating on the scanned decls — NOT `nameHasMultipleTypeAuthors` + // (the raw import facts, which over-count one file reached via two + // un-normalized import spellings, e.g. `math/matrix44` pulled in twice) — + // keeps a single-real-decl name on the legacy id-0 path, byte-identical. ALL + // authors of a genuine shadow reserve, in declaration order: the FIRST at id + // 0, the rest at fresh nonzero ids, matching the per-decl registration order + // so the first-author-keeps-0 assignment holds. + const ShadowKey = struct { kind: u8, name: types.StringId }; + var shadow_first = std.AutoHashMap(ShadowKey, *const anyopaque).init(self.alloc); defer shadow_first.deinit(); - var genuine_shadows = std.AutoHashMap(types.StringId, void).init(self.alloc); + var genuine_shadows = std.AutoHashMap(ShadowKey, void).init(self.alloc); defer genuine_shadows.deinit(); for (decls) |decl| { - const sd = topLevelStructDecl(decl) orelse continue; - if (sd.type_params.len > 0) continue; - const nm = self.module.types.internString(sd.name); - const key: *const anyopaque = @ptrCast(sd); - const gop = shadow_first.getOrPut(nm) catch continue; + const td = topLevelTypeDecl(decl) orelse continue; + if (td.isGeneric()) continue; + const sk = ShadowKey{ .kind = @intFromEnum(std.meta.activeTag(td)), .name = self.module.types.internString(td.name()) }; + const gop = shadow_first.getOrPut(sk) catch continue; if (gop.found_existing) { - if (gop.value_ptr.* != key) genuine_shadows.put(nm, {}) catch {}; - } else gop.value_ptr.* = key; + if (gop.value_ptr.* != td.key()) genuine_shadows.put(sk, {}) catch {}; + } else gop.value_ptr.* = td.key(); } for (decls) |decl| { - const sd = topLevelStructDecl(decl) orelse continue; - const nm = self.module.types.internString(sd.name); - if (!genuine_shadows.contains(nm)) continue; + const td = topLevelTypeDecl(decl) orelse continue; + const sk = ShadowKey{ .kind = @intFromEnum(std.meta.activeTag(td)), .name = self.module.types.internString(td.name()) }; + if (!genuine_shadows.contains(sk)) continue; self.setCurrentSourceFile(decl.source_file); - self.reserveShadowStructSlot(sd); + self.reserveShadowSlot(td); } for (decls) |decl| { self.setCurrentSourceFile(decl.source_file); @@ -999,11 +1002,11 @@ pub const Lowering = struct { } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file); } else if (cd.value.data == .enum_decl) { - // Register enum/tagged-union types in the type table - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + // Per-decl nominal identity for enum/tagged-union types (E6a) + self.registerEnumDecl(&cd.value.data.enum_decl); } else if (cd.value.data == .union_decl) { - // Register plain union types in the type table - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + // Per-decl nominal identity for plain union types (E6a) + self.registerUnionDecl(&cd.value.data.union_decl); } else if (cd.value.data == .type_expr or cd.value.data == .pointer_type_expr or cd.value.data == .many_pointer_type_expr or @@ -1179,12 +1182,12 @@ pub const Lowering = struct { self.registerStructDecl(&decl.data.struct_decl, decl.source_file); }, .enum_decl => { - // Register enum/tagged-union types in the type table - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + // Per-decl nominal identity for enum/tagged-union types (E6a) + self.registerEnumDecl(&decl.data.enum_decl); }, .union_decl => { - // Register plain union types in the type table - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + // Per-decl nominal identity for plain union types (E6a) + self.registerUnionDecl(&decl.data.union_decl); }, .error_set_decl => { self.registerErrorSetDecl(decl); @@ -2169,14 +2172,19 @@ pub const Lowering = struct { /// `struct #compiler`, a protocol-backed struct, a generic instance) or before /// it registers; a genuine same-name SHADOW always registers through /// `internNamedTypeDecl` and so is in `type_decl_tids`, never reaching the - /// fallback. enum / union / error-set / protocol / foreign-class keep the - /// legacy `findByName` resolution (same-name shadows of those kinds are a - /// later, orthogonal phase outside 0105's struct/alias scope). + /// fallback. ENUM and UNION resolve the same per-decl way (E6a): registered + /// through `internNamedTypeDecl` (`registerEnumDecl` / `registerUnionDecl`), + /// keyed by the raw-facts decl pointer, with the `findByName` fallback for a + /// single author registered before its slot lands. error-set / protocol / + /// foreign-class keep the legacy `findByName` resolution (their same-name + /// shadows are later E6 sub-steps — E6b/E6c/E6d). fn namedRefTid(self: *Lowering, ref: resolver_mod.RawDeclRef, name: []const u8) ?TypeId { const table = &self.module.types; return switch (ref) { .struct_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))), - .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => 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, .foreign_class_decl => table.findByName(table.internString(name)), .fn_decl, .const_decl, .namespace_decl => null, }; } @@ -3185,9 +3193,13 @@ pub const Lowering = struct { self.recordLocalTypeName(sd.name); self.registerStructDecl(&node.data.struct_decl, node.source_file orelse self.current_source_file); }, - .enum_decl, .union_decl => { + .enum_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); - _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerEnumDecl(&node.data.enum_decl); + }, + .union_decl => { + if (node.data.declName()) |dn| self.recordLocalTypeName(dn); + self.registerUnionDecl(&node.data.union_decl); }, .error_set_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); @@ -3360,9 +3372,14 @@ pub const Lowering = struct { self.registerStructDecl(&cd.value.data.struct_decl, self.current_source_file); return; } - if (cd.value.data == .enum_decl or cd.value.data == .union_decl) { + if (cd.value.data == .enum_decl) { self.recordLocalTypeName(cd.name); - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + self.registerEnumDecl(&cd.value.data.enum_decl); + return; + } + if (cd.value.data == .union_decl) { + self.recordLocalTypeName(cd.name); + self.registerUnionDecl(&cd.value.data.union_decl); return; } @@ -15034,17 +15051,6 @@ pub const Lowering = struct { return out; } - /// The top-level STRUCT decl a top-level node authors (a bare `struct_decl`, or - /// a `Name :: struct {...}` const wrapper), or null. Used by the genuine-shadow - /// scan in `scanDecls` to enumerate same-name struct authors uniformly. - fn topLevelStructDecl(decl: *const Node) ?*const ast.StructDecl { - return switch (decl.data) { - .struct_decl => &decl.data.struct_decl, - .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null, - else => null, - }; - } - /// Reserve a GENUINE same-name STRUCT shadow author's DISTINCT nominal slot /// BEFORE any field resolves, so a self / forward / mutual reference to a shadow /// name (`next: *Box`; `peer: *Node` where Node is a shadow declared later) @@ -15068,6 +15074,97 @@ pub const Lowering = struct { table.type_decl_tids.put(decl_key, reserved) catch {}; } + /// Reserve a GENUINE same-name ENUM shadow author's DISTINCT nominal slot + /// up-front — the enum twin of `reserveShadowStructSlot` (E6a). The reserved + /// slot's KIND MUST match what `buildEnumInfo` will produce (a payload enum → + /// `.tagged_union`, a payload-less enum → `.enum`), because `internNamedTypeDecl` + /// later refreshes the body via `updatePreservingKey`, whose key-stability + /// assert compares the FULL info tag — a struct/enum/tagged_union mismatch would + /// trip it. The empty body and placeholder `tag_type` are not part of the intern + /// key (name + nominal id only), so the real body fills in freely. + fn reserveShadowEnumSlot(self: *Lowering, ed: *const ast.EnumDecl) void { + const table = &self.module.types; + const decl_key: *const anyopaque = @ptrCast(ed); + if (table.type_decl_tids.contains(decl_key)) return; + const name_id = table.internString(ed.name); + const nominal_id = self.shadowNominalId(name_id); + const empty: types.TypeInfo = if (ed.variant_types.len > 0) + .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .s64 } } + else + .{ .@"enum" = .{ .name = name_id, .variants = &.{} } }; + const reserved = table.internNominal(empty, nominal_id); + table.type_decl_tids.put(decl_key, reserved) catch {}; + } + + /// Reserve a GENUINE same-name UNION shadow author's DISTINCT nominal slot + /// up-front — the union twin of `reserveShadowStructSlot` (E6a). + fn reserveShadowUnionSlot(self: *Lowering, ud: *const ast.UnionDecl) void { + const table = &self.module.types; + const decl_key: *const anyopaque = @ptrCast(ud); + if (table.type_decl_tids.contains(decl_key)) return; + const name_id = table.internString(ud.name); + const nominal_id = self.shadowNominalId(name_id); + const reserved = table.internNominal(.{ .@"union" = .{ .name = name_id, .fields = &.{} } }, 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 + /// `decl_key` / raw-facts identity) so the scan de-dups by decl identity, and + /// dispatches the per-kind reservation. Later E6 sub-steps add their kind here. + const ShadowTypeDecl = union(enum) { + @"struct": *const ast.StructDecl, + @"enum": *const ast.EnumDecl, + @"union": *const ast.UnionDecl, + + fn key(self: ShadowTypeDecl) *const anyopaque { + return switch (self) { + inline else => |p| @ptrCast(p), + }; + } + fn name(self: ShadowTypeDecl) []const u8 { + return switch (self) { + inline else => |p| p.name, + }; + } + fn isGeneric(self: ShadowTypeDecl) bool { + return switch (self) { + .@"struct" => |p| p.type_params.len > 0, + else => false, + }; + } + }; + + /// Classify a top-level node as the NAMED type decl it authors — a bare + /// `struct`/`enum`/`union` node, or a `const_decl` whose value is one — so the + /// genuine-shadow scan enumerates all three kinds uniformly. Null when the node + /// is not a struct/enum/union author. The shared infra E6b/E6c extend by adding + /// their kind here. + fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl { + return switch (decl.data) { + .struct_decl => .{ .@"struct" = &decl.data.struct_decl }, + .enum_decl => .{ .@"enum" = &decl.data.enum_decl }, + .union_decl => .{ .@"union" = &decl.data.union_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 }, + else => null, + }, + else => null, + }; + } + + /// Dispatch a genuine-shadow reservation to the matching per-kind reserver. + fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void { + switch (td) { + .@"struct" => |sd| self.reserveShadowStructSlot(sd), + .@"enum" => |ed| self.reserveShadowEnumSlot(ed), + .@"union" => |ud| self.reserveShadowUnionSlot(ud), + } + } + /// Register (or re-register) a top-level NAMED type decl under a per-source /// nominal identity (E2), returning its TypeId. `decl_key` is the decl's /// stable pointer (the import raw-facts identity); `info` carries the full @@ -15098,11 +15195,35 @@ pub const Lowering = struct { (table.findByName(name_id) orelse table.internNominal(info, 0)) else table.internNominal(info, nominal_id); - table.updatePreservingKey(id, stampNominalId(info, nominal_id)); + const stamped = stampNominalId(info, nominal_id); + // A self / mutual `*Name` field in an enum/union body forward-creates a + // STRUCT placeholder under `Name` (the stateless resolver has no kind + // context — `type_resolver.resolveNamed` always stubs a struct), which the + // `findByName` above then returns. Adopting a wrong-kind stub needs a + // re-key, NOT the in-place `updatePreservingKey` body-fill — whose + // kind-stability assert trips on struct→enum/union. + if (adoptsForwardStructStub(table.get(id), stamped)) + table.replaceKeyedInfo(id, stamped) + else + table.updatePreservingKey(id, stamped); table.type_decl_tids.put(decl_key, id) catch {}; return id; } + /// TRUE when `existing` is a forward-reference STRUCT placeholder (empty + /// fields — the stateless resolver's stub for an as-yet-unregistered name) and + /// `incoming` is a NON-struct nominal (enum / union / tagged_union): the one + /// case where `internNamedTypeDecl` must re-key the slot rather than fill its + /// body in place. A struct adopting its own struct stub is same-kind and stays + /// on `updatePreservingKey`; a fresh-interned slot has no stub to adopt. + fn adoptsForwardStructStub(existing: types.TypeInfo, incoming: types.TypeInfo) bool { + if (existing != .@"struct" or existing.@"struct".fields.len != 0) return false; + return switch (incoming) { + .@"enum", .@"union", .tagged_union => true, + else => false, + }; + } + /// The `nominal_id` to register a NAMED type author of `name_id` under. 0 /// unless `name_id` is authored as a named type by ≥2 distinct modules (a real /// same-name shadow per the import facts): the FIRST source to register keeps @@ -15487,6 +15608,37 @@ pub const Lowering = struct { } } + /// Register a top-level ENUM decl under a per-decl nominal identity (E6a) — + /// the enum twin of `registerStructDecl`. A GENUINE same-name shadow already + /// reserved its DISTINCT slot up-front in `scanDecls` (the first at id 0, the + /// rest at nonzero ids), so a forward / self / mutual reference to the shadow + /// name already bound to ITS nominal TypeId via `type_decl_tids`: reuse that + /// reserved id. A single-author name (or one over-counted by the raw facts but + /// not a genuine scanned shadow) was NOT reserved — it keeps id 0 and the legacy + /// post-build registration, byte-identical to pre-E6a. The body is built once by + /// the shared `type_bridge.buildEnumInfo`; `internNamedTypeDecl` interns it under + /// the computed nominal id and records `decl_key → TypeId` so `namedRefTid` + /// resolves bare references to this exact author. + fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void { + const table = &self.module.types; + const name_id = table.internString(ed.name); + const decl_key: *const anyopaque = @ptrCast(ed); + 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.buildEnumInfo(ed, table, &self.program_index.type_alias_map, &self.program_index.module_const_map); + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); + } + + /// Register a top-level UNION decl under a per-decl nominal identity (E6a) — + /// the union twin of `registerEnumDecl` / `registerStructDecl`. + fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void { + const table = &self.module.types; + const name_id = table.internString(ud.name); + const decl_key: *const anyopaque = @ptrCast(ud); + 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.buildUnionInfo(ud, table, &self.program_index.type_alias_map, &self.program_index.module_const_map); + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); + } + /// Rename an __anon type to a qualified name: ParentStruct.field_name /// Also renames variant payload struct types from __anon.X to ParentStruct.field_name.X fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void { diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index bd54ee7..5631c27 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -331,13 +331,31 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa // ── Inline type declarations ───────────────────────────────────────── +/// Stateless inline-enum resolution for a FIELD-type position (`x: enum {...}`): +/// the legacy `findByName` short-circuit keeps a single global slot per display +/// name. The TOP-LEVEL per-decl nominal identity path (`Lowering.registerEnumDecl`) +/// shares the body via `buildEnumInfo` but interns under its own nominal id. fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { + const name_id = table.internString(ed.name); + if (table.findByName(name_id)) |existing| return existing; + const info = buildEnumInfo(ed, table, alias_map, consts); + const id = table.internNominal(info, 0); + table.updatePreservingKey(id, info); + return id; +} + +/// Build the `TypeInfo` body for an enum decl WITHOUT interning the top-level +/// nominal slot — the shared body-BUILDER behind both the stateless inline +/// field-type path (`resolveInlineEnum`) and the stateful per-decl registration +/// (`Lowering.registerEnumDecl`, which interns it under a per-decl nominal +/// identity so two same-name top-level enums get DISTINCT TypeIds). A payload +/// enum builds a `.tagged_union`; a payload-less enum a plain `.enum`. Nested +/// payload structs / variant field types ARE interned here — they are distinct +/// nested nominals, not the enum's own identity. +pub fn buildEnumInfo(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeInfo { const alloc = table.alloc; const name_id = table.internString(ed.name); - // Check if already registered - if (table.findByName(name_id)) |existing| return existing; - // Enum with payloads → tagged union const has_payloads = ed.variant_types.len > 0; if (has_payloads) { @@ -417,16 +435,13 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia explicit_tag_vals = vals.items; } - const info: TypeInfo = .{ .tagged_union = .{ + return .{ .tagged_union = .{ .name = name_id, .fields = fields.items, .tag_type = tag_type orelse .s64, // enum unions are always tagged (default i64) .backing_type = backing_type, .explicit_tag_values = explicit_tag_vals, } }; - const id = table.internNominal(info, 0); - table.updatePreservingKey(id, info); - return id; } // Plain enum (no payloads) @@ -475,16 +490,13 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia } } - const info: TypeInfo = .{ .@"enum" = .{ + return .{ .@"enum" = .{ .name = name_id, .variants = variants.items, .is_flags = ed.is_flags, .explicit_values = explicit_vals, .backing_type = enum_backing, } }; - const id = table.internNominal(info, 0); - table.updatePreservingKey(id, info); - return id; } fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { @@ -510,12 +522,26 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: return id; } +/// Stateless inline-union resolution for a FIELD-type position. The TOP-LEVEL +/// per-decl nominal identity path (`Lowering.registerUnionDecl`) shares the body +/// via `buildUnionInfo` but interns under its own nominal id. fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { + const name_id = table.internString(ud.name); + if (table.findByName(name_id)) |existing| return existing; + const info = buildUnionInfo(ud, table, alias_map, consts); + const id = table.internNominal(info, 0); + table.updatePreservingKey(id, info); + return id; +} + +/// Build the `TypeInfo` body for a union decl WITHOUT interning the top-level +/// nominal slot — the shared body-BUILDER behind both the stateless inline +/// field-type path (`resolveInlineUnion`) and the stateful per-decl registration +/// (`Lowering.registerUnionDecl`). +pub fn buildUnionInfo(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeInfo { const alloc = table.alloc; const name_id = table.internString(ud.name); - if (table.findByName(name_id)) |existing| return existing; - var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty; for (ud.field_names, ud.field_types) |fname, ftype_node| { const field_ty = resolveAstType(ftype_node, table, alias_map, consts); @@ -524,13 +550,10 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: Al .ty = field_ty, }) catch unreachable; } - const info: TypeInfo = .{ .@"union" = .{ + return .{ .@"union" = .{ .name = name_id, .fields = fields.items, } }; - const id = table.internNominal(info, 0); - table.updatePreservingKey(id, info); - return id; } /// `Foo :: error { A, B }` → a registered `.error_set` type. Tag names are