Summary
Env::dropping_formula_for_term (src/refine/env.rs) recurses infinitely and overflows the stack when analyzing a recursive enum whose recursive Box<Self> (or &mut Self) is reached through a tuple/struct field instead of being a direct field of the variant. The recursive-ADT cutoff only recognizes a direct pointer field, so any indirection bypasses it.
This needs no mutable references — a purely-owned recursive data type triggers it — and the program being analyzed is trivially safe.
Reproduction (minimal)
enumL{Cons((i64,Box<L>)),Nil}// recursive Box<L> nested in a tuple fieldimpl thrust_models::ModelforL{typeTy = Self;}#[thrust::callable]fncheck(){let l = L::Cons((1,Box::new(L::Nil)));assert!(matches!(l,L::Cons(_)));// trivially true}fnmain(){}$ THRUST_SOLVER=z3 cargo run -q -- -Adead_code -C debug-assertions=false repro.rsthread 'rustc' has overflowed its stackfatal runtime error: stack overflow, aborting(exit 134)
The direct form — where the recursive Box<L> is a direct field of the variant — verifies cleanly:
enumL{Cons(i64,Box<L>),Nil}// direct recursive fieldimpl thrust_models::ModelforL{typeTy = Self;}#[thrust::callable]fncheck(){let l = L::Cons(1,Box::new(L::Nil));assert!(matches!(l,L::Cons(_, _)));// verifies: exit 0}fnmain(){}So the only difference between "crashes" and "verifies" is whether the recursive pointer sits directly in the variant or inside a tuple field of the variant.
Mechanism
src/refine/env.rs, dropping_formula_for_term (~lines 1120-1166). The three relevant branches:
}elseif ty.is_own(){// (A) Box: recurse into pointeelet inner = &ty.as_pointer().unwrap().elem.ty;self.dropping_formula_for_term(existentials, inner, term.box_current())}elseifletSome(tty) = ty.as_tuple(){// (B) tuple: recurse into each element
...self.dropping_formula_for_term(existentials,&elem.ty, term.clone().tuple_proj(i))
...
}elseifletSome(ety) = ty.as_enum(){// (C) enum: recurse into each variant field
...
for field_ty inenum_def.field_tys(){
...
ifletSome(p) = field_type.as_pointer(){ifmatches!(&p.elem.ty, rty::Type::Enum(e)if e.symbol == ety.symbol){// TODO: we need recursively defined drop_pred for the recursive ADTs!
tracing::warn!("skipping recursive variant");continue;// <-- the ONLY cycle cutoff}}self.dropping_formula_for_term(existentials,&field_type, field_term);}
...
}The cycle cutoff in branch (C) engages only when a variant field's type is itself a pointer to the same enum (field_type.as_pointer() → Enum(e) with the same symbol). For Cons((i64, Box<L>)) the field type is a tuple, so the as_pointer() check fails and the guard is skipped. The descent then goes:
enum L (C) → tuple (i64, Box<L>) (B) → Box<L> (A) → enum L (C) → tuple … (B) → Box<L> (A) → … ∞
Neither the tuple branch (B) nor the owned-pointer branch (A) has any cycle guard of its own, so the recursion never terminates. The TODO on line 1151 already flags that the current cutoff is a stopgap ("we need recursively defined drop_pred for the recursive ADTs"); this report is a concrete case where the stopgap is bypassed entirely and turns into a hard crash rather than a skipped obligation.
Notes
Environment
- thrust @
af7cd99 (current main) - rustc
nightly-2025-09-08 (per rust-toolchain.toml) - z3 4.16.0
Summary
Env::dropping_formula_for_term(src/refine/env.rs) recurses infinitely and overflows the stack when analyzing a recursiveenumwhose recursiveBox<Self>(or&mut Self) is reached through a tuple/struct field instead of being a direct field of the variant. The recursive-ADT cutoff only recognizes a direct pointer field, so any indirection bypasses it.This needs no mutable references — a purely-owned recursive data type triggers it — and the program being analyzed is trivially safe.
Reproduction (minimal)
The direct form — where the recursive
Box<L>is a direct field of the variant — verifies cleanly:So the only difference between "crashes" and "verifies" is whether the recursive pointer sits directly in the variant or inside a tuple field of the variant.
Mechanism
src/refine/env.rs,dropping_formula_for_term(~lines 1120-1166). The three relevant branches:The cycle cutoff in branch (C) engages only when a variant field's type is itself a pointer to the same enum (
field_type.as_pointer()→Enum(e)with the same symbol). ForCons((i64, Box<L>))the field type is a tuple, so theas_pointer()check fails and the guard is skipped. The descent then goes:Neither the tuple branch (B) nor the owned-pointer branch (A) has any cycle guard of its own, so the recursion never terminates. The
TODOon line 1151 already flags that the current cutoff is a stopgap ("we need recursively defined drop_pred for the recursive ADTs"); this report is a concrete case where the stopgap is bypassed entirely and turns into a hard crash rather than a skipped obligation.Notes
rustc; theassert!is always true.&mutprophecies stored in its recursive-position field, so safe programs are wrongly rejected #173 wrong-verdict for&mutin a recursive-position field; 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 Box-held&mutprophecy timing): this reproducer contains no&mutat all, and the symptom is non-termination / stack overflow, not a wrong soundness/completeness verdict. It is triggered purely by the shape of the owned recursive type during drop-formula construction.Box<L>), since the guard is keyed on a direct pointer field.Environment
af7cd99(currentmain)nightly-2025-09-08(perrust-toolchain.toml)