feat(stdlib/E6a): per-decl nominal identity for enum + union decls

Give top-level ENUM and UNION decls per-decl nominal identity so two
same-name flat enums/unions intern DISTINCT nominal TypeIds instead of
collapsing to one global last-wins entry. Establishes the reusable
non-struct register path the later E6 kind-steps (E6b error-set, E6c
protocol, E6d foreign-class) extend.

Registration side (was: stateless `type_bridge.resolveInlineEnum/Union`
`findByName` last-wins short-circuit, no Lowering access):
- Split the type_bridge inline builders into a body-BUILDER
  (`buildEnumInfo` / `buildUnionInfo`) + the existing thin interner
  wrappers (field-type positions keep the legacy single-slot path).
- Add `Lowering.registerEnumDecl` / `registerUnionDecl` mirroring
  `registerStructDecl`: build the TypeInfo, intern via
  `internNamedTypeDecl(decl_key, name_id, info, nominal_id)` under the
  per-decl nominal identity (reserved slot id, else `shadowNominalId`).
- Reroute all six enum/union registration dispatch sites (scanDecls
  const-wrapped + top-level, lowerDecls/comptime, block-local, local
  const) to the new path.

Shared infra generalized ONCE:
- Pass-0b genuine-shadow pre-pass now reserves struct/enum/union shadow
  slots of the MATCHING kind, grouped by (kind, name), via a kind-generic
  `topLevelTypeDecl` / `reserveShadowSlot`. A forward/self/mutual ref to a
  shadow name binds to the reserved nominal TypeId.
- `namedRefTid` consults `type_decl_tids` for `.enum_decl`/`.union_decl`
  before the global `findByName`.

No new per-kind resolution path: selectNominalLeaf / headTypeGate /
flatTypeAuthorCount already gate every kind. Single-author /
phantom-double-spelling names keep nominal_id 0 (byte-identical corpus).

Regressions 0795-0798 (enum + union: ambiguity over every bare-type form,
and own-wins with distinct nominal TypeIds), fail-before/pass-after:
0795/0797 exit 0 -> exit 1 with the loud "type is ambiguous" diagnostic;
0796 silently printed `own=.east` -> correct `own=.north`; 0798 hard
`field 'm' not found` error -> correct `own=5 dep=9`.

Gate: zig build && zig build test (423/423) && run_examples.sh (537/537)
all exit 0; m3te ios-sim build via the main binary exit 0.
This commit is contained in:
agra
2026-06-08 23:18:29 +03:00
parent e5db824477
commit eed2f99f76
24 changed files with 432 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
1

View File

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
own=.north dep=.west

View File

@@ -0,0 +1 @@
1

View File

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
own=5 dep=9

View File

@@ -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
@@ -15487,6 +15584,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 {

View File

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