You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stack overflow (non-termination) in TypeBuilder::build: a struct is always expanded structurally with no cycle cut, so any struct that reaches itself (struct Node { next: Option<Box<Node>> }, struct Tree { kids: Vec<Tree> }) aborts the compiler #249
refine::template::TypeBuilder::build (src/refine/template.rs:206, and the Scoped copy at src/refine/template.rs:392) gives an enum a nominal representation — rty::EnumType::new(sym, args), which names the datatype and never looks at its variants — but expands a structstructurally, into a tuple of the refinement types of all of its fields:
if def.is_enum(){let sym = refine::datatype_symbol(self.tcx, def.did());let args:IndexVec<_,_> = params.types().map(|ty| ...self.build(ty)...).collect();
rty::EnumType::new(sym, args).into()// nominal: variants are NOT visited}elseif def.is_struct(){// src/refine/template.rs:247 / :426let elem_tys = def
.all_fields().map(|field| {let ty = field.ty(self.tcx, params);// elaboration: all fields are boxed
rty::PointerType::own(self.build(ty)).into()// <- unconditional descent}).collect();
rty::TupleType::new(elem_tys).into()}
An enum therefore breaks any type-level cycle at its own name. A struct has no name in rty at all (rty.rs:585: "the current implementation uses tuples to represent structs … It is our TODO to improve the struct representation"), so there is nothing to break the cycle on, and there is no memo table or depth cutoff either. Any struct that can reach itself through its fields makes build recurse forever.
The recursion does not even have to be direct: it is enough that some field's type mentions the struct again, and build descends into an enum's generic arguments and into Box/Seq/Array model payloads. That covers the two most common recursive data structures in Rust:
structNode{v:i32,next:Option<Box<Node>>}// singly-linked liststructTree{v:i32,kids:Vec<Tree>}// n-ary tree
The result is a hard fatal runtime error: stack overflow (exit 134) with no diagnostic — the CHC system is never built and the solver is never invoked.
Minimal reproduction
struct S { b: Box<S> } is the smallest form; Option<Box<..>> and Vec<..> are the ones that actually show up in code.
$ cargo run -q -- -Adead_code -C debug-assertions=false min.rs;echo"exit=$?"thread 'rustc' (12366) has overflowed its stackfatal runtime error: stack overflow, abortingexit=134
All of the following abort the same way, and all of them are accepted by rustc:
structS{b:Box<S>}// directstructNode{v:i32,next:Option<Box<Node>>}// through an enum's generic argumentstructTree{v:i32,kids:Vec<Tree>}// through Seq/ArraystructA{b:Option<Box<B>>}// mutual recursionstructB{a:Option<Box<A>>}
A realistic program — a list length function — never gets as far as producing a verdict:
structNode{v:i32,next:Option<Box<Node>>}impl thrust_models::ModelforNode{typeTy = Self;}fnlen(n:&Option<Box<Node>>) -> i32{match n {Some(b) => 1 + len(&b.next),None => 0,}}#[thrust::callable]fnf(){let l = Some(Box::new(Node{v:1,next:None}));assert!(len(&l) == 1);}fnmain(){}
$ cargo run -q -- -Adead_code -C debug-assertions=false list.rs;echo"exit=$?"thread 'rustc' (12418) has overflowed its stackfatal runtime error: stack overflow, abortingexit=134
Independent of -C opt-level (0, 1, 2 all abort) — this is type-level work done before any MIR is analysed.
No value of the type is ever created
The reproducers above deliberately avoid constructing, matching or dropping a value, to separate this from the drop-time recursion of #178.
program
verdict
struct Node {..} declared, not named by any signature or body
#[thrust::callable] fn f() { let _n: Option<Node> = None; }
stack overflow
The middle two rows are the decisive ones: #[thrust::trusted] puts the def in skip_analysis, the function is never called, and no value of Node exists anywhere. The only work performed is turning the signature into a refinement type. That is TypeBuilder::build, and it is where the recursion is.
Enum vs. struct, side by side
The same cycle expressed with an enum at the recursive position terminates, because build stops at the enum's name:
type
result
enum L { Cons(i64, Box<L>), Nil }
safe
enum L { Cons(i64, Option<Box<L>>), Nil }
safe
enum L { Cons((i64, Box<L>)), Nil } (the #178 shape), signature only
safe
struct S { b: Box<S> }
stack overflow
struct Node { v: i32, next: Option<Box<Node>> }
stack overflow
struct Tree { v: i32, kids: Vec<Tree> }
stack overflow
enum W { V(Node), N } (Node recursive), signature only
safe — W's variants are not visited yet
enum W { V(Node), N }, a W value constructed and matched
stack overflow — see the second site below
So the trigger is precisely "the recursion passes through a struct", not "the type is recursive".
The recursion cycle
Adding a one-line eprintln! at the top of TypeBuilder::build (after resolve_model_ty) prints the cycle verbatim. struct S { b: Box<S> }, tail of the output before the abort (3153 calls in the ~1 s before the stack runs out):
DBGBUILD closed S
DBGBUILD closed thrust_models::model::Box<S>
DBGBUILD closed S
DBGBUILD closed thrust_models::model::Box<S>
...
struct Node { v: i32, next: Option<Box<Node>> } — the cycle closes through the Option's generic argument, which build does descend into even though the variants are not visited:
None of them is guarded, and there is no cache keyed on the mir_ty::Ty being built.
Second site
analyze::Analyzer::build_enum_def (src/analyze.rs:315) builds each variant's field types with the same TypeBuilder::build, so an enum whose variant holds a recursive struct also aborts as soon as the enum def is actually registered:
structNode{v:i32,next:Option<Box<Node>>}impl thrust_models::ModelforNode{typeTy = Self;}enumW{V(Node),N}impl thrust_models::ModelforW{typeTy = Self;}#[thrust::callable]fnf(){let w = W::N;match w {W::V(_) => {},W::N => {}}// registers W -> builds Node -> overflow}fnmain(){}
Any fix that only guards refine::template will want to cover this call too.
Not a numerical-range, unsigned or overflow issue: the reproducers contain the literal 1 and nothing else.
Suggested direction
Both build impls would need a cycle cut. The cheapest is a HashSet<mir_ty::Ty> (or a HashMap memo) of types currently being built, but a struct has no rty name to tie the knot to, so a real fix probably wants the nominal struct representation that rty.rs:585 already lists as a TODO — a datatype_symbol-keyed representation for structs, the way enums already have one, at which point the enum branch's cutoff mechanism applies to structs unchanged.
Summary
refine::template::TypeBuilder::build(src/refine/template.rs:206, and theScopedcopy atsrc/refine/template.rs:392) gives anenuma nominal representation —rty::EnumType::new(sym, args), which names the datatype and never looks at its variants — but expands astructstructurally, into a tuple of the refinement types of all of its fields:An enum therefore breaks any type-level cycle at its own name. A struct has no name in
rtyat all (rty.rs:585: "the current implementation uses tuples to represent structs … It is our TODO to improve the struct representation"), so there is nothing to break the cycle on, and there is no memo table or depth cutoff either. Any struct that can reach itself through its fields makesbuildrecurse forever.The recursion does not even have to be direct: it is enough that some field's type mentions the struct again, and
builddescends into an enum's generic arguments and intoBox/Seq/Arraymodel payloads. That covers the two most common recursive data structures in Rust:The result is a hard
fatal runtime error: stack overflow(exit 134) with no diagnostic — the CHC system is never built and the solver is never invoked.Minimal reproduction
struct S { b: Box<S> }is the smallest form;Option<Box<..>>andVec<..>are the ones that actually show up in code.All of the following abort the same way, and all of them are accepted by
rustc:A realistic program — a list length function — never gets as far as producing a verdict:
Independent of
-C opt-level(0,1,2all abort) — this is type-level work done before any MIR is analysed.No value of the type is ever created
The reproducers above deliberately avoid constructing, matching or dropping a value, to separate this from the drop-time recursion of #178.
struct Node {..}declared, not named by any signature or bodysafe(nothing builds the type)#[thrust::trusted] #[thrust::callable] fn make() -> Node(never called)#[thrust::trusted] #[thrust::callable] fn take(_n: &Node)(never called)#[thrust::callable] fn f() { let _n: Option<Node> = None; }The middle two rows are the decisive ones:
#[thrust::trusted]puts the def inskip_analysis, the function is never called, and no value ofNodeexists anywhere. The only work performed is turning the signature into a refinement type. That isTypeBuilder::build, and it is where the recursion is.Enum vs. struct, side by side
The same cycle expressed with an
enumat the recursive position terminates, becausebuildstops at the enum's name:enum L { Cons(i64, Box<L>), Nil }safeenum L { Cons(i64, Option<Box<L>>), Nil }safeenum L { Cons((i64, Box<L>)), Nil }(the #178 shape), signature onlysafestruct S { b: Box<S> }struct Node { v: i32, next: Option<Box<Node>> }struct Tree { v: i32, kids: Vec<Tree> }enum W { V(Node), N }(Noderecursive), signature onlysafe—W's variants are not visited yetenum W { V(Node), N }, aWvalue constructed and matchedSo the trigger is precisely "the recursion passes through a
struct", not "the type is recursive".The recursion cycle
Adding a one-line
eprintln!at the top ofTypeBuilder::build(afterresolve_model_ty) prints the cycle verbatim.struct S { b: Box<S> }, tail of the output before the abort (3153 calls in the ~1 s before the stack runs out):struct Node { v: i32, next: Option<Box<Node>> }— the cycle closes through theOption's generic argument, whichbuilddoes descend into even though the variants are not visited:struct Tree { v: i32, kids: Vec<Tree> }— through theSeq/Arraymodel:For contrast,
enum L { Cons(i64, Box<L>), Nil }callsbuild(L)exactly once for the whole run.The three descents that close a cycle are all in
builditself:src/refine/template.rs:247,:426) — every field;src/refine/template.rs:240,:421) — every generic argument (params.types()), which is howOption<Box<Node>>gets back toNode;model_adt—Box/Mut/Arraypayloads (src/refine/template.rs:172-200,:363-389).None of them is guarded, and there is no cache keyed on the
mir_ty::Tybeing built.Second site
analyze::Analyzer::build_enum_def(src/analyze.rs:315) builds each variant's field types with the sameTypeBuilder::build, so an enum whose variant holds a recursive struct also aborts as soon as the enum def is actually registered:Any fix that only guards
refine::templatewill want to cover this call too.Distinct from the known issues
dropping_formula_for_termwhen a recursive ADT's self-pointer is nested inside a tuple/struct field #178 is the other stack overflow, but inEnv::dropping_formula_for_term, and it is about the drop-formula cutoff for a recursive enum whose self-pointer sits inside a tuple/struct field. It needs a value to be constructed and dropped: its own reproducer shape (enum L { Cons((i64, Box<L>)), Nil }) issafehere when the type only appears in a never-called signature, whereas every reproducer above overflows with no value in existence. Different function, different trigger, and the enum-side cutoff Stack overflow (non-termination) indropping_formula_for_termwhen a recursive ADT's self-pointer is nested inside a tuple/struct field #178 discusses is not on the path at all — the recursion never leavesTypeBuilder::build.&mutprophecies stored in its recursive-position field, so safe programs are wrongly rejected #173 (dropping a recursively-defined ADT does not resolve&mutprophecies in the recursive-position field) and Incompleteness: a&mutstored in aBoxhas its prophecy resolved only at theBox's drop, so reading the referent before the box is dropped wrongly rejects safe programs #175 (&mutin aBox) are wrong-verdict issues about prophecies; there is no&mutanywhere in these reproducers and no verdict is ever produced.Box<T>(Own) is related invariantly in subtyping, wrongly rejecting valid refinement weakening through a box #157 (Boxrelated invariantly) and Refinements onBox(own-pointer) pointee in return position are not enforced — unsound #97 (Boxpointee refinement in return position) are aboutBoxsubtyping/refinement, not about building the type at all.EnumDefs, so any struct with an enum-typed field (struct W { o: Option<i32> }) aborts verification #221 (an enum reachable only through an ADT field is never registered) is the opposite failure — a type not being visited; here the visiting is what does not terminate.1and nothing else.Suggested direction
Both
buildimpls would need a cycle cut. The cheapest is aHashSet<mir_ty::Ty>(or aHashMapmemo) of types currently being built, but a struct has nortyname to tie the knot to, so a real fix probably wants the nominal struct representation thatrty.rs:585already lists as a TODO — adatatype_symbol-keyed representation for structs, the way enums already have one, at which point the enum branch's cutoff mechanism applies to structs unchanged.Environment
35eea46(currentmain)nightly-2025-09-08(perrust-toolchain.toml).github/actions/setup-z3pins), default solver configuration — never reached in any of these runs