-
Notifications
You must be signed in to change notification settings - Fork 13.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prefer built-in sized impls (and only sized impls) for rigid types always #138176
base: master
Are you sure you want to change the base?
Conversation
@bors try @rust-timer queue (and crater) |
This comment has been minimized.
This comment has been minimized.
…try> Prefer built-in sized obligations for rigid types always r? lcnr
☀️ Try build successful - checks-actions |
This comment has been minimized.
This comment has been minimized.
@craterbot check |
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
Finished benchmarking commit (51b1964): comparison URL. Overall result: ❌✅ regressions and improvements - please read the text belowBenchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf. Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @bors rollup=never Instruction countThis is the most reliable metric that we have; it was used to determine the overall result at the top of this comment. However, even this metric can sometimes exhibit noise.
Max RSS (memory usage)Results (primary 1.0%, secondary -1.5%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesResults (primary 2.7%, secondary 1.3%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 766.551s -> 766.651s (0.01%) |
🎉 Experiment
|
debug_assert_eq!(sized_candidates.next(), None); | ||
return Some(SizedCandidate); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you also try to remove the precedence for Builtin { nested: false }
, i.e. exclusively prefer Sized
. This would then match the new solver, if it causes breakage, we should instead hcange the list of "trivial builtin traits/impls"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e. exclusively prefer
Sized
Do you mean stop preferring other trivial built-in impls, like Copy
and Clone
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let me re-crater that then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feeling conflicted here. Preferring trivial builtin impls for traits with assoc types results in #133044. This also leaks whether impls are builtin or not. E.g. going from a builtin impl for fn-ptrs to an blanket impl for F: FnPtr
is a subtle breaking change.
In general:
- we prefer trivial builtin impls over where-bounds as the impl can never add any undesirable constraints. Where-bounds may add undesirable constraints, especially if the where-bound gets added by elaborating
trait Sub: Trait
. - if the builtin impl defines an associated type which references either regions or some param, we should use the where-bound as it's otherwise observable that we ignore the bound in case it's assoc type differs 🤔
- if the builtin impl has nested obligations, using it may result in undesirable errors, e.g. if you check
(T, T): Copy
and the environment contains(T, T): Copy
using the builtin impl for tuples causes this to error.- winnowing first evaluates all nested obligations of each candidate, so this is only an issue if proving the nested obligations is still ambig (due to inference vars instead of
T
), or if proving the nested obligations only cause unsatisfied region constraints
- winnowing first evaluates all nested obligations of each candidate, so this is only an issue if proving the nested obligations is still ambig (due to inference vars instead of
For Sized
we don't need to worry about region constraints from the builtin impl, even if it has nested obligations as Sized
impls never have region constraints not already required by well-formedness.
We do have the issue when proving Sized
for types still involving infer vars, i.e. the following should error with this PR
struct W<T: ?Sized>(T);
fn is_sized<T: Sized>(x: *const T) {}
fn dummy<T: ?Sized>() -> *const T { todo!() }
fn non_param_where_bound<T: ?Sized>()
where
W<T>: Sized,
{
let x: *const W<_> = dummy();
is_sized::<W<_>>(x);
let _: *const W<T> = x;
}
proving W<?x>: Sized
prefers the builtin impl over the where-bound as the nested obligations of that impl are still ambig. We then constrain ?x
to T
in which case proving the nested obligations fails.
Can you change the preference for Sized
to bail with ambiguity if the self type still has non-region infer vars?
a1c78bb
to
ee3359a
Compare
@bors try |
…try> Prefer built-in sized impls (and only sized impls) for rigid types always This PR changes the confirmation of `Sized` obligations to unconditionally prefer the built-in obligation, even if it has nested obligations. In the old solver, we register many builtin candidates with the `BuiltinCandidate { has_nested: bool }` candidate kind. The precedence this candidate takes over other candidates is based on the `has_nested` field. If it's false, then we prefer it over param-env candidates: https://github.com/rust-lang/rust/blob/2b4694a69804f89ff9d47d1a427f72c876f7f44c/compiler/rustc_trait_selection/src/traits/select/mod.rs#L1804-L1815 Otherwise we prefer param-env candidates over it: https://github.com/rust-lang/rust/blob/2b4694a69804f89ff9d47d1a427f72c876f7f44c/compiler/rustc_trait_selection/src/traits/select/mod.rs#L1847-L1866 I assume that the motivation for this behavior is to prefer built-in candidates if they have no nested obligations, but we can't as confidently prefer built-in candidates when they have where-clauses since we have no guarantee that the nested obligations for the built-in candidate are actually satisfyable (especially considering regions). However, for `Sized` candidates in particular, unlike the example above we never end up bottoming-out in a user-written impl, since *all* `Sized` impls are built-in. Thus, we don't have this problem, and preferring param-env candidates actually ends up leading to detrimental inference guidance, like: ```rust fn hello<T>() where (T,): Sized { let x: (_,) = Default::default(); // ^^ The `Sized` obligation on the variable infers `_ = T`. let x: (i32,) = x; // We error here, both a type mismatch and also b/c `T: Default` doesn't hold. } ``` Therefore this PR adjusts the candidate precedence of `Sized` obligations by making them a distinct candidate kind and unconditionally preferring them over all other candidate kinds. r? lcnr
ee3359a
to
08d8f3a
Compare
@bors try |
updated the PR description to be ready for an FCP, however, I do think there's still one way we ignore where-bounds with this change:
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,
{
// Preferring the builtin `Sized` impl of 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>,)>();
} @compiler-errors pls add this as a test, check my changes to the PR description and then start a types FCP :3 |
150ca15
to
37235a2
Compare
Team member @compiler-errors has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
Let's get an updated perf too @bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
…try> Prefer built-in sized impls (and only sized impls) for rigid types always This PR changes the confirmation of `Sized` obligations to unconditionally prefer the built-in impl, even if it has nested obligations. This also changes all other built-in impls (namely, `Copy`/`Clone`/`DiscriminantKind`/`Pointee`) to *not* prefer built-in impls over param-env impls. This aligns the old solver with the behavior of the new solver. --- In the old solver, we register many builtin candidates with the `BuiltinCandidate { has_nested: bool }` candidate kind. The precedence this candidate takes over other candidates is based on the `has_nested` field. We only prefer builtin impls over param-env candidates if `has_nested` is `false` https://github.com/rust-lang/rust/blob/2b4694a69804f89ff9d47d1a427f72c876f7f44c/compiler/rustc_trait_selection/src/traits/select/mod.rs#L1804-L1866 Preferring param-env candidates when the builtin candidate has nested obligations *still* ends up leading to detrimental inference guidance, like: ```rust fn hello<T>() where (T,): Sized { let x: (_,) = Default::default(); // ^^ The `Sized` obligation on the variable infers `_ = T`. let x: (i32,) = x; // We error here, both a type mismatch and also b/c `T: Default` doesn't hold. } ``` Therefore this PR adjusts the candidate precedence of `Sized` obligations by making them a distinct candidate kind and unconditionally preferring them over all other candidate kinds. Special-casing `Sized` this way is necessary as there are a lot of traits with a `Sized` super-trait bound, so a `&'a str: From<T>` where-bound results in an elaborated `&'a str: Sized` bound. People tend to not add explicit where-clauses which overlap with builtin impls, so this tends to not be an issue for other traits. We don't know of any tests/crates which need preference for other builtin traits. As this causes builtin impls to diverge from user-written impls we would like to minimize the affected traits. Otherwise e.g. moving impls for tuples to std by using variadic generics would be a breaking change. For other builtin impls it's also easier for the preference of builtin impls over where-bounds to result in issues. --- There are two ways preferring builtin impls over where-bounds can be incorrect and undesirable: - applying the builtin impl results in undesirable region constraints. E.g. if only `MyType<'static>` implements `Copy` then a goal like `(MyType<'a>,): Copy` would require `'a == 'static` so we must not prefer it over a `(MyType<'a>,): Copy` where-bound - this is mostly not an issue for `Sized` as all `Sized` impls are builtin and don't add any region constraints not already required for the type to be well-formed - however, even with `Sized` this is still an issue if a nested goal also gets proven via a where-bound: [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=30377da5b8a88f654884ab4ebc72f52b) - if the builtin impl has associated types, we should not prefer it over where-bounds when normalizing that associated type. This can result in normalization adding more region constraints than just proving trait bounds. rust-lang#133044 - not an issue for `Sized` as it doesn't have associated types. r? lcnr
Overall seems reasonable, but I would expect the tests in rust-lang/trait-system-refactor-initiative#162 to be included in this PR... |
☀️ Try build successful - checks-actions |
This comment has been minimized.
This comment has been minimized.
37235a2
to
c0230f4
Compare
Tests from rust-lang/trait-system-refactor-initiative#162 added. |
is_sized::<MaybeSized<_>>(); | ||
//~^ ERROR type annotations needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noting explicitly here that at least part of the point of the crater run was to check that nobody was relying on this inference behavior.
Finished benchmarking commit (7d2c602): comparison URL. Overall result: no relevant changes - no action neededBenchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf. @bors rollup=never Instruction countThis benchmark run did not return any relevant results for this metric. Max RSS (memory usage)Results (primary -3.8%, secondary -3.3%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesThis benchmark run did not return any relevant results for this metric. Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 775.02s -> 775.85s (0.11%) |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
This PR changes the confirmation of
Sized
obligations to unconditionally prefer the built-in impl, even if it has nested obligations. This also changes all other built-in impls (namely,Copy
/Clone
/DiscriminantKind
/Pointee
) to not prefer built-in impls over param-env impls. This aligns the old solver with the behavior of the new solver.In the old solver, we register many builtin candidates with the
BuiltinCandidate { has_nested: bool }
candidate kind. The precedence this candidate takes over other candidates is based on thehas_nested
field. We only prefer builtin impls over param-env candidates ifhas_nested
isfalse
rust/compiler/rustc_trait_selection/src/traits/select/mod.rs
Lines 1804 to 1866 in 2b4694a
Preferring param-env candidates when the builtin candidate has nested obligations still ends up leading to detrimental inference guidance, like:
Therefore this PR adjusts the candidate precedence of
Sized
obligations by making them a distinct candidate kind and unconditionally preferring them over all other candidate kinds.Special-casing
Sized
this way is necessary as there are a lot of traits with aSized
super-trait bound, so a&'a str: From<T>
where-bound results in an elaborated&'a str: Sized
bound. People tend to not add explicit where-clauses which overlap with builtin impls, so this tends to not be an issue for other traits.We don't know of any tests/crates which need preference for other builtin traits. As this causes builtin impls to diverge from user-written impls we would like to minimize the affected traits. Otherwise e.g. moving impls for tuples to std by using variadic generics would be a breaking change. For other builtin impls it's also easier for the preference of builtin impls over where-bounds to result in issues.
There are two ways preferring builtin impls over where-bounds can be incorrect and undesirable:
MyType<'static>
implementsCopy
then a goal like(MyType<'a>,): Copy
would require'a == 'static
so we must not prefer it over a(MyType<'a>,): Copy
where-boundSized
as allSized
impls are builtin and don't add any region constraints not already required for the type to be well-formedSized
this is still an issue if a nested goal also gets proven via a where-bound: playgroundSized
as it doesn't have associated types.r? lcnr