Skip to content

Prototype Fixing false E0581/E0582 errors in the next solver - #162745

Open
enginespot wants to merge 2 commits into
rust-lang:mainfrom
enginespot:e0582/pr-01-dependent-binder
Open

enginespot wants to merge 2 commits into
rust-lang:mainfrom
enginespot:e0582/pr-01-dependent-binder

Conversation

@enginespot

@enginespot enginespot commented Sep 14, 2026

Copy link
Copy Markdown

I’m addressing a class of E0581/E0582 errors under -Znext-solver=globally: rustc rejects a callable signature even though its input types and existing constraints determine its output. This affects both Fn bounds and function pointer types.

I’ll start with a callback that returns the same associated type it receives, then show how equalities and nested declarations support more involved signatures and actual calls. The later examples explain which restrictions still apply. Code snippets reuse earlier definitions; examples described as errors should be compiled separately.

Using the same associated type for input and output

The following apply passes value to a callback and returns the result. Both sides use T::View, but the previous check rejected the callable bound.

trait Family {
    type View<'a>;
}

fn apply<'a, T: Family, F>(f: F, value: T::View<'a>) -> T::View<'a>
where
    F: for<'b> Fn(T::View<'b>) -> T::View<'b>,
{
    f(value)
}

Why this was rejected

View<'a> does not necessarily use 'a. These two implementations illustrate the distinction:

struct Borrowed;

impl Family for Borrowed {
    type View<'a> = &'a u32;
}

struct Erased;

impl Family for Erased {
    type View<'a> = ();
}

Substituting each implementation into the callback bound gives:

T = Borrowed: for<'a> Fn(&'a u32) -> &'a u32
T = Erased:   Fn(()) -> ()

For Borrowed, the input is a reference whose type retains 'a. For Erased, the input is always (), regardless of how 'a is instantiated. A generic input written as T::View<'a> therefore does not establish that 'a can be recovered from that type.

In both cases, the complete input type still determines the output type. For Borrowed, both are &'a u32; for Erased, both are (). In the latter case, the output no longer depends on 'a either, so there is no need to recover it.

What the change allows

I changed the check to recognize complete input types reused in the output. In apply, both sides are T::View<'b>, so determining the output does not require recovering 'b from the projection. The generic definition now passes the check, and both calls below are accepted:

fn use_apply() {
    let value = 42;

    let borrowed = apply::<Borrowed, _>(|x| x, &value);
    assert_eq!(*borrowed, 42);

    let erased = apply::<Erased, _>(|x| x, ());
    assert_eq!(erased, ());
}

The call to f(value) inside apply is checked using the same type relationship. These examples exercise that call with both a reference and ().

Using an equality between different associated types

The input and output may use different projections. In this example, the callback takes T::View and returns U::View, while a separate where clause requires the two types to be equal for every 'b:

fn apply_equivalent<'a, T, U, F>(f: F, value: T::View<'a>) -> U::View<'a>
where
    U: Family,
    for<'b> T: Family<View<'b> = U::View<'b>>,
    F: for<'b> Fn(T::View<'b>) -> U::View<'b>,
{
    f(value)
}

I use the independent T: Family<View<'b> = U::View<'b>> bound to establish the equality when checking the callable bound. The Fn(...) -> ... binding being checked cannot serve as proof of its own validity.

When normalizing the input and output, both occurrences of 'b remain bound by the same quantifier. Any additional requirements introduced by normalization must also hold.

For a concrete call, another type can define View as the same reference type:

struct AlsoBorrowed;

impl Family for AlsoBorrowed {
    type View<'a> = &'a u32;
}

fn use_equivalent() {
    let value = 42;
    let result = apply_equivalent::<Borrowed, AlsoBorrowed, _>(|x| x, &value);
    assert_eq!(*result, 42);
}

Inside the generic function, the equality comes from the View<'b> = U::View<'b> bound. At this call site, both projections are &u32. Replacing AlsoBorrowed with Erased would give &u32 on one side and () on the other, so the equality would fail and the call would still be rejected.

Wrapping the result

The output can also contain the reused type inside a wrapper such as Option:

fn wrap_equivalent<'a, T, U, F>(mut f: F, value: T::View<'a>) -> Option<U::View<'a>>
where
    U: Family,
    for<'b> T: Family<View<'b> = U::View<'b>>,
    F: for<'b> FnMut(T::View<'b>) -> Option<U::View<'b>>,
{
    f(value)
}

fn use_wrapped() {
    let value = 42;
    let result = wrap_equivalent::<Borrowed, AlsoBorrowed, _>(|x| Some(x), &value);
    assert_eq!(result, Some(&value));
}

