diff --git a/examples/0757-modules-same-name-struct-self-ref.sx b/examples/0757-modules-same-name-struct-self-ref.sx new file mode 100644 index 0000000..bca1f60 --- /dev/null +++ b/examples/0757-modules-same-name-struct-self-ref.sx @@ -0,0 +1,16 @@ +// issue 0105 / F1 regression — a SELF-REFERENCE inside a same-name struct shadow. +// Two flat-imported modules each declare a top-level `Box`; module B's `Box` has +// a field `next: *Box` referencing its own name. The shadow must resolve that +// self-ref to ITS OWN nominal identity, not the first same-name author (A's +// `Box`), so `head.next.*.y` reads B's `y` (= 42). Proves the reserve-before- +// fields ordering: a shadow author's decl key is recorded before its fields are +// resolved, so a self / forward ref binds via `type_decl_tids`, never the global +// findByName first-author fallback. +#import "modules/std.sx"; +#import "0757-modules-same-name-struct-self-ref/a.sx"; +#import "0757-modules-same-name-struct-self-ref/b.sx"; + +main :: () -> s32 { + print("a={} b={}\n", a_box(), b_chain()); + 0 +} diff --git a/examples/0757-modules-same-name-struct-self-ref/a.sx b/examples/0757-modules-same-name-struct-self-ref/a.sx new file mode 100644 index 0000000..73db8d2 --- /dev/null +++ b/examples/0757-modules-same-name-struct-self-ref/a.sx @@ -0,0 +1,3 @@ +// Module A authors its OWN `Box` (one field `x`) — the FIRST same-name author. +Box :: struct { x: s64; } +a_box :: () -> Box { return Box.{ x = 7 }; } diff --git a/examples/0757-modules-same-name-struct-self-ref/b.sx b/examples/0757-modules-same-name-struct-self-ref/b.sx new file mode 100644 index 0000000..d2b558c --- /dev/null +++ b/examples/0757-modules-same-name-struct-self-ref/b.sx @@ -0,0 +1,12 @@ +// Module B authors a same-name `Box` shadow whose field SELF-REFERENCES its own +// name (`next: *Box`). Pre-fix the self-ref resolved to A's `Box` (registered +// first under the bare name), so `next.*.y` failed with "field 'y' not found on +// type 'Box'". The shadow's slot is now reserved BEFORE its fields resolve, so +// `*Box` binds to B's OWN nominal TypeId and the deref sees B's `y`. +Box :: struct { y: s64; next: *Box; } +b_chain :: () -> s64 { + tail := Box.{ y = 42, next = null }; + head := Box.{ y = 1, next = @tail }; + // Walk the self-referential link; reads B's own `y`, not A's `x`. + return head.next.*.y; +} diff --git a/examples/0758-modules-same-name-struct-mutual-ref.sx b/examples/0758-modules-same-name-struct-mutual-ref.sx new file mode 100644 index 0000000..2595b6c --- /dev/null +++ b/examples/0758-modules-same-name-struct-mutual-ref.sx @@ -0,0 +1,15 @@ +// issue 0105 / F1 regression — FORWARD + MUTUAL refs between same-name struct +// shadows. Two flat-imported modules each declare `Box` and `Node`; module B's +// `Box` forward-refs B's `Node` (declared later) and B's `Node` back-refs B's +// `Box`. Every cross-reference must bind to B's OWN nominal identities, proving +// the up-front genuine-shadow reservation: ALL of a genuine shadow's authors are +// reserved in `type_decl_tids` before any field resolves, so a forward / mutual +// ref never falls back to the global findByName first-author (A's `Node`). +#import "modules/std.sx"; +#import "0758-modules-same-name-struct-mutual-ref/a.sx"; +#import "0758-modules-same-name-struct-mutual-ref/b.sx"; + +main :: () -> s32 { + print("b={}\n", b_test()); + 0 +} diff --git a/examples/0758-modules-same-name-struct-mutual-ref/a.sx b/examples/0758-modules-same-name-struct-mutual-ref/a.sx new file mode 100644 index 0000000..f4869d4 --- /dev/null +++ b/examples/0758-modules-same-name-struct-mutual-ref/a.sx @@ -0,0 +1,3 @@ +// Module A authors its OWN `Box` and `Node` (the FIRST same-name authors). +Box :: struct { x: s64; } +Node :: struct { n: s64; } diff --git a/examples/0758-modules-same-name-struct-mutual-ref/b.sx b/examples/0758-modules-same-name-struct-mutual-ref/b.sx new file mode 100644 index 0000000..eb71b20 --- /dev/null +++ b/examples/0758-modules-same-name-struct-mutual-ref/b.sx @@ -0,0 +1,13 @@ +// Module B authors same-name `Box` and `Node` shadows that reference EACH OTHER. +// B's `Box` has a FORWARD ref to B's `Node` (declared after it), and B's `Node` +// back-refs B's `Box`. Both forward and mutual refs must resolve to B's OWN +// nominal TypeIds, not the first same-name authors in A. +Box :: struct { y: s64; peer: *Node; } +Node :: struct { m: s64; owner: *Box; } +b_test :: () -> s64 { + nd := Node.{ m = 99, owner = null }; + bx := Box.{ y = 7, peer = @nd }; + // Reads B's Node.m (99); pre-fix the forward ref bound to A's Node (which has + // field `n`, not `m`) → "field 'm' not found on type 'Node'". + return bx.peer.*.m; +} diff --git a/examples/expected/0757-modules-same-name-struct-self-ref.exit b/examples/expected/0757-modules-same-name-struct-self-ref.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0757-modules-same-name-struct-self-ref.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0757-modules-same-name-struct-self-ref.stderr b/examples/expected/0757-modules-same-name-struct-self-ref.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0757-modules-same-name-struct-self-ref.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0757-modules-same-name-struct-self-ref.stdout b/examples/expected/0757-modules-same-name-struct-self-ref.stdout new file mode 100644 index 0000000..5fdf763 --- /dev/null +++ b/examples/expected/0757-modules-same-name-struct-self-ref.stdout @@ -0,0 +1 @@ +a=Box{x: 7} b=42 diff --git a/examples/expected/0758-modules-same-name-struct-mutual-ref.exit b/examples/expected/0758-modules-same-name-struct-mutual-ref.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0758-modules-same-name-struct-mutual-ref.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0758-modules-same-name-struct-mutual-ref.stderr b/examples/expected/0758-modules-same-name-struct-mutual-ref.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0758-modules-same-name-struct-mutual-ref.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0758-modules-same-name-struct-mutual-ref.stdout b/examples/expected/0758-modules-same-name-struct-mutual-ref.stdout new file mode 100644 index 0000000..934809d --- /dev/null +++ b/examples/expected/0758-modules-same-name-struct-mutual-ref.stdout @@ -0,0 +1 @@ +b=99 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 7e7f054..9a82fc5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -863,6 +863,42 @@ pub const Lowering = struct { else => {}, } } + // Pass 0b: reserve every GENUINE same-name STRUCT shadow's DISTINCT nominal + // slot BEFORE the registration loop resolves any fields (E2/F1). A field + // type referencing a shadow name — self (`next: *Box`), or a forward / + // mutual ref to a shadow declared LATER in the same module (`peer: *Node`) + // — then binds to its OWN nominal TypeId via `type_decl_tids`, never the + // global findByName first-author fallback (issue 0105). + // + // "Genuine" = ≥2 DISTINCT struct decls in THIS scan author the name (so it + // needs ≥2 distinct nominal TypeIds). Gating on the scanned decls — NOT + // `nameHasMultipleTypeAuthors` (the raw import facts, which over-count one + // file reached via two un-normalized import spellings, e.g. `math/matrix44` + // pulled in twice) — keeps a single-real-decl name on the legacy id-0 path, + // byte-identical. ALL authors of a genuine shadow reserve, in declaration + // order: the FIRST at id 0, the rest at fresh nonzero ids, matching the + // per-decl registration order so the first-author-keeps-0 assignment holds. + var shadow_first = std.AutoHashMap(types.StringId, *const anyopaque).init(self.alloc); + defer shadow_first.deinit(); + var genuine_shadows = std.AutoHashMap(types.StringId, void).init(self.alloc); + defer genuine_shadows.deinit(); + for (decls) |decl| { + const sd = topLevelStructDecl(decl) orelse continue; + if (sd.type_params.len > 0) continue; + const nm = self.module.types.internString(sd.name); + const key: *const anyopaque = @ptrCast(sd); + const gop = shadow_first.getOrPut(nm) catch continue; + if (gop.found_existing) { + if (gop.value_ptr.* != key) genuine_shadows.put(nm, {}) catch {}; + } else gop.value_ptr.* = key; + } + for (decls) |decl| { + const sd = topLevelStructDecl(decl) orelse continue; + const nm = self.module.types.internString(sd.name); + if (!genuine_shadows.contains(nm)) continue; + self.setCurrentSourceFile(decl.source_file); + self.reserveShadowStructSlot(sd); + } for (decls) |decl| { self.setCurrentSourceFile(decl.source_file); const is_imported = if (self.main_file) |mf| @@ -14122,31 +14158,66 @@ pub const Lowering = struct { return out; } + /// The top-level STRUCT decl a top-level node authors (a bare `struct_decl`, or + /// a `Name :: struct {...}` const wrapper), or null. Used by the genuine-shadow + /// scan in `scanDecls` to enumerate same-name struct authors uniformly. + fn topLevelStructDecl(decl: *const Node) ?*const ast.StructDecl { + return switch (decl.data) { + .struct_decl => &decl.data.struct_decl, + .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null, + else => null, + }; + } + + /// Reserve a GENUINE same-name STRUCT shadow author's DISTINCT nominal slot + /// BEFORE any field resolves, so a self / forward / mutual reference to a shadow + /// name (`next: *Box`; `peer: *Node` where Node is a shadow declared later) + /// 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 {}; + } + /// 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). + /// 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). /// - /// 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 { + /// 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; - // Same decl seen again → reuse its slot + nominal id, refresh the body. + // 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 nominal_id: u32 = self.shadowNominalId(name_id); const id = if (nominal_id == 0) (table.findByName(name_id) orelse table.internNominal(info, 0)) else @@ -14282,6 +14353,20 @@ pub const Lowering = struct { 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; @@ -14336,13 +14421,13 @@ pub const Lowering = struct { } } - // 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. + // 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(@ptrCast(sd), name_id, info); + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); // Store field defaults for struct literal lowering if (sd.field_defaults.len > 0) {