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.
This commit is contained in:
343
src/ir/lower.zig
343
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 :: <type>`, 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) {
|
||||
|
||||
@@ -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).?);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user