The equality determines U::View<'b>, which also determines Option<U::View<'b>>. The check can recognize a reused type within the output; it does not require the entire output to be identical to the entire input. Any additional references in the output still need their lifetimes checked separately.

Following equalities through associated type declarations

A function’s where clause may provide an equality indirectly through a declaration. Here, the relevant equality appears in the declaration of Carrier::Assoc:

trait Identity {
    type Output: Family;
}

trait Carrier {
    type Assoc: Identity<Output = Self::Assoc>;
}

fn identity<'a, C: Carrier<Assoc = T>, T: Family>(
    value: T::View<'a>,
) -> <<T as Identity>::Output as Family>::View<'a> {
    value
}

Substituting the declared relationships reduces the return type to the input type:

The function requires: C::Assoc = T
Carrier requires:      C::Assoc: Identity<Output = C::Assoc>
Substituting T:         T: Identity<Output = T>

Return type:           <<T as Identity>::Output as Family>::View<'a>
Substituting Output:   <T as Family>::View<'a>
The input type:        T::View<'a>

The previous implementation did not fully use this relationship. I extended the solver to follow associated item and supertrait declarations for the current goal. Here, that establishes <T as Identity>::Output = T, allowing the body to return value directly.

The search follows the relationship needed for that goal. If a declaration carries additional where clauses, their requirements must also be checked; extracting an equality does not discard its premises.

Using the function through pointers and callbacks

The same identity function can be used through a function pointer. This requires checking both the pointer type itself and the coercion of the function item to that type:

fn call_pointer<'a, C: Carrier<Assoc = T>, T: Family>(
    value: T::View<'a>,
) -> T::View<'a> {
    let f: for<'b> fn(T::View<'b>) -> T::View<'b> = identity::<C, T>;
    f(value)
}

The type of f requires it to be callable for every 'b. I therefore check that identity satisfies its declaration’s requirements for every such lifetime before allowing the coercion. The resulting pointer can then be called with f(value).

The declared equality can also establish the return type when calling through Box<dyn Fn>:

fn call_object<'a, C: Carrier<Assoc = T>, T: Family>(
    f: Box<dyn for<'b> Fn(T::View<'b>) -> <<T as Identity>::Output as Family>::View<'b>>,
    value: T::View<'a>,
) -> T::View<'a> {
    f(value)
}

Adding the following implementations for Borrowed makes both forms available for concrete calls:

impl Identity for Borrowed {
    type Output = Self;
}

impl Carrier for Borrowed {
    type Assoc = Self;
}

fn use_identity() {
    let value = 42;
    let from_pointer = call_pointer::<Borrowed, Borrowed>(&value);
    let from_object = call_object::<Borrowed, Borrowed>(
        Box::new(identity::<Borrowed, Borrowed>),
        &value,
    );
    assert_eq!(*from_pointer, 42);
    assert_eq!(*from_object, 42);
}

Using lifetime bounds already provided by declarations

Avoiding redundant bounds

An associated type declaration can provide lifetime relationships as well as type equalities:

trait Has<'r> {
    type Assoc: 'r;
}

fn shorten<'a, 'r, C>(value: &'a ()) -> &'r ()
where
    C: Has<'r, Assoc = &'a ()>,
{
    value
}

The function does not explicitly require 'a: 'r, but that relationship follows from the declaration:

Has requires:        C::Assoc: 'r
The function states: C::Assoc = &'a ()
After substitution:  &'a (): 'r
Therefore:           'a outlives 'r, so &'a () can be used as &'r ()

I use this declared relationship in the relevant checks, allowing shorten to omit a redundant 'a: 'r bound. The solver can also use such a relationship when it needs to prove T: 'r before normalizing an associated type.

One complete proof is enough

A lifetime requirement can have more than one possible proof. In this example, the bound on D provides a sufficient one:

fn from_either<'a, 'b: 'r, 'r, C, D, T>(value: T) -> Box<dyn std::fmt::Debug + 'r>
where
    T: std::fmt::Debug,
    C: Has<'a, Assoc = T>,
    D: Has<'b, Assoc = T>,
{
    Box::new(value)
}

The two possible derivations are:

Through C: T: 'a, but no relationship between 'a and 'r is known.
Through D: T: 'b and 'b: 'r, which establish T: 'r.

The return type requires T: 'r. The proof through D establishes that, so there is no need to require 'a: 'r as well.

Both premises of that proof, T: 'b and 'b: 'r, must hold together. Taking T: 'a from the first path and 'b: 'r from the second would not prove T: 'r.

I preserve these alternatives only when the candidate proofs agree on their type and const results. A candidate producing u32 cannot be merged with one producing bool merely because both have satisfiable lifetime conditions.

The remaining conditions are carried through to borrow checking. Requirements that a closure passes to its enclosing context are preserved as well; determining the return type does not discharge those requirements.

Cases that remain rejected

Unrelated projections or independent lifetimes

Using the same lifetime argument does not establish a relationship between two projections. This bound provides no equality between T::View<'a> and U::View<'a>, so it still produces E0582:

fn unrelated_output<T: Family, U: Family, F>()
where
    F: for<'a> Fn(T::View<'a>) -> U::View<'a>,
{
}

