about summary refs log tree commit diff
diff options
context:
space:
mode:
authorBoxy <supbscripter@gmail.com>2023-06-08 19:10:07 +0100
committerBoxy <supbscripter@gmail.com>2023-06-19 09:01:37 +0100
commit3009b2c647ad70fbab1b1df53ef08eaaa6e8759a (patch)
tree63a67cad1c273c0d64b71f98b254dd52bbd6c065
parent8d1fa473dddd12efb2430302e5f5dfcc3eb73f8b (diff)
downloadrust-3009b2c647ad70fbab1b1df53ef08eaaa6e8759a.tar.gz
rust-3009b2c647ad70fbab1b1df53ef08eaaa6e8759a.zip
initial info dump
-rw-r--r--compiler/rustc_middle/src/traits/mod.rs2
-rw-r--r--compiler/rustc_middle/src/traits/query.rs2
-rw-r--r--compiler/rustc_middle/src/traits/solve.rs20
-rw-r--r--compiler/rustc_middle/src/traits/solve/inspect.rs168
-rw-r--r--compiler/rustc_trait_selection/src/solve/alias_relate.rs71
-rw-r--r--compiler/rustc_trait_selection/src/solve/eval_ctxt.rs175
-rw-r--r--compiler/rustc_trait_selection/src/solve/inspect/mod.rs244
-rw-r--r--compiler/rustc_trait_selection/src/solve/mod.rs1
-rw-r--r--compiler/rustc_trait_selection/src/solve/project_goals.rs322
-rw-r--r--compiler/rustc_trait_selection/src/solve/search_graph/mod.rs7
-rw-r--r--compiler/rustc_trait_selection/src/solve/trait_goals.rs349
11 files changed, 944 insertions, 417 deletions
diff --git a/compiler/rustc_middle/src/traits/mod.rs b/compiler/rustc_middle/src/traits/mod.rs
index 492b7228488..f0380204970 100644
--- a/compiler/rustc_middle/src/traits/mod.rs
+++ b/compiler/rustc_middle/src/traits/mod.rs
@@ -981,7 +981,7 @@ pub enum CodegenObligationError {
     FulfillmentError,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub enum DefiningAnchor {
     /// `DefId` of the item.
     Bind(LocalDefId),
diff --git a/compiler/rustc_middle/src/traits/query.rs b/compiler/rustc_middle/src/traits/query.rs
index eae5a280e11..60a38747fdf 100644
--- a/compiler/rustc_middle/src/traits/query.rs
+++ b/compiler/rustc_middle/src/traits/query.rs
@@ -92,7 +92,7 @@ pub type CanonicalTypeOpProvePredicateGoal<'tcx> =
 pub type CanonicalTypeOpNormalizeGoal<'tcx, T> =
     Canonical<'tcx, ty::ParamEnvAnd<'tcx, type_op::Normalize<T>>>;
 
-#[derive(Copy, Clone, Debug, HashStable, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Hash, HashStable, PartialEq, Eq)]
 pub struct NoSolution;
 
 impl<'tcx> From<TypeError<'tcx>> for NoSolution {
diff --git a/compiler/rustc_middle/src/traits/solve.rs b/compiler/rustc_middle/src/traits/solve.rs
index 2c5b64a59cd..af482a88960 100644
--- a/compiler/rustc_middle/src/traits/solve.rs
+++ b/compiler/rustc_middle/src/traits/solve.rs
@@ -11,6 +11,8 @@ use crate::ty::{
     TypeVisitor,
 };
 
+pub mod inspect;
+
 pub type EvaluationCache<'tcx> = Cache<CanonicalInput<'tcx>, QueryResult<'tcx>>;
 
 /// A goal is a statement, i.e. `predicate`, we want to prove
@@ -18,7 +20,7 @@ pub type EvaluationCache<'tcx> = Cache<CanonicalInput<'tcx>, QueryResult<'tcx>>;
 ///
 /// Most of the time the `param_env` contains the `where`-bounds of the function
 /// we're currently typechecking while the `predicate` is some trait bound.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub struct Goal<'tcx, P> {
     pub predicate: P,
     pub param_env: ty::ParamEnv<'tcx>,
@@ -39,7 +41,7 @@ impl<'tcx, P> Goal<'tcx, P> {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub struct Response<'tcx> {
     pub certainty: Certainty,
     pub var_values: CanonicalVarValues<'tcx>,
@@ -47,7 +49,7 @@ pub struct Response<'tcx> {
     pub external_constraints: ExternalConstraints<'tcx>,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub enum Certainty {
     Yes,
     Maybe(MaybeCause),
@@ -86,7 +88,7 @@ impl Certainty {
 }
 
 /// Why we failed to evaluate a goal.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub enum MaybeCause {
     /// We failed due to ambiguity. This ambiguity can either
     /// be a true ambiguity, i.e. there are multiple different answers,
@@ -96,7 +98,7 @@ pub enum MaybeCause {
     Overflow,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, HashStable, TypeFoldable, TypeVisitable)]
 pub struct QueryInput<'tcx, T> {
     pub goal: Goal<'tcx, T>,
     pub anchor: DefiningAnchor,
@@ -104,12 +106,12 @@ pub struct QueryInput<'tcx, T> {
 }
 
 /// Additional constraints returned on success.
-#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash, HashStable, Default)]
 pub struct PredefinedOpaquesData<'tcx> {
     pub opaque_types: Vec<(ty::OpaqueTypeKey<'tcx>, Ty<'tcx>)>,
 }
 
-#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
+#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, HashStable)]
 pub struct PredefinedOpaques<'tcx>(pub(crate) Interned<'tcx, PredefinedOpaquesData<'tcx>>);
 
 impl<'tcx> std::ops::Deref for PredefinedOpaques<'tcx> {
@@ -132,7 +134,7 @@ pub type CanonicalResponse<'tcx> = Canonical<'tcx, Response<'tcx>>;
 /// solver, merge the two responses again.
 pub type QueryResult<'tcx> = Result<CanonicalResponse<'tcx>, NoSolution>;
 
-#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
+#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, HashStable)]
 pub struct ExternalConstraints<'tcx>(pub(crate) Interned<'tcx, ExternalConstraintsData<'tcx>>);
 
 impl<'tcx> std::ops::Deref for ExternalConstraints<'tcx> {
@@ -144,7 +146,7 @@ impl<'tcx> std::ops::Deref for ExternalConstraints<'tcx> {
 }
 
 /// Additional constraints returned on success.
-#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash, HashStable, Default)]
 pub struct ExternalConstraintsData<'tcx> {
     // FIXME: implement this.
     pub region_constraints: QueryRegionConstraints<'tcx>,
diff --git a/compiler/rustc_middle/src/traits/solve/inspect.rs b/compiler/rustc_middle/src/traits/solve/inspect.rs
new file mode 100644
index 00000000000..8cbdb493ccd
--- /dev/null
+++ b/compiler/rustc_middle/src/traits/solve/inspect.rs
@@ -0,0 +1,168 @@
+use super::{CanonicalInput, Certainty, Goal, NoSolution, QueryInput, QueryResult};
+use crate::ty;
+use std::fmt::{Debug, Write};
+
+#[derive(Eq, PartialEq, Hash, HashStable)]
+pub struct GoalEvaluation<'tcx> {
+    pub uncanonicalized_goal: Goal<'tcx, ty::Predicate<'tcx>>,
+    pub canonicalized_goal: Option<CanonicalInput<'tcx>>,
+
+    /// To handle coinductive cycles we can end up re-evaluating a goal
+    /// multiple times with different results for a nested goal. Each rerun
+    /// is represented as an entry in this vec.
+    pub evaluation_steps: Vec<GoalEvaluationStep<'tcx>>,
+
+    pub cache_hit: bool,
+
+    pub result: Option<QueryResult<'tcx>>,
+}
+impl Debug for GoalEvaluation<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        ProofTreeFormatter { f, on_newline: true }.format_goal_evaluation(self)
+    }
+}
+
+#[derive(Eq, PartialEq, Hash, HashStable)]
+pub struct AddedGoalsEvaluation<'tcx> {
+    pub evaluations: Vec<Vec<GoalEvaluation<'tcx>>>,
+    pub result: Option<Result<Certainty, NoSolution>>,
+}
+impl Debug for AddedGoalsEvaluation<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        ProofTreeFormatter { f, on_newline: true }.format_nested_goal_evaluation(self)
+    }
+}
+
+#[derive(Eq, PartialEq, Hash, HashStable)]
+pub struct GoalEvaluationStep<'tcx> {
+    pub instantiated_goal: QueryInput<'tcx, ty::Predicate<'tcx>>,
+
+    pub nested_goal_evaluations: Vec<AddedGoalsEvaluation<'tcx>>,
+    pub candidates: Vec<GoalCandidate<'tcx>>,
+
+    pub result: Option<QueryResult<'tcx>>,
+}
+impl Debug for GoalEvaluationStep<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        ProofTreeFormatter { f, on_newline: true }.format_evaluation_step(self)
+    }
+}
+
+#[derive(Eq, PartialEq, Hash, HashStable)]
+pub struct GoalCandidate<'tcx> {
+    pub nested_goal_evaluations: Vec<AddedGoalsEvaluation<'tcx>>,
+    pub candidates: Vec<GoalCandidate<'tcx>>,
+
+    pub name: Option<String>,
+    pub result: Option<QueryResult<'tcx>>,
+}
+impl Debug for GoalCandidate<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        ProofTreeFormatter { f, on_newline: true }.format_candidate(self)
+    }
+}
+
+struct ProofTreeFormatter<'a, 'b> {
+    f: &'a mut (dyn Write + 'b),
+    on_newline: bool,
+}
+
+impl Write for ProofTreeFormatter<'_, '_> {
+    fn write_str(&mut self, s: &str) -> std::fmt::Result {
+        for line in s.split_inclusive("\n") {
+            if self.on_newline {
+                self.f.write_str("    ")?;
+            }
+            self.on_newline = line.ends_with("\n");
+            self.f.write_str(line)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl ProofTreeFormatter<'_, '_> {
+    fn nested(&mut self) -> ProofTreeFormatter<'_, '_> {
+        ProofTreeFormatter { f: self, on_newline: true }
+    }
+
+    fn format_goal_evaluation(&mut self, goal: &GoalEvaluation<'_>) -> std::fmt::Result {
+        let f = &mut *self.f;
+        writeln!(f, "GOAL: {:?}", goal.uncanonicalized_goal)?;
+        writeln!(f, "CANONICALIZED: {:?}", goal.canonicalized_goal)?;
+
+        match goal.cache_hit {
+            true => writeln!(f, "CACHE HIT: {:?}", goal.result),
+            false => {
+                for (n, step) in goal.evaluation_steps.iter().enumerate() {
+                    let f = &mut *self.f;
+                    writeln!(f, "REVISION {n}: {:?}", step.result.unwrap())?;
+                    let mut f = self.nested();
+                    f.format_evaluation_step(step)?;
+                }
+
+                let f = &mut *self.f;
+                writeln!(f, "RESULT: {:?}", goal.result.unwrap())
+            }
+        }
+    }
+
+    fn format_evaluation_step(
+        &mut self,
+        evaluation_step: &GoalEvaluationStep<'_>,
+    ) -> std::fmt::Result {
+        let f = &mut *self.f;
+        writeln!(f, "INSTANTIATED: {:?}", evaluation_step.instantiated_goal)?;
+
+        for candidate in &evaluation_step.candidates {
+            let mut f = self.nested();
+            f.format_candidate(candidate)?;
+        }
+        for nested_goal_evaluation in &evaluation_step.nested_goal_evaluations {
+            let mut f = self.nested();
+            f.format_nested_goal_evaluation(nested_goal_evaluation)?;
+        }
+
+        Ok(())
+    }
+
+    fn format_candidate(&mut self, candidate: &GoalCandidate<'_>) -> std::fmt::Result {
+        let f = &mut *self.f;
+
+        match (candidate.name.as_ref(), candidate.result) {
+            (Some(name), Some(result)) => writeln!(f, "CANDIDATE {}: {:?}", name, result,)?,
+            (None, None) => writeln!(f, "MISC PROBE")?,
+            (None, Some(_)) => unreachable!("unexpected probe with no name but a result"),
+            (Some(_), None) => unreachable!("unexpected probe with a name but no candidate"),
+        };
+
+        let mut f = self.nested();
+        for candidate in &candidate.candidates {
+            f.format_candidate(candidate)?;
+        }
+        for nested_evaluations in &candidate.nested_goal_evaluations {
+            f.format_nested_goal_evaluation(nested_evaluations)?;
+        }
+
+        Ok(())
+    }
+
+    fn format_nested_goal_evaluation(
+        &mut self,
+        nested_goal_evaluation: &AddedGoalsEvaluation<'_>,
+    ) -> std::fmt::Result {
+        let f = &mut *self.f;
+        writeln!(f, "TRY_EVALUATE_ADDED_GOALS: {:?}", nested_goal_evaluation.result.unwrap())?;
+
+        for (n, revision) in nested_goal_evaluation.evaluations.iter().enumerate() {
+            let f = &mut *self.f;
+            writeln!(f, "REVISION {n}")?;
+            let mut f = self.nested();
+            for goal_evaluation in revision {
+                f.format_goal_evaluation(goal_evaluation)?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/compiler/rustc_trait_selection/src/solve/alias_relate.rs b/compiler/rustc_trait_selection/src/solve/alias_relate.rs
index 66a4d36a1e5..8d096c88a15 100644
--- a/compiler/rustc_trait_selection/src/solve/alias_relate.rs
+++ b/compiler/rustc_trait_selection/src/solve/alias_relate.rs
@@ -109,10 +109,13 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         direction: ty::AliasRelationDirection,
         invert: Invert,
     ) -> QueryResult<'tcx> {
-        self.probe(|ecx| {
-            ecx.normalizes_to_inner(param_env, alias, other, direction, invert)?;
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+        self.probe_candidate(
+            |ecx| {
+                ecx.normalizes_to_inner(param_env, alias, other, direction, invert)?;
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "normalizes-to".into(),
+        )
     }
 
     fn normalizes_to_inner(
@@ -153,18 +156,21 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         alias_rhs: ty::AliasTy<'tcx>,
         direction: ty::AliasRelationDirection,
     ) -> QueryResult<'tcx> {
-        self.probe(|ecx| {
-            match direction {
-                ty::AliasRelationDirection::Equate => {
-                    ecx.eq(param_env, alias_lhs, alias_rhs)?;
-                }
-                ty::AliasRelationDirection::Subtype => {
-                    ecx.sub(param_env, alias_lhs, alias_rhs)?;
+        self.probe_candidate(
+            |ecx| {
+                match direction {
+                    ty::AliasRelationDirection::Equate => {
+                        ecx.eq(param_env, alias_lhs, alias_rhs)?;
+                    }
+                    ty::AliasRelationDirection::Subtype => {
+                        ecx.sub(param_env, alias_lhs, alias_rhs)?;
+                    }
                 }
-            }
 
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "substs relate".into(),
+        )
     }
 
     fn assemble_bidirectional_normalizes_to_candidate(
@@ -174,22 +180,25 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         rhs: ty::Term<'tcx>,
         direction: ty::AliasRelationDirection,
     ) -> QueryResult<'tcx> {
-        self.probe(|ecx| {
-            ecx.normalizes_to_inner(
-                param_env,
-                lhs.to_alias_ty(ecx.tcx()).unwrap(),
-                rhs,
-                direction,
-                Invert::No,
-            )?;
-            ecx.normalizes_to_inner(
-                param_env,
-                rhs.to_alias_ty(ecx.tcx()).unwrap(),
-                lhs,
-                direction,
-                Invert::Yes,
-            )?;
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+        self.probe_candidate(
+            |ecx| {
+                ecx.normalizes_to_inner(
+                    param_env,
+                    lhs.to_alias_ty(ecx.tcx()).unwrap(),
+                    rhs,
+                    direction,
+                    Invert::No,
+                )?;
+                ecx.normalizes_to_inner(
+                    param_env,
+                    rhs.to_alias_ty(ecx.tcx()).unwrap(),
+                    lhs,
+                    direction,
+                    Invert::Yes,
+                )?;
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "bidir normalizes-to".into(),
+        )
     }
 }
diff --git a/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs b/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
index 8625958ff5a..6aff9e45cb4 100644
--- a/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
+++ b/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
@@ -21,8 +21,10 @@ use rustc_middle::ty::{
 use rustc_span::DUMMY_SP;
 use std::ops::ControlFlow;
 
+use crate::solve::inspect::DebugSolver;
 use crate::traits::specialization_graph;
 
+use super::inspect::InspectSolve;
 use super::search_graph::{self, OverflowHandler};
 use super::SolverMode;
 use super::{search_graph::SearchGraph, Goal};
@@ -73,6 +75,8 @@ pub struct EvalCtxt<'a, 'tcx> {
     // ambiguous goals. Instead, a probe needs to be introduced somewhere in the
     // evaluation code.
     tainted: Result<(), NoSolution>,
+
+    inspect: Box<dyn InspectSolve<'tcx> + 'tcx>,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -143,9 +147,18 @@ impl<'tcx> InferCtxtEvalExt<'tcx> for InferCtxt<'tcx> {
             var_values: CanonicalVarValues::dummy(),
             nested_goals: NestedGoals::new(),
             tainted: Ok(()),
+            inspect: Box::new(DebugSolver::new()),
         };
         let result = ecx.evaluate_goal(IsNormalizesToHack::No, goal);
 
+        let tree = match ecx.inspect.into_debug_solver() {
+            Some(tree) => match Box::leak(tree) {
+                DebugSolver::GoalEvaluation(tree) => tree,
+                _ => unreachable!("unable to convert to `DebugSolver::GoalEvaluation`"),
+            },
+            _ => unreachable!("unable to convert to `DebugSolver::GoalEvaluation`"),
+        };
+
         assert!(
             ecx.nested_goals.is_empty(),
             "root `EvalCtxt` should not have any goals added to it"
@@ -170,58 +183,72 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
     /// Instead of calling this function directly, use either [EvalCtxt::evaluate_goal]
     /// if you're inside of the solver or [InferCtxtEvalExt::evaluate_root_goal] if you're
     /// outside of it.
-    #[instrument(level = "debug", skip(tcx, search_graph), ret)]
+    #[instrument(level = "debug", skip(tcx, search_graph, goal_evaluation), ret)]
     fn evaluate_canonical_goal(
         tcx: TyCtxt<'tcx>,
         search_graph: &'a mut search_graph::SearchGraph<'tcx>,
         canonical_input: CanonicalInput<'tcx>,
+        mut goal_evaluation: &mut dyn InspectSolve<'tcx>,
     ) -> QueryResult<'tcx> {
+        goal_evaluation.canonicalized_goal(canonical_input);
+
         // Deal with overflow, caching, and coinduction.
         //
         // The actual solver logic happens in `ecx.compute_goal`.
-        search_graph.with_new_goal(tcx, canonical_input, |search_graph| {
-            let intercrate = match search_graph.solver_mode() {
-                SolverMode::Normal => false,
-                SolverMode::Coherence => true,
-            };
-            let (ref infcx, input, var_values) = tcx
-                .infer_ctxt()
-                .intercrate(intercrate)
-                .with_next_trait_solver(true)
-                .with_opaque_type_inference(canonical_input.value.anchor)
-                .build_with_canonical(DUMMY_SP, &canonical_input);
-
-            let mut ecx = EvalCtxt {
-                infcx,
-                var_values,
-                predefined_opaques_in_body: input.predefined_opaques_in_body,
-                max_input_universe: canonical_input.max_universe,
-                search_graph,
-                nested_goals: NestedGoals::new(),
-                tainted: Ok(()),
-            };
-
-            for &(key, ty) in &input.predefined_opaques_in_body.opaque_types {
-                ecx.insert_hidden_type(key, input.goal.param_env, ty)
-                    .expect("failed to prepopulate opaque types");
-            }
+        search_graph.with_new_goal(
+            tcx,
+            canonical_input,
+            goal_evaluation,
+            |search_graph, goal_evaluation| {
+                let intercrate = match search_graph.solver_mode() {
+                    SolverMode::Normal => false,
+                    SolverMode::Coherence => true,
+                };
+                let (ref infcx, input, var_values) = tcx
+                    .infer_ctxt()
+                    .intercrate(intercrate)
+                    .with_next_trait_solver(true)
+                    .with_opaque_type_inference(canonical_input.value.anchor)
+                    .build_with_canonical(DUMMY_SP, &canonical_input);
+
+                let mut ecx = EvalCtxt {
+                    infcx,
+                    var_values,
+                    predefined_opaques_in_body: input.predefined_opaques_in_body,
+                    max_input_universe: canonical_input.max_universe,
+                    search_graph,
+                    nested_goals: NestedGoals::new(),
+                    tainted: Ok(()),
+                    inspect: goal_evaluation.new_goal_evaluation_step(input),
+                };
+
+                for &(key, ty) in &input.predefined_opaques_in_body.opaque_types {
+                    ecx.insert_hidden_type(key, input.goal.param_env, ty)
+                        .expect("failed to prepopulate opaque types");
+                }
 
-            if !ecx.nested_goals.is_empty() {
-                panic!("prepopulating opaque types shouldn't add goals: {:?}", ecx.nested_goals);
-            }
+                if !ecx.nested_goals.is_empty() {
+                    panic!(
+                        "prepopulating opaque types shouldn't add goals: {:?}",
+                        ecx.nested_goals
+                    );
+                }
 
-            let result = ecx.compute_goal(input.goal);
+                let result = ecx.compute_goal(input.goal);
+                ecx.inspect.query_result(result);
+                goal_evaluation.goal_evaluation_step(ecx.inspect);
 
-            // When creating a query response we clone the opaque type constraints
-            // instead of taking them. This would cause an ICE here, since we have
-            // assertions against dropping an `InferCtxt` without taking opaques.
-            // FIXME: Once we remove support for the old impl we can remove this.
-            if input.anchor != DefiningAnchor::Error {
-                let _ = infcx.take_opaque_types();
-            }
+                // When creating a query response we clone the opaque type constraints
+                // instead of taking them. This would cause an ICE here, since we have
+                // assertions against dropping an `InferCtxt` without taking opaques.
+                // FIXME: Once we remove support for the old impl we can remove this.
+                if input.anchor != DefiningAnchor::Error {
+                    let _ = infcx.take_opaque_types();
+                }
 
-            result
-        })
+                result
+            },
+        )
     }
 
     /// Recursively evaluates `goal`, returning whether any inference vars have
@@ -232,8 +259,16 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
         goal: Goal<'tcx, ty::Predicate<'tcx>>,
     ) -> Result<(bool, Certainty, Vec<Goal<'tcx, ty::Predicate<'tcx>>>), NoSolution> {
         let (orig_values, canonical_goal) = self.canonicalize_goal(goal);
-        let canonical_response =
-            EvalCtxt::evaluate_canonical_goal(self.tcx(), self.search_graph, canonical_goal)?;
+        let mut goal_evaluation = self.inspect.new_goal_evaluation(goal);
+        let canonical_response = EvalCtxt::evaluate_canonical_goal(
+            self.tcx(),
+            self.search_graph,
+            canonical_goal,
+            &mut *goal_evaluation,
+        );
+        goal_evaluation.query_result(canonical_response);
+        self.inspect.goal_evaluation(goal_evaluation);
+        let canonical_response = canonical_response?;
 
         let has_changed = !canonical_response.value.var_values.is_identity()
             || !canonical_response.value.external_constraints.opaque_types.is_empty();
@@ -261,8 +296,13 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
         {
             debug!("rerunning goal to check result is stable");
             let (_orig_values, canonical_goal) = self.canonicalize_goal(goal);
-            let new_canonical_response =
-                EvalCtxt::evaluate_canonical_goal(self.tcx(), self.search_graph, canonical_goal)?;
+            let new_canonical_response = EvalCtxt::evaluate_canonical_goal(
+                self.tcx(),
+                self.search_graph,
+                canonical_goal,
+                // FIXME(-Ztrait-solver=next): we do not track what happens in `evaluate_canonical_goal`
+                &mut (),
+            )?;
             // We only check for modulo regions as we convert all regions in
             // the input to new existentials, even if they're expected to be
             // `'static` or a placeholder region.
@@ -353,12 +393,17 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
     // the certainty of all the goals.
     #[instrument(level = "debug", skip(self))]
     pub(super) fn try_evaluate_added_goals(&mut self) -> Result<Certainty, NoSolution> {
+        let inspect = self.inspect.new_evaluate_added_goals();
+        let inspect = core::mem::replace(&mut self.inspect, inspect);
+
         let mut goals = core::mem::replace(&mut self.nested_goals, NestedGoals::new());
         let mut new_goals = NestedGoals::new();
 
         let response = self.repeat_while_none(
             |_| Ok(Certainty::Maybe(MaybeCause::Overflow)),
             |this| {
+                this.inspect.evaluate_added_goals_loop_start();
+
                 let mut has_changed = Err(Certainty::Yes);
 
                 if let Some(goal) = goals.normalizes_to_hack_goal.take() {
@@ -447,10 +492,15 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
             },
         );
 
+        self.inspect.eval_added_goals_result(response);
+
         if response.is_err() {
             self.tainted = Err(NoSolution);
         }
 
+        let goal_evaluations = std::mem::replace(&mut self.inspect, inspect);
+        self.inspect.added_goals_evaluation(goal_evaluations);
+
         self.nested_goals = goals;
         response
     }
@@ -466,8 +516,24 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
             search_graph: self.search_graph,
             nested_goals: self.nested_goals.clone(),
             tainted: self.tainted,
+            inspect: self.inspect.new_goal_candidate(),
         };
-        self.infcx.probe(|_| f(&mut ecx))
+        let r = self.infcx.probe(|_| f(&mut ecx));
+        self.inspect.goal_candidate(ecx.inspect);
+        r
+    }
+
+    pub(super) fn probe_candidate(
+        &mut self,
+        f: impl FnOnce(&mut EvalCtxt<'_, 'tcx>) -> QueryResult<'tcx>,
+        mut name: impl FnMut() -> String,
+    ) -> QueryResult<'tcx> {
+        self.probe(|ecx| {
+            let result = f(ecx);
+            ecx.inspect.candidate_name(&mut name);
+            ecx.inspect.query_result(result);
+            result
+        })
     }
 
     pub(super) fn tcx(&self) -> TyCtxt<'tcx> {
@@ -781,14 +847,17 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
             if candidate_key.def_id != key.def_id {
                 continue;
             }
-            values.extend(self.probe(|ecx| {
-                for (a, b) in std::iter::zip(candidate_key.substs, key.substs) {
-                    ecx.eq(param_env, a, b)?;
-                }
-                ecx.eq(param_env, candidate_ty, ty)?;
-                ecx.add_item_bounds_for_hidden_type(candidate_key, param_env, candidate_ty);
-                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-            }));
+            values.extend(self.probe(
+                |ecx| {
+                    for (a, b) in std::iter::zip(candidate_key.substs, key.substs) {
+                        ecx.eq(param_env, a, b)?;
+                    }
+                    ecx.eq(param_env, candidate_ty, ty)?;
+                    ecx.add_item_bounds_for_hidden_type(candidate_key, param_env, candidate_ty);
+                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                },
+                || "opaque type storage".into(),
+            ));
         }
         values
     }
diff --git a/compiler/rustc_trait_selection/src/solve/inspect/mod.rs b/compiler/rustc_trait_selection/src/solve/inspect/mod.rs
new file mode 100644
index 00000000000..eb05315b164
--- /dev/null
+++ b/compiler/rustc_trait_selection/src/solve/inspect/mod.rs
@@ -0,0 +1,244 @@
+use rustc_middle::{
+    traits::{
+        query::NoSolution,
+        solve::{inspect::*, CanonicalInput, Certainty, Goal, QueryInput, QueryResult},
+    },
+    ty,
+};
+
+#[derive(Debug)]
+pub enum DebugSolver<'tcx> {
+    Root,
+    GoalEvaluation(GoalEvaluation<'tcx>),
+    AddedGoalsEvaluation(AddedGoalsEvaluation<'tcx>),
+    GoalEvaluationStep(GoalEvaluationStep<'tcx>),
+    GoalCandidate(GoalCandidate<'tcx>),
+}
+
+pub trait InspectSolve<'tcx> {
+    fn into_debug_solver(self: Box<Self>) -> Option<Box<DebugSolver<'tcx>>>;
+
+    fn new_goal_evaluation(
+        &mut self,
+        goal: Goal<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx>;
+    fn canonicalized_goal(&mut self, canonical_goal: CanonicalInput<'tcx>);
+    fn cache_hit(&mut self);
+    fn goal_evaluation(&mut self, goal_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>);
+
+    fn new_goal_evaluation_step(
+        &mut self,
+        instantiated_goal: QueryInput<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx>;
+    fn goal_evaluation_step(&mut self, goal_eval_step: Box<dyn InspectSolve<'tcx> + 'tcx>);
+
+    fn new_goal_candidate(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx>;
+    fn candidate_name(&mut self, f: &mut dyn FnMut() -> String);
+    fn goal_candidate(&mut self, candidate: Box<dyn InspectSolve<'tcx> + 'tcx>);
+
+    fn new_evaluate_added_goals(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx>;
+    fn evaluate_added_goals_loop_start(&mut self);
+    fn eval_added_goals_result(&mut self, result: Result<Certainty, NoSolution>);
+    fn added_goals_evaluation(&mut self, goals_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>);
+
+    fn query_result(&mut self, result: QueryResult<'tcx>);
+}
+
+/// No-op `InspectSolve` impl to use for normal trait solving when we do not want
+/// to take a performance hit from recording information about how things are being
+/// proven.
+impl<'tcx> InspectSolve<'tcx> for () {
+    fn into_debug_solver(self: Box<Self>) -> Option<Box<DebugSolver<'tcx>>> {
+        None
+    }
+
+    fn new_goal_evaluation(
+        &mut self,
+        _goal: Goal<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(())
+    }
+    fn canonicalized_goal(&mut self, _canonical_goal: CanonicalInput<'tcx>) {}
+    fn cache_hit(&mut self) {}
+    fn goal_evaluation(&mut self, _goal_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>) {}
+
+    fn new_goal_evaluation_step(
+        &mut self,
+        _instantiated_goal: QueryInput<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(())
+    }
+    fn goal_evaluation_step(&mut self, _goal_eval_step: Box<dyn InspectSolve<'tcx> + 'tcx>) {}
+
+    fn new_goal_candidate(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(())
+    }
+    fn candidate_name(&mut self, _f: &mut dyn FnMut() -> String) {}
+    fn goal_candidate(&mut self, _candidate: Box<dyn InspectSolve<'tcx> + 'tcx>) {}
+
+    fn new_evaluate_added_goals(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(())
+    }
+    fn evaluate_added_goals_loop_start(&mut self) {}
+    fn eval_added_goals_result(&mut self, _result: Result<Certainty, NoSolution>) {}
+    fn added_goals_evaluation(&mut self, _goals_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>) {}
+
+    fn query_result(&mut self, _result: QueryResult<'tcx>) {}
+}
+
+impl<'tcx> DebugSolver<'tcx> {
+    pub fn new() -> Self {
+        Self::Root
+    }
+}
+impl<'tcx> InspectSolve<'tcx> for DebugSolver<'tcx> {
+    fn into_debug_solver(self: Box<Self>) -> Option<Box<DebugSolver<'tcx>>> {
+        Some(self)
+    }
+
+    fn new_goal_evaluation(
+        &mut self,
+        goal: Goal<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(DebugSolver::GoalEvaluation(GoalEvaluation {
+            uncanonicalized_goal: goal,
+            canonicalized_goal: None,
+            evaluation_steps: vec![],
+            cache_hit: false,
+            result: None,
+        }))
+    }
+    fn canonicalized_goal(&mut self, canonical_goal: CanonicalInput<'tcx>) {
+        match self {
+            DebugSolver::GoalEvaluation(goal_evaluation) => {
+                assert!(goal_evaluation.canonicalized_goal.is_none());
+                goal_evaluation.canonicalized_goal = Some(canonical_goal)
+            }
+            _ => unreachable!(),
+        }
+    }
+    fn cache_hit(&mut self) {
+        match self {
+            DebugSolver::GoalEvaluation(goal_evaluation) => goal_evaluation.cache_hit = true,
+            _ => unreachable!(),
+        };
+    }
+    fn goal_evaluation(&mut self, goal_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>) {
+        let goal_evaluation = goal_evaluation.into_debug_solver().unwrap();
+        match (self, *goal_evaluation) {
+            (
+                DebugSolver::AddedGoalsEvaluation(AddedGoalsEvaluation { evaluations, .. }),
+                DebugSolver::GoalEvaluation(goal_evaluation),
+            ) => evaluations.last_mut().unwrap().push(goal_evaluation),
+            (this @ DebugSolver::Root, goal_evaluation) => *this = goal_evaluation,
+            _ => unreachable!(),
+        }
+    }
+
+    fn new_goal_evaluation_step(
+        &mut self,
+        instantiated_goal: QueryInput<'tcx, ty::Predicate<'tcx>>,
+    ) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(DebugSolver::GoalEvaluationStep(GoalEvaluationStep {
+            instantiated_goal,
+            nested_goal_evaluations: vec![],
+            candidates: vec![],
+            result: None,
+        }))
+    }
+    fn goal_evaluation_step(&mut self, goal_eval_step: Box<dyn InspectSolve<'tcx> + 'tcx>) {
+        let goal_eval_step = goal_eval_step.into_debug_solver().unwrap();
+        match (self, *goal_eval_step) {
+            (DebugSolver::GoalEvaluation(goal_eval), DebugSolver::GoalEvaluationStep(step)) => {
+                goal_eval.evaluation_steps.push(step);
+            }
+            _ => unreachable!(),
+        }
+    }
+
+    fn new_goal_candidate(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(DebugSolver::GoalCandidate(GoalCandidate {
+            nested_goal_evaluations: vec![],
+            candidates: vec![],
+            name: None,
+            result: None,
+        }))
+    }
+    fn candidate_name(&mut self, f: &mut dyn FnMut() -> String) {
+        let name = f();
+
+        match self {
+            DebugSolver::GoalCandidate(goal_candidate) => {
+                assert!(goal_candidate.name.is_none());
+                goal_candidate.name = Some(name);
+            }
+            _ => unreachable!(),
+        }
+    }
+    fn goal_candidate(&mut self, candidate: Box<dyn InspectSolve<'tcx> + 'tcx>) {
+        let candidate = candidate.into_debug_solver().unwrap();
+        match (self, *candidate) {
+            (
+                DebugSolver::GoalCandidate(GoalCandidate { candidates, .. })
+                | DebugSolver::GoalEvaluationStep(GoalEvaluationStep { candidates, .. }),
+                DebugSolver::GoalCandidate(candidate),
+            ) => candidates.push(candidate),
+            _ => unreachable!(),
+        }
+    }
+
+    fn new_evaluate_added_goals(&mut self) -> Box<dyn InspectSolve<'tcx> + 'tcx> {
+        Box::new(DebugSolver::AddedGoalsEvaluation(AddedGoalsEvaluation {
+            evaluations: vec![],
+            result: None,
+        }))
+    }
+    fn evaluate_added_goals_loop_start(&mut self) {
+        match self {
+            DebugSolver::AddedGoalsEvaluation(this) => {
+                this.evaluations.push(vec![]);
+            }
+            _ => unreachable!(),
+        }
+    }
+    fn eval_added_goals_result(&mut self, result: Result<Certainty, NoSolution>) {
+        match self {
+            DebugSolver::AddedGoalsEvaluation(this) => {
+                assert!(this.result.is_none());
+                this.result = Some(result);
+            }
+            _ => unreachable!(),
+        }
+    }
+    fn added_goals_evaluation(&mut self, goals_evaluation: Box<dyn InspectSolve<'tcx> + 'tcx>) {
+        let goals_evaluation = goals_evaluation.into_debug_solver().unwrap();
+        match (self, *goals_evaluation) {
+            (
+                DebugSolver::GoalEvaluationStep(GoalEvaluationStep {
+                    nested_goal_evaluations, ..
+                })
+                | DebugSolver::GoalCandidate(GoalCandidate { nested_goal_evaluations, .. }),
+                DebugSolver::AddedGoalsEvaluation(added_goals_evaluation),
+            ) => nested_goal_evaluations.push(added_goals_evaluation),
+            _ => unreachable!(),
+        }
+    }
+
+    fn query_result(&mut self, result: QueryResult<'tcx>) {
+        match self {
+            DebugSolver::GoalEvaluation(goal_evaluation) => {
+                assert!(goal_evaluation.result.is_none());
+                goal_evaluation.result = Some(result);
+            }
+            DebugSolver::Root | DebugSolver::AddedGoalsEvaluation(_) => unreachable!(),
+            DebugSolver::GoalEvaluationStep(evaluation_step) => {
+                assert!(evaluation_step.result.is_none());
+                evaluation_step.result = Some(result);
+            }
+            DebugSolver::GoalCandidate(candidate) => {
+                assert!(candidate.result.is_none());
+                candidate.result = Some(result);
+            }
+        }
+    }
+}
diff --git a/compiler/rustc_trait_selection/src/solve/mod.rs b/compiler/rustc_trait_selection/src/solve/mod.rs
index a30a14df80b..49fecedc0ca 100644
--- a/compiler/rustc_trait_selection/src/solve/mod.rs
+++ b/compiler/rustc_trait_selection/src/solve/mod.rs
@@ -25,6 +25,7 @@ mod assembly;
 mod canonicalize;
 mod eval_ctxt;
 mod fulfill;
+pub mod inspect;
 mod opaques;
 mod project_goals;
 mod search_graph;
diff --git a/compiler/rustc_trait_selection/src/solve/project_goals.rs b/compiler/rustc_trait_selection/src/solve/project_goals.rs
index e9600968f48..09fd6fa4b1f 100644
--- a/compiler/rustc_trait_selection/src/solve/project_goals.rs
+++ b/compiler/rustc_trait_selection/src/solve/project_goals.rs
@@ -112,7 +112,7 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
         if let Some(projection_pred) = assumption.as_projection_clause()
             && projection_pred.projection_def_id() == goal.predicate.def_id()
         {
-            ecx.probe(|ecx| {
+            ecx.probe_candidate(|ecx| {
                 let assumption_projection_pred =
                     ecx.instantiate_binder_with_infer(projection_pred);
                 ecx.eq(
@@ -123,7 +123,7 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
                 ecx.eq(goal.param_env, goal.predicate.term, assumption_projection_pred.term)
                     .expect("expected goal term to be fully unconstrained");
                 then(ecx)
-            })
+            }, || "assumption".into())
         } else {
             Err(NoSolution)
         }
@@ -143,87 +143,90 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
             return Err(NoSolution);
         }
 
-        ecx.probe(|ecx| {
-            let impl_substs = ecx.fresh_substs_for_item(impl_def_id);
-            let impl_trait_ref = impl_trait_ref.subst(tcx, impl_substs);
-
-            ecx.eq(goal.param_env, goal_trait_ref, impl_trait_ref)?;
-
-            let where_clause_bounds = tcx
-                .predicates_of(impl_def_id)
-                .instantiate(tcx, impl_substs)
-                .predicates
-                .into_iter()
-                .map(|pred| goal.with(tcx, pred));
-            ecx.add_goals(where_clause_bounds);
-
-            // In case the associated item is hidden due to specialization, we have to
-            // return ambiguity this would otherwise be incomplete, resulting in
-            // unsoundness during coherence (#105782).
-            let Some(assoc_def) = fetch_eligible_assoc_item_def(
-                ecx,
-                goal.param_env,
-                goal_trait_ref,
-                goal.predicate.def_id(),
-                impl_def_id
-            )? else {
-                return ecx.evaluate_added_goals_and_make_canonical_response(Certainty::AMBIGUOUS);
-            };
+        ecx.probe_candidate(
+            |ecx| {
+                let impl_substs = ecx.fresh_substs_for_item(impl_def_id);
+                let impl_trait_ref = impl_trait_ref.subst(tcx, impl_substs);
+
+                ecx.eq(goal.param_env, goal_trait_ref, impl_trait_ref)?;
+
+                let where_clause_bounds = tcx
+                    .predicates_of(impl_def_id)
+                    .instantiate(tcx, impl_substs)
+                    .predicates
+                    .into_iter()
+                    .map(|pred| goal.with(tcx, pred));
+                ecx.add_goals(where_clause_bounds);
+
+                // In case the associated item is hidden due to specialization, we have to
+                // return ambiguity this would otherwise be incomplete, resulting in
+                // unsoundness during coherence (#105782).
+                let Some(assoc_def) = fetch_eligible_assoc_item_def(
+                    ecx,
+                    goal.param_env,
+                    goal_trait_ref,
+                    goal.predicate.def_id(),
+                    impl_def_id
+                )? else {
+                    return ecx.evaluate_added_goals_and_make_canonical_response(Certainty::AMBIGUOUS);
+                };
+
+                if !assoc_def.item.defaultness(tcx).has_value() {
+                    let guar = tcx.sess.delay_span_bug(
+                        tcx.def_span(assoc_def.item.def_id),
+                        "missing value for assoc item in impl",
+                    );
+                    let error_term = match assoc_def.item.kind {
+                        ty::AssocKind::Const => tcx
+                            .const_error(
+                                tcx.type_of(goal.predicate.def_id())
+                                    .subst(tcx, goal.predicate.projection_ty.substs),
+                                guar,
+                            )
+                            .into(),
+                        ty::AssocKind::Type => tcx.ty_error(guar).into(),
+                        ty::AssocKind::Fn => unreachable!(),
+                    };
+                    ecx.eq(goal.param_env, goal.predicate.term, error_term)
+                        .expect("expected goal term to be fully unconstrained");
+                    return ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
+                }
 
-            if !assoc_def.item.defaultness(tcx).has_value() {
-                let guar = tcx.sess.delay_span_bug(
-                    tcx.def_span(assoc_def.item.def_id),
-                    "missing value for assoc item in impl",
+                // Getting the right substitutions here is complex, e.g. given:
+                // - a goal `<Vec<u32> as Trait<i32>>::Assoc<u64>`
+                // - the applicable impl `impl<T> Trait<i32> for Vec<T>`
+                // - and the impl which defines `Assoc` being `impl<T, U> Trait<U> for Vec<T>`
+                //
+                // We first rebase the goal substs onto the impl, going from `[Vec<u32>, i32, u64]`
+                // to `[u32, u64]`.
+                //
+                // And then map these substs to the substs of the defining impl of `Assoc`, going
+                // from `[u32, u64]` to `[u32, i32, u64]`.
+                let impl_substs_with_gat = goal.predicate.projection_ty.substs.rebase_onto(
+                    tcx,
+                    goal_trait_ref.def_id,
+                    impl_substs,
+                );
+                let substs = ecx.translate_substs(
+                    goal.param_env,
+                    impl_def_id,
+                    impl_substs_with_gat,
+                    assoc_def.defining_node,
                 );
-                let error_term = match assoc_def.item.kind {
-                    ty::AssocKind::Const => tcx
-                        .const_error(
-                            tcx.type_of(goal.predicate.def_id())
-                                .subst(tcx, goal.predicate.projection_ty.substs),
-                            guar,
-                        )
-                        .into(),
-                    ty::AssocKind::Type => tcx.ty_error(guar).into(),
-                    ty::AssocKind::Fn => unreachable!(),
-                };
-                ecx.eq(goal.param_env, goal.predicate.term, error_term)
-                    .expect("expected goal term to be fully unconstrained");
-                return ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
-            }
 
-            // Getting the right substitutions here is complex, e.g. given:
-            // - a goal `<Vec<u32> as Trait<i32>>::Assoc<u64>`
-            // - the applicable impl `impl<T> Trait<i32> for Vec<T>`
-            // - and the impl which defines `Assoc` being `impl<T, U> Trait<U> for Vec<T>`
-            //
-            // We first rebase the goal substs onto the impl, going from `[Vec<u32>, i32, u64]`
-            // to `[u32, u64]`.
-            //
-            // And then map these substs to the substs of the defining impl of `Assoc`, going
-            // from `[u32, u64]` to `[u32, i32, u64]`.
-            let impl_substs_with_gat = goal.predicate.projection_ty.substs.rebase_onto(
-                tcx,
-                goal_trait_ref.def_id,
-                impl_substs,
-            );
-            let substs = ecx.translate_substs(
-                goal.param_env,
-                impl_def_id,
-                impl_substs_with_gat,
-                assoc_def.defining_node,
-            );
-
-            // Finally we construct the actual value of the associated type.
-            let term = match assoc_def.item.kind {
-                ty::AssocKind::Type => tcx.type_of(assoc_def.item.def_id).map_bound(|ty| ty.into()),
-                ty::AssocKind::Const => bug!("associated const projection is not supported yet"),
-                ty::AssocKind::Fn => unreachable!("we should never project to a fn"),
-            };
+                // Finally we construct the actual value of the associated type.
+                let term = match assoc_def.item.kind {
+                    ty::AssocKind::Type => tcx.type_of(assoc_def.item.def_id).map_bound(|ty| ty.into()),
+                    ty::AssocKind::Const => bug!("associated const projection is not supported yet"),
+                    ty::AssocKind::Fn => unreachable!("we should never project to a fn"),
+                };
 
-            ecx.eq(goal.param_env, goal.predicate.term, term.subst(tcx, substs))
-                .expect("expected goal term to be fully unconstrained");
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+                ecx.eq(goal.param_env, goal.predicate.term, term.subst(tcx, substs))
+                    .expect("expected goal term to be fully unconstrained");
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "impl".into(),
+        )
     }
 
     fn consider_auto_trait_candidate(
@@ -318,53 +321,69 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
         goal: Goal<'tcx, Self>,
     ) -> QueryResult<'tcx> {
         let tcx = ecx.tcx();
-        ecx.probe(|ecx| {
-            let metadata_ty = match goal.predicate.self_ty().kind() {
-                ty::Bool
-                | ty::Char
-                | ty::Int(..)
-                | ty::Uint(..)
-                | ty::Float(..)
-                | ty::Array(..)
-                | ty::RawPtr(..)
-                | ty::Ref(..)
-                | ty::FnDef(..)
-                | ty::FnPtr(..)
-                | ty::Closure(..)
-                | ty::Infer(ty::IntVar(..) | ty::FloatVar(..))
-                | ty::Generator(..)
-                | ty::GeneratorWitness(..)
-                | ty::GeneratorWitnessMIR(..)
-                | ty::Never
-                | ty::Foreign(..) => tcx.types.unit,
-
-                ty::Error(e) => tcx.ty_error(*e),
-
-                ty::Str | ty::Slice(_) => tcx.types.usize,
-
-                ty::Dynamic(_, _, _) => {
-                    let dyn_metadata = tcx.require_lang_item(LangItem::DynMetadata, None);
-                    tcx.type_of(dyn_metadata)
-                        .subst(tcx, &[ty::GenericArg::from(goal.predicate.self_ty())])
-                }
+        ecx.probe_candidate(
+            |ecx| {
+                let metadata_ty = match goal.predicate.self_ty().kind() {
+                    ty::Bool
+                    | ty::Char
+                    | ty::Int(..)
+                    | ty::Uint(..)
+                    | ty::Float(..)
+                    | ty::Array(..)
+                    | ty::RawPtr(..)
+                    | ty::Ref(..)
+                    | ty::FnDef(..)
+                    | ty::FnPtr(..)
+                    | ty::Closure(..)
+                    | ty::Infer(ty::IntVar(..) | ty::FloatVar(..))
+                    | ty::Generator(..)
+                    | ty::GeneratorWitness(..)
+                    | ty::GeneratorWitnessMIR(..)
+                    | ty::Never
+                    | ty::Foreign(..) => tcx.types.unit,
+
+                    ty::Error(e) => tcx.ty_error(*e),
+
+                    ty::Str | ty::Slice(_) => tcx.types.usize,
+
+                    ty::Dynamic(_, _, _) => {
+                        let dyn_metadata = tcx.require_lang_item(LangItem::DynMetadata, None);
+                        tcx.type_of(dyn_metadata)
+                            .subst(tcx, &[ty::GenericArg::from(goal.predicate.self_ty())])
+                    }
 
-                ty::Alias(_, _) | ty::Param(_) | ty::Placeholder(..) => {
-                    // FIXME(ptr_metadata): It would also be possible to return a `Ok(Ambig)` with no constraints.
-                    let sized_predicate = ty::TraitRef::from_lang_item(
-                        tcx,
-                        LangItem::Sized,
-                        DUMMY_SP,
-                        [ty::GenericArg::from(goal.predicate.self_ty())],
-                    );
-                    ecx.add_goal(goal.with(tcx, sized_predicate));
-                    tcx.types.unit
-                }
+                    ty::Alias(_, _) | ty::Param(_) | ty::Placeholder(..) => {
+                        // FIXME(ptr_metadata): It would also be possible to return a `Ok(Ambig)` with no constraints.
+                        let sized_predicate = ty::TraitRef::from_lang_item(
+                            tcx,
+                            LangItem::Sized,
+                            DUMMY_SP,
+                            [ty::GenericArg::from(goal.predicate.self_ty())],
+                        );
+                        ecx.add_goal(goal.with(tcx, sized_predicate));
+                        tcx.types.unit
+                    }
 
-                ty::Adt(def, substs) if def.is_struct() => {
-                    match def.non_enum_variant().fields.raw.last() {
+                    ty::Adt(def, substs) if def.is_struct() => {
+                        match def.non_enum_variant().fields.raw.last() {
+                            None => tcx.types.unit,
+                            Some(field_def) => {
+                                let self_ty = field_def.ty(tcx, substs);
+                                ecx.add_goal(goal.with(
+                                    tcx,
+                                    ty::Binder::dummy(goal.predicate.with_self_ty(tcx, self_ty)),
+                                ));
+                                return ecx.evaluate_added_goals_and_make_canonical_response(
+                                    Certainty::Yes,
+                                );
+                            }
+                        }
+                    }
+                    ty::Adt(_, _) => tcx.types.unit,
+
+                    ty::Tuple(elements) => match elements.last() {
                         None => tcx.types.unit,
-                        Some(field_def) => {
-                            let self_ty = field_def.ty(tcx, substs);
+                        Some(&self_ty) => {
                             ecx.add_goal(goal.with(
                                 tcx,
                                 ty::Binder::dummy(goal.predicate.with_self_ty(tcx, self_ty)),
@@ -372,35 +391,23 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
                             return ecx
                                 .evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
                         }
-                    }
-                }
-                ty::Adt(_, _) => tcx.types.unit,
-
-                ty::Tuple(elements) => match elements.last() {
-                    None => tcx.types.unit,
-                    Some(&self_ty) => {
-                        ecx.add_goal(goal.with(
-                            tcx,
-                            ty::Binder::dummy(goal.predicate.with_self_ty(tcx, self_ty)),
-                        ));
-                        return ecx
-                            .evaluate_added_goals_and_make_canonical_response(Certainty::Yes);
-                    }
-                },
-
-                ty::Infer(
-                    ty::TyVar(_) | ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_),
-                )
-                | ty::Bound(..) => bug!(
-                    "unexpected self ty `{:?}` when normalizing `<T as Pointee>::Metadata`",
-                    goal.predicate.self_ty()
-                ),
-            };
+                    },
+
+                    ty::Infer(
+                        ty::TyVar(_) | ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_),
+                    )
+                    | ty::Bound(..) => bug!(
+                        "unexpected self ty `{:?}` when normalizing `<T as Pointee>::Metadata`",
+                        goal.predicate.self_ty()
+                    ),
+                };
 
-            ecx.eq(goal.param_env, goal.predicate.term, metadata_ty.into())
-                .expect("expected goal term to be fully unconstrained");
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+                ecx.eq(goal.param_env, goal.predicate.term, metadata_ty.into())
+                    .expect("expected goal term to be fully unconstrained");
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "builtin pointee".into(),
+        )
     }
 
     fn consider_builtin_future_candidate(
@@ -535,11 +542,14 @@ impl<'tcx> assembly::GoalKind<'tcx> for ProjectionPredicate<'tcx> {
             ),
         };
 
-        ecx.probe(|ecx| {
-            ecx.eq(goal.param_env, goal.predicate.term, discriminant_ty.into())
-                .expect("expected goal term to be fully unconstrained");
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+        ecx.probe_candidate(
+            |ecx| {
+                ecx.eq(goal.param_env, goal.predicate.term, discriminant_ty.into())
+                    .expect("expected goal term to be fully unconstrained");
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "builtin discriminant kind".into(),
+        )
     }
 
     fn consider_builtin_destruct_candidate(
diff --git a/compiler/rustc_trait_selection/src/solve/search_graph/mod.rs b/compiler/rustc_trait_selection/src/solve/search_graph/mod.rs
index 19e4b23009a..131c72a5232 100644
--- a/compiler/rustc_trait_selection/src/solve/search_graph/mod.rs
+++ b/compiler/rustc_trait_selection/src/solve/search_graph/mod.rs
@@ -12,6 +12,7 @@ use rustc_middle::traits::solve::{CanonicalInput, Certainty, MaybeCause, QueryRe
 use rustc_middle::ty::TyCtxt;
 use std::{collections::hash_map::Entry, mem};
 
+use super::inspect::InspectSolve;
 use super::SolverMode;
 
 rustc_index::newtype_index! {
@@ -205,11 +206,13 @@ impl<'tcx> SearchGraph<'tcx> {
         &mut self,
         tcx: TyCtxt<'tcx>,
         canonical_input: CanonicalInput<'tcx>,
-        mut loop_body: impl FnMut(&mut Self) -> QueryResult<'tcx>,
+        inspect: &mut dyn InspectSolve<'tcx>,
+        mut loop_body: impl FnMut(&mut Self, &mut dyn InspectSolve<'tcx>) -> QueryResult<'tcx>,
     ) -> QueryResult<'tcx> {
         if self.should_use_global_cache() {
             if let Some(result) = tcx.new_solver_evaluation_cache.get(&canonical_input, tcx) {
                 debug!(?canonical_input, ?result, "cache hit");
+                inspect.cache_hit();
                 return result;
             }
         }
@@ -231,7 +234,7 @@ impl<'tcx> SearchGraph<'tcx> {
                     result
                 },
                 |this| {
-                    let result = loop_body(this);
+                    let result = loop_body(this, inspect);
                     this.try_finalize_goal(canonical_input, result).then(|| result)
                 },
             )
diff --git a/compiler/rustc_trait_selection/src/solve/trait_goals.rs b/compiler/rustc_trait_selection/src/solve/trait_goals.rs
index 279fc1229d4..b5cd8760b2a 100644
--- a/compiler/rustc_trait_selection/src/solve/trait_goals.rs
+++ b/compiler/rustc_trait_selection/src/solve/trait_goals.rs
@@ -61,21 +61,24 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
             },
         };
 
-        ecx.probe(|ecx| {
-            let impl_substs = ecx.fresh_substs_for_item(impl_def_id);
-            let impl_trait_ref = impl_trait_ref.subst(tcx, impl_substs);
-
-            ecx.eq(goal.param_env, goal.predicate.trait_ref, impl_trait_ref)?;
-            let where_clause_bounds = tcx
-                .predicates_of(impl_def_id)
-                .instantiate(tcx, impl_substs)
-                .predicates
-                .into_iter()
-                .map(|pred| goal.with(tcx, pred));
-            ecx.add_goals(where_clause_bounds);
-
-            ecx.evaluate_added_goals_and_make_canonical_response(maximal_certainty)
-        })
+        ecx.probe_candidate(
+            |ecx| {
+                let impl_substs = ecx.fresh_substs_for_item(impl_def_id);
+                let impl_trait_ref = impl_trait_ref.subst(tcx, impl_substs);
+
+                ecx.eq(goal.param_env, goal.predicate.trait_ref, impl_trait_ref)?;
+                let where_clause_bounds = tcx
+                    .predicates_of(impl_def_id)
+                    .instantiate(tcx, impl_substs)
+                    .predicates
+                    .into_iter()
+                    .map(|pred| goal.with(tcx, pred));
+                ecx.add_goals(where_clause_bounds);
+
+                ecx.evaluate_added_goals_and_make_canonical_response(maximal_certainty)
+            },
+            || "impl".into(),
+        )
     }
 
     fn probe_and_match_goal_against_assumption(
@@ -89,7 +92,7 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
             && trait_clause.polarity() == goal.predicate.polarity
         {
             // FIXME: Constness
-            ecx.probe(|ecx| {
+            ecx.probe_candidate(|ecx| {
                 let assumption_trait_pred =
                     ecx.instantiate_binder_with_infer(trait_clause);
                 ecx.eq(
@@ -98,7 +101,7 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
                     assumption_trait_pred.trait_ref,
                 )?;
                 then(ecx)
-            })
+            }, || "assumption".into())
         } else {
             Err(NoSolution)
         }
@@ -132,13 +135,16 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
 
         let tcx = ecx.tcx();
 
-        ecx.probe(|ecx| {
-            let nested_obligations = tcx
-                .predicates_of(goal.predicate.def_id())
-                .instantiate(tcx, goal.predicate.trait_ref.substs);
-            ecx.add_goals(nested_obligations.predicates.into_iter().map(|p| goal.with(tcx, p)));
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+        ecx.probe_candidate(
+            |ecx| {
+                let nested_obligations = tcx
+                    .predicates_of(goal.predicate.def_id())
+                    .instantiate(tcx, goal.predicate.trait_ref.substs);
+                ecx.add_goals(nested_obligations.predicates.into_iter().map(|p| goal.with(tcx, p)));
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "trait alias".into(),
+        )
     }
 
     fn consider_builtin_sized_candidate(
@@ -344,109 +350,116 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
         if b_ty.is_ty_var() {
             return ecx.evaluate_added_goals_and_make_canonical_response(Certainty::AMBIGUOUS);
         }
-        ecx.probe(|ecx| {
-            match (a_ty.kind(), b_ty.kind()) {
-                // Trait upcasting, or `dyn Trait + Auto + 'a` -> `dyn Trait + 'b`
-                (&ty::Dynamic(_, _, ty::Dyn), &ty::Dynamic(_, _, ty::Dyn)) => {
-                    // Dyn upcasting is handled separately, since due to upcasting,
-                    // when there are two supertraits that differ by substs, we
-                    // may return more than one query response.
-                    Err(NoSolution)
-                }
-                // `T` -> `dyn Trait` unsizing
-                (_, &ty::Dynamic(data, region, ty::Dyn)) => {
-                    // Can only unsize to an object-safe type
-                    if data
-                        .principal_def_id()
-                        .is_some_and(|def_id| !tcx.check_is_object_safe(def_id))
-                    {
-                        return Err(NoSolution);
+        ecx.probe_candidate(
+            |ecx| {
+                match (a_ty.kind(), b_ty.kind()) {
+                    // Trait upcasting, or `dyn Trait + Auto + 'a` -> `dyn Trait + 'b`
+                    (&ty::Dynamic(_, _, ty::Dyn), &ty::Dynamic(_, _, ty::Dyn)) => {
+                        // Dyn upcasting is handled separately, since due to upcasting,
+                        // when there are two supertraits that differ by substs, we
+                        // may return more than one query response.
+                        Err(NoSolution)
                     }
-
-                    let Some(sized_def_id) = tcx.lang_items().sized_trait() else {
+                    // `T` -> `dyn Trait` unsizing
+                    (_, &ty::Dynamic(data, region, ty::Dyn)) => {
+                        // Can only unsize to an object-safe type
+                        if data
+                            .principal_def_id()
+                            .is_some_and(|def_id| !tcx.check_is_object_safe(def_id))
+                        {
+                            return Err(NoSolution);
+                        }
+
+                        let Some(sized_def_id) = tcx.lang_items().sized_trait() else {
                         return Err(NoSolution);
                     };
-                    // Check that the type implements all of the predicates of the def-id.
-                    // (i.e. the principal, all of the associated types match, and any auto traits)
-                    ecx.add_goals(
-                        data.iter().map(|pred| goal.with(tcx, pred.with_self_ty(tcx, a_ty))),
-                    );
-                    // The type must be Sized to be unsized.
-                    ecx.add_goal(goal.with(tcx, ty::TraitRef::new(tcx, sized_def_id, [a_ty])));
-                    // The type must outlive the lifetime of the `dyn` we're unsizing into.
-                    ecx.add_goal(
-                        goal.with(tcx, ty::Binder::dummy(ty::OutlivesPredicate(a_ty, region))),
-                    );
-                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-                }
-                // `[T; n]` -> `[T]` unsizing
-                (&ty::Array(a_elem_ty, ..), &ty::Slice(b_elem_ty)) => {
-                    // We just require that the element type stays the same
-                    ecx.eq(goal.param_env, a_elem_ty, b_elem_ty)?;
-                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-                }
-                // Struct unsizing `Struct<T>` -> `Struct<U>` where `T: Unsize<U>`
-                (&ty::Adt(a_def, a_substs), &ty::Adt(b_def, b_substs))
-                    if a_def.is_struct() && a_def.did() == b_def.did() =>
-                {
-                    let unsizing_params = tcx.unsizing_params_for_adt(a_def.did());
-                    // We must be unsizing some type parameters. This also implies
-                    // that the struct has a tail field.
-                    if unsizing_params.is_empty() {
-                        return Err(NoSolution);
+                        // Check that the type implements all of the predicates of the def-id.
+                        // (i.e. the principal, all of the associated types match, and any auto traits)
+                        ecx.add_goals(
+                            data.iter().map(|pred| goal.with(tcx, pred.with_self_ty(tcx, a_ty))),
+                        );
+                        // The type must be Sized to be unsized.
+                        ecx.add_goal(goal.with(tcx, ty::TraitRef::new(tcx, sized_def_id, [a_ty])));
+                        // The type must outlive the lifetime of the `dyn` we're unsizing into.
+                        ecx.add_goal(
+                            goal.with(tcx, ty::Binder::dummy(ty::OutlivesPredicate(a_ty, region))),
+                        );
+                        ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
                     }
-
-                    let tail_field = a_def
-                        .non_enum_variant()
-                        .fields
-                        .raw
-                        .last()
-                        .expect("expected unsized ADT to have a tail field");
-                    let tail_field_ty = tcx.type_of(tail_field.did);
-
-                    let a_tail_ty = tail_field_ty.subst(tcx, a_substs);
-                    let b_tail_ty = tail_field_ty.subst(tcx, b_substs);
-
-                    // Substitute just the unsizing params from B into A. The type after
-                    // this substitution must be equal to B. This is so we don't unsize
-                    // unrelated type parameters.
-                    let new_a_substs =
-                        tcx.mk_substs_from_iter(a_substs.iter().enumerate().map(|(i, a)| {
-                            if unsizing_params.contains(i as u32) { b_substs[i] } else { a }
-                        }));
-                    let unsized_a_ty = tcx.mk_adt(a_def, new_a_substs);
-
-                    // Finally, we require that `TailA: Unsize<TailB>` for the tail field
-                    // types.
-                    ecx.eq(goal.param_env, unsized_a_ty, b_ty)?;
-                    ecx.add_goal(goal.with(
-                        tcx,
-                        ty::TraitRef::new(tcx, goal.predicate.def_id(), [a_tail_ty, b_tail_ty]),
-                    ));
-                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-                }
-                // Tuple unsizing `(.., T)` -> `(.., U)` where `T: Unsize<U>`
-                (&ty::Tuple(a_tys), &ty::Tuple(b_tys))
-                    if a_tys.len() == b_tys.len() && !a_tys.is_empty() =>
-                {
-                    let (a_last_ty, a_rest_tys) = a_tys.split_last().unwrap();
-                    let b_last_ty = b_tys.last().unwrap();
-
-                    // Substitute just the tail field of B., and require that they're equal.
-                    let unsized_a_ty =
-                        tcx.mk_tup_from_iter(a_rest_tys.iter().chain([b_last_ty]).copied());
-                    ecx.eq(goal.param_env, unsized_a_ty, b_ty)?;
-
-                    // Similar to ADTs, require that the rest of the fields are equal.
-                    ecx.add_goal(goal.with(
-                        tcx,
-                        ty::TraitRef::new(tcx, goal.predicate.def_id(), [*a_last_ty, *b_last_ty]),
-                    ));
-                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                    // `[T; n]` -> `[T]` unsizing
+                    (&ty::Array(a_elem_ty, ..), &ty::Slice(b_elem_ty)) => {
+                        // We just require that the element type stays the same
+                        ecx.eq(goal.param_env, a_elem_ty, b_elem_ty)?;
+                        ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                    }
+                    // Struct unsizing `Struct<T>` -> `Struct<U>` where `T: Unsize<U>`
+                    (&ty::Adt(a_def, a_substs), &ty::Adt(b_def, b_substs))
+                        if a_def.is_struct() && a_def.did() == b_def.did() =>
+                    {
+                        let unsizing_params = tcx.unsizing_params_for_adt(a_def.did());
+                        // We must be unsizing some type parameters. This also implies
+                        // that the struct has a tail field.
+                        if unsizing_params.is_empty() {
+                            return Err(NoSolution);
+                        }
+
+                        let tail_field = a_def
+                            .non_enum_variant()
+                            .fields
+                            .raw
+                            .last()
+                            .expect("expected unsized ADT to have a tail field");
+                        let tail_field_ty = tcx.type_of(tail_field.did);
+
+                        let a_tail_ty = tail_field_ty.subst(tcx, a_substs);
+                        let b_tail_ty = tail_field_ty.subst(tcx, b_substs);
+
+                        // Substitute just the unsizing params from B into A. The type after
+                        // this substitution must be equal to B. This is so we don't unsize
+                        // unrelated type parameters.
+                        let new_a_substs =
+                            tcx.mk_substs_from_iter(a_substs.iter().enumerate().map(|(i, a)| {
+                                if unsizing_params.contains(i as u32) { b_substs[i] } else { a }
+                            }));
+                        let unsized_a_ty = tcx.mk_adt(a_def, new_a_substs);
+
+                        // Finally, we require that `TailA: Unsize<TailB>` for the tail field
+                        // types.
+                        ecx.eq(goal.param_env, unsized_a_ty, b_ty)?;
+                        ecx.add_goal(goal.with(
+                            tcx,
+                            ty::TraitRef::new(tcx, goal.predicate.def_id(), [a_tail_ty, b_tail_ty]),
+                        ));
+                        ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                    }
+                    // Tuple unsizing `(.., T)` -> `(.., U)` where `T: Unsize<U>`
+                    (&ty::Tuple(a_tys), &ty::Tuple(b_tys))
+                        if a_tys.len() == b_tys.len() && !a_tys.is_empty() =>
+                    {
+                        let (a_last_ty, a_rest_tys) = a_tys.split_last().unwrap();
+                        let b_last_ty = b_tys.last().unwrap();
+
+                        // Substitute just the tail field of B., and require that they're equal.
+                        let unsized_a_ty =
+                            tcx.mk_tup_from_iter(a_rest_tys.iter().chain([b_last_ty]).copied());
+                        ecx.eq(goal.param_env, unsized_a_ty, b_ty)?;
+
+                        // Similar to ADTs, require that the rest of the fields are equal.
+                        ecx.add_goal(goal.with(
+                            tcx,
+                            ty::TraitRef::new(
+                                tcx,
+                                goal.predicate.def_id(),
+                                [*a_last_ty, *b_last_ty],
+                            ),
+                        ));
+                        ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                    }
+                    _ => Err(NoSolution),
                 }
-                _ => Err(NoSolution),
-            }
-        })
+            },
+            || "builtin unsize".into(),
+        )
     }
 
     fn consider_builtin_dyn_upcast_candidates(
@@ -475,34 +488,39 @@ impl<'tcx> assembly::GoalKind<'tcx> for TraitPredicate<'tcx> {
             return vec![];
         }
 
-        let mut unsize_dyn_to_principal = |principal: Option<ty::PolyExistentialTraitRef<'tcx>>| {
-            ecx.probe(|ecx| -> Result<_, NoSolution> {
-                // Require that all of the trait predicates from A match B, except for
-                // the auto traits. We do this by constructing a new A type with B's
-                // auto traits, and equating these types.
-                let new_a_data = principal
-                    .into_iter()
-                    .map(|trait_ref| trait_ref.map_bound(ty::ExistentialPredicate::Trait))
-                    .chain(a_data.iter().filter(|a| {
-                        matches!(a.skip_binder(), ty::ExistentialPredicate::Projection(_))
-                    }))
-                    .chain(
-                        b_data
-                            .auto_traits()
-                            .map(ty::ExistentialPredicate::AutoTrait)
-                            .map(ty::Binder::dummy),
-                    );
-                let new_a_data = tcx.mk_poly_existential_predicates_from_iter(new_a_data);
-                let new_a_ty = tcx.mk_dynamic(new_a_data, b_region, ty::Dyn);
-
-                // We also require that A's lifetime outlives B's lifetime.
-                ecx.eq(goal.param_env, new_a_ty, b_ty)?;
-                ecx.add_goal(
-                    goal.with(tcx, ty::Binder::dummy(ty::OutlivesPredicate(a_region, b_region))),
-                );
-                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-            })
-        };
+        let mut unsize_dyn_to_principal =
+            |principal: Option<ty::PolyExistentialTraitRef<'tcx>>| {
+                ecx.probe_candidate(
+                    |ecx| -> Result<_, NoSolution> {
+                        // Require that all of the trait predicates from A match B, except for
+                        // the auto traits. We do this by constructing a new A type with B's
+                        // auto traits, and equating these types.
+                        let new_a_data = principal
+                            .into_iter()
+                            .map(|trait_ref| trait_ref.map_bound(ty::ExistentialPredicate::Trait))
+                            .chain(a_data.iter().filter(|a| {
+                                matches!(a.skip_binder(), ty::ExistentialPredicate::Projection(_))
+                            }))
+                            .chain(
+                                b_data
+                                    .auto_traits()
+                                    .map(ty::ExistentialPredicate::AutoTrait)
+                                    .map(ty::Binder::dummy),
+                            );
+                        let new_a_data = tcx.mk_poly_existential_predicates_from_iter(new_a_data);
+                        let new_a_ty = tcx.mk_dynamic(new_a_data, b_region, ty::Dyn);
+
+                        // We also require that A's lifetime outlives B's lifetime.
+                        ecx.eq(goal.param_env, new_a_ty, b_ty)?;
+                        ecx.add_goal(goal.with(
+                            tcx,
+                            ty::Binder::dummy(ty::OutlivesPredicate(a_region, b_region)),
+                        ));
+                        ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                    },
+                    || "upcast dyn to principle".into(),
+                )
+            };
 
         let mut responses = vec![];
         // If the principal def ids match (or are both none), then we're not doing
@@ -698,20 +716,23 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         goal: Goal<'tcx, TraitPredicate<'tcx>>,
         constituent_tys: impl Fn(&EvalCtxt<'_, 'tcx>, Ty<'tcx>) -> Result<Vec<Ty<'tcx>>, NoSolution>,
     ) -> QueryResult<'tcx> {
-        self.probe(|ecx| {
-            ecx.add_goals(
-                constituent_tys(ecx, goal.predicate.self_ty())?
-                    .into_iter()
-                    .map(|ty| {
-                        goal.with(
-                            ecx.tcx(),
-                            ty::Binder::dummy(goal.predicate.with_self_ty(ecx.tcx(), ty)),
-                        )
-                    })
-                    .collect::<Vec<_>>(),
-            );
-            ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-        })
+        self.probe_candidate(
+            |ecx| {
+                ecx.add_goals(
+                    constituent_tys(ecx, goal.predicate.self_ty())?
+                        .into_iter()
+                        .map(|ty| {
+                            goal.with(
+                                ecx.tcx(),
+                                ty::Binder::dummy(goal.predicate.with_self_ty(ecx.tcx(), ty)),
+                            )
+                        })
+                        .collect::<Vec<_>>(),
+                );
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            },
+            || "constituent tys".into(),
+        )
     }
 
     #[instrument(level = "debug", skip(self))]