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).
This commit is contained in:
16
examples/1178-diagnostics-infinite-size-self-reference.sx
Normal file
16
examples/1178-diagnostics-infinite-size-self-reference.sx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Diagnostic: a type that contains ITSELF by value has no finite size and must
|
||||||
|
// be rejected loudly (not infinite-loop the size computation into a crash). A
|
||||||
|
// pointer payload (`*Tree`) would break the cycle and is the fix the message
|
||||||
|
// suggests. Covers both source decls and comptime-constructed types — this is
|
||||||
|
// the source form (regression for issue 0139).
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Tree :: enum {
|
||||||
|
node: Tree; // by-VALUE self-reference → infinitely sized
|
||||||
|
leaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
t : Tree = .leaf;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
error: type 'Tree' is infinitely sized (it contains itself by value); use a pointer ('*Tree') to break the cycle
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# 0139 — by-value self-referential type segfaults (`typeSizeBytes` infinite recursion)
|
# 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
|
**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
|
a pointer) crashes the compiler with a stack-overflow segfault instead of a loud
|
||||||
"infinite size" diagnostic. Observed: `Segmentation fault` inside
|
"infinite size" diagnostic. Observed: `Segmentation fault` inside
|
||||||
|
|||||||
@@ -1642,6 +1642,10 @@ pub const Lowering = struct {
|
|||||||
pub const ensureTerminator = lower_control_flow.ensureTerminator;
|
pub const ensureTerminator = lower_control_flow.ensureTerminator;
|
||||||
|
|
||||||
// --- moved to lower/decl.zig (lower_decl) ---
|
// --- moved to lower/decl.zig (lower_decl) ---
|
||||||
|
pub const checkInfiniteSize = lower_decl.checkInfiniteSize;
|
||||||
|
pub const dfsByValueCycle = lower_decl.dfsByValueCycle;
|
||||||
|
pub const poisonAggregateField = lower_decl.poisonAggregateField;
|
||||||
|
pub const diagInfiniteSize = lower_decl.diagInfiniteSize;
|
||||||
pub const SelectedFunc = lower_decl.SelectedFunc;
|
pub const SelectedFunc = lower_decl.SelectedFunc;
|
||||||
pub const BareCallee = lower_decl.BareCallee;
|
pub const BareCallee = lower_decl.BareCallee;
|
||||||
pub const VisibleStructAuthor = lower_decl.VisibleStructAuthor;
|
pub const VisibleStructAuthor = lower_decl.VisibleStructAuthor;
|
||||||
|
|||||||
@@ -38,6 +38,103 @@ const topLevelTypeDecl = Lowering.topLevelTypeDecl;
|
|||||||
const isFloat = Lowering.isFloat;
|
const isFloat = Lowering.isFloat;
|
||||||
const isPackFn = Lowering.isPackFn;
|
const isPackFn = Lowering.isPackFn;
|
||||||
|
|
||||||
|
/// Reject infinitely-sized types: a nominal aggregate (struct / enum-with-payload
|
||||||
|
/// / union) that contains ITSELF — or a mutual peer — BY VALUE has no finite
|
||||||
|
/// layout, and would otherwise infinite-recurse `typeSizeBytes` into a stack
|
||||||
|
/// overflow. Walk the by-VALUE containment graph (a pointer / slice / optional
|
||||||
|
/// payload is finite-size and breaks the cycle, so `*Self` recursion is fine);
|
||||||
|
/// on a back-edge, emit a loud diagnostic and POISON the offending field to
|
||||||
|
/// `.unresolved`, breaking the cycle so later sizing can't crash before the
|
||||||
|
/// build halts on the error. Covers both source decls and comptime-constructed
|
||||||
|
/// (`declare`/`define`) types.
|
||||||
|
pub fn checkInfiniteSize(self: *Lowering) void {
|
||||||
|
const n = self.module.types.infos.items.len;
|
||||||
|
if (n == 0) return;
|
||||||
|
const color = self.alloc.alloc(u8, n) catch return; // 0=white 1=gray 2=black
|
||||||
|
defer self.alloc.free(color);
|
||||||
|
@memset(color, 0);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < n) : (i += 1) {
|
||||||
|
if (color[i] == 0) self.dfsByValueCycle(TypeId.fromIndex(@intCast(i)), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dfsByValueCycle(self: *Lowering, tid: TypeId, color: []u8) void {
|
||||||
|
const idx = tid.index();
|
||||||
|
if (idx >= color.len) return;
|
||||||
|
color[idx] = 1; // gray (on the current containment path)
|
||||||
|
if (byValueAggregateFields(&self.module.types, tid)) |fields| {
|
||||||
|
for (fields, 0..) |f, k| {
|
||||||
|
if (!isByValueAggregate(&self.module.types, f.ty)) continue; // pointer/slice/etc. break the cycle
|
||||||
|
const fidx = f.ty.index();
|
||||||
|
if (fidx >= color.len) continue;
|
||||||
|
if (color[fidx] == 1) {
|
||||||
|
// Back-edge: `f.ty` is on the current path → infinitely sized.
|
||||||
|
self.diagInfiniteSize(f.ty);
|
||||||
|
self.poisonAggregateField(tid, k);
|
||||||
|
} else if (color[fidx] == 0) {
|
||||||
|
self.dfsByValueCycle(f.ty, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color[idx] = 2; // black (fully explored)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The by-value fields of a nominal aggregate, or null for any other type.
|
||||||
|
fn byValueAggregateFields(table: *const types.TypeTable, tid: TypeId) ?[]const types.TypeInfo.StructInfo.Field {
|
||||||
|
if (tid.isBuiltin()) return null;
|
||||||
|
return switch (table.get(tid)) {
|
||||||
|
.@"struct" => |s| s.fields,
|
||||||
|
.tagged_union => |u| u.fields,
|
||||||
|
.@"union" => |u| u.fields,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True iff a field of type `ty` contributes its FULL size by value (a nominal
|
||||||
|
/// aggregate), so a cycle through it is infinite. Pointers / slices / optionals /
|
||||||
|
/// functions are finite-size and break the cycle.
|
||||||
|
fn isByValueAggregate(table: *const types.TypeTable, ty: TypeId) bool {
|
||||||
|
if (ty.isBuiltin()) return false;
|
||||||
|
return switch (table.get(ty)) {
|
||||||
|
.@"struct", .tagged_union, .@"union" => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Break a by-value cycle: replace field `k` of nominal `tid` with `.unresolved`.
|
||||||
|
/// The name + nominal id are untouched, so the intern key is stable
|
||||||
|
/// (`updatePreservingKey`). The diagnostic is the user-facing signal; this just
|
||||||
|
/// stops `typeSizeBytes` recursing before the build halts on the error.
|
||||||
|
pub fn poisonAggregateField(self: *Lowering, tid: TypeId, k: usize) void {
|
||||||
|
const table = &self.module.types;
|
||||||
|
const info = table.get(tid);
|
||||||
|
var new_info = info;
|
||||||
|
const src_fields = switch (info) {
|
||||||
|
.@"struct" => |s| s.fields,
|
||||||
|
.tagged_union => |u| u.fields,
|
||||||
|
.@"union" => |u| u.fields,
|
||||||
|
else => return,
|
||||||
|
};
|
||||||
|
const nf = self.alloc.dupe(types.TypeInfo.StructInfo.Field, src_fields) catch return;
|
||||||
|
if (k >= nf.len) return;
|
||||||
|
nf[k].ty = .unresolved;
|
||||||
|
switch (new_info) {
|
||||||
|
.@"struct" => |*s| s.fields = nf,
|
||||||
|
.tagged_union => |*u| u.fields = nf,
|
||||||
|
.@"union" => |*u| u.fields = nf,
|
||||||
|
else => return,
|
||||||
|
}
|
||||||
|
table.updatePreservingKey(tid, new_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diagInfiniteSize(self: *Lowering, ty: TypeId) void {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
const nm = self.module.types.typeName(ty);
|
||||||
|
d.addFmt(.err, null, "type '{s}' is infinitely sized (it contains itself by value); use a pointer ('*{s}') to break the cycle", .{ nm, nm });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Names that must keep external LLVM linkage because the OS loader (not
|
/// Names that must keep external LLVM linkage because the OS loader (not
|
||||||
/// sx code) is the caller. Without this they'd default to internal and
|
/// sx code) is the caller. Without this they'd default to internal and
|
||||||
/// either DCE away or stay hidden from the dynamic symbol table.
|
/// either DCE away or stay hidden from the dynamic symbol table.
|
||||||
@@ -116,6 +213,12 @@ pub fn lowerRoot(self: *Lowering, root: *const Node) void {
|
|||||||
};
|
};
|
||||||
checker.run(decls);
|
checker.run(decls);
|
||||||
}
|
}
|
||||||
|
// Pass 1g: reject infinitely-sized types — a nominal aggregate that contains
|
||||||
|
// ITSELF (or a mutual peer) BY VALUE has no finite layout and would otherwise
|
||||||
|
// infinite-loop `typeSizeBytes` into a stack overflow during body lowering.
|
||||||
|
// Runs after every type is registered (source AND comptime-constructed) and
|
||||||
|
// before body lowering, which is the first consumer of type sizes.
|
||||||
|
self.checkInfiniteSize();
|
||||||
// Pass 2: lower main (and comptime side-effects)
|
// Pass 2: lower main (and comptime side-effects)
|
||||||
self.lowerMainAndComptime(decls);
|
self.lowerMainAndComptime(decls);
|
||||||
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
|
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
|
||||||
|
|||||||
Reference in New Issue
Block a user