blob: c010add0fc50fb27a2e9e5f0218dac221cb56a8f [file] [log] [blame]
//! An infrastructure to mechanically analyse proof trees.
//!
//! It is unavoidable that this representation is somewhat
//! lossy as it should hide quite a few semantically relevant things,
//! e.g. canonicalization and the order of nested goals.
//!
//! @lcnr: However, a lot of the weirdness here is not strictly necessary
//! and could be improved in the future. This is mostly good enough for
//! coherence right now and was annoying to implement, so I am leaving it
//! as is until we start using it for something else.
use std::assert_matches::assert_matches;
use rustc_infer::infer::InferCtxt;
use rustc_infer::traits::Obligation;
use rustc_macros::extension;
use rustc_middle::traits::ObligationCause;
use rustc_middle::traits::solve::{Certainty, Goal, GoalSource, NoSolution, QueryResult};
use rustc_middle::ty::{TyCtxt, VisitorResult, try_visit};
use rustc_middle::{bug, ty};
use rustc_next_trait_solver::canonical::instantiate_canonical_state;
use rustc_next_trait_solver::resolve::eager_resolve_vars;
use rustc_next_trait_solver::solve::{MaybeCause, SolverDelegateEvalExt as _, inspect};
use rustc_span::Span;
use tracing::instrument;
use crate::solve::delegate::SolverDelegate;
use crate::traits::ObligationCtxt;
pub struct InspectConfig {
pub max_depth: usize,
}
pub struct InspectGoal<'a, 'tcx> {
infcx: &'a SolverDelegate<'tcx>,
depth: usize,
orig_values: Vec<ty::GenericArg<'tcx>>,
goal: Goal<'tcx, ty::Predicate<'tcx>>,
result: Result<Certainty, NoSolution>,
final_revision: &'tcx inspect::Probe<TyCtxt<'tcx>>,
normalizes_to_term_hack: Option<NormalizesToTermHack<'tcx>>,
source: GoalSource,
}
/// The expected term of a `NormalizesTo` goal gets replaced
/// with an unconstrained inference variable when computing
/// `NormalizesTo` goals and we return the nested goals to the
/// caller, who also equates the actual term with the expected.
///
/// This is an implementation detail of the trait solver and
/// not something we want to leak to users. We therefore
/// treat `NormalizesTo` goals as if they apply the expected
/// type at the end of each candidate.
#[derive(Copy, Clone)]
struct NormalizesToTermHack<'tcx> {
term: ty::Term<'tcx>,
unconstrained_term: ty::Term<'tcx>,
}
impl<'tcx> NormalizesToTermHack<'tcx> {
/// Relate the `term` with the new `unconstrained_term` created
/// when computing the proof tree for this `NormalizesTo` goals.
/// This handles nested obligations.
fn constrain_and(
&self,
infcx: &InferCtxt<'tcx>,
span: Span,
param_env: ty::ParamEnv<'tcx>,
f: impl FnOnce(&ObligationCtxt<'_, 'tcx>),
) -> Result<Certainty, NoSolution> {
let ocx = ObligationCtxt::new(infcx);
ocx.eq(
&ObligationCause::dummy_with_span(span),
param_env,
self.term,
self.unconstrained_term,
)?;
f(&ocx);
let errors = ocx.select_all_or_error();
if errors.is_empty() {
Ok(Certainty::Yes)
} else if errors.iter().all(|e| !e.is_true_error()) {
Ok(Certainty::AMBIGUOUS)
} else {
Err(NoSolution)
}
}
}
pub struct InspectCandidate<'a, 'tcx> {
goal: &'a InspectGoal<'a, 'tcx>,
kind: inspect::ProbeKind<TyCtxt<'tcx>>,
steps: Vec<&'a inspect::ProbeStep<TyCtxt<'tcx>>>,
final_state: inspect::CanonicalState<TyCtxt<'tcx>, ()>,
result: QueryResult<'tcx>,
shallow_certainty: Certainty,
}
impl<'a, 'tcx> InspectCandidate<'a, 'tcx> {
pub fn kind(&self) -> inspect::ProbeKind<TyCtxt<'tcx>> {
self.kind
}
pub fn result(&self) -> Result<Certainty, NoSolution> {
self.result.map(|c| c.value.certainty)
}
pub fn goal(&self) -> &'a InspectGoal<'a, 'tcx> {
self.goal
}
/// Certainty passed into `evaluate_added_goals_and_make_canonical_response`.
///
/// If this certainty is `Yes`, then we must be confident that the candidate
/// must hold iff it's nested goals hold. This is not true if the certainty is
/// `Maybe(..)`, which suggests we forced ambiguity instead.
///
/// This is *not* the certainty of the candidate's full nested evaluation, which
/// can be accessed with [`Self::result`] instead.
pub fn shallow_certainty(&self) -> Certainty {
self.shallow_certainty
}
/// Visit all nested goals of this candidate without rolling
/// back their inference constraints. This function modifies
/// the state of the `infcx`.
pub fn visit_nested_no_probe<V: ProofTreeVisitor<'tcx>>(&self, visitor: &mut V) -> V::Result {
for goal in self.instantiate_nested_goals(visitor.span()) {
try_visit!(goal.visit_with(visitor));
}
V::Result::output()
}
/// Instantiate the nested goals for the candidate without rolling back their
/// inference constraints. This function modifies the state of the `infcx`.
///
/// See [`Self::instantiate_impl_args`] if you need the impl args too.
#[instrument(
level = "debug",
skip_all,
fields(goal = ?self.goal.goal, steps = ?self.steps)
)]
pub fn instantiate_nested_goals(&self, span: Span) -> Vec<InspectGoal<'a, 'tcx>> {
let infcx = self.goal.infcx;
let param_env = self.goal.goal.param_env;
let mut orig_values = self.goal.orig_values.to_vec();
let mut instantiated_goals = vec![];
for step in &self.steps {
match **step {
inspect::ProbeStep::AddGoal(source, goal) => instantiated_goals.push((
source,
instantiate_canonical_state(infcx, span, param_env, &mut orig_values, goal),
)),
inspect::ProbeStep::RecordImplArgs { .. } => {}
inspect::ProbeStep::MakeCanonicalResponse { .. }
| inspect::ProbeStep::NestedProbe(_) => unreachable!(),
}
}
let () =
instantiate_canonical_state(infcx, span, param_env, &mut orig_values, self.final_state);
if let Some(term_hack) = &self.goal.normalizes_to_term_hack {
// FIXME: We ignore the expected term of `NormalizesTo` goals
// when computing the result of its candidates. This is
// scuffed.
let _ = term_hack.constrain_and(infcx, span, param_env, |_| {});
}
instantiated_goals
.into_iter()
.map(|(source, goal)| self.instantiate_proof_tree_for_nested_goal(source, goal, span))
.collect()
}
/// Instantiate the args of an impl if this candidate came from a
/// `CandidateSource::Impl`. This function modifies the state of the
/// `infcx`.
#[instrument(
level = "debug",
skip_all,
fields(goal = ?self.goal.goal, steps = ?self.steps)
)]
pub fn instantiate_impl_args(&self, span: Span) -> ty::GenericArgsRef<'tcx> {
let infcx = self.goal.infcx;
let param_env = self.goal.goal.param_env;
let mut orig_values = self.goal.orig_values.to_vec();
for step in &self.steps {
match **step {
inspect::ProbeStep::RecordImplArgs { impl_args } => {
let impl_args = instantiate_canonical_state(
infcx,
span,
param_env,
&mut orig_values,
impl_args,
);
let () = instantiate_canonical_state(
infcx,
span,
param_env,
&mut orig_values,
self.final_state,
);
// No reason we couldn't support this, but we don't need to for select.
assert!(
self.goal.normalizes_to_term_hack.is_none(),
"cannot use `instantiate_impl_args` with a `NormalizesTo` goal"
);
return eager_resolve_vars(infcx, impl_args);
}
inspect::ProbeStep::AddGoal(..) => {}
inspect::ProbeStep::MakeCanonicalResponse { .. }
| inspect::ProbeStep::NestedProbe(_) => unreachable!(),
}
}
bug!("expected impl args probe step for `instantiate_impl_args`");
}
pub fn instantiate_proof_tree_for_nested_goal(
&self,
source: GoalSource,
goal: Goal<'tcx, ty::Predicate<'tcx>>,
span: Span,
) -> InspectGoal<'a, 'tcx> {
let infcx = self.goal.infcx;
match goal.predicate.kind().no_bound_vars() {
Some(ty::PredicateKind::NormalizesTo(ty::NormalizesTo { alias, term })) => {
let unconstrained_term = infcx.next_term_var_of_kind(term, span);
let goal =
goal.with(infcx.tcx, ty::NormalizesTo { alias, term: unconstrained_term });
// We have to use a `probe` here as evaluating a `NormalizesTo` can constrain the
// expected term. This means that candidates which only fail due to nested goals
// and which normalize to a different term then the final result could ICE: when
// building their proof tree, the expected term was unconstrained, but when
// instantiating the candidate it is already constrained to the result of another
// candidate.
let normalizes_to_term_hack = NormalizesToTermHack { term, unconstrained_term };
let (proof_tree, nested_goals_result) = infcx.probe(|_| {
// Here, if we have any nested goals, then we make sure to apply them
// considering the constrained RHS, and pass the resulting certainty to
// `InspectGoal::new` so that the goal has the right result (and maintains
// the impression that we don't do this normalizes-to infer hack at all).
let (nested, proof_tree) = infcx.evaluate_root_goal_for_proof_tree(goal, span);
let nested_goals_result = nested.and_then(|nested| {
normalizes_to_term_hack.constrain_and(
infcx,
span,
proof_tree.uncanonicalized_goal.param_env,
|ocx| {
ocx.register_obligations(nested.0.into_iter().map(|(_, goal)| {
Obligation::new(
infcx.tcx,
ObligationCause::dummy_with_span(span),
goal.param_env,
goal.predicate,
)
}));
},
)
});
(proof_tree, nested_goals_result)
});
InspectGoal::new(
infcx,
self.goal.depth + 1,
proof_tree,
Some((normalizes_to_term_hack, nested_goals_result)),
source,
)
}
_ => {
// We're using a probe here as evaluating a goal could constrain
// inference variables by choosing one candidate. If we then recurse
// into another candidate who ends up with different inference
// constraints, we get an ICE if we already applied the constraints
// from the chosen candidate.
let proof_tree =
infcx.probe(|_| infcx.evaluate_root_goal_for_proof_tree(goal, span).1);
InspectGoal::new(infcx, self.goal.depth + 1, proof_tree, None, source)
}
}
}
/// Visit all nested goals of this candidate, rolling back
/// all inference constraints.
pub fn visit_nested_in_probe<V: ProofTreeVisitor<'tcx>>(&self, visitor: &mut V) -> V::Result {
self.goal.infcx.probe(|_| self.visit_nested_no_probe(visitor))
}
}
impl<'a, 'tcx> InspectGoal<'a, 'tcx> {
pub fn infcx(&self) -> &'a InferCtxt<'tcx> {
self.infcx
}
pub fn goal(&self) -> Goal<'tcx, ty::Predicate<'tcx>> {
self.goal
}
pub fn result(&self) -> Result<Certainty, NoSolution> {
self.result
}
pub fn source(&self) -> GoalSource {
self.source
}
pub fn depth(&self) -> usize {
self.depth
}
fn candidates_recur(
&'a self,
candidates: &mut Vec<InspectCandidate<'a, 'tcx>>,
steps: &mut Vec<&'a inspect::ProbeStep<TyCtxt<'tcx>>>,
probe: &'a inspect::Probe<TyCtxt<'tcx>>,
) {
let mut shallow_certainty = None;
for step in &probe.steps {
match *step {
inspect::ProbeStep::AddGoal(..) | inspect::ProbeStep::RecordImplArgs { .. } => {
steps.push(step)
}
inspect::ProbeStep::MakeCanonicalResponse { shallow_certainty: c } => {
assert_matches!(
shallow_certainty.replace(c),
None | Some(Certainty::Maybe { cause: MaybeCause::Ambiguity, .. })
);
}
inspect::ProbeStep::NestedProbe(ref probe) => {
match probe.kind {
// These never assemble candidates for the goal we're trying to solve.
inspect::ProbeKind::ProjectionCompatibility
| inspect::ProbeKind::ShadowedEnvProbing => continue,
inspect::ProbeKind::NormalizedSelfTyAssembly
| inspect::ProbeKind::UnsizeAssembly
| inspect::ProbeKind::Root { .. }
| inspect::ProbeKind::TraitCandidate { .. }
| inspect::ProbeKind::OpaqueTypeStorageLookup { .. }
| inspect::ProbeKind::RigidAlias { .. } => {
// Nested probes have to prove goals added in their parent
// but do not leak them, so we truncate the added goals
// afterwards.
let num_steps = steps.len();
self.candidates_recur(candidates, steps, probe);
steps.truncate(num_steps);
}
}
}
}
}
match probe.kind {
inspect::ProbeKind::ProjectionCompatibility
| inspect::ProbeKind::ShadowedEnvProbing => {
bug!()
}
inspect::ProbeKind::NormalizedSelfTyAssembly | inspect::ProbeKind::UnsizeAssembly => {}
// We add a candidate even for the root evaluation if there
// is only one way to prove a given goal, e.g. for `WellFormed`.
inspect::ProbeKind::Root { result }
| inspect::ProbeKind::TraitCandidate { source: _, result }
| inspect::ProbeKind::OpaqueTypeStorageLookup { result }
| inspect::ProbeKind::RigidAlias { result } => {
// We only add a candidate if `shallow_certainty` was set, which means
// that we ended up calling `evaluate_added_goals_and_make_canonical_response`.
if let Some(shallow_certainty) = shallow_certainty {
candidates.push(InspectCandidate {
goal: self,
kind: probe.kind,
steps: steps.clone(),
final_state: probe.final_state,
shallow_certainty,
result,
});
}
}
}
}
pub fn candidates(&'a self) -> Vec<InspectCandidate<'a, 'tcx>> {
let mut candidates = vec![];
let mut nested_goals = vec![];
self.candidates_recur(&mut candidates, &mut nested_goals, &self.final_revision);
candidates
}
/// Returns the single candidate applicable for the current goal, if it exists.
///
/// Returns `None` if there are either no or multiple applicable candidates.
pub fn unique_applicable_candidate(&'a self) -> Option<InspectCandidate<'a, 'tcx>> {
// FIXME(-Znext-solver): This does not handle impl candidates
// hidden by env candidates.
let mut candidates = self.candidates();
candidates.retain(|c| c.result().is_ok());
candidates.pop().filter(|_| candidates.is_empty())
}
fn new(
infcx: &'a InferCtxt<'tcx>,
depth: usize,
root: inspect::GoalEvaluation<TyCtxt<'tcx>>,
term_hack_and_nested_certainty: Option<(
NormalizesToTermHack<'tcx>,
Result<Certainty, NoSolution>,
)>,
source: GoalSource,
) -> Self {
let infcx = <&SolverDelegate<'tcx>>::from(infcx);
let inspect::GoalEvaluation { uncanonicalized_goal, orig_values, final_revision, result } =
root;
// If there's a normalizes-to goal, AND the evaluation result with the result of
// constraining the normalizes-to RHS and computing the nested goals.
let result = result.and_then(|ok| {
let nested_goals_certainty =
term_hack_and_nested_certainty.map_or(Ok(Certainty::Yes), |(_, c)| c)?;
Ok(ok.value.certainty.and(nested_goals_certainty))
});
InspectGoal {
infcx,
depth,
orig_values,
goal: eager_resolve_vars(infcx, uncanonicalized_goal),
result,
final_revision,
normalizes_to_term_hack: term_hack_and_nested_certainty.map(|(n, _)| n),
source,
}
}
pub(crate) fn visit_with<V: ProofTreeVisitor<'tcx>>(&self, visitor: &mut V) -> V::Result {
if self.depth < visitor.config().max_depth {
try_visit!(visitor.visit_goal(self));
}
V::Result::output()
}
}
/// The public API to interact with proof trees.
pub trait ProofTreeVisitor<'tcx> {
type Result: VisitorResult = ();
fn span(&self) -> Span;
fn config(&self) -> InspectConfig {
InspectConfig { max_depth: 10 }
}
fn visit_goal(&mut self, goal: &InspectGoal<'_, 'tcx>) -> Self::Result;
}
#[extension(pub trait ProofTreeInferCtxtExt<'tcx>)]
impl<'tcx> InferCtxt<'tcx> {
fn visit_proof_tree<V: ProofTreeVisitor<'tcx>>(
&self,
goal: Goal<'tcx, ty::Predicate<'tcx>>,
visitor: &mut V,
) -> V::Result {
self.visit_proof_tree_at_depth(goal, 0, visitor)
}
fn visit_proof_tree_at_depth<V: ProofTreeVisitor<'tcx>>(
&self,
goal: Goal<'tcx, ty::Predicate<'tcx>>,
depth: usize,
visitor: &mut V,
) -> V::Result {
let (_, proof_tree) = <&SolverDelegate<'tcx>>::from(self)
.evaluate_root_goal_for_proof_tree(goal, visitor.span());
visitor.visit_goal(&InspectGoal::new(self, depth, proof_tree, None, GoalSource::Misc))
}
}