about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMatthias Krüger <476013+matthiaskrgr@users.noreply.github.com>2025-03-31 14:36:20 +0200
committerGitHub <noreply@github.com>2025-03-31 14:36:20 +0200
commite15161d528f4d14c998f8023526e595f468bd5b0 (patch)
tree5c6fac1a470ed1e309d56869472a4d6aea4a3f79
parent10a76d634781180b4f5402c519f0c237d3be6ee6 (diff)
parentc0230f4a2904e968f4afd833e44db1f4ebe29b21 (diff)
downloadrust-e15161d528f4d14c998f8023526e595f468bd5b0.tar.gz
rust-e15161d528f4d14c998f8023526e595f468bd5b0.zip
Rollup merge of #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. https://github.com/rust-lang/rust/issues/133044
  - not an issue for `Sized` as it doesn't have associated types.

r? lcnr
-rw-r--r--compiler/rustc_middle/src/traits/select.rs8
-rw-r--r--compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs26
-rw-r--r--compiler/rustc_trait_selection/src/traits/select/confirmation.rs5
-rw-r--r--compiler/rustc_trait_selection/src/traits/select/mod.rs27
-rw-r--r--tests/ui/sized/dont-incompletely-prefer-built-in.rs21
-rw-r--r--tests/ui/traits/incomplete-infer-via-sized-wc.current.stderr9
-rw-r--r--tests/ui/traits/incomplete-infer-via-sized-wc.next.stderr9
-rw-r--r--tests/ui/traits/incomplete-infer-via-sized-wc.rs19
-rw-r--r--tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc-2.rs28
-rw-r--r--tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.current.stderr27
-rw-r--r--tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.next.stderr25
-rw-r--r--tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.rs26
12 files changed, 214 insertions, 16 deletions
diff --git a/compiler/rustc_middle/src/traits/select.rs b/compiler/rustc_middle/src/traits/select.rs
index 811bd8fb458..aa2ee756bc5 100644
--- a/compiler/rustc_middle/src/traits/select.rs
+++ b/compiler/rustc_middle/src/traits/select.rs
@@ -95,10 +95,16 @@ pub type EvaluationCache<'tcx, ENV> = Cache<(ENV, ty::PolyTraitPredicate<'tcx>),
 /// parameter environment.
 #[derive(PartialEq, Eq, Debug, Clone, TypeVisitable)]
 pub enum SelectionCandidate<'tcx> {
+    /// A built-in implementation for the `Sized` trait. This is preferred
+    /// over all other candidates.
+    SizedCandidate {
+        has_nested: bool,
+    },
+
     /// A builtin implementation for some specific traits, used in cases
     /// where we cannot rely an ordinary library implementations.
     ///
-    /// The most notable examples are `sized`, `Copy` and `Clone`. This is also
+    /// The most notable examples are `Copy` and `Clone`. This is also
     /// used for the `DiscriminantKind` and `Pointee` trait, both of which have
     /// an associated type.
     BuiltinCandidate {
diff --git a/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs b/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
index 4cfd8149b1e..d15c9afef3a 100644
--- a/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
@@ -86,10 +86,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                 // `Pointee` is automatically implemented for every type.
                 candidates.vec.push(BuiltinCandidate { has_nested: false });
             } else if tcx.is_lang_item(def_id, LangItem::Sized) {
-                // Sized is never implementable by end-users, it is
-                // always automatically computed.
-                let sized_conditions = self.sized_conditions(obligation);
-                self.assemble_builtin_bound_candidates(sized_conditions, &mut candidates);
+                self.assemble_builtin_sized_candidate(obligation, &mut candidates);
             } else if tcx.is_lang_item(def_id, LangItem::Unsize) {
                 self.assemble_candidates_for_unsizing(obligation, &mut candidates);
             } else if tcx.is_lang_item(def_id, LangItem::Destruct) {
@@ -1061,6 +1058,27 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
     /// Assembles the trait which are built-in to the language itself:
     /// `Copy`, `Clone` and `Sized`.
     #[instrument(level = "debug", skip(self, candidates))]
+    fn assemble_builtin_sized_candidate(
+        &mut self,
+        obligation: &PolyTraitObligation<'tcx>,
+        candidates: &mut SelectionCandidateSet<'tcx>,
+    ) {
+        match self.sized_conditions(obligation) {
+            BuiltinImplConditions::Where(nested) => {
+                candidates
+                    .vec
+                    .push(SizedCandidate { has_nested: !nested.skip_binder().is_empty() });
+            }
+            BuiltinImplConditions::None => {}
+            BuiltinImplConditions::Ambiguous => {
+                candidates.ambiguous = true;
+            }
+        }
+    }
+
+    /// Assembles the trait which are built-in to the language itself:
+    /// e.g. `Copy` and `Clone`.
+    #[instrument(level = "debug", skip(self, candidates))]
     fn assemble_builtin_bound_candidates(
         &mut self,
         conditions: BuiltinImplConditions<'tcx>,
diff --git a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
index a66c958c109..630241725fd 100644
--- a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
@@ -40,6 +40,11 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
         candidate: SelectionCandidate<'tcx>,
     ) -> Result<Selection<'tcx>, SelectionError<'tcx>> {
         let mut impl_src = match candidate {
+            SizedCandidate { has_nested } => {
+                let data = self.confirm_builtin_candidate(obligation, has_nested);
+                ImplSource::Builtin(BuiltinImplSource::Misc, data)
+            }
+
             BuiltinCandidate { has_nested } => {
                 let data = self.confirm_builtin_candidate(obligation, has_nested);
                 ImplSource::Builtin(BuiltinImplSource::Misc, data)
diff --git a/compiler/rustc_trait_selection/src/traits/select/mod.rs b/compiler/rustc_trait_selection/src/traits/select/mod.rs
index e439df76cd4..0679dbf1296 100644
--- a/compiler/rustc_trait_selection/src/traits/select/mod.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/mod.rs
@@ -1801,17 +1801,21 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
             return Some(candidates.pop().unwrap().candidate);
         }
 
-        // We prefer trivial builtin candidates, i.e. builtin impls without any nested
-        // requirements, over all others. This is a fix for #53123 and prevents winnowing
-        // from accidentally extending the lifetime of a variable.
-        let mut trivial_builtin = candidates
-            .iter()
-            .filter(|c| matches!(c.candidate, BuiltinCandidate { has_nested: false }));
-        if let Some(_trivial) = trivial_builtin.next() {
-            // There should only ever be a single trivial builtin candidate
+        // We prefer `Sized` candidates over everything.
+        let mut sized_candidates =
+            candidates.iter().filter(|c| matches!(c.candidate, SizedCandidate { has_nested: _ }));
+        if let Some(sized_candidate) = sized_candidates.next() {
+            // There should only ever be a single sized candidate
             // as they would otherwise overlap.
-            debug_assert_eq!(trivial_builtin.next(), None);
-            return Some(BuiltinCandidate { has_nested: false });
+            debug_assert_eq!(sized_candidates.next(), None);
+            // Only prefer the built-in `Sized` candidate if its nested goals are certain.
+            // Otherwise, we may encounter failure later on if inference causes this candidate
+            // to not hold, but a where clause would've applied instead.
+            if sized_candidate.evaluation.must_apply_modulo_regions() {
+                return Some(sized_candidate.candidate.clone());
+            } else {
+                return None;
+            }
         }
 
         // Before we consider where-bounds, we have to deduplicate them here and also
@@ -1940,7 +1944,8 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
             // Don't use impl candidates which overlap with other candidates.
             // This should pretty much only ever happen with malformed impls.
             if candidates.iter().all(|c| match c.candidate {
-                BuiltinCandidate { has_nested: _ }
+                SizedCandidate { has_nested: _ }
+                | BuiltinCandidate { has_nested: _ }
                 | TransmutabilityCandidate
                 | AutoImplCandidate
                 | ClosureCandidate { .. }
diff --git a/tests/ui/sized/dont-incompletely-prefer-built-in.rs b/tests/ui/sized/dont-incompletely-prefer-built-in.rs
new file mode 100644
index 00000000000..f5bf0c8915e
--- /dev/null
+++ b/tests/ui/sized/dont-incompletely-prefer-built-in.rs
@@ -0,0 +1,21 @@
+//@ check-pass
+//@ revisions: current next
+//@ ignore-compare-mode-next-solver (explicit revisions)
+//@[next] compile-flags: -Znext-solver
+
+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;
+}
+
+fn main() {}
diff --git a/tests/ui/traits/incomplete-infer-via-sized-wc.current.stderr b/tests/ui/traits/incomplete-infer-via-sized-wc.current.stderr
new file mode 100644
index 00000000000..f4930bf890c
--- /dev/null
+++ b/tests/ui/traits/incomplete-infer-via-sized-wc.current.stderr
@@ -0,0 +1,9 @@
+error[E0282]: type annotations needed
+  --> $DIR/incomplete-infer-via-sized-wc.rs:15:5
+   |
+LL |     is_sized::<MaybeSized<_>>();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `T` declared on the function `is_sized`
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0282`.
diff --git a/tests/ui/traits/incomplete-infer-via-sized-wc.next.stderr b/tests/ui/traits/incomplete-infer-via-sized-wc.next.stderr
new file mode 100644
index 00000000000..f4930bf890c
--- /dev/null
+++ b/tests/ui/traits/incomplete-infer-via-sized-wc.next.stderr
@@ -0,0 +1,9 @@
+error[E0282]: type annotations needed
+  --> $DIR/incomplete-infer-via-sized-wc.rs:15:5
+   |
+LL |     is_sized::<MaybeSized<_>>();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `T` declared on the function `is_sized`
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0282`.
diff --git a/tests/ui/traits/incomplete-infer-via-sized-wc.rs b/tests/ui/traits/incomplete-infer-via-sized-wc.rs
new file mode 100644
index 00000000000..9dcddea3551
--- /dev/null
+++ b/tests/ui/traits/incomplete-infer-via-sized-wc.rs
@@ -0,0 +1,19 @@
+//@ revisions: current next
+//@ ignore-compare-mode-next-solver (explicit revisions)
+//@[next] compile-flags: -Znext-solver
+
+// Exercises change in <https://github.com/rust-lang/rust/pull/138176>.
+
+struct MaybeSized<T: ?Sized>(T);
+
+fn is_sized<T: Sized>() -> Box<T> { todo!() }
+
+fn foo<T: ?Sized>()
+where
+    MaybeSized<T>: Sized,
+{
+    is_sized::<MaybeSized<_>>();
+    //~^ ERROR type annotations needed
+}
+
+fn main() {}
diff --git a/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc-2.rs b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc-2.rs
new file mode 100644
index 00000000000..8a8f7b933b5
--- /dev/null
+++ b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc-2.rs
@@ -0,0 +1,28 @@
+//@ check-pass
+//@ revisions: current next
+//@ ignore-compare-mode-next-solver (explicit revisions)
+//@[next] compile-flags: -Znext-solver
+
+// Exercises change in <https://github.com/rust-lang/rust/pull/138176>.
+
+trait Trait<T>: Sized {}
+impl<T> Trait<T> for T {}
+
+fn is_sized<T: Sized>() {}
+
+fn normal_ref<'a, 'b, T>()
+where
+    &'a u32: Trait<T>,
+{
+    is_sized::<&'b u32>();
+}
+
+struct MyRef<'a, U: ?Sized = ()>(&'a u32, U);
+fn my_ref<'a, 'b, T>()
+where
+    MyRef<'a>: Trait<T>,
+{
+    is_sized::<MyRef<'b>>();
+}
+
+fn main() {}
diff --git a/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.current.stderr b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.current.stderr
new file mode 100644
index 00000000000..dd9393fae85
--- /dev/null
+++ b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.current.stderr
@@ -0,0 +1,27 @@
+error[E0308]: mismatched types
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:13:23
+   |
+LL |     (MyType<'a, T>,): Sized,
+   |                       ^^^^^ lifetime mismatch
+   |
+   = note: expected trait `<MyType<'a, T> as Sized>`
+              found trait `<MyType<'static, T> as Sized>`
+note: the lifetime `'a` as defined here...
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:11:8
+   |
+LL | fn foo<'a, T: ?Sized>()
+   |        ^^
+   = note: ...does not necessarily outlive the static lifetime
+
+error: lifetime may not live long enough
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:22:5
+   |
+LL | fn foo<'a, T: ?Sized>()
+   |        -- lifetime `'a` defined here
+...
+LL |     is_sized::<(MyType<'a, T>,)>();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
+
+error: aborting due to 2 previous errors
+
+For more information about this error, try `rustc --explain E0308`.
diff --git a/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.next.stderr b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.next.stderr
new file mode 100644
index 00000000000..05861877d41
--- /dev/null
+++ b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.next.stderr
@@ -0,0 +1,25 @@
+error[E0478]: lifetime bound not satisfied
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:13:23
+   |
+LL |     (MyType<'a, T>,): Sized,
+   |                       ^^^^^
+   |
+note: lifetime parameter instantiated with the lifetime `'a` as defined here
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:11:8
+   |
+LL | fn foo<'a, T: ?Sized>()
+   |        ^^
+   = note: but lifetime parameter must outlive the static lifetime
+
+error: lifetime may not live long enough
+  --> $DIR/lifetime-incomplete-prefer-sized-builtin-over-wc.rs:22:5
+   |
+LL | fn foo<'a, T: ?Sized>()
+   |        -- lifetime `'a` defined here
+...
+LL |     is_sized::<(MyType<'a, T>,)>();
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
+
+error: aborting due to 2 previous errors
+
+For more information about this error, try `rustc --explain E0478`.
diff --git a/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.rs b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.rs
new file mode 100644
index 00000000000..ae7a6c9bba3
--- /dev/null
+++ b/tests/ui/traits/lifetime-incomplete-prefer-sized-builtin-over-wc.rs
@@ -0,0 +1,26 @@
+//@ revisions: current next
+//@ ignore-compare-mode-next-solver (explicit revisions)
+//@[next] compile-flags: -Znext-solver
+
+// Exercises change in <https://github.com/rust-lang/rust/pull/138176>.
+
+struct MyType<'a, T: ?Sized>(&'a (), T);
+
+fn is_sized<T>() {}
+
+fn foo<'a, T: ?Sized>()
+where
+    (MyType<'a, T>,): Sized,
+    //[current]~^ ERROR mismatched types
+    //[next]~^^ ERROR lifetime bound not satisfied
+    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>,)>();
+    //~^ ERROR lifetime may not live long enough
+}
+
+fn main() {}