sembr src/solve/candidate-preference.md

This commit is contained in:
Tshepang Mbambo
2026-03-23 23:07:32 +01:00
parent 0c3435c3e1
commit b638ac4c3b
@@ -1,16 +1,23 @@
# Candidate preference
There are multiple ways to prove `Trait` and `NormalizesTo` goals. Each such option is called a [`Candidate`]. If there are multiple applicable candidates, we prefer some candidates over others. We store the relevant information in their [`CandidateSource`].
There are multiple ways to prove `Trait` and `NormalizesTo` goals.
Each such option is called a [`Candidate`].
If there are multiple applicable candidates, we prefer some candidates over others.
We store the relevant information in their [`CandidateSource`].
This preference may result in incorrect inference or region constraints and would therefore be unsound during coherence. Because of this, we simply try to merge all candidates in coherence.
This preference may result in incorrect inference or region constraints and would therefore be unsound during coherence.
Because of this, we simply try to merge all candidates in coherence.
## `Trait` goals
Trait goals merge their applicable candidates in [`fn merge_trait_candidates`]. This document provides additional details and references to explain *why* we've got the current preference rules.
Trait goals merge their applicable candidates in [`fn merge_trait_candidates`].
This document provides additional details and references to explain *why* we've got the current preference rules.
### `CandidateSource::BuiltinImpl(BuiltinImplSource::Trivial))`
Trivial builtin impls are builtin impls which are known to be always applicable for well-formed types. This means that if one exists, using another candidate should never have fewer constraints. We currently only consider `Sized` - and `MetaSized` - impls to be trivial.
Trivial builtin impls are builtin impls which are known to be always applicable for well-formed types.
This means that if one exists, using another candidate should never have fewer constraints.
We currently only consider `Sized` - and `MetaSized` - impls to be trivial.
This is necessary to prevent a lifetime error for the following pattern
@@ -50,7 +57,8 @@ where
### `CandidateSource::ParamEnv`
Once there's at least one *non-global* `ParamEnv` candidate, we prefer *all* `ParamEnv` candidates over other candidate kinds.
A where-bound is global if it is not higher-ranked and doesn't contain any generic parameters. It may contain `'static`.
A where-bound is global if it is not higher-ranked and doesn't contain any generic parameters.
It may contain `'static`.
We try to apply where-bounds over other candidates as users tends to have the most control over them, so they can most easily
adjust them in case our candidate preference is incorrect.
@@ -68,7 +76,8 @@ fn foo<'a, T: Trait<'a>>() {
}
```
We also need this as shadowed impls can result in currently ambiguous solver cycles: [trait-system-refactor-initiative#76]. Without preference we'd be forced to fail with ambiguity
We also need this as shadowed impls can result in currently ambiguous solver cycles: [trait-system-refactor-initiative#76].
Without preference we'd be forced to fail with ambiguity
errors if the where-bound results in region constraints to avoid incompleteness.
```rust
trait Super {
@@ -94,7 +103,9 @@ fn overflow<T: Trait>() {
}
```
This preference causes a lot of issues. See [#24066]. Most of the
This preference causes a lot of issues.
See [#24066].
Most of the
issues are caused by preferring where-bounds over impls even if the where-bound guides type inference:
```rust
trait Trait<T> {
@@ -167,7 +178,10 @@ where
#### Why no preference for global where-bounds
Global where-bounds are either fully implied by an impl or unsatisfiable. If they are unsatisfiable, we don't really care what happens. If a where-bound is fully implied then using the impl to prove the trait goal cannot result in additional constraints. For trait goals this is only useful for where-bounds which use `'static`:
Global where-bounds are either fully implied by an impl or unsatisfiable.
If they are unsatisfiable, we don't really care what happens.
If a where-bound is fully implied then using the impl to prove the trait goal cannot result in additional constraints.
For trait goals this is only useful for where-bounds which use `'static`:
```rust
trait A {
@@ -181,13 +195,15 @@ where
x.test();
}
```
More importantly, by using impls here we prevent global where-bounds from shadowing impls when normalizing associated types. There are no known issues from preferring impls over global where-bounds.
More importantly, by using impls here we prevent global where-bounds from shadowing impls when normalizing associated types.
There are no known issues from preferring impls over global where-bounds.
#### Why still consider global where-bounds
Given that we just use impls even if there exists a global where-bounds, you may ask why we don't just ignore these global where-bounds entirely: we use them to weaken the inference guidance from non-global where-bounds.
Without a global where-bound, we currently prefer non-global where bounds even though there would be an applicable impl as well. By adding a non-global where-bound, this unnecessary inference guidance is disabled, allowing the following to compile:
Without a global where-bound, we currently prefer non-global where bounds even though there would be an applicable impl as well.
By adding a non-global where-bound, this unnecessary inference guidance is disabled, allowing the following to compile:
```rust
fn check<Color>(color: Color)
where
@@ -209,7 +225,9 @@ impl From<Vec> for f32 {
### `CandidateSource::AliasBound`
We prefer alias-bound candidates over impls. We currently use this preference to guide type inference, causing the following to compile. I personally don't think this preference is desirable 🤷
We prefer alias-bound candidates over impls.
We currently use this preference to guide type inference, causing the following to compile.
I personally don't think this preference is desirable 🤷
```rust
pub trait Dyn {
type Word: Into<u64>;
@@ -254,7 +272,9 @@ fn foo<'a, T: Trait<'a>>() {
### `CandidateSource::BuiltinImpl(BuiltinImplSource::Object(_))`
We prefer builtin trait object impls over user-written impls. This is **unsound** and should be remoed in the future. See [#57893](https://github.com/rust-lang/rust/issues/57893) and [#141347](https://github.com/rust-lang/rust/pull/141347) for more details.
We prefer builtin trait object impls over user-written impls.
This is **unsound** and should be remoed in the future.
See [#57893](https://github.com/rust-lang/rust/issues/57893) and [#141347](https://github.com/rust-lang/rust/pull/141347) for more details.
## `NormalizesTo` goals
@@ -336,7 +356,7 @@ Even if the trait goal was proven via an impl, we still prefer `ParamEnv` candid
#### We prefer "orphaned" where-bounds
We add "orphaned" `Projection` clauses into the `ParamEnv` when normalizing item bounds of GATs and RPITIT in `fn check_type_bounds`.
We need to prefer these `ParamEnv` candidates over impls and other where-bounds.
We need to prefer these `ParamEnv` candidates over impls and other where-bounds.
```rust
#![feature(associated_type_defaults)]
trait Foo {
@@ -355,7 +375,8 @@ I don't fully understand the cases where this preference is actually necessary a
#### We prefer global where-bounds over impls
This is necessary for the following to compile. I don't know whether anything relies on it in practice 🤷
This is necessary for the following to compile.
I don't know whether anything relies on it in practice 🤷
```rust
trait Id {
type This;
@@ -423,7 +444,8 @@ where
#### RPITIT `type_of` cycles
We currently have to avoid impl candidates if there are where-bounds to avoid query cycles for RPITIT, see [#139762]. It feels desirable to me to stop relying on auto-trait leakage of during RPITIT computation to remove this issue, see [#139788].
We currently have to avoid impl candidates if there are where-bounds to avoid query cycles for RPITIT, see [#139762].
It feels desirable to me to stop relying on auto-trait leakage of during RPITIT computation to remove this issue, see [#139788].
```rust
use std::future::Future;
@@ -457,8 +479,8 @@ where
#### Trait definition cannot use associated types from always applicable impls
The `T: Trait` assumption in the trait definition prevents it from normalizing
`<Self as Trait>::Assoc` to `T` by using the blanket impl. This feels like a somewhat
desirable constraint, if not incredibly so.
`<Self as Trait>::Assoc` to `T` by using the blanket impl.
This feels like a somewhat desirable constraint, if not incredibly so.
```rust
trait Eq<T> {}
@@ -486,4 +508,4 @@ impl<T> Trait for T {
[#24066]: https://github.com/rust-lang/rust/issues/24066
[#133044]: https://github.com/rust-lang/rust/issues/133044
[#139762]: https://github.com/rust-lang/rust/pull/139762
[#139788]: https://github.com/rust-lang/rust/issues/139788
[#139788]: https://github.com/rust-lang/rust/issues/139788