Skip to content

Prototype Callable lifetime dependencies through associated types - #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

Hi everyone. I’m working on a class of E0581/E0582 errors in Rust: a callback takes and returns the same associated type, or an existing equality establishes that relationship, but rustc still reports an unconstrained output lifetime.

The prototype enables the new output dependency rule when the next solver is enabled globally (-Znext-solver=globally). Cyclic equalities and normalization consistency still have unresolved cases.

1. Problem and motivation

An associated type with a lifetime parameter lets a library describe different forms of a data view through one interface. One implementation may use a borrow, while another uses a value that contains no borrow. Generic code can refer to the associated type without exposing either representation in its interface.

Before this change, adding a callback that takes and returns that view produces E0582. The following apply simply passes a value to the callback and returns the result:

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)
}

The for<'b> bound must hold for every 'b. The previous check reports E0582 on the callback output T::View<'b>; the function pointer type for<'b> fn(T::View<'b>) -> T::View<'b> has the corresponding E0581 problem.

This is the pattern discussed in #107572. I want generic callbacks to use the type relationships already guaranteed by the interface, so libraries can continue to encapsulate their representations behind associated types.

For example, the same Family interface can have these two implementations:

struct Borrowed;

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

struct Erased;

impl Family for Erased {
    type View<'a> = ();
}
T Callback input Callback output
Borrowed &'b u32 &'b u32
Erased () ()

Erased ignores 'b entirely, so an associated type’s result cannot be used to recover its lifetime argument. The existing check therefore does not consider 'b constrained when it appears only as an argument to an input projection.

In apply, however, both sides always use the same complete type. If lifetime information is erased, it disappears from the output as well as the input. That relationship determines the return type, but the previous check did not use it. With the change, these calls 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, ());
}

2. Design rule: what determines the output

I base the check on whether complete input types determine the output type.

With the outer type parameters and independently available bounds fixed, the intended relationship is: if two lifetime instantiations both satisfy the signature’s requirements and produce equal input types, they must also produce equal output types.

Input type: I('a)    Output type: O('a)

When the requirements of both instantiations hold:
I('a₁) = I('a₂)  ⇒  O('a₁) = O('a₂)

For I('a) = O('a) = T::View<'a>, this follows directly without recovering 'a from the projection. The complete input type can also determine an output such as Option<T::View<'a>>.

The implementation first checks this relationship through structural matching, then uses proved type equalities for cases the structural check cannot establish. If equality reasoning provides no further evidence, the structural check’s result is retained.

The bounds below differ in what information their inputs provide:

Bound shape Basis for the output dependency Behavior after the change
Fn(&'a T) -> &'a U The input reference directly constrains 'a; T and U may differ Already accepted
Fn(T::View<'a>) -> T::View<'a> The output reuses a complete input type Newly accepted
Fn(T::View<'a>) -> Option<T::View<'a>> The projected type is known and the wrapper adds no lifetime Newly accepted
Fn(T::View<'a>) -> U::View<'a> Requires an independent equality or another sufficient basis Still rejected with only T: Family, U: Family
Fn(T::View<'a>) -> (T::View<'a>, &'a ()) The additional reference needs its lifetime constrained separately Still rejected

For example, with T = Erased and U = Borrowed, the fourth row becomes Fn(()) -> &'a u32: the input provides no lifetime to constrain the output reference. In the first row, T and U may differ because the input reference directly supplies 'a.

3. Implementation design

Checking equalities in a complete context

The input and output need not have the same spelling. Here, a where clause already requires the two projections to be equal:

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 run output checks that depend on type equalities after collecting the types and bounds. A separate inference context uses established equalities to normalize the input and output and verifies the resulting lifetime requirements. Both sides remain within the same for<...> lifetime scope (binder) so their corresponding lifetimes are not instantiated independently.

This check uses only independently established bounds. Equalities that have not passed the structural output dependency check are excluded from the evidence, preventing self-justification. If normalization fails, inference variables remain unresolved, or lifetime conditions are unsatisfied, the structural result from before normalization is retained. Generic parameters and associated types that can remain abstract are not unresolved inference variables.

Using associated item declarations for the current goal

An equality may also come from a nested declaration:

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
}

The derivation is:

C::Assoc = T
C::Assoc: Identity<Output = C::Assoc>
    ⇒ T: Identity<Output = T>
    ⇒ The return type normalizes to T::View<'a>

Normalizing to T::View<'a> is sufficient. The input and output now use the same type expression; it need not be reduced further to a reference, (), or another concrete type.

I use an internal BoundFromClause(established clause, target goal) query to derive the bounds needed for the current goal from associated item and supertrait declarations. Each candidate checks type argument well-formedness, a GAT’s own where clauses, and scoped premises.

Using the declaration requires an independently established positive trait bound, such as C: Carrier here. Equations temporarily installed for normalization during impl checking cannot prove that the impl satisfies its own declaration. Self well-formedness already supplied by an established source is not required again as a circular premise; requirements introduced by instantiating a quantified Self are still checked.

Expanding declarations eagerly can keep generating new types along recursive relationships. I therefore handle nested declarations through solver goal queries, searching for consequences needed by the current goal. The region environment still collects finite, direct outlives relationships, while the queries themselves must handle cycles.

Sharing a signature between function item matching and pointer coercion

Accepting a callable declaration is not enough to make a function item satisfy it. The earlier identity must also support uses such as:

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)
}

I compute a callable signature through fn_sig_for_fn_traits for Fn matching, function pointer coercions, and the corresponding MIR checks. Before generalizing a lifetime, its declaration requirements must be proved for arbitrary valid lifetime instantiations, using independent bounds and conditions implied by the inputs. The output dependency check must also succeed. Direct calls and explicit lifetime arguments continue to use the declared signature.

Carrying conditions through to region checking

An associated type declaration can itself supply an outlives relationship:

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

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

Together, C::Assoc: 'r and C::Assoc = &'a () establish 'a: 'r, exactly the condition needed for shorten to return this reference. The derived lifetime relationship must reach the later checks.

The same conclusion can also have proofs with different conditions:

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 proof through D gives T: 'b, and 'b: 'r is sufficient to establish T: 'r. Requiring the conditions from both proofs would add an unnecessary 'a: 'r bound.

Conditions within one proof therefore remain conjunctive (AND), while alternative complete proofs retain disjunctive conditions (OR). These pass through inference, MIR, closure requirements, and the final borrow check.

For declaration candidates, merging these alternatives also requires each result to have Certainty::Yes, with matching canonical variable kinds, maximum universe, and substitutions, and no opaque type results or deferred normalization goals.

4. Correctness requirements and boundaries

Even when input and output normalize to the same type, the check must preserve these requirements:

  • Equality of associated type results must not imply equality of their arguments. T::View<'a> = T::View<'b> does not establish 'a = 'b.
  • Normalization must retain the requirements for using a type. For example, the requirement on type View<'a> = () where 'a: 'static cannot disappear because the result is ().
  • Equalities must remain within their original binder scopes. An equality for corresponding occurrences of 'a cannot be applied to an independent 'b.
  • An equality or lifetime generalization requirement cannot prove itself. A candidate whose inputs depend on the projection being computed cannot supply an independent normalization proof.
  • Matching type results must still carry their lifetime requirements through to verification. Recording a condition does not establish that it holds.

For example, a function whose requirements hold only for 'static still cannot be coerced to a function pointer callable for every lifetime.

5. Compatibility and behavior changes

Known changes

Area Change
Target signatures with the next solver enabled globally The false rejections described above become accepted, including function item matching and pointer coercions whose premises hold.
Experimental assumptions-on-binders configuration Shared lifetime constraint handling now verifies conditions remaining after leaving a binder; previously unchecked uses in test-infra-works.rs produce errors.
The old solver and coherence-only configuration The new output dependency rules and declaration reasoning are gated on the global configuration and do not enable the same relaxation in these modes.

The change in the experimental configuration comes from shared checking. With only T: Trait, T::Assoc may contain a short borrow, such as &'s u32, so it cannot be assumed to satisfy T::Assoc: 'a for every 'a. The previous path recorded this requirement but omitted its final verification. The change adds that step: these uses are rejected when the premise is missing and accepted when T::Assoc: 'static is added.

Inference and candidate selection

Adding a proof source can affect candidate selection, associated type normalization, and inference even in programs that did not previously encounter E0582. Making more function items satisfy a callable bound can also introduce different inference results or ambiguity where a call previously had one applicable match.

To limit interference with existing proofs, I retain priority rules for direct environment bounds, existing alias bounds, and builtin proofs requiring no additional conditions. Declaration facts are considered before impl search for abstract Self types; for concrete Self types, applicable impls are assembled first.

I have not run a broad compatibility comparison across existing crates, so I do not yet know which other previously accepted programs these changes may affect.

6. Compilation costs

I have not yet measured the effect of this change on compile time, instruction counts, or peak memory.

The added work is concentrated in these areas:

Potential cost Current control Measurement needed
Output type traversal and equality normalization Track visited types, perform structural checks first, and normalize where equalities are needed Overhead on unrelated code and query cost for many GAT signatures
Associated item and supertrait declaration search Filter declarations that cannot reach the goal and use solver queries and cycle handling Candidate and query counts, cache hits, and growth under branching recursion
Function item signature generalization Store the callable signature in a separate query and check whether lifetime parameters need processing Cost for many generic functions and repeated matching
Alternative lifetime constraints Keep constraints in batches and simplify duplicate or subsumed branches rather than combining everything on each insertion Merge time, peak memory, and growth under nested closures

Performance comparisons need the version before the entire change as their baseline, with the same compiler configuration. Alongside ordinary crates, workloads need to include substantial GAT/equality usage and stress cases for branching recursion and closure constraints, to expose the costs as the numbers of queries and alternative conditions grow.

The design adds no runtime checks and does not change runtime type representation. The performance of generated code has not been measured separately.

7. Unresolved issues

Cyclic normalization through nested equalities

These declarations already express that T::Out and U::Out are equal, but the current solver cannot complete the check:

trait Item {
    type Out;
}

trait Left<U: Item> {
    type Assoc: Item<Out = U::Out>;
}

trait Right<T: Item> {
    type Assoc: Item<Out = T::Out>;
}

fn same<C, D, T, U>(value: T::Out) -> U::Out
where
    T: Item,
    U: Item,
    C: Left<U, Assoc = T>,
    D: Right<T, Assoc = U>,
{
    value
}

The bound C: Left<U, Assoc = T> requires T to satisfy Item<Out = U::Out>, establishing T::Out = U::Out. The bound on D supplies the equality in the other direction. The function body only needs this equality; it does not need both projections reduced to concrete types.

The two declarations allow normalization of the projections to depend on each other: T::Out → U::Out → T::Out. The next solver already detects cycles, but the current declaration reasoning cannot produce a definite result for this example and reports E0275. This valid use is still rejected; encountering a repeated goal does not, by itself, prove that goal.

Assertion in the rigid alias consistency check

The current declaration-equality-recursive-wf.rs passes in ordinary globally enabled mode. With -Zrenormalize-rigid-aliases and -Zdisable-fast-paths additionally enabled, it triggers the rigid alias is further normalized assertion and crashes the compiler.

In this example, the Outer implementation for Wrapper<T> defines Inner as Storage<T>. The solver nevertheless retains <Wrapper<T> as Outer>::Inner as a rigid alias, indicating that it needs no further normalization in the current typing environment. The additional check recomputes it and obtains Storage<T>, which differs from the form previously retained.

This exposes an inconsistency between the normalization state and the later solver result. The cause has not yet been identified, and this internal compiler error remains unfixed.

8. Related issues

Issue Behavior in this prototype
#107572 The pattern represented by apply, using the same GAT projection for input and output, is accepted.
#86702 The original example has no equality between the two projections and still produces E0581/E0582.
#121437 The original minimal Not<Output = T::Native<'a>> example passes type checking.

@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
@enginespot enginespot changed the title Prototype Fixing false E0581/E0582 errors in the next solver Prototype Callable lifetime dependencies through associated types Sep 19, 2026

This branch has not been deployed

No deployments
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