From b9a67d1042f11c739822fc69e32cb57b5acc5f03 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 23:55:46 +0300 Subject: [PATCH] fix(stdlib/E6a): adopt forward struct stub for recursive enum/union (E6A-1) attempt-1's per-decl enum/union register path panicked on any valid self- or mutually-referential top-level enum/union: a `*Name` field in the body is resolved through the stateless `type_resolver.resolveNamed`, which has no kind context and forward-stubs an as-yet-unregistered name as a STRUCT. `internNamedTypeDecl` then `findByName`-adopted that struct stub and called `updatePreservingKey`, whose kind-stability assert tripped on struct -> enum/union (types.zig:446). The corpus had no recursive enum/union, so the gate missed it. Fix: when the slot `findByName` returns is a wrong-kind forward struct placeholder (empty-fields struct) for an enum/union/tagged_union registration, re-key it in place (`replaceKeyedInfo`) under the same TypeId instead of `updatePreservingKey`. This mirrors how a self-ref struct adopts its own (same-kind) forward stub; the new helper `adoptsForwardStructStub` gates the re-key precisely to that case, so a struct adopting a struct stub and every non-recursive enum/union stay on the byte-identical `updatePreservingKey`/fresh-intern path. Regression 0799 (single-author): self-ref union linked cells (`next: *Node`), self-ref enum/tagged-union (`branch: *Tree`), and a mutual-ref pair (A holds *B, B holds *A); builds and walks each recursive link. Fail-before: panic at registerUnionDecl on eed2f99. Pass-after: exit 0, "union=7 enum=42 mutual=99". Gate: zig build && zig build test && run_examples.sh all exit 0 (538 passed, 0 failed; 0795-0798 + 0752-0794 + FFI byte-identical); m3te ios-sim build via the main binary exit 0. --- ...799-types-self-ref-recursive-enum-union.sx | 45 +++++++++++++++++++ ...9-types-self-ref-recursive-enum-union.exit | 1 + ...types-self-ref-recursive-enum-union.stderr | 1 + ...types-self-ref-recursive-enum-union.stdout | 1 + src/ir/lower.zig | 26 ++++++++++- 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 examples/0799-types-self-ref-recursive-enum-union.sx create mode 100644 examples/expected/0799-types-self-ref-recursive-enum-union.exit create mode 100644 examples/expected/0799-types-self-ref-recursive-enum-union.stderr create mode 100644 examples/expected/0799-types-self-ref-recursive-enum-union.stdout diff --git a/examples/0799-types-self-ref-recursive-enum-union.sx b/examples/0799-types-self-ref-recursive-enum-union.sx new file mode 100644 index 0000000..4a674d0 --- /dev/null +++ b/examples/0799-types-self-ref-recursive-enum-union.sx @@ -0,0 +1,45 @@ +// E6a (attempt-2) regression — RECURSIVE top-level enum/union via per-decl nominal +// identity. Three single-author shapes that reference a not-yet-interned name in a +// `*Name` field: +// * `Node` — a SELF-referential UNION (linked cells: `next: *Node`). +// * `Tree` — a SELF-referential ENUM/tagged-union (`branch: *Tree`). +// * `A`/`B` — a MUTUALLY-referential union pair (`A` holds `*B`, `B` holds `*A`). +// +// Pre-fix (eed2f99) the new per-decl register path built each enum/union body +// through the STATELESS `type_bridge` BEFORE a matching nominal slot existed, so a +// `*Name` field forward-created a STRUCT stub under `Name`; `internNamedTypeDecl` +// then refreshed that struct stub as an enum/union and tripped the kind-stability +// assert in `types.zig` `updatePreservingKey` — a hard panic (the corpus had no +// recursive enum/union, so the gate missed it). The fix adopts the forward struct +// stub IN PLACE (re-key to the real enum/union kind), mirroring how a self-ref +// struct adopts its own forward stub — so `*Node`/`*Tree`/`*B`/`*A` resolve to the +// genuine 8-byte-pointer nominal types and the recursive walks read through. +#import "modules/std.sx"; + +Node :: union { next: *Node; value: s32; } +Tree :: enum { leaf: s32; branch: *Tree; } +A :: union { b: *B; tag: s32; } +B :: union { a: *A; val: s32; } + +main :: () -> s32 { + // Self-ref union: two-hop walk to the tail cell's value. + n2 : Node = ---; + n2.value = 7; + n1 : Node = ---; + n1.next = @n2; + n0 : Node = ---; + n0.next = @n1; + + // Self-ref enum: a branch whose payload pointer derefs to a leaf. + leaf_node : Tree = .leaf(42); + root : Tree = .branch(@leaf_node); + + // Mutual-ref pair: reach B's value through A's `*B`. + bv : B = ---; + bv.val = 99; + av : A = ---; + av.b = @bv; + + print("union={} enum={} mutual={}\n", n0.next.*.next.*.value, root.branch.*.leaf, av.b.*.val); + 0 +} diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.exit b/examples/expected/0799-types-self-ref-recursive-enum-union.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.stderr b/examples/expected/0799-types-self-ref-recursive-enum-union.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0799-types-self-ref-recursive-enum-union.stdout b/examples/expected/0799-types-self-ref-recursive-enum-union.stdout new file mode 100644 index 0000000..6b3ade7 --- /dev/null +++ b/examples/expected/0799-types-self-ref-recursive-enum-union.stdout @@ -0,0 +1 @@ +union=7 enum=42 mutual=99 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3d79504..4a54cf9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -15195,11 +15195,35 @@ pub const Lowering = struct { (table.findByName(name_id) orelse table.internNominal(info, 0)) else table.internNominal(info, nominal_id); - table.updatePreservingKey(id, stampNominalId(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