Merge pull request #2529 from lcnr/type-system-invariants
add a chapter documenting candidate preference
This commit is contained in:
commit
c277f4e64d
2 changed files with 427 additions and 0 deletions
|
|
@ -176,6 +176,7 @@
|
|||
- [Next-gen trait solving](./solve/trait-solving.md)
|
||||
- [Invariants of the type system](./solve/invariants.md)
|
||||
- [The solver](./solve/the-solver.md)
|
||||
- [Candidate preference](./solve/candidate-preference.md)
|
||||
- [Canonicalization](./solve/canonicalization.md)
|
||||
- [Coinduction](./solve/coinduction.md)
|
||||
- [Caching](./solve/caching.md)
|
||||
|
|
|
|||
426
src/doc/rustc-dev-guide/src/solve/candidate-preference.md
Normal file
426
src/doc/rustc-dev-guide/src/solve/candidate-preference.md
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
# 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`].
|
||||
|
||||
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.
|
||||
|
||||
### `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.
|
||||
|
||||
This is necessary to prevent a lifetime error for the following pattern
|
||||
|
||||
```rust
|
||||
trait Trait<T>: Sized {}
|
||||
impl<'a> Trait<u32> for &'a str {}
|
||||
impl<'a> Trait<i32> for &'a str {}
|
||||
fn is_sized<T: Sized>(_: T) {}
|
||||
fn foo<'a, 'b, T>(x: &'b str)
|
||||
where
|
||||
&'a str: Trait<T>,
|
||||
{
|
||||
// Elaborating the `&'a str: Trait<T>` where-bound results in a
|
||||
// `&'a str: Sized` where-bound. We do not want to prefer this
|
||||
// over the builtin impl.
|
||||
is_sized(x);
|
||||
}
|
||||
```
|
||||
|
||||
This preference is incorrect in case the builtin impl has a nested goal which relies on a non-param where-clause
|
||||
```rust
|
||||
struct MyType<'a, T: ?Sized>(&'a (), T);
|
||||
fn is_sized<T>() {}
|
||||
fn foo<'a, T: ?Sized>()
|
||||
where
|
||||
(MyType<'a, T>,): Sized,
|
||||
MyType<'static, T>: Sized,
|
||||
{
|
||||
// The where-bound is trivial while the builtin `Sized` impl for tuples
|
||||
// requires proving `MyType<'a, T>: Sized` which can only be proven by
|
||||
// using the where-clause, adding an unnecessary `'static` constraint.
|
||||
is_sized::<(MyType<'a, T>,)>();
|
||||
//~^ ERROR lifetime may not live long enough
|
||||
}
|
||||
```
|
||||
|
||||
### `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`.
|
||||
|
||||
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.
|
||||
|
||||
#### Preference over `Impl` candidates
|
||||
|
||||
This is necessary to avoid region errors in the following example
|
||||
|
||||
```rust
|
||||
trait Trait<'a> {}
|
||||
impl<T> Trait<'static> for T {}
|
||||
fn impls_trait<'a, T: Trait<'a>>() {}
|
||||
fn foo<'a, T: Trait<'a>>() {
|
||||
impls_trait::<'a, T>();
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
type SuperAssoc;
|
||||
}
|
||||
|
||||
trait Trait: Super<SuperAssoc = Self::TraitAssoc> {
|
||||
type TraitAssoc;
|
||||
}
|
||||
|
||||
impl<T, U> Trait for T
|
||||
where
|
||||
T: Super<SuperAssoc = U>,
|
||||
{
|
||||
type TraitAssoc = U;
|
||||
}
|
||||
|
||||
fn overflow<T: Trait>() {
|
||||
// We can use the elaborated `Super<SuperAssoc = Self::TraitAssoc>` where-bound
|
||||
// to prove the where-bound of the `T: Trait` implementation. This currently results in
|
||||
// overflow.
|
||||
let x: <T as Trait>::TraitAssoc;
|
||||
}
|
||||
```
|
||||
|
||||
This preference causes a lot of issues. See https://github.com/rust-lang/rust/issues/24066. Most of the
|
||||
issues are caused by prefering where-bounds over impls even if the where-bound guides type inference:
|
||||
```rust
|
||||
trait Trait<T> {
|
||||
fn call_me(&self, x: T) {}
|
||||
}
|
||||
impl<T> Trait<u32> for T {}
|
||||
impl<T> Trait<i32> for T {}
|
||||
fn bug<T: Trait<U>, U>(x: T) {
|
||||
x.call_me(1u32);
|
||||
//~^ ERROR mismatched types
|
||||
}
|
||||
```
|
||||
However, even if we only apply this preference if the where-bound doesn't guide inference, it may still result
|
||||
in incorrect lifetime constraints:
|
||||
```rust
|
||||
trait Trait<'a> {}
|
||||
impl<'a> Trait<'a> for &'a str {}
|
||||
fn impls_trait<'a, T: Trait<'a>>(_: T) {}
|
||||
fn foo<'a, 'b>(x: &'b str)
|
||||
where
|
||||
&'a str: Trait<'b>
|
||||
{
|
||||
// Need to prove `&'x str: Trait<'b>` with `'b: 'x`.
|
||||
impls_trait::<'b, _>(x);
|
||||
//~^ ERROR lifetime may not live long enough
|
||||
}
|
||||
```
|
||||
|
||||
#### Preference over `AliasBound` candidates
|
||||
|
||||
This is necessary to avoid region errors in the following example
|
||||
```rust
|
||||
trait Bound<'a> {}
|
||||
trait Trait<'a> {
|
||||
type Assoc: Bound<'a>;
|
||||
}
|
||||
|
||||
fn impls_bound<'b, T: Bound<'b>>() {}
|
||||
fn foo<'a, 'b, 'c, T>()
|
||||
where
|
||||
T: Trait<'a>,
|
||||
for<'hr> T::Assoc: Bound<'hr>,
|
||||
{
|
||||
impls_bound::<'b, T::Assoc>();
|
||||
impls_bound::<'c, T::Assoc>();
|
||||
}
|
||||
```
|
||||
It can also result in unnecessary constraints
|
||||
```rust
|
||||
trait Bound<'a> {}
|
||||
trait Trait<'a> {
|
||||
type Assoc: Bound<'a>;
|
||||
}
|
||||
|
||||
fn impls_bound<'b, T: Bound<'b>>() {}
|
||||
fn foo<'a, 'b, T>()
|
||||
where
|
||||
T: for<'hr> Trait<'hr>,
|
||||
<T as Trait<'b>>::Assoc: Bound<'a>,
|
||||
{
|
||||
// Using the where-bound for `<T as Trait<'a>>::Assoc: Bound<'a>`
|
||||
// unnecessarily equates `<T as Trait<'a>>::Assoc` with the
|
||||
// `<T as Trait<'b>>::Assoc` from the env.
|
||||
impls_bound::<'a, <T as Trait<'a>>::Assoc>();
|
||||
// For a `<T as Trait<'b>>::Assoc: Bound<'b>` the self type of the
|
||||
// where-bound matches, but the arguments of the trait bound don't.
|
||||
impls_bound::<'b, <T as Trait<'b>>::Assoc>();
|
||||
}
|
||||
```
|
||||
|
||||
#### 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`:
|
||||
|
||||
```rust
|
||||
trait A {
|
||||
fn test(&self);
|
||||
}
|
||||
|
||||
fn foo(x: &dyn A)
|
||||
where
|
||||
dyn A + 'static: A, // Using this bound would lead to a lifetime error.
|
||||
{
|
||||
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.
|
||||
|
||||
#### 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:
|
||||
```rust
|
||||
fn check<Color>(color: Color)
|
||||
where
|
||||
Vec: Into<Color> + Into<f32>,
|
||||
{
|
||||
let _: f32 = Vec.into();
|
||||
// Without the global `Vec: Into<f32>` bound we'd
|
||||
// eagerly use the non-global `Vec: Into<Color>` bound
|
||||
// here, causing this to fail.
|
||||
}
|
||||
|
||||
struct Vec;
|
||||
impl From<Vec> for f32 {
|
||||
fn from(_: Vec) -> Self {
|
||||
loop {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `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 🤷
|
||||
```rust
|
||||
pub trait Dyn {
|
||||
type Word: Into<u64>;
|
||||
fn d_tag(&self) -> Self::Word;
|
||||
fn tag32(&self) -> Option<u32> {
|
||||
self.d_tag().into().try_into().ok()
|
||||
// prove `Self::Word: Into<?0>` and then select a method
|
||||
// on `?0`, needs eager inference.
|
||||
}
|
||||
}
|
||||
```
|
||||
```rust
|
||||
fn impl_trait() -> impl Into<u32> {
|
||||
0u16
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// There are two possible types for `x`:
|
||||
// - `u32` by using the "alias bound" of `impl Into<u32>`
|
||||
// - `impl Into<u32>`, i.e. `u16`, by using `impl<T> From<T> for T`
|
||||
//
|
||||
// We infer the type of `x` to be `u32` even though this is not
|
||||
// strictly necessary and can even lead to surprising errors.
|
||||
let x = impl_trait().into();
|
||||
println!("{}", std::mem::size_of_val(&x));
|
||||
}
|
||||
```
|
||||
This preference also avoids ambiguity due to region constraints, I don't know whether people rely on this in practice.
|
||||
```rust
|
||||
trait Bound<'a> {}
|
||||
impl<T> Bound<'static> for T {}
|
||||
trait Trait<'a> {
|
||||
type Assoc: Bound<'a>;
|
||||
}
|
||||
|
||||
fn impls_bound<'b, T: Bound<'b>>() {}
|
||||
fn foo<'a, T: Trait<'a>>() {
|
||||
// Should we infer this to `'a` or `'static`.
|
||||
impls_bound::<'_, T::Assoc>();
|
||||
}
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
## `NormalizesTo` goals
|
||||
|
||||
The candidate preference behavior during normalization is implemented in [`fn assemble_and_merge_candidates`].
|
||||
|
||||
### Where-bounds shadow impls
|
||||
|
||||
Normalization of associated items does not consider impls if the corresponding trait goal has been proven via a `ParamEnv` or `AliasBound` candidate.
|
||||
This means that for where-bounds which do not constrain associated types, the associated types remain *rigid*.
|
||||
|
||||
This is necessary to avoid unnecessary region constraints from applying impls.
|
||||
```rust
|
||||
trait Trait<'a> {
|
||||
type Assoc;
|
||||
}
|
||||
impl Trait<'static> for u32 {
|
||||
type Assoc = u32;
|
||||
}
|
||||
|
||||
fn bar<'b, T: Trait<'b>>() -> T::Assoc { todo!() }
|
||||
fn foo<'a>()
|
||||
where
|
||||
u32: Trait<'a>,
|
||||
{
|
||||
// Normalizing the return type would use the impl, proving
|
||||
// the `T: Trait` where-bound would use the where-bound, resulting
|
||||
// in different region constraints.
|
||||
bar::<'_, u32>();
|
||||
}
|
||||
```
|
||||
|
||||
### We always consider `AliasBound` candidates
|
||||
|
||||
In case the where-bound does not specify the associated item, we consider `AliasBound` candidates instead of treating the alias as rigid, even though the trait goal was proven via a `ParamEnv` candidate.
|
||||
|
||||
```rust
|
||||
trait Super {
|
||||
type Assoc;
|
||||
}
|
||||
trait Bound {
|
||||
type Assoc: Super<Assoc = u32>;
|
||||
}
|
||||
trait Trait: Super {}
|
||||
|
||||
// Elaborating the environment results in a `T::Assoc: Super` where-bound.
|
||||
// This where-bound must not prevent normalization via the `Super<Assoc = u32>`
|
||||
// item bound.
|
||||
fn heck<T: Bound<Assoc: Trait>>(x: <T::Assoc as Super>::Assoc) -> u32 {
|
||||
x
|
||||
}
|
||||
```
|
||||
Using such an alias can result in additional region constraints, cc [#133044].
|
||||
```rust
|
||||
trait Bound<'a> {
|
||||
type Assoc;
|
||||
}
|
||||
trait Trait {
|
||||
type Assoc: Bound<'static, Assoc = u32>;
|
||||
}
|
||||
|
||||
fn heck<'a, T: Trait<Assoc: Bound<'a>>>(x: <T::Assoc as Bound<'a>>::Assoc) {
|
||||
// Normalizing the associated type requires `T::Assoc: Bound<'static>` as it
|
||||
// uses the `Bound<'static>` alias-bound instead of keeping the alias rigid.
|
||||
drop(x);
|
||||
}
|
||||
```
|
||||
|
||||
### We prefer `ParamEnv` candidates over `AliasBound`
|
||||
|
||||
While we use `AliasBound` candidates if the where-bound does not specify the associated type, in case it does, we prefer the where-bound.
|
||||
This is necessary for the following example:
|
||||
```rust
|
||||
// Make sure we prefer the `I::IntoIterator: Iterator<Item = ()>`
|
||||
// where-bound over the `I::Intoiterator: Iterator<Item = I::Item>`
|
||||
// alias-bound.
|
||||
|
||||
trait Iterator {
|
||||
type Item;
|
||||
}
|
||||
|
||||
trait IntoIterator {
|
||||
type Item;
|
||||
type IntoIter: Iterator<Item = Self::Item>;
|
||||
}
|
||||
|
||||
fn normalize<I: Iterator<Item = ()>>() {}
|
||||
|
||||
fn foo<I>()
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::IntoIter: Iterator<Item = ()>,
|
||||
{
|
||||
// We need to prefer the `I::IntoIterator: Iterator<Item = ()>`
|
||||
// where-bound over the `I::Intoiterator: Iterator<Item = I::Item>`
|
||||
// alias-bound.
|
||||
normalize::<I::IntoIter>();
|
||||
}
|
||||
```
|
||||
|
||||
### We always consider where-bounds
|
||||
|
||||
Even if the trait goal was proven via an impl, we still prefer `ParamEnv` candidates, if any exist.
|
||||
|
||||
#### 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.
|
||||
```rust
|
||||
#![feature(associated_type_defaults)]
|
||||
trait Foo {
|
||||
// We should be able to prove that `i32: Baz<Self>` because of
|
||||
// the impl below, which requires that `Self::Bar<()>: Eq<i32>`
|
||||
// which is true, because we assume `for<T> Self::Bar<T> = i32`.
|
||||
type Bar<T>: Baz<Self> = i32;
|
||||
}
|
||||
trait Baz<T: ?Sized> {}
|
||||
impl<T: Foo + ?Sized> Baz<T> for i32 where T::Bar<()>: Eq<i32> {}
|
||||
trait Eq<T> {}
|
||||
impl<T> Eq<T> for T {}
|
||||
```
|
||||
|
||||
I don't fully understand the cases where this preference is actually necessary and haven't been able to exploit this in fun ways yet, but 🤷
|
||||
|
||||
#### 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 🤷
|
||||
```rust
|
||||
trait Id {
|
||||
type This;
|
||||
}
|
||||
impl<T> Id for T {
|
||||
type This = T;
|
||||
}
|
||||
|
||||
fn foo<T>(x: T) -> <u32 as Id>::This
|
||||
where
|
||||
u32: Id<This = T>,
|
||||
{
|
||||
x
|
||||
}
|
||||
```
|
||||
This means normalization can result in additional region constraints, cc [#133044].
|
||||
```rust
|
||||
trait Trait {
|
||||
type Assoc;
|
||||
}
|
||||
|
||||
impl Trait for &u32 {
|
||||
type Assoc = u32;
|
||||
}
|
||||
|
||||
fn trait_bound<T: Trait>() {}
|
||||
fn normalize<T: Trait<Assoc = u32>>() {}
|
||||
|
||||
fn foo<'a>()
|
||||
where
|
||||
&'static u32: Trait<Assoc = u32>,
|
||||
{
|
||||
trait_bound::<&'a u32>(); // ok, proven via impl
|
||||
normalize::<&'a u32>(); // error, proven via where-bound
|
||||
}
|
||||
```
|
||||
|
||||
[`Candidate`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_next_trait_solver/solve/assembly/struct.Candidate.html
|
||||
[`CandidateSource`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_next_trait_solver/solve/enum.CandidateSource.html
|
||||
[`fn merge_trait_candidates`]: https://github.com/rust-lang/rust/blob/e3ee7f7aea5b45af3b42b5e4713da43876a65ac9/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs#L1342-L1424
|
||||
[`fn assemble_and_merge_candidates`]: https://github.com/rust-lang/rust/blob/e3ee7f7aea5b45af3b42b5e4713da43876a65ac9/compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs#L920-L1003
|
||||
[trait-system-refactor-initiative#76]: https://github.com/rust-lang/trait-system-refactor-initiative/issues/76
|
||||
[#133044]: https://github.com/rust-lang/rust/issues/133044
|
||||
Loading…
Add table
Add a link
Reference in a new issue