-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Prefer built-in sized impls (and only sized impls) for rigid types always #138176
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
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 |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
@bors r+ rollup |
…iaskrgr Rollup of 6 pull requests Successful merges: - rust-lang#138176 (Prefer built-in sized impls (and only sized impls) for rigid types always) - rust-lang#138749 (Fix closure recovery for missing block when return type is specified) - rust-lang#138842 (Emit `unused_attributes` for `#[inline]` on exported functions) - rust-lang#139153 (Encode synthetic by-move coroutine body with a different `DefPathData`) - rust-lang#139157 (Remove mention of `exhaustive_patterns` from `never` docs) - rust-lang#139167 (Remove Amanieu from the libs review rotation) r? `@ghost` `@rustbot` modify labels: rollup
Rollup merge of rust-lang#138176 - compiler-errors:rigid-sized-obl, r=lcnr 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
Bors thinks this is still in the queue, so: @bors r- |
Remove manual WF hack We do not need this hack anymore since we fixed the candidate selection problems with `Sized` bounds. We prefer built-in sized bounds now since rust-lang#138176, which fixes the only regression this hack was intended to fix. While this theoretically is broken for some code, for example, when there a param-env bound that shadows an impl or built-in trait, we don't see it in practice and IMO it's not worth the burden of having to maintain this wart in `compare_method_predicate_entailment`. The code that regresses is, for example: ```rust trait Bar<'a> {} trait Foo<'a, T> { fn method(&self) where Self: Bar<'a>; } struct W<'a, T>(&'a T) where Self: Bar<'a>; impl<'a, 'b, T> Bar<'a> for W<'b, T> {} impl<'a, 'b, T> Foo<'a, T> for W<'b, T> { fn method(&self) {} } ``` Specifically, I don't believe this is really going to be encountered in practice. For this to fail, there must be a where clause in the *trait method* that would shadow an impl or built-in (non-`Sized`) candidate in the trait, and this shadowing would need to be encountered when solving a nested WF goal from the impl self type. See rust-lang#108544 for the original regression. Crater run is clean! r? lcnr
Pkgsrc changes: * Adjust patches to adapt to upstream changes and new versions. * associated checksums Upstream changes relative to 1.87.0: Version 1.88.0 (2025-06-26) ========================== Language -------- - [Stabilize `#![feature(let_chains)]` in the 2024 edition.] (rust-lang/rust#132833) This feature allows `&&`-chaining `let` statements inside `if` and `while`, allowing intermixture with boolean expressions. The patterns inside the `let` sub-expressions can be irrefutable or refutable. - [Stabilize `#![feature(naked_functions)]`.] (rust-lang/rust#134213) Naked functions allow writing functions with no compiler-generated epilogue and prologue, allowing full control over the generated assembly for a particular function. - [Stabilize `#![feature(cfg_boolean_literals)]`.] (rust-lang/rust#138632) This allows using boolean literals as `cfg` predicates, e.g. `#[cfg(true)]` and `#[cfg(false)]`. - [Fully de-stabilize the `#[bench]` attribute] (rust-lang/rust#134273). Usage of `#[bench]` without `#![feature(custom_test_frameworks)]` already triggered a deny-by-default future-incompatibility lint since Rust 1.77, but will now become a hard error. - [Add warn-by-default `dangerous_implicit_autorefs` lint against implicit autoref of raw pointer dereference.] (rust-lang/rust#123239) The lint [will be bumped to deny-by-default] (rust-lang/rust#141661) in the next version of Rust. - [Add `invalid_null_arguments` lint to prevent invalid usage of null pointers.] (rust-lang/rust#119220) This lint is uplifted from `clippy::invalid_null_ptr_usage`. - [Change trait impl candidate preference for builtin impls and trivial where-clauses.] (rust-lang/rust#138176) - [Check types of generic const parameter defaults] (rust-lang/rust#139646) Compiler -------- - [Stabilize `-Cdwarf-version` for selecting the version of DWARF debug information to generate.] (rust-lang/rust#136926) Platform Support ---------------- - [Demote `i686-pc-windows-gnu` to Tier 2.] (https://blog.rust-lang.org/2025/05/26/demoting-i686-pc-windows-gnu/) Refer to Rust's [platform support page][platform-support-doc] for more information on Rust's tiered platform support. [platform-support-doc]: https://doc.rust-lang.org/rustc/platform-support.html Libraries --------- - [Remove backticks from `#[should_panic]` test failure message.] (rust-lang/rust#136160) - [Guarantee that `[T; N]::from_fn` is generated in order of increasing indices.] (rust-lang/rust#139099), for those passing it a stateful closure. - [The libtest flag `--nocapture` is deprecated in favor of the more consistent `--no-capture` flag.] (rust-lang/rust#139224) - [Guarantee that `{float}::NAN` is a quiet NaN.] (rust-lang/rust#139483) Stabilized APIs --------------- - [`Cell::update`] (https://doc.rust-lang.org/stable/std/cell/struct.Cell.html#method.update) - [`impl Default for *const T`] (https://doc.rust-lang.org/nightly/std/primitive.pointer.html#impl-Default-for-*const+T) - [`impl Default for *mut T`] (https://doc.rust-lang.org/nightly/std/primitive.pointer.html#impl-Default-for-*mut+T) - [`HashMap::extract_if`] (https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html#method.extract_if) - [`HashSet::extract_if`] (https://doc.rust-lang.org/stable/std/collections/struct.HashSet.html#method.extract_if) - [`proc_macro::Span::line`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.line) - [`proc_macro::Span::column`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.column) - [`proc_macro::Span::start`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.start) - [`proc_macro::Span::end`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.end) - [`proc_macro::Span::file`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.file) - [`proc_macro::Span::local_file`] (https://doc.rust-lang.org/stable/proc_macro/struct.Span.html#method.local_file) These previously stable APIs are now stable in const contexts: - [`NonNull<T>::replace`] (https://doc.rust-lang.org/stable/std/ptr/struct.NonNull.html#method.replace) - [`<*mut T>::replace`] (https://doc.rust-lang.org/stable/std/primitive.pointer.html#method.replace) - [`std::ptr::swap_nonoverlapping`] (rust-lang/rust#137280) - [`Cell::{replace, get, get_mut, from_mut, as_slice_of_cells}`] (rust-lang/rust#137928) Cargo ----- - [Stabilize automatic garbage collection.] (rust-lang/cargo#14287) - [use `zlib-rs` for gzip compression in rust code] (rust-lang/cargo#15417) Rustdoc ----- - [Doctests can be ignored based on target names using `ignore-*` attributes.] (rust-lang/rust#137096) - [Stabilize the `--test-runtool` and `--test-runtool-arg` CLI options to specify a program (like qemu) and its arguments to run a doctest.] (rust-lang/rust#137096) Compatibility Notes ------------------- - [Finish changing the internal representation of pasted tokens] (rust-lang/rust#124141). Certain invalid declarative macros that were previously accepted in obscure circumstances are now correctly rejected by the compiler. Use of a `tt` fragment specifier can often fix these macros. - [Fully de-stabilize the `#[bench]` attribute] (rust-lang/rust#134273). Usage of `#[bench]` without `#![feature(custom_test_frameworks)]` already triggered a deny-by-default future-incompatibility lint since Rust 1.77, but will now become a hard error. - [Fix borrow checking some always-true patterns.] (rust-lang/rust#139042) The borrow checker was overly permissive in some cases, allowing programs that shouldn't have compiled. - [Update the minimum external LLVM to 19.] (rust-lang/rust#139275) - [Make it a hard error to use a vector type with a non-Rust ABI without enabling the required target feature.] (rust-lang/rust#139309)
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