Files
sx/issues/0139-byvalue-self-reference-segfault.md
agra 2f0905b407 fix(0139): reject by-value self-referential types loudly (was a segfault)
A nominal aggregate that contains itself (or a mutual peer) BY VALUE has no
finite layout and infinite-recursed typeSizeBytes into a stack overflow —
for SOURCE enums/structs as well as comptime-constructed types.

New `checkInfiniteSize` pass (lower/decl.zig, Pass 1g — after type
registration, before body lowering): walks the by-VALUE containment graph
(pointer/slice/optional payloads break the cycle, so `*Self` stays valid);
on a back-edge it emits a loud diagnostic — "type 'X' is infinitely sized
(it contains itself by value); use a pointer ('*X') to break the cycle" —
and poisons the offending field to `.unresolved` so sizing can't recurse
before the build halts on the error. Covers source + declare/define types,
direct + mutual recursion.

examples/1178 locks the diagnostic; issue 0139 marked RESOLVED. This also
completes METATYPE PLAN F5's by-value-self-reference rejection. Full suite
green (675).
2026-06-16 22:24:31 +03:00

3.5 KiB

0139 — by-value self-referential type segfaults (typeSizeBytes infinite recursion)

RESOLVED. Root cause: typeSizeBytes (and the layout path) recursed into each by-value aggregate field with no cycle guard, so a by-value self/mutual reference looped to a stack overflow. Fix: a new checkInfiniteSize pass (src/ir/lower/decl.zig, Pass 1g — after type registration, before body lowering) walks the by-VALUE containment graph; on a back-edge it emits a loud diagnostic (type 'X' is infinitely sized (it contains itself by value); use a pointer ('*X') to break the cycle) and poisons the offending field to .unresolved, breaking the recursion before any sizing runs. A pointer / slice / optional payload breaks the cycle, so *Self recursion stays valid. Covers both source decls and comptime-constructed (declare/define) types. Regression test: examples/1178-diagnostics-infinite-size-self-reference.sx.

Symptom — a type whose field/variant payload is ITSELF by value (not behind a pointer) crashes the compiler with a stack-overflow segfault instead of a loud "infinite size" diagnostic. Observed: Segmentation fault inside src/ir/types.zig:typeSizeBytes (unbounded self-recursion through the field loop). Expected: a clean compile error naming the offending type and suggesting *Self.

Pre-existing / scope — NOT specific to the comptime declare/define metaprogramming; a hand-written SOURCE enum reproduces it identically. So the fix belongs in the general type-system size/layout path, and the comptime construction path (METATYPE F5) inherits the protection for free once it lands.

Reproduction (source enum — no metaprogramming needed):

#import "modules/std.sx";

Bad :: enum {
    node: Bad;     // by-VALUE self-reference → infinite size
    leaf;
}

main :: () -> i32 {
    x : Bad = .leaf;
    return 0;
}

Same crash via the metaprogramming path (#import "modules/std/meta.sx"):

make_bad :: () -> Type {
    h := declare("Bad");
    return define(h, .enum(.{ variants = .[
        EnumVariant.{ name = "node", payload = Bad },   // by-value self-ref
        EnumVariant.{ name = "leaf", payload = void } ] }));
}
Bad :: make_bad();

Investigation prompt — A by-value self-referential (or mutually-recursive) aggregate has infinite size and must be rejected loudly, not recursed into forever. The crash is in src/ir/types.zig:typeSizeBytes (~line 736), which recurses into each struct/tagged-union field's type with no cycle guard; a field typed as its own (or an enclosing) nominal type recurses unboundedly → stack overflow. Fix: detect the cycle — walk the nominal-type dependency with a visited set (or a recursion-depth / on-stack-nominal guard), and when a nominal type reaches itself by value (no pointer indirection on the path), emit a diagnostic via self.diagnostics.addFmt(.err, span, "type '{s}' is infinitely sized (it contains itself by value); use a pointer (*{s}) to break the cycle", …) and return a sentinel size (e.g. 0 with the error already raised, or poison) rather than recursing. A pointer field (*Bad) breaks the cycle and must stay allowed (pointers have a fixed size and don't recurse into the pointee). Verification: run the repro above — expect the loud diagnostic + non-zero exit, NOT a segfault. Add an examples/11xx-diagnostics-* (or issues/) pin once the message is settled. This also closes METATYPE PLAN F5's "by-VALUE self-reference rejected" item for the comptime path.