diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 7f2f32c62..f984f1196 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/solve/candidate-preference.md b/src/solve/candidate-preference.md new file mode 100644 index 000000000..af066415a --- /dev/null +++ b/src/solve/candidate-preference.md @@ -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: Sized {} +impl<'a> Trait for &'a str {} +impl<'a> Trait for &'a str {} +fn is_sized(_: T) {} +fn foo<'a, 'b, T>(x: &'b str) +where + &'a str: Trait, +{ + // Elaborating the `&'a str: Trait` 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() {} +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 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 { + type TraitAssoc; +} + +impl Trait for T +where + T: Super, +{ + type TraitAssoc = U; +} + +fn overflow() { + // We can use the elaborated `Super` where-bound + // to prove the where-bound of the `T: Trait` implementation. This currently results in + // overflow. + let x: ::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 { + fn call_me(&self, x: T) {} +} +impl Trait for T {} +impl Trait for T {} +fn bug, 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>, + >::Assoc: Bound<'a>, +{ + // Using the where-bound for `>::Assoc: Bound<'a>` + // unnecessarily equates `>::Assoc` with the + // `>::Assoc` from the env. + impls_bound::<'a, >::Assoc>(); + // For a `>::Assoc: Bound<'b>` the self type of the + // where-bound matches, but the arguments of the trait bound don't. + impls_bound::<'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) +where + Vec: Into + Into, +{ + let _: f32 = Vec.into(); + // Without the global `Vec: Into` bound we'd + // eagerly use the non-global `Vec: Into` bound + // here, causing this to fail. +} + +struct Vec; +impl From 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; + fn d_tag(&self) -> Self::Word; + fn tag32(&self) -> Option { + self.d_tag().into().try_into().ok() + // prove `Self::Word: Into` and then select a method + // on `?0`, needs eager inference. + } +} +``` +```rust +fn impl_trait() -> impl Into { + 0u16 +} + +fn main() { + // There are two possible types for `x`: + // - `u32` by using the "alias bound" of `impl Into` + // - `impl Into`, i.e. `u16`, by using `impl From 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 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; +} +trait Trait: Super {} + +// Elaborating the environment results in a `T::Assoc: Super` where-bound. +// This where-bound must not prevent normalization via the `Super` +// item bound. +fn heck>(x: ::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>>(x: >::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` +// where-bound over the `I::Intoiterator: Iterator` +// alias-bound. + +trait Iterator { + type Item; +} + +trait IntoIterator { + type Item; + type IntoIter: Iterator; +} + +fn normalize>() {} + +fn foo() +where + I: IntoIterator, + I::IntoIter: Iterator, +{ + // We need to prefer the `I::IntoIterator: Iterator` + // where-bound over the `I::Intoiterator: Iterator` + // alias-bound. + normalize::(); +} +``` + +### 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` because of + // the impl below, which requires that `Self::Bar<()>: Eq` + // which is true, because we assume `for Self::Bar = i32`. + type Bar: Baz = i32; +} +trait Baz {} +impl Baz for i32 where T::Bar<()>: Eq {} +trait Eq {} +impl Eq 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 Id for T { + type This = T; +} + +fn foo(x: T) -> ::This +where + u32: Id, +{ + 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() {} +fn normalize>() {} + +fn foo<'a>() +where + &'static u32: Trait, +{ + 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 \ No newline at end of file