Summary
rustc hangs indefinitely instead of emitting a recursion limit or overflow error when compiling a generic function that uses polymorphic recursion over a non-regular recursive type.
By hiding the non-regular recursion behind a dyn Fn thunk, the compiler's drop-check overflow checks are bypassed. The generic function then triggers an infinite sequence of distinct monomorphizations, causing rustc to hang without termination or diagnostics.
Code
#![allow(dead_code)]
use std::fmt::Debug;
enum PerfectPrime<A> {
Fst(A),
Snd(Box<dyn Fn() -> PerfectPrime<(A, A)>>),
}
fn myprint<A: Debug>(p: PerfectPrime<A>) {
match p {
PerfectPrime::Fst(a) => println!("{:?}", a),
PerfectPrime::Snd(f) => myprint(f()),
}
}
fn main() {
let p0: PerfectPrime<i32> = PerfectPrime::Fst(42);
myprint(p0);
}
Expected behavior
rustc should terminate and emit an error, such as an infinite monomorphization diagnostic.
Actual behavior
rustc hangs indefinitely during compilation. In local testing, the compilation did not finish after running for over 1 hour, and the compiler output remained stuck on:
No diagnostic, error, or panic is emitted.
Analysis
This issue exposes a fundamental conflict between non-regular recursive types (where the type parameter changes in the recursive definition, e.g., A to (A, A)) and monomorphization.
1. The Language Design Conflict
In languages like OCaml and Haskell, generics are implemented using uniform representation (type erasure or boxing). A polymorphic function is compiled exactly once and operates on pointers. Consequently, polymorphic recursion over a non-regular type is perfectly valid.
For example, the equivalent non-regular type and polymorphic recursion in Haskell compiles and runs perfectly (laziness makes the explicit thunk unnecessary):
data Perfect a = Fst a | Snd (Perfect (a, a))
myPrint :: Show a => Perfect a -> IO ()
myPrint (Fst a) = print a
myPrint (Snd p) = myPrint p
main :: IO ()
main = do
let p0 = Snd (Fst (1, 2))
myPrint p0
Similarly, in OCaml, the exact thunked pattern is standard and accepted:
type 'a perfect_prime =
| Fst of 'a
| Snd of (unit -> ('a * 'a) perfect_prime)
Rust, however, uses monomorphization. Every time a generic function is called with a new type, the compiler generates a specialized copy of that function. Thus, non-regular polymorphic recursion is fundamentally incompatible with Rust's compilation model because it requires generating infinitely many distinct function bodies.
2. Bypassing Type-Level Limits
Normally, Rust catches this non-regular recursion during instantiation. Simply defining a directly recursive non-regular type is allowed, but instantiating it triggers a drop-check overflow:
enum Perfect<A> {
Fst(A),
Snd(Box<Perfect<(A, A)>>),
}
fn main() {
let p0: Perfect<i32> = Perfect::Fst(42);
}
This fails correctly with:
error[E0320]: overflow while adding drop-check rules for `Perfect<i32>`
However, introducing the dyn Fn thunk in PerfectPrime successfully hides the infinitely expanding type behind a dynamically dispatched trait object. Because dyn Fn breaks the visible structural recursion for the drop-checker, instantiation succeeds, and the type definition bypasses structural limits.
3. Runaway Monomorphization
Because instantiation is accepted, the non-regular recursion reappears at the function level. In the Snd branch of myprint, f() returns PerfectPrime<(A, A)>. Compiling myprint::<A> forces the compiler to monomorphize myprint::<(A, A)>, which requires myprint::<((A, A), (A, A))>, and so on ad infinitum:
myprint::<A>
myprint::<(A, A)>
myprint::<((A, A), (A, A))>
myprint::<(((A, A), (A, A)), ((A, A), (A, A)))>
...
rustc continuously attempts to generate these larger instantiations but fails to trigger standard monomorphization recursion limits (like #![recursion_limit]), resulting in a silent hang.
Meta
rustc --version --verbose:
rustc 1.97.0-nightly (e95e73209 2026-05-05)
binary: rustc
commit-hash: e95e73209faf6ead2bc5c7636e45e589a751b79b
commit-date: 2026-05-05
host: x86_64-pc-windows-msvc
release: 1.97.0-nightly
LLVM version: 22.1.4
(No backtrace is available because compilation does not panic or complete).
Summary
rustchangs indefinitely instead of emitting a recursion limit or overflow error when compiling a generic function that uses polymorphic recursion over a non-regular recursive type.By hiding the non-regular recursion behind a
dyn Fnthunk, the compiler's drop-check overflow checks are bypassed. The generic function then triggers an infinite sequence of distinct monomorphizations, causingrustcto hang without termination or diagnostics.Code
Expected behavior
rustcshould terminate and emit an error, such as an infinite monomorphization diagnostic.Actual behavior
rustchangs indefinitely during compilation. In local testing, the compilation did not finish after running for over 1 hour, and the compiler output remained stuck on:No diagnostic, error, or panic is emitted.
Analysis
This issue exposes a fundamental conflict between non-regular recursive types (where the type parameter changes in the recursive definition, e.g.,
Ato(A, A)) and monomorphization.1. The Language Design Conflict
In languages like OCaml and Haskell, generics are implemented using uniform representation (type erasure or boxing). A polymorphic function is compiled exactly once and operates on pointers. Consequently, polymorphic recursion over a non-regular type is perfectly valid.
For example, the equivalent non-regular type and polymorphic recursion in Haskell compiles and runs perfectly (laziness makes the explicit thunk unnecessary):
Similarly, in OCaml, the exact thunked pattern is standard and accepted:
Rust, however, uses monomorphization. Every time a generic function is called with a new type, the compiler generates a specialized copy of that function. Thus, non-regular polymorphic recursion is fundamentally incompatible with Rust's compilation model because it requires generating infinitely many distinct function bodies.
2. Bypassing Type-Level Limits
Normally, Rust catches this non-regular recursion during instantiation. Simply defining a directly recursive non-regular type is allowed, but instantiating it triggers a drop-check overflow:
This fails correctly with:
However, introducing the
dyn Fnthunk inPerfectPrimesuccessfully hides the infinitely expanding type behind a dynamically dispatched trait object. Becausedyn Fnbreaks the visible structural recursion for the drop-checker, instantiation succeeds, and the type definition bypasses structural limits.3. Runaway Monomorphization
Because instantiation is accepted, the non-regular recursion reappears at the function level. In the
Sndbranch ofmyprint,f()returnsPerfectPrime<(A, A)>. Compilingmyprint::<A>forces the compiler to monomorphizemyprint::<(A, A)>, which requiresmyprint::<((A, A), (A, A))>, and so on ad infinitum:rustccontinuously attempts to generate these larger instantiations but fails to trigger standard monomorphization recursion limits (like#![recursion_limit]), resulting in a silent hang.Meta
rustc --version --verbose:(No backtrace is available because compilation does not panic or complete).