Skip to content

Compile-time resource exhaustion from type complexity via recursive generic traits #142840

Open
@caomingpei

Description

@caomingpei

I tried this code:

use std::marker::PhantomData;

pub trait Seq {}

pub struct Empty;
impl Seq for Empty {}

// `Branch` to connect Branch<A,B>
pub struct Branch<L: Seq, R: Seq> {
    _p: PhantomData<(L, R)>,
}
impl<L: Seq, R: Seq> Seq for Branch<L, R> {}

// Define L-system rewrite rules
// `A` and `B` are the basic elements of the sequence.
pub struct A;
pub struct B;
impl Seq for A {}
impl Seq for B {}

// `Rewrite` trait defines the rewriting rules
pub trait Rewrite {
    type Result: Seq;
}

// Rule 1: A -> Branch(A, B)
impl Rewrite for A {
    type Result = Branch<A, B>;
}

// Rule 2: B -> A
impl Rewrite for B {
    type Result = A;
}

// Recursively rewrite the entire sequence tree. Similar to L-system concepts:
// N=0: A
// N=1: Branch<A, B>
// N=2: Branch<Rewrite(A), Rewrite(B)> -> Branch<Branch<A, B>, A>
// N=3: Branch<Rewrite(Branch<A, B>), Rewrite(A)> -> Branch<Branch<Branch<A, B>, A>, Branch<A, B>>
// Fibonacci-like growth of the sequence tree.
impl Rewrite for Empty {
    type Result = Empty;
}
impl<L: Seq, R: Seq> Rewrite for Branch<L, R>
where
    L: Rewrite,
    R: Rewrite,
{
    type Result = Branch<<L as Rewrite>::Result, <R as Rewrite>::Result>;
}

// Define traits for iteration and forced evaluation
// `Nat` and `Succ`/`Zero` are used for type-level counting.
// Represents numbers at the type level.
pub trait Nat {}
pub struct Zero;
impl Nat for Zero {}
pub struct Succ<N: Nat>(PhantomData<N>);
impl<N: Nat> Nat for Succ<N> {}

// `ApplyRewrite` trait applies the specified rewriting rules N times.
pub trait ApplyRewrite<N: Nat> {
    type Result: Seq;
}

// Base case: 0 iterations, the result is the type itself.
impl<S: Seq> ApplyRewrite<Zero> for S {
    type Result = S;
}

// Recursive step: N+1 iterations = apply N iterations, then rewrite the result once.
impl<S: Seq, P: Nat> ApplyRewrite<Succ<P>> for S
where
    S: ApplyRewrite<P>,
    <S as ApplyRewrite<P>>::Result: Rewrite,
{
    type Result = <<S as ApplyRewrite<P>>::Result as Rewrite>::Result;
}

// Define a more complex hash calculation trait
/// `SeqHash` trait is used to compute the hash value of a sequence at compile time.
pub trait SeqHash {
    const HASH: u64;
}

//  Define the hash values for the basic elements and branches. (random large primes)
impl SeqHash for Empty {
    const HASH: u64 = 0x9e3779b97f4a7c15;
}
impl SeqHash for A {
    const HASH: u64 = 0x6a09e667f3bcc908;
}
impl SeqHash for B {
    const HASH: u64 = 0xbb67ae8584caa73b;
}

impl<L: Seq, R: Seq> SeqHash for Branch<L, R>
where
    L: SeqHash,
    R: SeqHash,
{
    // Introduce a complex hash calculation method, using various bitwise operations and constants
    const HASH: u64 = ((L::HASH.wrapping_mul(0x94d049bb133111eb).rotate_left(19))
        ^ (R::HASH.wrapping_mul(0xbf58476d1ce4e5b9).rotate_right(23))
            .wrapping_add(0xc4ceb9fe1a85ec53))
    .wrapping_mul(0x9e3779b97f4a7c15) // A large prime constant, Empty
    .rotate_left(37)
    .wrapping_add(L::HASH ^ R::HASH);
}

// main function to trigger the compile-time evaluation.
fn main() {
    // Define initial state.
    type InitialState = A;

    // Note: the following is harmful. Set iteration count to N=40.
    // Complex successor tree
    // Result: Compilation bomb! Compilation leads to OOM, takes a very long time, and causes a Denial of Service.
    type N40 = Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Succ<Zero>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>;

    // Generate final result type.
    type FinalState = <InitialState as ApplyRewrite<N40>>::Result;

    // Force the compiler to evaluate and traverse the type.
    const FINAL_HASH: u64 = <FinalState as SeqHash>::HASH;

    // Print the result to ensure it won't be optimized away.
    println!(
        "Compilation succeeded! The final complex hash calculated at compile-time is: {}",
        FINAL_HASH
    );
}

I expected to see this happen: compile finished

Instead, this happened: compile stuck

Meta

rustc --version --verbose:

rustc 1.87.0 (17067e9ac 2025-05-09)
binary: rustc
commit-hash: 17067e9ac6d7ecb70e50f92c1944e545188d2359
commit-date: 2025-05-09
host: aarch64-apple-darwin
release: 1.87.0
LLVM version: 20.1.1
Backtrace

<backtrace>

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-trait-systemArea: Trait systemC-bugCategory: This is a bug.E-needs-mcveCall for participation: This issue has a repro, but needs a Minimal Complete and Verifiable ExampleI-compilememIssue: Problems and improvements with respect to memory usage during compilation.I-hangIssue: The compiler never terminates, due to infinite loops, deadlock, livelock, etc.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions