Skip to content

Tracking issue for future-compatibility warning recursion_depth_exceeding_limit #159228

Description

@adwinwhite

This is the summary issue for the recursion_depth_exceeding_limit future-compatibility warning. The goal of this page is to describe why this change was made and how you can fix code that is affected by it. It also provides a place to ask questions
or register a complaint if you feel the change should not be made. For more information on the policy around future-compatibility warnings, see our breaking change policy guidelines.

What is this warning for

The recursion_depth_exceeding_limit lint detects cases where the compiler previously did not correctly track the required recursion depth. Properly tracking the required depth then causes overflow errors as it exceeds the value set for the #[recursion_limit = "N"]. We are temporarily weakening these errors to a future compatibility warning.

Example

#![recursion_limit = "8"]

// The field order matters 😂
struct Foo<T> {
    t: T,
    opt_t: Option<T>,
}

fn require_sync<T: Sync>() {}

fn main() {
    require_sync::<Foo<Foo<Foo<Foo<Foo<Foo<()>>>>>>>();
}

The above code compiles with the old solver, while it requires #[recursion_limit = "12"] for the next solver.To prove Foo^N: Sync, we have to prove constituent nested goals Foo^N-1: Sync and Option<Foo^N-1>: Sync. If we require depth d to prove the former, the next solver requires the depth d + 1 for the later, because we goes down one depth from Option<Foo^N-1>: Sync to its constituent goal Foo^N-1: Sync, and we hit the cache with required depth d for this nested goal.
So, the total required depth for the root goal is proportional to N with slope 2. But for the old solver, we check the recursion limit when we actually consider the obligation and don't record/lookup the required depth for its deeply nested goals.
Therefore, the total required depth for the root goal is proportional to N with slope 1 and the next-solver requires twice more recursion limits as N grows.

How to best fix this

It's strongly recommended to add manual impls for auto traits like below, if possible.

// Adapted from crate `non_structural_derive`. You may want to use it directly.
unsafe impl<T: Send> Send for Foo<T> {}
fn _check_bound<T: Send>(_: &T) {}
fn _validate_fields<T: Send>(x: &Foo<T>) {
    let Foo { t, opt_t } = x;
    _check_bound::<T>(&t);
    _check_bound::<Option<T>>(&opt_t);
}

This helps your downstream users avoid having to increase the recursion_limit when they use your deeply nested types. This also improves the performance of the type system when compared to the builtin auto-trait impl which may matter for very large types.

If your overflow isn't caused by auto traits, you have to increase the recursion_limit by adding the following to your crate root.

#![recursion_limit = "a big number like 256"]

Impact

This affects wgpu = "25.0.2" and a lot of other crates. This was investigated and explained by @ShoyuVanilla. zulip discussion

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.WG-trait-system-refactorThe Rustc Trait System Refactor Initiative (-Znext-solver)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions