about summary refs log tree commit diff
diff options
context:
space:
mode:
authorChris Denton <chris@chrisdenton.dev>2025-04-19 15:09:34 +0000
committerGitHub <noreply@github.com>2025-04-19 15:09:34 +0000
commit688478fe45857ec20d4f615b41e0e37406915c52 (patch)
tree75148d56db5c722df33ec0e98c004a24deec38a7
parent1a5e4860687a33d59e74b203dd066fa8b4df570c (diff)
parente882ff4e7ebc4653cdc69e57bc656fe558a4af88 (diff)
downloadrust-688478fe45857ec20d4f615b41e0e37406915c52.tar.gz
rust-688478fe45857ec20d4f615b41e0e37406915c52.zip
Rollup merge of #139762 - compiler-errors:non-env, r=lcnr
Don't assemble non-env/bound candidates if projection is rigid

Putting this up for an initial review, it's still missing comments, clean-up, and possibly a tweak to deal with ambiguities in the `BestObligation` folder.

This PR fixes https://github.com/rust-lang/trait-system-refactor-initiative/issues/173. Specifically, we're creating an unnecessary query cycle in normalization by assembling an *impl candidate* even if we know later on during `merge_candidates` that we'll be filtering out that impl candidate.

This PR adjusts the `merge_candidates` to assemble *only* env/bound candidates if we have `TraitGoalProvenVia::ParamEnv | TraitGoalProvenVia::AliasBound`.

I'll leave some thoughts/comments in the code.

r? lcnr
-rw-r--r--compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs87
-rw-r--r--compiler/rustc_next_trait_solver/src/solve/effect_goals.rs3
-rw-r--r--compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs3
-rw-r--r--compiler/rustc_next_trait_solver/src/solve/trait_goals.rs4
-rw-r--r--tests/ui/impl-unused-tps.stderr34
-rw-r--r--tests/ui/traits/next-solver/rpitit-cycle-due-to-rigid.rs32
6 files changed, 107 insertions, 56 deletions
diff --git a/compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs b/compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs
index 83b2465d05a..ecb57cc0ad7 100644
--- a/compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/assembly/mod.rs
@@ -288,6 +288,21 @@ where
     ) -> Vec<Candidate<I>>;
 }
 
+/// Allows callers of `assemble_and_evaluate_candidates` to choose whether to limit
+/// candidate assembly to param-env and alias-bound candidates.
+///
+/// On top of being a micro-optimization, as it avoids doing unnecessary work when
+/// a param-env trait bound candidate shadows impls for normalization, this is also
+/// required to prevent query cycles due to RPITIT inference. See the issue at:
+/// <https://github.com/rust-lang/trait-system-refactor-initiative/issues/173>.
+pub(super) enum AssembleCandidatesFrom {
+    All,
+    /// Only assemble candidates from the environment and alias bounds, ignoring
+    /// user-written and built-in impls. We only expect `ParamEnv` and `AliasBound`
+    /// candidates to be assembled.
+    EnvAndBounds,
+}
+
 impl<D, I> EvalCtxt<'_, D>
 where
     D: SolverDelegate<Interner = I>,
@@ -296,6 +311,7 @@ where
     pub(super) fn assemble_and_evaluate_candidates<G: GoalKind<D>>(
         &mut self,
         goal: Goal<I, G>,
+        assemble_from: AssembleCandidatesFrom,
     ) -> Vec<Candidate<I>> {
         let Ok(normalized_self_ty) =
             self.structurally_normalize_ty(goal.param_env, goal.predicate.self_ty())
@@ -322,16 +338,18 @@ where
             }
         }
 
-        self.assemble_impl_candidates(goal, &mut candidates);
-
-        self.assemble_builtin_impl_candidates(goal, &mut candidates);
-
         self.assemble_alias_bound_candidates(goal, &mut candidates);
-
-        self.assemble_object_bound_candidates(goal, &mut candidates);
-
         self.assemble_param_env_candidates(goal, &mut candidates);
 
+        match assemble_from {
+            AssembleCandidatesFrom::All => {
+                self.assemble_impl_candidates(goal, &mut candidates);
+                self.assemble_builtin_impl_candidates(goal, &mut candidates);
+                self.assemble_object_bound_candidates(goal, &mut candidates);
+            }
+            AssembleCandidatesFrom::EnvAndBounds => {}
+        }
+
         candidates
     }
 
@@ -754,6 +772,9 @@ where
         })
     }
 
+    /// Assemble and merge candidates for goals which are related to an underlying trait
+    /// goal. Right now, this is normalizes-to and host effect goals.
+    ///
     /// We sadly can't simply take all possible candidates for normalization goals
     /// and check whether they result in the same constraints. We want to make sure
     /// that trying to normalize an alias doesn't result in constraints which aren't
@@ -782,47 +803,44 @@ where
     ///
     /// See trait-system-refactor-initiative#124 for more details.
     #[instrument(level = "debug", skip(self, inject_normalize_to_rigid_candidate), ret)]
-    pub(super) fn merge_candidates(
+    pub(super) fn assemble_and_merge_candidates<G: GoalKind<D>>(
         &mut self,
         proven_via: Option<TraitGoalProvenVia>,
-        candidates: Vec<Candidate<I>>,
+        goal: Goal<I, G>,
         inject_normalize_to_rigid_candidate: impl FnOnce(&mut EvalCtxt<'_, D>) -> QueryResult<I>,
     ) -> QueryResult<I> {
         let Some(proven_via) = proven_via else {
             // We don't care about overflow. If proving the trait goal overflowed, then
             // it's enough to report an overflow error for that, we don't also have to
             // overflow during normalization.
-            return Ok(self.make_ambiguous_response_no_constraints(MaybeCause::Ambiguity));
+            //
+            // We use `forced_ambiguity` here over `make_ambiguous_response_no_constraints`
+            // because the former will also record a built-in candidate in the inspector.
+            return self.forced_ambiguity(MaybeCause::Ambiguity).map(|cand| cand.result);
         };
 
         match proven_via {
             TraitGoalProvenVia::ParamEnv | TraitGoalProvenVia::AliasBound => {
-                let mut considered_candidates = Vec::new();
-                considered_candidates.extend(
-                    candidates
-                        .iter()
-                        .filter(|c| matches!(c.source, CandidateSource::ParamEnv(_)))
-                        .map(|c| c.result),
-                );
-
                 // Even when a trait bound has been proven using a where-bound, we
                 // still need to consider alias-bounds for normalization, see
-                // tests/ui/next-solver/alias-bound-shadowed-by-env.rs.
-                //
+                // `tests/ui/next-solver/alias-bound-shadowed-by-env.rs`.
+                let candidates_from_env_and_bounds: Vec<_> = self
+                    .assemble_and_evaluate_candidates(goal, AssembleCandidatesFrom::EnvAndBounds);
+
                 // We still need to prefer where-bounds over alias-bounds however.
-                // See tests/ui/winnowing/norm-where-bound-gt-alias-bound.rs.
-                //
-                // FIXME(const_trait_impl): should this behavior also be used by
-                // constness checking. Doing so is *at least theoretically* breaking,
-                // see github.com/rust-lang/rust/issues/133044#issuecomment-2500709754
-                if considered_candidates.is_empty() {
-                    considered_candidates.extend(
-                        candidates
-                            .iter()
-                            .filter(|c| matches!(c.source, CandidateSource::AliasBound))
-                            .map(|c| c.result),
-                    );
-                }
+                // See `tests/ui/winnowing/norm-where-bound-gt-alias-bound.rs`.
+                let mut considered_candidates: Vec<_> = if candidates_from_env_and_bounds
+                    .iter()
+                    .any(|c| matches!(c.source, CandidateSource::ParamEnv(_)))
+                {
+                    candidates_from_env_and_bounds
+                        .into_iter()
+                        .filter(|c| matches!(c.source, CandidateSource::ParamEnv(_)))
+                        .map(|c| c.result)
+                        .collect()
+                } else {
+                    candidates_from_env_and_bounds.into_iter().map(|c| c.result).collect()
+                };
 
                 // If the trait goal has been proven by using the environment, we want to treat
                 // aliases as rigid if there are no applicable projection bounds in the environment.
@@ -839,6 +857,9 @@ where
                 }
             }
             TraitGoalProvenVia::Misc => {
+                let candidates =
+                    self.assemble_and_evaluate_candidates(goal, AssembleCandidatesFrom::All);
+
                 // Prefer "orphaned" param-env normalization predicates, which are used
                 // (for example, and ideally only) when proving item bounds for an impl.
                 let candidates_from_env: Vec<_> = candidates
diff --git a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
index 0b61c368d8e..7752a705cd1 100644
--- a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs
@@ -399,12 +399,11 @@ where
         &mut self,
         goal: Goal<I, ty::HostEffectPredicate<I>>,
     ) -> QueryResult<I> {
-        let candidates = self.assemble_and_evaluate_candidates(goal);
         let (_, proven_via) = self.probe(|_| ProbeKind::ShadowedEnvProbing).enter(|ecx| {
             let trait_goal: Goal<I, ty::TraitPredicate<I>> =
                 goal.with(ecx.cx(), goal.predicate.trait_ref);
             ecx.compute_trait_goal(trait_goal)
         })?;
-        self.merge_candidates(proven_via, candidates, |_ecx| Err(NoSolution))
+        self.assemble_and_merge_candidates(proven_via, goal, |_ecx| Err(NoSolution))
     }
 }
diff --git a/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs b/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
index fdeb276a58e..9466901683e 100644
--- a/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs
@@ -32,14 +32,13 @@ where
         let cx = self.cx();
         match goal.predicate.alias.kind(cx) {
             ty::AliasTermKind::ProjectionTy | ty::AliasTermKind::ProjectionConst => {
-                let candidates = self.assemble_and_evaluate_candidates(goal);
                 let trait_ref = goal.predicate.alias.trait_ref(cx);
                 let (_, proven_via) =
                     self.probe(|_| ProbeKind::ShadowedEnvProbing).enter(|ecx| {
                         let trait_goal: Goal<I, ty::TraitPredicate<I>> = goal.with(cx, trait_ref);
                         ecx.compute_trait_goal(trait_goal)
                     })?;
-                self.merge_candidates(proven_via, candidates, |ecx| {
+                self.assemble_and_merge_candidates(proven_via, goal, |ecx| {
                     ecx.probe(|&result| ProbeKind::RigidAlias { result }).enter(|this| {
                         this.structurally_instantiate_normalizes_to_term(
                             goal,
diff --git a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
index 409af8568d7..7bd1300f34e 100644
--- a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
+++ b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs
@@ -13,7 +13,7 @@ use tracing::{instrument, trace};
 
 use crate::delegate::SolverDelegate;
 use crate::solve::assembly::structural_traits::{self, AsyncCallableRelevantTypes};
-use crate::solve::assembly::{self, Candidate};
+use crate::solve::assembly::{self, AssembleCandidatesFrom, Candidate};
 use crate::solve::inspect::ProbeKind;
 use crate::solve::{
     BuiltinImplSource, CandidateSource, Certainty, EvalCtxt, Goal, GoalSource, MaybeCause,
@@ -1365,7 +1365,7 @@ where
         &mut self,
         goal: Goal<I, TraitPredicate<I>>,
     ) -> Result<(CanonicalResponse<I>, Option<TraitGoalProvenVia>), NoSolution> {
-        let candidates = self.assemble_and_evaluate_candidates(goal);
+        let candidates = self.assemble_and_evaluate_candidates(goal, AssembleCandidatesFrom::All);
         self.merge_trait_candidates(goal, candidates)
     }
 }
diff --git a/tests/ui/impl-unused-tps.stderr b/tests/ui/impl-unused-tps.stderr
index 09c3fce641c..eff5ffff9b6 100644
--- a/tests/ui/impl-unused-tps.stderr
+++ b/tests/ui/impl-unused-tps.stderr
@@ -7,23 +7,6 @@ LL | impl<T> Foo<T> for [isize; 0] {
 LL | impl<T, U> Foo<T> for U {
    | ^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `[isize; 0]`
 
-error[E0207]: the type parameter `U` is not constrained by the impl trait, self type, or predicates
-  --> $DIR/impl-unused-tps.rs:32:9
-   |
-LL | impl<T, U> Bar for T {
-   |         ^ unconstrained type parameter
-
-error[E0119]: conflicting implementations of trait `Bar`
-  --> $DIR/impl-unused-tps.rs:40:1
-   |
-LL |   impl<T, U> Bar for T {
-   |   -------------------- first implementation here
-...
-LL | / impl<T, U> Bar for T
-LL | | where
-LL | |     T: Bar<Out = U>,
-   | |____________________^ conflicting implementation
-
 error[E0119]: conflicting implementations of trait `Foo<[isize; 0]>` for type `[isize; 0]`
   --> $DIR/impl-unused-tps.rs:49:1
    |
@@ -53,6 +36,12 @@ LL | impl<T, U> Foo<T> for [isize; 1] {
    |         ^ unconstrained type parameter
 
 error[E0207]: the type parameter `U` is not constrained by the impl trait, self type, or predicates
+  --> $DIR/impl-unused-tps.rs:32:9
+   |
+LL | impl<T, U> Bar for T {
+   |         ^ unconstrained type parameter
+
+error[E0207]: the type parameter `U` is not constrained by the impl trait, self type, or predicates
   --> $DIR/impl-unused-tps.rs:40:9
    |
 LL | impl<T, U> Bar for T
@@ -70,6 +59,17 @@ error[E0207]: the type parameter `V` is not constrained by the impl trait, self
 LL | impl<T, U, V> Foo<T> for T
    |            ^ unconstrained type parameter
 
+error[E0119]: conflicting implementations of trait `Bar`
+  --> $DIR/impl-unused-tps.rs:40:1
+   |
+LL |   impl<T, U> Bar for T {
+   |   -------------------- first implementation here
+...
+LL | / impl<T, U> Bar for T
+LL | | where
+LL | |     T: Bar<Out = U>,
+   | |____________________^ conflicting implementation
+
 error: aborting due to 9 previous errors
 
 Some errors have detailed explanations: E0119, E0207.
diff --git a/tests/ui/traits/next-solver/rpitit-cycle-due-to-rigid.rs b/tests/ui/traits/next-solver/rpitit-cycle-due-to-rigid.rs
new file mode 100644
index 00000000000..ec3d710ef37
--- /dev/null
+++ b/tests/ui/traits/next-solver/rpitit-cycle-due-to-rigid.rs
@@ -0,0 +1,32 @@
+//@ compile-flags: -Znext-solver
+//@ check-pass
+//@ edition: 2024
+
+// Ensure we don't end up in a query cycle due to trying to assemble an impl candidate
+// for an RPITIT normalizes-to goal, even though that impl candidate would *necessarily*
+// be made rigid by a where clause. This query cycle is thus avoidable by not assembling
+// that impl candidate which we *know* we are going to throw away anyways.
+
+use std::future::Future;
+
+pub trait ReactiveFunction: Send {
+    type Output;
+
+    fn invoke(self) -> Self::Output;
+}
+
+trait AttributeValue {
+    fn resolve(self) -> impl Future<Output = ()> + Send;
+}
+
+impl<F, V> AttributeValue for F
+where
+    F: ReactiveFunction<Output = V>,
+    V: AttributeValue,
+{
+    async fn resolve(self) {
+        self.invoke().resolve().await
+    }
+}
+
+fn main() {}