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).
72 lines
3.5 KiB
Markdown
72 lines
3.5 KiB
Markdown
# 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):
|
|
|
|
```sx
|
|
#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"`):
|
|
|
|
```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.
|