From 5928d9f0670d5e8cbaf45e052c6f666b33704104 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 13:51:15 +0300 Subject: [PATCH] refactor(B4.2): move nominal-type registration to lower/nominal.zig Verbatim relocation of the 23-method nominal-type cluster (struct/enum/ union/error-set registration, anon-type qualification, nominal-id stamping, shadow-slot reservation, named-type interning, generic struct templates + alias registration) plus the nested ShadowTypeDecl union into src/ir/lower/nominal.zig. 23 aliases on Lowering keep all call sites unchanged. Method pub-flip: instantiateGenericStruct. nominal.zig reaches VisibleStructAuthor and structDeclOfRaw (both relocated to decl.zig in B4.1) via Lowering-namespace alias consts. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn. --- src/ir/lower.zig | 723 ++----------------------------------- src/ir/lower/nominal.zig | 746 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 773 insertions(+), 696 deletions(-) create mode 100644 src/ir/lower/nominal.zig diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0e73497..20e43af 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -38,6 +38,7 @@ const lower_comptime = @import("lower/comptime.zig"); const lower_stmt = @import("lower/stmt.zig"); const lower_control_flow = @import("lower/control_flow.zig"); const lower_decl = @import("lower/decl.zig"); +const lower_nominal = @import("lower/nominal.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -9263,7 +9264,7 @@ pub const Lowering = struct { } } - fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { + pub fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId { const table = &self.module.types; // Build mangled name dynamically: StructName__arg1_arg2 @@ -9666,701 +9667,6 @@ pub const Lowering = struct { // ── Type registration ─────────────────────────────────────────── - /// Register a struct declaration's fields and methods in the IR type table. - /// Register a `Foo :: error { A, B }` declaration as an error-set type. - /// Rejects an empty set here (sema gate) since type_bridge has no - /// diagnostics; non-empty sets are interned via type_bridge. - pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { - const esd = node.data.error_set_decl; - if (esd.tag_names.len == 0) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name}); - } - return; - } - _ = 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; - } - - /// 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) - /// binds to ITS nominal TypeId via `type_decl_tids` instead of the global - /// findByName first-author fallback (issue 0105 / F1). Called only from the - /// `scanDecls` genuine-shadow pass, which has already established that ≥2 - /// distinct struct decls author this name; ALL of them reserve — the FIRST at - /// id 0, the rest at fresh nonzero ids — so none falls through to the name-only - /// `findByName` (which, once a shadow is interned, no longer uniquely identifies - /// the first author). Idempotent per decl key: an already-reserved decl returns - /// before re-invoking `shadowNominalId`, so the shadow id is computed once. - /// Generic templates resolve lazily on instantiation and are skipped. - fn reserveShadowStructSlot(self: *Lowering, sd: *const ast.StructDecl) void { - if (sd.type_params.len > 0) return; - const table = &self.module.types; - const decl_key: *const anyopaque = @ptrCast(sd); - if (table.type_decl_tids.contains(decl_key)) return; - const name_id = table.internString(sd.name); - const nominal_id = self.shadowNominalId(name_id); // 0 for the first author, nonzero for the rest - const reserved = table.internNominal(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }, nominal_id); - 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, - - pub fn key(self: ShadowTypeDecl) *const anyopaque { - return switch (self) { - inline else => |p| @ptrCast(p), - }; - } - pub fn name(self: ShadowTypeDecl) []const u8 { - return switch (self) { - inline else => |p| p.name, - }; - } - pub 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. - pub 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. - pub 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 - /// body; `nominal_id` is the slot's identity (0 for a single / first author, - /// nonzero for a later same-name shadow) — computed once by the caller - /// (`registerStructDecl`), which reuses the id reserved up-front in `scanDecls` - /// for a genuine shadow (so its fields' self / forward / mutual refs already - /// resolved against it). This stamps the id and records the `decl_key → TypeId` - /// map (`type_decl_tids`, the `fn_decl_fids` analogue). - /// - /// A `nominal_id == 0` author adopts any forward-reference stub (`findByName` - /// orelse intern) — BYTE-IDENTICAL to pre-E2 registration. 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 — OR an up-front - /// shadow reservation — reuses the recorded slot, refreshing its body via - /// `updatePreservingKey` (key-stable because a struct's intern key is its - /// name + nominal id, not its fields). - fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo, nominal_id: u32) TypeId { - const table = &self.module.types; - // Slot already recorded (re-registration, or a reserve-before-fields shadow - // reservation) → 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 id = if (nominal_id == 0) - (table.findByName(name_id) orelse table.internNominal(info, 0)) - else - table.internNominal(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 - /// 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, - }; - } - - /// Build an owned generic-struct template (type params, field names, field - /// type nodes) for `sd`, pinned to its declaring `source_file`. The returned - /// template is heap-owned via `self.alloc`; callers register it under a bare - /// or namespace-qualified key. Null on OOM. - fn buildGenericStructTemplate(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) ?StructTemplate { - const owned_name = self.alloc.dupe(u8, sd.name) catch return null; - - const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return null; - for (sd.type_params, 0..) |tp, i| { - const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: { - const cname = tp.constraint.data.type_expr.name; - // "Type" or a protocol name → type param - break :blk std.mem.eql(u8, cname, "Type") or - self.program_index.protocol_decl_map.contains(cname) or - self.program_index.protocol_ast_map.contains(cname); - } else false); - tps[i] = .{ - .name = self.alloc.dupe(u8, tp.name) catch return null, - // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params. - // `..$Ts: []Type` (variadic) is a type-pack param. Only value - // params like $N: u32 are non-type. - .is_type_param = is_type_param, - .is_variadic = tp.is_variadic, - // Capture a value param's declared type name (`$K: u32` → - // "u32") so instantiation can range-check the folded arg. - .value_type = if (!is_type_param and tp.constraint.data == .type_expr) - (self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null) - else - null, - }; - } - - const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return null; - for (sd.field_names, 0..) |fn_str, i| { - fnames[i] = self.alloc.dupe(u8, fn_str) catch return null; - } - - // Field type nodes are *Node pointers into the AST; copy the slice of - // pointers (the nodes themselves are heap-allocated). - const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return null; - - return .{ - .name = owned_name, - .type_params = tps, - .field_names = fnames, - .field_type_nodes = ftype_nodes, - .source_file = source_file, - .decl = sd, - }; - } - - /// Select the generic struct template AUTHORED by namespace `alias`'s target - /// module (the `importer → alias → NamespaceTarget` edge), not the bare - /// last-wins `struct_template_map`. A qualified head `ns.Box(..)` must - /// instantiate ns's OWN `Box`, even when another module's same-name `Box` won - /// the bare map. Null when the alias is unknown in the current source or its - /// module authors no such generic struct — the caller then falls back to the - /// legacy bare lookup. - fn qualifiedStructTemplate(self: *Lowering, alias: []const u8, member: []const u8) ?StructTemplate { - const edges = self.program_index.namespace_edges orelse return null; - const from = self.current_source_file orelse return null; - const alias_map = edges.getPtr(from) orelse return null; - const target = alias_map.get(alias) orelse return null; - for (target.own_decls) |decl| { - // A top-level struct is authored either as a bare `struct_decl` node - // or a `const_decl` whose value is one (`Box :: struct($T){...}`). - const sd: *const ast.StructDecl = switch (decl.data) { - .struct_decl => |*s| s, - .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else continue, - else => continue, - }; - if (!std.mem.eql(u8, sd.name, member)) continue; - if (sd.type_params.len == 0) continue; - return self.buildGenericStructTemplate(sd, decl.source_file orelse target.target_module_path); - } - return null; - } - - /// TRUE iff `alias` is a KNOWN namespace in the current source but its target - /// module authors NO member named `member` at all. A qualified generic head - /// `a.Box(..)` whose namespace lacks `Box` must diagnose the missing member — - /// never silently fall back to the bare last-wins `struct_template_map` (which - /// would instantiate an unrelated module's same-name `Box`, E4 finding #2). - /// FALSE when `alias` is not a namespace at all (leave the caller's existing - /// non-namespace handling), or when the namespace DOES author `member` (a - /// generic struct → `qualifiedStructTemplate` already selected it; any other - /// kind → the type-fn / named-type arms handle it). - fn qualifiedMemberMissing(self: *Lowering, alias: []const u8, member: []const u8) bool { - const edges = self.program_index.namespace_edges orelse return false; - const from = self.current_source_file orelse return false; - const alias_map = edges.getPtr(from) orelse return false; - const target = alias_map.get(alias) orelse return false; - for (target.own_decls) |decl| { - const dn = decl.data.declName() orelse continue; - if (std.mem.eql(u8, dn, member)) return false; - } - return true; - } - - /// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` + - /// defining source) when that author is NOT the one the global last-wins - /// `struct_template_map` already holds — the E4 non-transitive selection for a - /// bare generic head / alias / static-method head whose visible author (own or - /// a single 1-hop flat import) is shadowed in the global map by a NON-visible - /// (≥2-flat-hop) same-name template (finding #1). Exposing the decl (not just a - /// rebuilt template) lets a static-method head source-pin the METHOD body too, - /// not only the type layout. Null — caller uses the global map unchanged - /// (byte-identical) — when: no source context; the single visible author IS the - /// canonical map author (the common single-author case, matched by source - /// file); or the visible picture is not a clean single generic-struct author - /// (own non-generic shadow, or ≥2 flat authors whose ambiguity `headTypeLeak` - /// has already diagnosed + poisoned before this is consulted). - fn bareVisibleStructDecl(self: *Lowering, name: []const u8) ?VisibleStructAuthor { - if (self.emitting_default_context) return null; - const from = self.current_source_file orelse return null; - const canon = self.program_index.struct_template_map.get(name) orelse return null; - const canon_src = canon.source_file orelse ""; - - var res_walk = self.resolver(); - const set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); - defer if (set.flat.len > 0) self.alloc.free(set.flat); - - // Own author wins — must be a generic struct to count. - if (set.own) |own| { - const sd = structDeclOfRaw(own.raw) orelse return null; // alias / fn / other → skip - if (sd.type_params.len == 0) return null; - if (std.mem.eql(u8, from, canon_src)) return null; - return .{ .sd = sd, .source = from }; - } - - // Single flat-import generic-struct author. - var picked: ?*const ast.StructDecl = null; - var picked_src: []const u8 = ""; - for (set.flat) |fa| { - const sd = structDeclOfRaw(fa.raw) orelse continue; - if (sd.type_params.len == 0) continue; - if (picked != null) return null; // ≥2 visible authors - picked = sd; - picked_src = fa.source; - } - const sd = picked orelse return null; - if (std.mem.eql(u8, picked_src, canon_src)) return null; - return .{ .sd = sd, .source = picked_src }; - } - - /// The rebuilt, source-pinned generic struct TEMPLATE of the single bare-VISIBLE - /// author (`bareVisibleStructDecl`) — instantiate this INSTEAD of the global - /// last-wins map entry. Null under the same conditions `bareVisibleStructDecl` - /// returns null (caller keeps the global map, byte-identical). - fn bareVisibleStructTemplate(self: *Lowering, name: []const u8) ?StructTemplate { - const v = self.bareVisibleStructDecl(name) orelse return null; - return self.buildGenericStructTemplate(v.sd, v.source); - } - - /// Instantiate a generic struct template and register the result under an - /// alias name (`Vec3 :: Vec(3, f32)` / `ABox :: a.Box(s64)`). Shared by the - /// `.call` and `.parameterized_type_expr` const-decl alias branches and the - /// qualified-head selection that precedes the bare `struct_template_map` - /// fallback in each. - pub fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void { - const inst_id = self.instantiateGenericStruct(tmpl, args); - const alias_name_id = self.module.types.internString(alias_name); - const inst_info = self.module.types.get(inst_id); - if (inst_info != .@"struct") return; - const alias_info: types.TypeInfo = .{ .@"struct" = .{ - .name = alias_name_id, - .fields = inst_info.@"struct".fields, - } }; - const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); - self.module.types.updatePreservingKey(alias_id, alias_info); - // A generic-struct instantiation alias IS a type author: route it through - // the unified writer so it lands in `type_aliases_by_source` and the - // bare-TYPE gate treats it like any other alias. - self.putTypeAlias(self.current_source_file, alias_name, alias_id); - // CP-3: the alias display name (`ABox`) is the struct type name a receiver - // typed `x: ABox` reports, so method dispatch on it looks up the instance - // maps under `ABox`. Mirror the mangled instance's template/bindings/author - // onto the alias name so an alias-typed receiver is a first-class dispatch - // instance (runs the selected author's body + bindings), not a dead end. - const inst_name = self.formatTypeName(inst_id); - if (self.struct_instance_author.get(inst_name)) |author_decl| { - const tmpl_name = self.struct_instance_template.get(inst_name) orelse return; - const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse return; - self.struct_instance_template.put(self.alloc.dupe(u8, alias_name) catch return, tmpl_name) catch {}; - self.struct_instance_bindings.put(self.alloc.dupe(u8, alias_name) catch return, bindings.*) catch {}; - self.struct_instance_author.put(self.alloc.dupe(u8, alias_name) catch return, author_decl) catch {}; - } - } - - pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { - const table = &self.module.types; - const name_id = table.internString(sd.name); - - // Generic structs: store as owned template, don't resolve fields yet - if (sd.type_params.len > 0) { - const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return; - self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {}; - - // S1.1 (additive): key the template by DeclId in parallel. Nothing - // reads this for selection yet; `struct_template_map` stays the live - // consumer. A template whose decl is not in the table (comptime / - // block-local registration with facts unwired) keeps only the - // name-keyed entry. - if (self.program_index.decl_table) |dt| { - if (dt.declIdForStructDecl(sd)) |id| { - self.program_index.struct_template_by_decl.put(id, tmpl) catch {}; - } - } - - // Register methods under "TemplateName.method" in fn_ast_map - for (sd.methods) |method_node| { - if (method_node.data == .fn_decl) { - const method_fd = &method_node.data.fn_decl; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; - self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; - } - } - return; - } - - // Per-decl nominal identity (E2). EACH author of a GENUINE same-name STRUCT - // shadow already reserved its distinct slot up-front in `scanDecls` (the - // first at id 0, the rest at nonzero ids), so a self / forward / mutual - // reference to the shadow name bound to ITS nominal TypeId via - // `type_decl_tids`, not the global findByName first-author fallback (issue - // 0105 / F1): reuse that reserved id. A single-author name (or a phantom - // over-counted by the raw import facts) was NOT reserved — it keeps id 0 and - // the legacy post-field registration, byte-identical to pre-F1. - // `shadowNominalId` here only fires for the non-scanDecls registration paths - // (comptime `lowerDecls`, block-local), where module facts are unwired so it - // returns 0. - const decl_key: *const anyopaque = @ptrCast(sd); - const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); - - // Build field list, expanding #using entries - var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - var field_idx: usize = 0; - var using_idx: usize = 0; - const total_explicit = sd.field_names.len; - while (field_idx < total_explicit or using_idx < sd.using_entries.len) { - // Insert #using fields at their declared positions - while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) { - const ue = sd.using_entries[using_idx]; - const used_name_id = table.internString(ue.type_name); - if (table.findByName(used_name_id)) |used_ty| { - const used_info = table.get(used_ty); - if (used_info == .@"struct") { - for (used_info.@"struct".fields) |f| { - fields.append(self.alloc, f) catch unreachable; - } - } - } - using_idx += 1; - } - if (field_idx < total_explicit) { - const field_ty = self.resolveType(sd.field_types[field_idx]); - fields.append(self.alloc, .{ - .name = table.internString(sd.field_names[field_idx]), - .ty = field_ty, - }) catch unreachable; - field_idx += 1; - } else break; - } - // Append remaining #using entries after all explicit fields - while (using_idx < sd.using_entries.len) { - const ue = sd.using_entries[using_idx]; - const used_name_id = table.internString(ue.type_name); - if (table.findByName(used_name_id)) |used_ty| { - const used_info = table.get(used_ty); - if (used_info == .@"struct") { - for (used_info.@"struct".fields) |f| { - fields.append(self.alloc, f) catch unreachable; - } - } - } - using_idx += 1; - } - - // Qualify inline __anon type names: __anon → StructName.field_name - for (sd.field_names, 0..) |fname, fi| { - if (fi < fields.items.len) { - const field_ty = fields.items[fi].ty; - if (!field_ty.isBuiltin()) { - self.qualifyAnonType(table, field_ty, sd.name, fname); - } - } - } - - // Register under the per-decl nominal identity computed above. A non-first - // shadow author's slot was already reserved before fields resolved, so this - // fills it (key-stable updatePreservingKey); a first / single author adopts - // any forward-reference stub. Same-name structs in DIFFERENT sources get - // distinct TypeIds instead of last-wins clobbering the first (issue 0105). - const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; - _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); - - // Store field defaults for struct literal lowering - if (sd.field_defaults.len > 0) { - var has_any_default = false; - for (sd.field_defaults) |d| { - if (d != null) { has_any_default = true; break; } - } - if (has_any_default) { - self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {}; - } - } - - // Register struct methods as StructName.method in fn_ast_map - for (sd.methods) |method_node| { - if (method_node.data == .fn_decl) { - const method_fd = &method_node.data.fn_decl; - // Build qualified name: StructName.method - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; - self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; - // Declare extern stub (body is lowered lazily on demand) - self.declareFunction(method_fd, qualified); - } - } - - // Register struct-level constants (e.g., GRAVITY :f32: 9.81) - for (sd.constants) |const_node| { - if (const_node.data == .const_decl) { - const cd = const_node.data.const_decl; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue; - const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null; - self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; - } - } - } - - /// 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. - pub 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`. - pub 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 { - const ti = table.get(ty); - switch (ti) { - .@"union" => |u| { - const old_name = table.getString(u.name); - if (!std.mem.eql(u8, old_name, "__anon")) return; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; - const qname_id = table.internString(qualified); - table.replaceKeyedInfo(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } }); - }, - .tagged_union => |u| { - const old_name = table.getString(u.name); - if (!std.mem.eql(u8, old_name, "__anon")) return; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; - const qname_id = table.internString(qualified); - // Rename variant payload structs: __anon.X → ParentStruct.field.X - for (u.fields) |f| { - if (!f.ty.isBuiltin()) { - const finfo = table.get(f.ty); - if (finfo == .@"struct") { - const sname = table.getString(finfo.@"struct".name); - if (std.mem.startsWith(u8, sname, "__anon.")) { - const suffix = sname["__anon".len..]; // .VariantName - const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue; - const sq_id = table.internString(sq); - table.replaceKeyedInfo(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } }); - } - } - } - } - table.replaceKeyedInfo(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } }); - }, - .@"enum" => |e| { - const old_name = table.getString(e.name); - if (!std.mem.eql(u8, old_name, "__anon")) return; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; - const qname_id = table.internString(qualified); - table.replaceKeyedInfo(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } }); - }, - .@"struct" => |s| { - const old_name = table.getString(s.name); - if (!std.mem.eql(u8, old_name, "__anon")) return; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; - const qname_id = table.internString(qualified); - table.replaceKeyedInfo(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } }); - }, - else => {}, - } - } - /// Register a protocol declaration as a struct type in the IR type table. /// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... } /// Non-inline protocols: { ctx: *void, __vtable: *void } @@ -13827,6 +13133,31 @@ pub const Lowering = struct { pub const dropModuleConst = lower_decl.dropModuleConst; pub const emitModuleConst = lower_decl.emitModuleConst; pub const emitPlaceholder = lower_decl.emitPlaceholder; + + // --- moved to lower/nominal.zig (lower_nominal) --- + pub const registerErrorSetDecl = lower_nominal.registerErrorSetDecl; + pub const registerStructDecl = lower_nominal.registerStructDecl; + pub const registerEnumDecl = lower_nominal.registerEnumDecl; + pub const registerUnionDecl = lower_nominal.registerUnionDecl; + pub const qualifyAnonType = lower_nominal.qualifyAnonType; + pub const nominalIdOf = lower_nominal.nominalIdOf; + pub const stampNominalId = lower_nominal.stampNominalId; + pub const reserveShadowStructSlot = lower_nominal.reserveShadowStructSlot; + pub const reserveShadowEnumSlot = lower_nominal.reserveShadowEnumSlot; + pub const reserveShadowUnionSlot = lower_nominal.reserveShadowUnionSlot; + pub const topLevelTypeDecl = lower_nominal.topLevelTypeDecl; + pub const reserveShadowSlot = lower_nominal.reserveShadowSlot; + pub const internNamedTypeDecl = lower_nominal.internNamedTypeDecl; + pub const adoptsForwardStructStub = lower_nominal.adoptsForwardStructStub; + pub const shadowNominalId = lower_nominal.shadowNominalId; + pub const nameHasMultipleTypeAuthors = lower_nominal.nameHasMultipleTypeAuthors; + pub const rawNamedTypePtr = lower_nominal.rawNamedTypePtr; + pub const buildGenericStructTemplate = lower_nominal.buildGenericStructTemplate; + pub const qualifiedStructTemplate = lower_nominal.qualifiedStructTemplate; + pub const qualifiedMemberMissing = lower_nominal.qualifiedMemberMissing; + pub const bareVisibleStructDecl = lower_nominal.bareVisibleStructDecl; + pub const bareVisibleStructTemplate = lower_nominal.bareVisibleStructTemplate; + pub const registerGenericStructAlias = lower_nominal.registerGenericStructAlias; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/nominal.zig b/src/ir/lower/nominal.zig new file mode 100644 index 0000000..d587f15 --- /dev/null +++ b/src/ir/lower/nominal.zig @@ -0,0 +1,746 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; +const VisibleStructAuthor = Lowering.VisibleStructAuthor; +const structDeclOfRaw = Lowering.structDeclOfRaw; + +/// Register a struct declaration's fields and methods in the IR type table. +/// Register a `Foo :: error { A, B }` declaration as an error-set type. +/// Rejects an empty set here (sema gate) since type_bridge has no +/// diagnostics; non-empty sets are interned via type_bridge. +pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { + const esd = node.data.error_set_decl; + if (esd.tag_names.len == 0) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name}); + } + return; + } + _ = 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. +pub 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. +pub 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; +} + +/// 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) +/// binds to ITS nominal TypeId via `type_decl_tids` instead of the global +/// findByName first-author fallback (issue 0105 / F1). Called only from the +/// `scanDecls` genuine-shadow pass, which has already established that ≥2 +/// distinct struct decls author this name; ALL of them reserve — the FIRST at +/// id 0, the rest at fresh nonzero ids — so none falls through to the name-only +/// `findByName` (which, once a shadow is interned, no longer uniquely identifies +/// the first author). Idempotent per decl key: an already-reserved decl returns +/// before re-invoking `shadowNominalId`, so the shadow id is computed once. +/// Generic templates resolve lazily on instantiation and are skipped. +pub fn reserveShadowStructSlot(self: *Lowering, sd: *const ast.StructDecl) void { + if (sd.type_params.len > 0) return; + const table = &self.module.types; + const decl_key: *const anyopaque = @ptrCast(sd); + if (table.type_decl_tids.contains(decl_key)) return; + const name_id = table.internString(sd.name); + const nominal_id = self.shadowNominalId(name_id); // 0 for the first author, nonzero for the rest + const reserved = table.internNominal(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }, nominal_id); + 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. +pub 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). +pub 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, + + pub fn key(self: ShadowTypeDecl) *const anyopaque { + return switch (self) { + inline else => |p| @ptrCast(p), + }; + } + pub fn name(self: ShadowTypeDecl) []const u8 { + return switch (self) { + inline else => |p| p.name, + }; + } + pub 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. +pub 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. +pub 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 +/// body; `nominal_id` is the slot's identity (0 for a single / first author, +/// nonzero for a later same-name shadow) — computed once by the caller +/// (`registerStructDecl`), which reuses the id reserved up-front in `scanDecls` +/// for a genuine shadow (so its fields' self / forward / mutual refs already +/// resolved against it). This stamps the id and records the `decl_key → TypeId` +/// map (`type_decl_tids`, the `fn_decl_fids` analogue). +/// +/// A `nominal_id == 0` author adopts any forward-reference stub (`findByName` +/// orelse intern) — BYTE-IDENTICAL to pre-E2 registration. 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 — OR an up-front +/// shadow reservation — reuses the recorded slot, refreshing its body via +/// `updatePreservingKey` (key-stable because a struct's intern key is its +/// name + nominal id, not its fields). +pub fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo, nominal_id: u32) TypeId { + const table = &self.module.types; + // Slot already recorded (re-registration, or a reserve-before-fields shadow + // reservation) → 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 id = if (nominal_id == 0) + (table.findByName(name_id) orelse table.internNominal(info, 0)) + else + table.internNominal(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. +pub 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 +/// 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. +pub 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. +pub 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. +pub 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, + }; +} + +/// Build an owned generic-struct template (type params, field names, field +/// type nodes) for `sd`, pinned to its declaring `source_file`. The returned +/// template is heap-owned via `self.alloc`; callers register it under a bare +/// or namespace-qualified key. Null on OOM. +pub fn buildGenericStructTemplate(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) ?StructTemplate { + const owned_name = self.alloc.dupe(u8, sd.name) catch return null; + + const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return null; + for (sd.type_params, 0..) |tp, i| { + const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: { + const cname = tp.constraint.data.type_expr.name; + // "Type" or a protocol name → type param + break :blk std.mem.eql(u8, cname, "Type") or + self.program_index.protocol_decl_map.contains(cname) or + self.program_index.protocol_ast_map.contains(cname); + } else false); + tps[i] = .{ + .name = self.alloc.dupe(u8, tp.name) catch return null, + // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params. + // `..$Ts: []Type` (variadic) is a type-pack param. Only value + // params like $N: u32 are non-type. + .is_type_param = is_type_param, + .is_variadic = tp.is_variadic, + // Capture a value param's declared type name (`$K: u32` → + // "u32") so instantiation can range-check the folded arg. + .value_type = if (!is_type_param and tp.constraint.data == .type_expr) + (self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null) + else + null, + }; + } + + const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return null; + for (sd.field_names, 0..) |fn_str, i| { + fnames[i] = self.alloc.dupe(u8, fn_str) catch return null; + } + + // Field type nodes are *Node pointers into the AST; copy the slice of + // pointers (the nodes themselves are heap-allocated). + const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return null; + + return .{ + .name = owned_name, + .type_params = tps, + .field_names = fnames, + .field_type_nodes = ftype_nodes, + .source_file = source_file, + .decl = sd, + }; +} + +/// Select the generic struct template AUTHORED by namespace `alias`'s target +/// module (the `importer → alias → NamespaceTarget` edge), not the bare +/// last-wins `struct_template_map`. A qualified head `ns.Box(..)` must +/// instantiate ns's OWN `Box`, even when another module's same-name `Box` won +/// the bare map. Null when the alias is unknown in the current source or its +/// module authors no such generic struct — the caller then falls back to the +/// legacy bare lookup. +pub fn qualifiedStructTemplate(self: *Lowering, alias: []const u8, member: []const u8) ?StructTemplate { + const edges = self.program_index.namespace_edges orelse return null; + const from = self.current_source_file orelse return null; + const alias_map = edges.getPtr(from) orelse return null; + const target = alias_map.get(alias) orelse return null; + for (target.own_decls) |decl| { + // A top-level struct is authored either as a bare `struct_decl` node + // or a `const_decl` whose value is one (`Box :: struct($T){...}`). + const sd: *const ast.StructDecl = switch (decl.data) { + .struct_decl => |*s| s, + .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else continue, + else => continue, + }; + if (!std.mem.eql(u8, sd.name, member)) continue; + if (sd.type_params.len == 0) continue; + return self.buildGenericStructTemplate(sd, decl.source_file orelse target.target_module_path); + } + return null; +} + +/// TRUE iff `alias` is a KNOWN namespace in the current source but its target +/// module authors NO member named `member` at all. A qualified generic head +/// `a.Box(..)` whose namespace lacks `Box` must diagnose the missing member — +/// never silently fall back to the bare last-wins `struct_template_map` (which +/// would instantiate an unrelated module's same-name `Box`, E4 finding #2). +/// FALSE when `alias` is not a namespace at all (leave the caller's existing +/// non-namespace handling), or when the namespace DOES author `member` (a +/// generic struct → `qualifiedStructTemplate` already selected it; any other +/// kind → the type-fn / named-type arms handle it). +pub fn qualifiedMemberMissing(self: *Lowering, alias: []const u8, member: []const u8) bool { + const edges = self.program_index.namespace_edges orelse return false; + const from = self.current_source_file orelse return false; + const alias_map = edges.getPtr(from) orelse return false; + const target = alias_map.get(alias) orelse return false; + for (target.own_decls) |decl| { + const dn = decl.data.declName() orelse continue; + if (std.mem.eql(u8, dn, member)) return false; + } + return true; +} + +/// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` + +/// defining source) when that author is NOT the one the global last-wins +/// `struct_template_map` already holds — the E4 non-transitive selection for a +/// bare generic head / alias / static-method head whose visible author (own or +/// a single 1-hop flat import) is shadowed in the global map by a NON-visible +/// (≥2-flat-hop) same-name template (finding #1). Exposing the decl (not just a +/// rebuilt template) lets a static-method head source-pin the METHOD body too, +/// not only the type layout. Null — caller uses the global map unchanged +/// (byte-identical) — when: no source context; the single visible author IS the +/// canonical map author (the common single-author case, matched by source +/// file); or the visible picture is not a clean single generic-struct author +/// (own non-generic shadow, or ≥2 flat authors whose ambiguity `headTypeLeak` +/// has already diagnosed + poisoned before this is consulted). +pub fn bareVisibleStructDecl(self: *Lowering, name: []const u8) ?VisibleStructAuthor { + if (self.emitting_default_context) return null; + const from = self.current_source_file orelse return null; + const canon = self.program_index.struct_template_map.get(name) orelse return null; + const canon_src = canon.source_file orelse ""; + + var res_walk = self.resolver(); + const set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); + + // Own author wins — must be a generic struct to count. + if (set.own) |own| { + const sd = structDeclOfRaw(own.raw) orelse return null; // alias / fn / other → skip + if (sd.type_params.len == 0) return null; + if (std.mem.eql(u8, from, canon_src)) return null; + return .{ .sd = sd, .source = from }; + } + + // Single flat-import generic-struct author. + var picked: ?*const ast.StructDecl = null; + var picked_src: []const u8 = ""; + for (set.flat) |fa| { + const sd = structDeclOfRaw(fa.raw) orelse continue; + if (sd.type_params.len == 0) continue; + if (picked != null) return null; // ≥2 visible authors + picked = sd; + picked_src = fa.source; + } + const sd = picked orelse return null; + if (std.mem.eql(u8, picked_src, canon_src)) return null; + return .{ .sd = sd, .source = picked_src }; +} + +/// The rebuilt, source-pinned generic struct TEMPLATE of the single bare-VISIBLE +/// author (`bareVisibleStructDecl`) — instantiate this INSTEAD of the global +/// last-wins map entry. Null under the same conditions `bareVisibleStructDecl` +/// returns null (caller keeps the global map, byte-identical). +pub fn bareVisibleStructTemplate(self: *Lowering, name: []const u8) ?StructTemplate { + const v = self.bareVisibleStructDecl(name) orelse return null; + return self.buildGenericStructTemplate(v.sd, v.source); +} + +/// Instantiate a generic struct template and register the result under an +/// alias name (`Vec3 :: Vec(3, f32)` / `ABox :: a.Box(s64)`). Shared by the +/// `.call` and `.parameterized_type_expr` const-decl alias branches and the +/// qualified-head selection that precedes the bare `struct_template_map` +/// fallback in each. +pub fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void { + const inst_id = self.instantiateGenericStruct(tmpl, args); + const alias_name_id = self.module.types.internString(alias_name); + const inst_info = self.module.types.get(inst_id); + if (inst_info != .@"struct") return; + const alias_info: types.TypeInfo = .{ .@"struct" = .{ + .name = alias_name_id, + .fields = inst_info.@"struct".fields, + } }; + const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); + self.module.types.updatePreservingKey(alias_id, alias_info); + // A generic-struct instantiation alias IS a type author: route it through + // the unified writer so it lands in `type_aliases_by_source` and the + // bare-TYPE gate treats it like any other alias. + self.putTypeAlias(self.current_source_file, alias_name, alias_id); + // CP-3: the alias display name (`ABox`) is the struct type name a receiver + // typed `x: ABox` reports, so method dispatch on it looks up the instance + // maps under `ABox`. Mirror the mangled instance's template/bindings/author + // onto the alias name so an alias-typed receiver is a first-class dispatch + // instance (runs the selected author's body + bindings), not a dead end. + const inst_name = self.formatTypeName(inst_id); + if (self.struct_instance_author.get(inst_name)) |author_decl| { + const tmpl_name = self.struct_instance_template.get(inst_name) orelse return; + const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse return; + self.struct_instance_template.put(self.alloc.dupe(u8, alias_name) catch return, tmpl_name) catch {}; + self.struct_instance_bindings.put(self.alloc.dupe(u8, alias_name) catch return, bindings.*) catch {}; + self.struct_instance_author.put(self.alloc.dupe(u8, alias_name) catch return, author_decl) catch {}; + } +} + +pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void { + const table = &self.module.types; + const name_id = table.internString(sd.name); + + // Generic structs: store as owned template, don't resolve fields yet + if (sd.type_params.len > 0) { + const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return; + self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {}; + + // S1.1 (additive): key the template by DeclId in parallel. Nothing + // reads this for selection yet; `struct_template_map` stays the live + // consumer. A template whose decl is not in the table (comptime / + // block-local registration with facts unwired) keeps only the + // name-keyed entry. + if (self.program_index.decl_table) |dt| { + if (dt.declIdForStructDecl(sd)) |id| { + self.program_index.struct_template_by_decl.put(id, tmpl) catch {}; + } + } + + // Register methods under "TemplateName.method" in fn_ast_map + for (sd.methods) |method_node| { + if (method_node.data == .fn_decl) { + const method_fd = &method_node.data.fn_decl; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; + self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; + } + } + return; + } + + // Per-decl nominal identity (E2). EACH author of a GENUINE same-name STRUCT + // shadow already reserved its distinct slot up-front in `scanDecls` (the + // first at id 0, the rest at nonzero ids), so a self / forward / mutual + // reference to the shadow name bound to ITS nominal TypeId via + // `type_decl_tids`, not the global findByName first-author fallback (issue + // 0105 / F1): reuse that reserved id. A single-author name (or a phantom + // over-counted by the raw import facts) was NOT reserved — it keeps id 0 and + // the legacy post-field registration, byte-identical to pre-F1. + // `shadowNominalId` here only fires for the non-scanDecls registration paths + // (comptime `lowerDecls`, block-local), where module facts are unwired so it + // returns 0. + const decl_key: *const anyopaque = @ptrCast(sd); + const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id); + + // Build field list, expanding #using entries + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + var field_idx: usize = 0; + var using_idx: usize = 0; + const total_explicit = sd.field_names.len; + while (field_idx < total_explicit or using_idx < sd.using_entries.len) { + // Insert #using fields at their declared positions + while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) { + const ue = sd.using_entries[using_idx]; + const used_name_id = table.internString(ue.type_name); + if (table.findByName(used_name_id)) |used_ty| { + const used_info = table.get(used_ty); + if (used_info == .@"struct") { + for (used_info.@"struct".fields) |f| { + fields.append(self.alloc, f) catch unreachable; + } + } + } + using_idx += 1; + } + if (field_idx < total_explicit) { + const field_ty = self.resolveType(sd.field_types[field_idx]); + fields.append(self.alloc, .{ + .name = table.internString(sd.field_names[field_idx]), + .ty = field_ty, + }) catch unreachable; + field_idx += 1; + } else break; + } + // Append remaining #using entries after all explicit fields + while (using_idx < sd.using_entries.len) { + const ue = sd.using_entries[using_idx]; + const used_name_id = table.internString(ue.type_name); + if (table.findByName(used_name_id)) |used_ty| { + const used_info = table.get(used_ty); + if (used_info == .@"struct") { + for (used_info.@"struct".fields) |f| { + fields.append(self.alloc, f) catch unreachable; + } + } + } + using_idx += 1; + } + + // Qualify inline __anon type names: __anon → StructName.field_name + for (sd.field_names, 0..) |fname, fi| { + if (fi < fields.items.len) { + const field_ty = fields.items[fi].ty; + if (!field_ty.isBuiltin()) { + self.qualifyAnonType(table, field_ty, sd.name, fname); + } + } + } + + // Register under the per-decl nominal identity computed above. A non-first + // shadow author's slot was already reserved before fields resolved, so this + // fills it (key-stable updatePreservingKey); a first / single author adopts + // any forward-reference stub. Same-name structs in DIFFERENT sources get + // distinct TypeIds instead of last-wins clobbering the first (issue 0105). + const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); + + // Store field defaults for struct literal lowering + if (sd.field_defaults.len > 0) { + var has_any_default = false; + for (sd.field_defaults) |d| { + if (d != null) { has_any_default = true; break; } + } + if (has_any_default) { + self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {}; + } + } + + // Register struct methods as StructName.method in fn_ast_map + for (sd.methods) |method_node| { + if (method_node.data == .fn_decl) { + const method_fd = &method_node.data.fn_decl; + // Build qualified name: StructName.method + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue; + self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; + // Declare extern stub (body is lowered lazily on demand) + self.declareFunction(method_fd, qualified); + } + } + + // Register struct-level constants (e.g., GRAVITY :f32: 9.81) + for (sd.constants) |const_node| { + if (const_node.data == .const_decl) { + const cd = const_node.data.const_decl; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue; + const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null; + self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; + } + } +} + +/// 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. +pub 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`. +pub 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 +pub fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void { + const ti = table.get(ty); + switch (ti) { + .@"union" => |u| { + const old_name = table.getString(u.name); + if (!std.mem.eql(u8, old_name, "__anon")) return; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; + const qname_id = table.internString(qualified); + table.replaceKeyedInfo(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } }); + }, + .tagged_union => |u| { + const old_name = table.getString(u.name); + if (!std.mem.eql(u8, old_name, "__anon")) return; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; + const qname_id = table.internString(qualified); + // Rename variant payload structs: __anon.X → ParentStruct.field.X + for (u.fields) |f| { + if (!f.ty.isBuiltin()) { + const finfo = table.get(f.ty); + if (finfo == .@"struct") { + const sname = table.getString(finfo.@"struct".name); + if (std.mem.startsWith(u8, sname, "__anon.")) { + const suffix = sname["__anon".len..]; // .VariantName + const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue; + const sq_id = table.internString(sq); + table.replaceKeyedInfo(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } }); + } + } + } + } + table.replaceKeyedInfo(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } }); + }, + .@"enum" => |e| { + const old_name = table.getString(e.name); + if (!std.mem.eql(u8, old_name, "__anon")) return; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; + const qname_id = table.internString(qualified); + table.replaceKeyedInfo(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } }); + }, + .@"struct" => |s| { + const old_name = table.getString(s.name); + if (!std.mem.eql(u8, old_name, "__anon")) return; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return; + const qname_id = table.internString(qualified); + table.replaceKeyedInfo(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } }); + }, + else => {}, + } +}