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.
This commit is contained in:
agra
2026-06-08 23:55:46 +03:00
parent eed2f99f76
commit b9a67d1042
5 changed files with 73 additions and 1 deletions

View File

@@ -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 structenum/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