Prototype Callable lifetime dependencies through associated types - #162745
enginespot wants to merge 2 commits into
Conversation
|
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:
|
This comment has been minimized.
This comment has been minimized.
b47fc22 to
7a8b709
Compare
This comment has been minimized.
This comment has been minimized.
|
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. |
7a8b709 to
8010641
Compare
|
This PR changes rustc_public cc @oli-obk, @celinval, @ouz-a, @makai410 Some changes occurred to the CTFE machinery changes to the core type system cc @lcnr
cc @rust-lang/clippy changes to the core type system cc @lcnr |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
8010641 to
4c74fed
Compare
|
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. |
This comment has been minimized.
This comment has been minimized.
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.
4c74fed to
02c25f8
Compare
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
applysimply passes a value to the callback and returns the result:The
for<'b>bound must hold for every'b. The previous check reports E0582 on the callback outputT::View<'b>; the function pointer typefor<'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
Familyinterface can have these two implementations:TBorrowed&'b u32&'b u32Erased()()Erasedignores'bentirely, so an associated type’s result cannot be used to recover its lifetime argument. The existing check therefore does not consider'bconstrained 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: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.
For
I('a) = O('a) = T::View<'a>, this follows directly without recovering'afrom the projection. The complete input type can also determine an output such asOption<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:
Fn(&'a T) -> &'a U'a;TandUmay differFn(T::View<'a>) -> T::View<'a>Fn(T::View<'a>) -> Option<T::View<'a>>Fn(T::View<'a>) -> U::View<'a>T: Family, U: FamilyFn(T::View<'a>) -> (T::View<'a>, &'a ())For example, with
T = ErasedandU = Borrowed, the fourth row becomesFn(()) -> &'a u32: the input provides no lifetime to constrain the output reference. In the first row,TandUmay 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
whereclause already requires the two projections to be equal: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:
The derivation is:
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 ownwhereclauses, and scoped premises.Using the declaration requires an independently established positive trait bound, such as
C: Carrierhere. Equations temporarily installed for normalization during impl checking cannot prove that the impl satisfies its own declaration.Selfwell-formedness already supplied by an established source is not required again as a circular premise; requirements introduced by instantiating a quantifiedSelfare 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
identitymust also support uses such as:I compute a callable signature through
fn_sig_for_fn_traitsforFnmatching, 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:
Together,
C::Assoc: 'randC::Assoc = &'a ()establish'a: 'r, exactly the condition needed forshortento return this reference. The derived lifetime relationship must reach the later checks.The same conclusion can also have proofs with different conditions:
The proof through
DgivesT: 'b, and'b: 'ris sufficient to establishT: 'r. Requiring the conditions from both proofs would add an unnecessary'a: 'rbound.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:
T::View<'a> = T::View<'b>does not establish'a = 'b.type View<'a> = () where 'a: 'staticcannot disappear because the result is().'acannot be applied to an independent'b.For example, a function whose requirements hold only for
'staticstill cannot be coerced to a function pointer callable for every lifetime.5. Compatibility and behavior changes
Known changes
assumptions-on-bindersconfigurationtest-infra-works.rsproduce errors.The change in the experimental configuration comes from shared checking. With only
T: Trait,T::Assocmay contain a short borrow, such as&'s u32, so it cannot be assumed to satisfyT::Assoc: 'afor 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 whenT::Assoc: 'staticis 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
Selftypes; for concreteSelftypes, 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:
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::OutandU::Outare equal, but the current solver cannot complete the check:The bound
C: Left<U, Assoc = T>requiresTto satisfyItem<Out = U::Out>, establishingT::Out = U::Out. The bound onDsupplies 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.rspasses in ordinary globally enabled mode. With-Zrenormalize-rigid-aliasesand-Zdisable-fast-pathsadditionally enabled, it triggers therigid alias is further normalizedassertion and crashes the compiler.In this example, the
Outerimplementation forWrapper<T>definesInnerasStorage<T>. The solver nevertheless retains<Wrapper<T> as Outer>::Inneras a rigid alias, indicating that it needs no further normalization in the current typing environment. The additional check recomputes it and obtainsStorage<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
apply, using the same GAT projection for input and output, is accepted.Not<Output = T::Native<'a>>example passes type checking.