Even with an equality, substitution must preserve the corresponding lifetime. The independent 'a and 'b below still cause E0582:

fn wrong_lifetime<T, U, F>()
where
    U: Family,
    for<'a> T: Family<View<'a> = U::View<'a>>,
    F: for<'a, 'b> Fn(T::View<'a>) -> U::View<'b>,
{
}

The equality establishes T::View<'a> = U::View<'a>. It does not establish T::View<'a> = U::View<'b>. Variables in nested for<...> binders likewise retain their own scopes; matching names do not make them interchangeable.

An additional reference in the output

Reusing the input type does not justify an additional output reference whose lifetime has no independent basis:

fn extra_reference<T: Family, F>()
where
    F: for<'a> Fn(T::View<'a>) -> (T::View<'a>, &'a ()),
{
}

Substituting Erased makes the distinction explicit:

The earlier valid callback: Fn(()) -> ()
This callback:              for<'a> Fn(()) -> ((), &'a ())

The input is still just (), but the output now includes a reference that depends on 'a. The generic bound does not establish that lifetime dependency, so it still produces E0582.

Unsatisfied requirements on an associated type

The following associated type normalizes to (), but its declaration restricts its lifetime argument to 'static:

trait StaticOnly {
    type View<'a> where 'a: 'static;
}

impl StaticOnly for () {
    type View<'a> = () where 'a: 'static;
}

fn missing_requirement<F>()
where
    F: for<'a> Fn() -> <() as StaticOnly>::View<'a>,
{
}

The for<'a> bound must hold for every 'a, while StaticOnly::View<'a> requires 'a: 'static. I retain that requirement during normalization, so missing_requirement remains rejected. Replacing the output with () cannot erase the condition under which the associated type is valid.

A function that only supports restricted lifetimes

A function may return its input type and still impose a lifetime restriction. Here, restricted explicitly requires 'a: 'static:

fn restricted<'a: 'static, T: Family>(value: T::View<'a>) -> T::View<'a> {
    value
}

fn bad_pointer<T: Family>() {
    let _: for<'a> fn(T::View<'a>) -> T::View<'a> = restricted::<T>;
}

The coercion in bad_pointer still produces a type mismatch. The pointer must be callable for every 'a, but the function has a narrower requirement. Matching input and output types does not remove that restriction.

A circular equality is not an independent normalization proof

Suppose the only available equalities are:

A::Output = B::Output
B::Output = A::Output

These state that the two projections are equal. Reversing the first equality does not add evidence. The solver cannot treat a return to the original goal as proof that it has determined the normalization result.

I preserve recursive reasoning supported by established premises, and the solver does not need to expand declarations unrelated to the current goal. Some mutually recursive projections with no determinable normalization result can still reach the recursion limit. The two equations above illustrate the relationship; they do not imply that every program containing such equalities will overflow.

Changes to the compiler

I changed several checks to support the examples above:

  • Output dependency checks recognize complete input types and their structural components. Once the where clauses are available, they use independent equalities to normalize the input and output while checking the required lifetime conditions.
  • The next solver follows associated item and supertrait declarations to prove the trait, type equality, or outlives goal at hand. It retains the premises attached to each declaration.
  • Function item matching and function pointer coercions use a consistent callable signature. A lifetime is generalized into for<'a> only when the relevant declaration requirements hold for every 'a.
  • Inference, MIR, and borrow checking carry and validate the lifetime conditions left by solving. Alternative conditions and requirements passed out of closures must survive these transitions.

A behavior change with assumptions-on-binders

The accepted examples above use -Znext-solver=globally; they do not require -Zassumptions-on-binders. When that additional mode is enabled, the change also checks lifetime requirements that remain after leaving a quantified scope.

Two cases in tests/ui/assumptions_on_binders/test-infra-works.rs can be described as follows:

Known in the outer context: T: Trait
Required inside the scope:  T::Assoc: 'a for every 'a
After leaving the scope:   the requirement must still be proved

T: Trait alone cannot prove the remaining requirement. Both cases were previously accepted, and existing FIXME comments already identified that acceptance as incorrect. I changed the test from check-pass to check-fail because preserving and checking those lifetime requirements now rejects the two cases.

