From 4b2a0679917a271d247cf12e4d16c906f05b234b Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 22:17:07 +0300 Subject: [PATCH] wip(E2): partial nominal-identity/shadow work [stdlib E2 attempt-1 WIP checkpoint] Incomplete WIP from a worker killed at the 30-min wall; committed as a checkpoint so the resumed session continues on a clean tree. May not build. --- src/ir/lower.zig | 343 ++++++++++++++++++++++++++++++++++-------- src/ir/types.test.zig | 7 +- src/ir/types.zig | 16 +- 3 files changed, 292 insertions(+), 74 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d872352..4e2a583 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -164,6 +164,15 @@ pub const Lowering = struct { /// records which specific FuncIds have had a real body lowered (fix-0102b). lowered_fids: std.AutoHashMap(FuncId, void), local_fn_counter: u32 = 0, // unique counter for mangling local function names + /// Per-declaration nominal identity bookkeeping (E2). The FIRST source to + /// register a given top-level type NAME keeps `nominal_id = 0` (structural — + /// byte-identical to pre-E2 single-author registration); a later registration + /// of the same name from a DIFFERENT source is a same-name SHADOW and gets a + /// fresh id from `next_nominal_id`, so the two authors intern to DISTINCT + /// TypeIds (closing issue 0105's last-wins collapse). `nominal_name_authors` + /// records each name's first author source to make that decision. + nominal_name_authors: std.AutoHashMap(types.StringId, []const u8), + next_nominal_id: u32 = 0, /// Declaration-name / import / visibility facts (architecture phase A1, /// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` / /// `import_graph` from the compilation driver. Reached via @@ -422,6 +431,7 @@ pub const Lowering = struct { .lowered_functions = std.StringHashMap(void).init(module.alloc), .fn_decl_fids = std.AutoHashMap(*const ast.FnDecl, FuncId).init(module.alloc), .lowered_fids = std.AutoHashMap(FuncId, void).init(module.alloc), + .nominal_name_authors = std.AutoHashMap(types.StringId, []const u8).init(module.alloc), .program_index = ProgramIndex.init(module.alloc), }; } @@ -700,8 +710,8 @@ pub const Lowering = struct { .comptime_expr => |ct| { self.lowerComptimeSideEffect(ct.expr); }, - .struct_decl => |sd| { - self.registerStructDecl(&sd); + .struct_decl => { + self.registerStructDecl(&decl.data.struct_decl); }, .enum_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); @@ -953,7 +963,10 @@ pub const Lowering = struct { if (self.current_source_file orelse self.main_file) |from| { switch (self.selectNominalLeaf(rhs.name, from, rhs.is_raw)) { .resolved => |tid| self.putTypeAlias(self.current_source_file, cd.name, tid), - .pending, .undeclared, .not_visible => {}, + // `.ambiguous` (same-name RHS authored by ≥2 flat + // imports) leaves A unwritten like `.not_visible`; + // the loud diagnostic fires where A is USED. + .pending, .undeclared, .not_visible, .ambiguous => {}, } } } @@ -1064,8 +1077,8 @@ pub const Lowering = struct { } } }, - .struct_decl => |sd| { - self.registerStructDecl(&sd); + .struct_decl => { + self.registerStructDecl(&decl.data.struct_decl); }, .enum_decl => { // Register enum/tagged-union types in the type table @@ -1415,9 +1428,11 @@ pub const Lowering = struct { }, // B not yet a resolved type author from this source: a forward // alias still pending (re-tried next round), an undeclared - // name, or a namespaced-only type that is not bare-aliasable. - // Leave A unwritten — no global last-wins leak. - .pending, .undeclared, .not_visible => {}, + // name, a namespaced-only type that is not bare-aliasable, or + // an ambiguous same-name shadow (≥2 flat authors). Leave A + // unwritten — no global last-wins leak; the ambiguity surfaces + // where A is used. + .pending, .undeclared, .not_visible, .ambiguous => {}, } } } @@ -1713,6 +1728,11 @@ pub const Lowering = struct { /// (which would leak the type) and NEVER a silent empty-struct stub (which /// would mis-size it). not_visible, + /// ≥2 DISTINCT same-name type authors are flat-visible from the querying + /// source and none is its own (E2, issue 0105). The selection is genuinely + /// ambiguous: `resolveNominalLeaf` emits a loud diagnostic and returns the + /// `.unresolved` poison sentinel — never a silent first-/last-wins pick. + ambiguous, }; /// THE plain bare-name call selector (fix-0102c, R5 §C). `resolveBareCallee`'s @@ -1867,24 +1887,37 @@ pub const Lowering = struct { } // 1. A flat-import-visible TYPE author (named type OR alias) — resolve to - // its declared TypeId. Single-author (E1): at most one author across - // own ∪ flat closure, so this is byte-identical to the legacy leaf. - // The author KIND decides resolution, decoupled from `findByName` - // timing: an ALIAS resolves to its `type_aliases_by_source` target; a - // NAMED type resolves to its `findByName` entry, OR — when that entry - // is not registered yet (a forward / self reference like - // `next: *ArenaChunk` resolved mid-registration) — to the legacy - // empty-struct stub, reconciled by `updatePreservingKey` when the type - // finally registers. E2 routes named resolution through the - // collector-selected author's per-source `nominal_id` once same-name - // type shadows register. - if (self.flatVisibleTypeAuthor(name, from)) |author| switch (author) { + // its PER-SOURCE declared TypeId (E2). The author KIND decides + // resolution: an ALIAS resolves to its `type_aliases_by_source` target; + // a NAMED type resolves to its per-decl `type_decl_tids` nominal + // identity — so two same-name structs authored in different sources + // return their OWN distinct TypeIds instead of collapsing last-wins + // (issue 0105). A named author not registered yet (a forward / self + // reference like `next: *ArenaChunk` resolved mid-registration) yields + // `.undeclared` → the legacy empty-struct stub, reconciled by + // `internNamedTypeDecl` adopting that stub when the type registers. + // + // The querying source's OWN author wins outright (own-wins, 0105 case + // 3); otherwise the transitive flat-import closure is searched, and ≥2 + // DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single- + // author (E1) keeps ≤1 author across the closure, so this stays byte- + // identical to the legacy leaf. + if (self.moduleTypeAuthor(from, name)) |author| switch (author) { .alias => |tid| return .{ .resolved = tid }, - .named => { - if (registered) |existing| return .{ .resolved = existing }; + .named => |ref| { + if (self.namedRefTid(ref, name)) |tid| return .{ .resolved = tid }; return .undeclared; }, }; + switch (self.flatTypeAuthorCount(name, from)) { + .none => {}, + .one => |tid| return .{ .resolved = tid }, + .ambiguous => return .ambiguous, + // A flat author exists but is not registered as a findByName-able type + // yet (a forward reference, or a foreign / lazily-registered class) → + // the legacy empty-struct stub, NOT a namespaced-only leak (arm 3). + .unregistered => return .undeclared, + } // 2. A block-local type (declared inside a fn / init body) clobbers the // global entry for its name, so `existing` IS that local type — never a @@ -1917,7 +1950,7 @@ pub const Lowering = struct { /// ≤1) and, if its alias target is not yet in `type_aliases_by_source`, /// returns `.pending` so the forward-alias fixpoint re-resolves it (source- /// aware in E1.5). A resolved flat-visible alias is already returned by - /// `flatVisibleTypeAuthor` above, so the `inner.get` here only catches a + /// `moduleTypeAuthor` (arm 1) above, so the `inner.get` here only catches a /// const author reachable via `collectVisibleAuthors` whose target landed /// between the two reads — the fixpoint path is the common outcome. fn forwardAliasOrUndeclared(self: *Lowering, name: []const u8, from: []const u8) TypeHeadResolution { @@ -1957,25 +1990,24 @@ pub const Lowering = struct { /// A module's authorship of a bare type `name`: an ALIAS (carrying the /// resolved target `TypeId` from `type_aliases_by_source`) or a NAMED type - /// (struct/enum/union/error-set/protocol/foreign class — its TypeId is - /// resolved at the use site from `findByName`, decoupled from this predicate - /// so a not-yet-registered forward / self reference is still recognised as an - /// author). + /// (struct/enum/union/error-set/protocol/foreign class — carrying its + /// `RawDeclRef` so the use site can resolve its PER-DECL nominal TypeId via + /// `type_decl_tids`, decoupled from registration timing so a not-yet-registered + /// forward / self reference is still recognised as an author). const FlatTypeAuthor = union(enum) { alias: TypeId, - named, + named: resolver_mod.RawDeclRef, }; /// How module `path` authors `name` AS A TYPE, or null if it does not. A type /// author is EITHER a type ALIAS (`Name :: `, recorded in E0's /// `type_aliases_by_source` — checked first via the source-keyed cache) OR a - /// NAMED type (recognised by its `module_decls` decl KIND, NOT by `findByName` - /// — so a forward / self reference resolved before the type registers is still - /// an author). A same-name VALUE/FUNCTION is NOT a type author (R1); a - /// value-const (`N :: 7`) lives in `module_consts_by_source`, never - /// `type_aliases_by_source`, so it returns null too. THE per-module "is `name` - /// a type author here?" predicate — the single source of truth for the - /// visibility walk (R4). + /// NAMED type (recognised by its `module_decls` decl KIND; the `RawDeclRef` is + /// carried so the use site resolves its per-decl nominal TypeId). A same-name + /// VALUE/FUNCTION is NOT a type author (R1); a value-const (`N :: 7`) lives in + /// `module_consts_by_source`, never `type_aliases_by_source`, so it returns + /// null too. THE per-module "is `name` a type author here?" predicate — the + /// single source of truth for the visibility walk (R4). fn moduleTypeAuthor(self: *Lowering, path: []const u8, name: []const u8) ?FlatTypeAuthor { if (self.program_index.type_aliases_by_source.get(path)) |inner| { if (inner.get(name)) |tid| return .{ .alias = tid }; @@ -1984,35 +2016,70 @@ pub const Lowering = struct { const m = decls.get(path) orelse return null; const ref = m.names.get(name) orelse return null; if (!isNamedTypeKind(ref)) return null; - return .named; + return .{ .named = ref }; } - /// The flat-import-visible TYPE author for bare `name` from `from`, or null - /// when no flat-reachable module authors `name` as a type. Walks own decls ∪ - /// the TRANSITIVE flat-import closure (every transitively flat-imported - /// module), returning the first author found via `moduleTypeAuthor` (named - /// type OR alias — the single source of truth, R4). Single-author (E1): at - /// most one author across the closure, so the first hit IS the unique author. - /// - /// TRANSITIVE rather than single-hop is the open R3 asymmetry (types - /// transitive, values non-transitive — 0706; sequenced as E4 per Agra): a - /// library template's INTERNAL type ref (`List.append`'s `alloc: Allocator`, - /// declared in `std.sx` but instantiated in the caller's source context) is - /// two flat hops from the caller, and the value/function source-pin that would - /// let the type gate go single-hop too is not yet wired for generic - /// instantiation. The closure walk lives in `lower.zig`, NOT `resolver.zig`, - /// so the single-graph-walk invariant (one `flat_import_graph` iterator in - /// `resolver.zig`) is untouched. Returns null when the flat graph is unwired - /// (the caller has already special-cased that to the legacy resolution). - fn flatVisibleTypeAuthor(self: *Lowering, name: []const u8, from: []const u8) ?FlatTypeAuthor { - const graph = self.program_index.flat_import_graph orelse return null; - if (self.moduleTypeAuthor(from, name)) |a| return a; + /// The per-decl nominal TypeId of a NAMED-type `RawDeclRef` author, or null + /// when its slot is not registered yet (a forward / self reference resolved + /// mid-registration → the caller yields the legacy empty-struct stub). A + /// STRUCT resolves first through its `type_decl_tids` nominal identity (E2) + /// keyed by the raw-facts decl pointer, so two same-name struct authors in + /// different sources resolve to their OWN distinct TypeIds (issue 0105). A + /// `type_decl_tids` MISS falls back to the global `findByName` — correct for a + /// SINGLE-author struct registered via a non-`internNamedTypeDecl` path (a + /// `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). + 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)), + .fn_decl, .const_decl, .namespace_decl => null, + }; + } + + /// The per-source TypeId module `path` authors for bare `name`, or null. The + /// alias-or-named resolution behind the ambiguity walk: an ALIAS yields its + /// source-keyed target; a NAMED type yields its per-decl nominal TypeId (null + /// while still unregistered, so it does not count toward ambiguity mid-scan). + fn moduleTypeAuthorTid(self: *Lowering, path: []const u8, name: []const u8) ?TypeId { + return switch (self.moduleTypeAuthor(path, name) orelse return null) { + .alias => |tid| tid, + .named => |ref| self.namedRefTid(ref, name), + }; + } + + /// What bare `name`'s type authors look like across the TRANSITIVE + /// flat-import closure of `from` (the querying source's OWN author is consulted + /// by `selectNominalLeaf` first — own-wins — so this surveys only the + /// cross-module flat authors): + /// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4); + /// - `.one` — exactly one distinct resolved TypeId; + /// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId + /// yet (a forward reference, or a foreign/lazily-registered author with no + /// `findByName` slot) → the caller yields the legacy stub, NOT a leak; + /// - `.none` — no flat author at all → the caller proceeds to the + /// local / leak / forward-alias arms. + /// Distinctness is BY TypeId: each distinct author holds a distinct + /// `nominal_id` TypeId, while a diamond import of the SAME module yields the + /// same TypeId, so byte-identical de-dup falls out. The closure walk lives in + /// `lower.zig`, NOT `resolver.zig` — the single-graph-walk invariant (one + /// `flat_import_graph` iterator in `resolver.zig`) is untouched. + const FlatTypeAuthorCount = union(enum) { none, one: TypeId, ambiguous, unregistered }; + fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount { + const graph = self.program_index.flat_import_graph orelse return .none; + var found: ?TypeId = null; + var saw_author = false; var visited = std.StringHashMap(void).init(self.alloc); defer visited.deinit(); var queue = std.ArrayList([]const u8).empty; defer queue.deinit(self.alloc); - visited.put(from, {}) catch return null; - queue.append(self.alloc, from) catch return null; + visited.put(from, {}) catch return .none; + queue.append(self.alloc, from) catch return .none; var i: usize = 0; while (i < queue.items.len) : (i += 1) { const deps = graph.get(queue.items[i]) orelse continue; @@ -2021,11 +2088,19 @@ pub const Lowering = struct { const dep = kv.key_ptr.*; if (visited.contains(dep)) continue; visited.put(dep, {}) catch continue; - if (self.moduleTypeAuthor(dep, name)) |a| return a; + if (self.moduleTypeAuthor(dep, name) != null) { + saw_author = true; + if (self.moduleTypeAuthorTid(dep, name)) |tid| { + if (found) |f| { + if (tid != f) return .ambiguous; + } else found = tid; + } + } queue.append(self.alloc, dep) catch continue; } } - return null; + if (found) |t| return .{ .one = t }; + return if (saw_author) .unregistered else .none; } /// TRUE iff `name` is authored as a TYPE — a NAMED type OR a type ALIAS — in @@ -2088,6 +2163,14 @@ pub const Lowering = struct { d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); return .unresolved; }, + // ≥2 distinct same-name type authors flat-visible, none own (issue + // 0105 case 4): a genuine collision the source can't disambiguate. + // Emit a loud diagnostic and poison — never a silent first-/last-wins. + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name}); + return .unresolved; + }, }; } @@ -2854,7 +2937,7 @@ pub const Lowering = struct { // Block-local type declarations .struct_decl => |sd| { self.recordLocalTypeName(sd.name); - self.registerStructDecl(&sd); + self.registerStructDecl(&node.data.struct_decl); }, .enum_decl, .union_decl => { if (node.data.declName()) |dn| self.recordLocalTypeName(dn); @@ -13999,6 +14082,134 @@ pub const Lowering = struct { _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } + /// The `nominal_id` stamped on a nominal `TypeInfo` (0 for non-nominal / + /// structural). Reading it back lets a re-registration preserve the slot's + /// existing key when refreshing a forward-stubbed body. + fn nominalIdOf(info: types.TypeInfo) u32 { + return switch (info) { + .@"struct" => |s| s.nominal_id, + .@"enum" => |e| e.nominal_id, + .@"union" => |u| u.nominal_id, + .tagged_union => |u| u.nominal_id, + .error_set => |e| e.nominal_id, + else => 0, + }; + } + + /// Return `info` with its nominal arm's `nominal_id` set to `nid` (a no-op for + /// non-nominal infos). Used to build the key-matching body for + /// `updatePreservingKey` after a shadow author interned at a nonzero id. + fn stampNominalId(info: types.TypeInfo, nid: u32) types.TypeInfo { + var out = info; + switch (out) { + .@"struct" => |*s| s.nominal_id = nid, + .@"enum" => |*e| e.nominal_id = nid, + .@"union" => |*u| u.nominal_id = nid, + .tagged_union => |*u| u.nominal_id = nid, + .error_set => |*e| e.nominal_id = nid, + else => {}, + } + return out; + } + + /// 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 + /// body with `nominal_id` left 0 — this stamps the right id and records the + /// `decl_key → TypeId` map (`type_decl_tids`, the `fn_decl_fids` analogue). + /// + /// Shadow detection is gated on the IMPORT FACTS, not registration-time + /// source: only a name authored AS A NAMED TYPE by ≥2 distinct modules + /// (`nameHasMultipleTypeAuthors`) can produce a same-name shadow. A + /// single-author name keeps `nominal_id = 0` and adopts any forward-reference + /// stub (`findByName` orelse intern) — BYTE-IDENTICAL to pre-E2 registration, + /// and immune to the compiler re-registering one logical type from several + /// contexts (default-context emission, comptime eval) under a shifting + /// `current_source_file`. For a genuinely multi-authored name, the FIRST + /// source keeps id 0 and later sources get fresh ids → DISTINCT TypeIds, so + /// the authors no longer collapse last-wins (issue 0105). Idempotent per + /// `decl_key`: a re-registration reuses the recorded slot, refreshing its body. + fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo) TypeId { + const table = &self.module.types; + // Same decl seen again → reuse its slot + nominal id, refresh the body. + if (table.type_decl_tids.get(decl_key)) |existing_id| { + table.updatePreservingKey(existing_id, stampNominalId(info, nominalIdOf(table.get(existing_id)))); + return existing_id; + } + const nominal_id: u32 = self.shadowNominalId(name_id); + const id = if (nominal_id == 0) + (table.findByName(name_id) orelse table.internNominal(info, 0)) + else + table.internNominal(info, nominal_id); + table.updatePreservingKey(id, stampNominalId(info, nominal_id)); + table.type_decl_tids.put(decl_key, id) catch {}; + return id; + } + + /// 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 + /// 0, each later source gets a fresh monotonic id. Gating on the import facts + /// keeps the single-author path at id 0 (byte-identical) even when one logical + /// type is re-registered from several `current_source_file` contexts. + fn shadowNominalId(self: *Lowering, name_id: types.StringId) u32 { + if (!self.nameHasMultipleTypeAuthors(self.module.types.getString(name_id))) return 0; + const src = self.current_source_file orelse self.main_file orelse ""; + const gop = self.nominal_name_authors.getOrPut(name_id) catch return 0; + if (!gop.found_existing) { + gop.value_ptr.* = src; + return 0; + } + if (std.mem.eql(u8, gop.value_ptr.*, src)) return 0; + self.next_nominal_id += 1; + return self.next_nominal_id; + } + + /// TRUE iff `name` is authored AS A NAMED TYPE (struct / enum / union / + /// error-set / protocol / foreign class) by ≥2 DISTINCT modules in the import + /// raw facts — the authoritative same-name-shadow signal (the only case where + /// distinct `nominal_id`s are needed). Module distinctness is by LEXICALLY + /// NORMALIZED path: one logical file reached through several spellings + /// (`testpkg/../allocators.sx` vs `allocators.sx`) is cached — and so parsed — + /// twice, landing two `module_decls` entries with two decl pointers for the + /// SAME source; normalizing collapses them to one author, NOT a false shadow. + /// False when the facts are unwired (comptime / registration host with no + /// `module_decls`): the single-author path applies, correct there. + fn nameHasMultipleTypeAuthors(self: *Lowering, name: []const u8) bool { + const decls = self.program_index.module_decls orelse return false; + var first_norm: ?[]const u8 = null; + defer if (first_norm) |f| self.alloc.free(f); + var it = decls.iterator(); + while (it.next()) |entry| { + const m = entry.value_ptr; + const ref = m.names.get(name) orelse continue; + if (rawNamedTypePtr(ref) == null) continue; + const norm = std.fs.path.resolvePosix(self.alloc, &.{entry.key_ptr.*}) catch continue; + if (first_norm) |f| { + defer self.alloc.free(norm); + if (!std.mem.eql(u8, f, norm)) return true; + } else { + first_norm = norm; + } + } + return false; + } + + /// The opaque decl-pointer identity of a NAMED-type `RawDeclRef`, or null when + /// the ref is not a named type (fn / value-const / namespace alias). Used to + /// de-dup same-name authors by decl identity. + fn rawNamedTypePtr(ref: resolver_mod.RawDeclRef) ?*const anyopaque { + return switch (ref) { + .struct_decl => |d| @ptrCast(d), + .enum_decl => |d| @ptrCast(d), + .union_decl => |d| @ptrCast(d), + .error_set_decl => |d| @ptrCast(d), + .protocol_decl => |d| @ptrCast(d), + .foreign_class_decl => |d| @ptrCast(d), + .fn_decl, .const_decl, .namespace_decl => null, + }; + } + fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void { const table = &self.module.types; const name_id = table.internString(sd.name); @@ -14115,11 +14326,13 @@ pub const Lowering = struct { } } - // Check if a forward-reference placeholder already exists (with empty fields) - // If so, update it in-place rather than creating a duplicate + // Register under a per-decl nominal identity (E2). A forward-reference + // placeholder (empty-field stub) is adopted in place; a same-name struct + // authored in a DIFFERENT source gets its own distinct TypeId instead of + // last-wins clobbering the first (issue 0105). `&decl.data.struct_decl` + // (the stable import-raw-facts pointer) is the identity key. const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; - const id = if (table.findByName(name_id)) |existing| existing else table.internNominal(info, 0); - table.updatePreservingKey(id, info); + _ = self.internNamedTypeDecl(@ptrCast(sd), name_id, info); // Store field defaults for struct literal lowering if (sd.field_defaults.len > 0) { diff --git a/src/ir/types.test.zig b/src/ir/types.test.zig index 7d0375f..f252b4f 100644 --- a/src/ir/types.test.zig +++ b/src/ir/types.test.zig @@ -474,13 +474,14 @@ test "phase D: findUniqueByName returns the sole match" { try std.testing.expectEqual(id, table.findUniqueByName(foo).?); } -test "phase D: type_decl_tids maps decl node to TypeId" { +test "phase D: type_decl_tids maps decl pointer to TypeId" { const alloc = std.testing.allocator; var table = TypeTable.init(alloc); defer table.deinit(); const id = table.internNominal(.{ .@"struct" = .{ .name = table.internString("Node1"), .fields = &.{} } }, 0); var node = ast.Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 0 } } }; - try table.type_decl_tids.put(&node, id); - try std.testing.expectEqual(id, table.type_decl_tids.get(&node).?); + const key: *const anyopaque = @ptrCast(&node); + try table.type_decl_tids.put(key, id); + try std.testing.expectEqual(id, table.type_decl_tids.get(key).?); } diff --git a/src/ir/types.zig b/src/ir/types.zig index 460f3c6..8bc2836 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -332,11 +332,15 @@ pub const TypeTable = struct { tags: TagRegistry, /// Maps TypeInfo → TypeId for dedup of structural types intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80), - /// Stable nominal identity: type-decl AST node → its TypeId. The - /// `fn_decl_fids` analogue — one entry per declaring node, so two - /// same-display-name declarations resolve to distinct TypeIds via their - /// own node pointer. Populated by the resolver when it assigns nominal ids. - type_decl_tids: std.AutoHashMap(*const ast.Node, TypeId), + /// Stable nominal identity: the declaring decl's pointer → its TypeId. The + /// `fn_decl_fids` analogue — one entry per declaring decl, so two + /// same-display-name declarations resolve to distinct TypeIds via their own + /// decl pointer. Keyed by the opaque `RawDeclRef` inner pointer (e.g. + /// `*const ast.StructDecl`) — the SAME pointer the import raw-facts hold and + /// `registerStructDecl` receives, so registration and resolution agree on + /// identity without threading the wrapping `ast.Node`. Populated by the + /// resolver (E2) as it assigns nominal ids. + type_decl_tids: std.AutoHashMap(*const anyopaque, TypeId), alloc: Allocator, /// Owns the element/param slices duped by the type constructors /// (`functionType*`, `closureType*`, `packType`). Freed wholesale in @@ -353,7 +357,7 @@ pub const TypeTable = struct { .strings = StringPool.init(alloc), .tags = TagRegistry.init(alloc), .intern_map = std.HashMap(TypeKey, TypeId, TypeKeyContext, 80).init(alloc), - .type_decl_tids = std.AutoHashMap(*const ast.Node, TypeId).init(alloc), + .type_decl_tids = std.AutoHashMap(*const anyopaque, TypeId).init(alloc), .alloc = alloc, .slice_arena = std.heap.ArenaAllocator.init(alloc), };