Scope and related issues

This output dependency analysis applies to the next solver when enabled globally. It uses complete types and established equalities while preserving projection opacity and checking any additional output lifetimes.

Issue Relationship to this change
#107572 The main motivation: using the same GAT projection for a callback’s input and output causes E0582. The examples above cover this pattern.
#86702 The original example uses two different projections without an equality connecting them. That example still produces E0581/E0582, so I do not consider the issue resolved by this change.
#121437 The original example uses T::Native<'a>: Not<Output = T::Native<'a>>. Reusing a complete input type also applies to this associated output constraint. The original minimal example now passes type checking with the next solver enabled globally.

@rustbot

rustbot commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

This PR changes MIR

cc @oli-obk, @RalfJung, @JakobDegen, @vakaras

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri, @RalfJung, @oli-obk, @lcnr

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Sep 14, 2026
@rustbot

rustbot commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust Project has assigned @khyperia (or someone else) to review your changes, you should hear from them (or someone else) within the next two weeks.

Please see the contribution instructions and our LLM policy for more information.

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler, types
  • compiler, types expanded to 76 candidates
  • Random selection from 18 candidates

@rust-log-analyzer

This comment has been minimized.

@enginespot
enginespot force-pushed the e0582/pr-01-dependent-binder branch from b47fc22 to 7a8b709 Compare September 14, 2026 02:23
@rust-log-analyzer

This comment has been minimized.

@jackh726

Copy link
Copy Markdown
Member

This needs significant discussion with the types team; Zulip is the right place for that.

I'll leave this open for now, but am going to mark this as experimental. It is not going to be reviewed without discussion.

@jackh726 jackh726 added S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 14, 2026
@jackh726 jackh726 added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Sep 14, 2026
@oli-obk oli-obk added the llm-assisted An LLM-assisted PR as defined by the LLM policy. Requires ahead-of-time consent by assignee. label Sep 14, 2026
@enginespot
enginespot force-pushed the e0582/pr-01-dependent-binder branch from 7a8b709 to 8010641 Compare September 14, 2026 14:02
@rustbot

rustbot commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

This PR changes rustc_public

cc @oli-obk, @celinval, @ouz-a, @makai410

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

changes to the core type system

cc @lcnr

clippy is developed in its own repository. If possible, consider making this change to rust-lang/rust-clippy instead.

cc @rust-lang/clippy

changes to the core type system

cc @lcnr

@rustbot rustbot added the T-clippy Relevant to the Clippy team. label Sep 14, 2026
@rust-log-analyzer

This comment has been minimized.

@enginespot

enginespot commented Sep 14, 2026

Copy link
Copy Markdown
Author

@jackh726 Thanks for the guidance. I’ve started a discussion in #t-types Zulip.
I proposed the core idea and used AI tools extensively in this work. I hope to learn from the team and help
move this issue toward a solution.

@rust-bors

This comment has been minimized.

@enginespot
enginespot force-pushed the e0582/pr-01-dependent-binder branch from 8010641 to 4c74fed Compare September 18, 2026 09:11
@rustbot

rustbot commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

changes to inspect_obligations.rs

cc @lcnr

HIR ty lowering was modified

cc @fmease

Some changes occurred to the core trait solver

cc @rust-lang/initiative-trait-system-refactor

@rustbot

rustbot commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@rust-log-analyzer

This comment has been minimized.

@enginespot enginespot changed the title Add dependent binders and explicit trait evidence representation Prototype Fixing false E0581/E0582 errors in the next solver Sep 18, 2026
Accept callable outputs determined by complete input types and independent
projection equalities with the next solver enabled globally. Check the
requirements for normalization and function-item lifetime generalization.

Use associated-item and supertrait declarations to prove nested type
equalities, trait bounds, and outlives goals. Preserve quantified premises
without recursively proving the well-formedness of an established source.

Carry alternative region conditions through canonical responses, MIR,
borrow checking, and closure requirements.
Cover higher-ranked outputs, nested declaration equalities, GATs, scoped
premises, and actual calls through function items, pointers, and trait
objects, including cross-crate uses and missing-premise rejection.

Exercise recursive declaration premises from COM object traits and verify
that quantified Self types retain their lifetime requirements. Update the
affected diagnostics and test region-constraint rollback.
@enginespot
enginespot force-pushed the e0582/pr-01-dependent-binder branch from 4c74fed to 02c25f8 Compare September 18, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-assisted An LLM-assisted PR as defined by the LLM policy. Requires ahead-of-time consent by assignee. S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. T-clippy Relevant to the Clippy team. T-types Relevant to the types team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants