From a213bb36c953d2c342d8ae9303704e57bc8aab33 Mon Sep 17 00:00:00 2001 From: lcnr Date: Sun, 4 Dec 2022 03:19:10 +0000 Subject: [PATCH] implement the skeleton of the updated trait solver --- .../src/infer/canonical/query_response.rs | 6 +- compiler/rustc_middle/src/infer/canonical.rs | 10 + compiler/rustc_middle/src/traits/query.rs | 2 +- .../src/traits/specialization_graph.rs | 1 + compiler/rustc_middle/src/ty/mod.rs | 41 ++- compiler/rustc_middle/src/ty/sty.rs | 12 +- compiler/rustc_middle/src/ty/subst.rs | 4 + compiler/rustc_trait_selection/src/lib.rs | 2 + .../rustc_trait_selection/src/solve/cache.rs | 257 ++++++++++++++ .../src/solve/fulfill.rs | 92 +++++ .../src/solve/infcx_ext.rs | 55 +++ .../rustc_trait_selection/src/solve/mod.rs | 308 +++++++++++++++++ .../src/solve/overflow.rs | 80 +++++ .../src/solve/project_goals.rs | 324 ++++++++++++++++++ .../src/solve/trait_goals.rs | 282 +++++++++++++++ .../src/traits/project.rs | 59 +--- .../src/traits/select/mod.rs | 14 +- .../traits/specialize/specialization_graph.rs | 49 +++ .../inductive-canonical-cycle.rs | 28 ++ .../inductive-canonical-cycle.stderr | 26 ++ triagebot.toml | 4 + 21 files changed, 1569 insertions(+), 87 deletions(-) create mode 100644 compiler/rustc_trait_selection/src/solve/cache.rs create mode 100644 compiler/rustc_trait_selection/src/solve/fulfill.rs create mode 100644 compiler/rustc_trait_selection/src/solve/infcx_ext.rs create mode 100644 compiler/rustc_trait_selection/src/solve/mod.rs create mode 100644 compiler/rustc_trait_selection/src/solve/overflow.rs create mode 100644 compiler/rustc_trait_selection/src/solve/project_goals.rs create mode 100644 compiler/rustc_trait_selection/src/solve/trait_goals.rs create mode 100644 src/test/ui/traits/solver-cycles/inductive-canonical-cycle.rs create mode 100644 src/test/ui/traits/solver-cycles/inductive-canonical-cycle.stderr diff --git a/compiler/rustc_infer/src/infer/canonical/query_response.rs b/compiler/rustc_infer/src/infer/canonical/query_response.rs index 996b1c40e3f..a722613e331 100644 --- a/compiler/rustc_infer/src/infer/canonical/query_response.rs +++ b/compiler/rustc_infer/src/infer/canonical/query_response.rs @@ -151,7 +151,11 @@ fn make_query_response( }) } - fn take_opaque_types_for_query_response(&self) -> Vec<(Ty<'tcx>, Ty<'tcx>)> { + /// FIXME: This method should only be used for canonical queries and therefore be private. + /// + /// As the new solver does canonicalization slightly differently, this is also used there + /// for now. This should hopefully change fairly soon. + pub fn take_opaque_types_for_query_response(&self) -> Vec<(Ty<'tcx>, Ty<'tcx>)> { self.inner .borrow_mut() .opaque_type_storage diff --git a/compiler/rustc_middle/src/infer/canonical.rs b/compiler/rustc_middle/src/infer/canonical.rs index 0331d764b38..0b32f67a81e 100644 --- a/compiler/rustc_middle/src/infer/canonical.rs +++ b/compiler/rustc_middle/src/infer/canonical.rs @@ -300,6 +300,16 @@ pub fn unchecked_map(self, map_op: impl FnOnce(V) -> W) -> Canonical<'tcx, W> let Canonical { max_universe, variables, value } = self; Canonical { max_universe, variables, value: map_op(value) } } + + /// Allows you to map the `value` of a canonical while keeping the same set of + /// bound variables. + /// + /// **WARNING:** This function is very easy to mis-use, hence the name! See + /// the comment of [Canonical::unchecked_map] for more details. + pub fn unchecked_rebind(self, value: W) -> Canonical<'tcx, W> { + let Canonical { max_universe, variables, value: _ } = self; + Canonical { max_universe, variables, value } + } } pub type QueryOutlivesConstraint<'tcx> = ( diff --git a/compiler/rustc_middle/src/traits/query.rs b/compiler/rustc_middle/src/traits/query.rs index 7380c62a669..6a149be3137 100644 --- a/compiler/rustc_middle/src/traits/query.rs +++ b/compiler/rustc_middle/src/traits/query.rs @@ -96,7 +96,7 @@ pub fn new(value: T) -> Self { pub type CanonicalTypeOpNormalizeGoal<'tcx, T> = Canonical<'tcx, ty::ParamEnvAnd<'tcx, type_op::Normalize>>; -#[derive(Copy, Clone, Debug, HashStable)] +#[derive(Copy, Clone, Debug, HashStable, PartialEq, Eq)] pub struct NoSolution; pub type Fallible = Result; diff --git a/compiler/rustc_middle/src/traits/specialization_graph.rs b/compiler/rustc_middle/src/traits/specialization_graph.rs index cccedc9ec6e..a4dd22801e6 100644 --- a/compiler/rustc_middle/src/traits/specialization_graph.rs +++ b/compiler/rustc_middle/src/traits/specialization_graph.rs @@ -180,6 +180,7 @@ fn next(&mut self) -> Option { } /// Information about the most specialized definition of an associated item. +#[derive(Debug)] pub struct LeafDef { /// The associated item described by this `LeafDef`. pub item: ty::AssocItem, diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index a8e1253e670..f01d74539a1 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -535,6 +535,17 @@ pub fn without_const(mut self, tcx: TyCtxt<'tcx>) -> Self { self } + #[instrument(level = "debug", skip(tcx), ret)] + pub fn is_coinductive(self, tcx: TyCtxt<'tcx>) -> bool { + match self.kind().skip_binder() { + ty::PredicateKind::Clause(ty::Clause::Trait(data)) => { + tcx.trait_is_coinductive(data.def_id()) + } + ty::PredicateKind::WellFormed(_) => true, + _ => false, + } + } + /// Whether this projection can be soundly normalized. /// /// Wf predicates must not be normalized, as normalization @@ -1018,6 +1029,24 @@ pub struct ProjectionPredicate<'tcx> { pub term: Term<'tcx>, } +impl<'tcx> ProjectionPredicate<'tcx> { + pub fn self_ty(self) -> Ty<'tcx> { + self.projection_ty.self_ty() + } + + pub fn with_self_ty(self, tcx: TyCtxt<'tcx>, self_ty: Ty<'tcx>) -> ProjectionPredicate<'tcx> { + Self { projection_ty: self.projection_ty.with_self_ty(tcx, self_ty), ..self } + } + + pub fn trait_def_id(self, tcx: TyCtxt<'tcx>) -> DefId { + self.projection_ty.trait_def_id(tcx) + } + + pub fn def_id(self) -> DefId { + self.projection_ty.def_id + } +} + pub type PolyProjectionPredicate<'tcx> = Binder<'tcx, ProjectionPredicate<'tcx>>; impl<'tcx> PolyProjectionPredicate<'tcx> { @@ -1054,18 +1083,6 @@ pub fn projection_def_id(&self) -> DefId { } } -impl<'tcx> ProjectionPredicate<'tcx> { - pub fn with_self_ty(self, tcx: TyCtxt<'tcx>, self_ty: Ty<'tcx>) -> Self { - Self { - projection_ty: tcx.mk_alias_ty( - self.projection_ty.def_id, - [self_ty.into()].into_iter().chain(self.projection_ty.substs.iter().skip(1)), - ), - ..self - } - } -} - pub trait ToPolyTraitRef<'tcx> { fn to_poly_trait_ref(&self) -> PolyTraitRef<'tcx>; } diff --git a/compiler/rustc_middle/src/ty/sty.rs b/compiler/rustc_middle/src/ty/sty.rs index 66aeebab88b..890a4da6387 100644 --- a/compiler/rustc_middle/src/ty/sty.rs +++ b/compiler/rustc_middle/src/ty/sty.rs @@ -1169,7 +1169,7 @@ pub struct AliasTy<'tcx> { } impl<'tcx> AliasTy<'tcx> { - pub fn trait_def_id(&self, tcx: TyCtxt<'tcx>) -> DefId { + pub fn trait_def_id(self, tcx: TyCtxt<'tcx>) -> DefId { match tcx.def_kind(self.def_id) { DefKind::AssocTy | DefKind::AssocConst => tcx.parent(self.def_id), DefKind::ImplTraitPlaceholder => { @@ -1183,7 +1183,7 @@ pub fn trait_def_id(&self, tcx: TyCtxt<'tcx>) -> DefId { /// For example, if this is a projection of `::Item<'a>`, /// then this function would return a `T: Iterator` trait reference and `['a]` as the own substs pub fn trait_ref_and_own_substs( - &self, + self, tcx: TyCtxt<'tcx>, ) -> (ty::TraitRef<'tcx>, &'tcx [ty::GenericArg<'tcx>]) { debug_assert!(matches!(tcx.def_kind(self.def_id), DefKind::AssocTy | DefKind::AssocConst)); @@ -1202,14 +1202,18 @@ pub fn trait_ref_and_own_substs( /// WARNING: This will drop the substs for generic associated types /// consider calling [Self::trait_ref_and_own_substs] to get those /// as well. - pub fn trait_ref(&self, tcx: TyCtxt<'tcx>) -> ty::TraitRef<'tcx> { + pub fn trait_ref(self, tcx: TyCtxt<'tcx>) -> ty::TraitRef<'tcx> { let def_id = self.trait_def_id(tcx); tcx.mk_trait_ref(def_id, self.substs.truncate_to(tcx, tcx.generics_of(def_id))) } - pub fn self_ty(&self) -> Ty<'tcx> { + pub fn self_ty(self) -> Ty<'tcx> { self.substs.type_at(0) } + + pub fn with_self_ty(self, tcx: TyCtxt<'tcx>, self_ty: Ty<'tcx>) -> Self { + tcx.mk_alias_ty(self.def_id, [self_ty.into()].into_iter().chain(self.substs.iter().skip(1))) + } } #[derive(Copy, Clone, Debug, TypeFoldable, TypeVisitable, Lift)] diff --git a/compiler/rustc_middle/src/ty/subst.rs b/compiler/rustc_middle/src/ty/subst.rs index a04b15f8cf1..237b36701c2 100644 --- a/compiler/rustc_middle/src/ty/subst.rs +++ b/compiler/rustc_middle/src/ty/subst.rs @@ -573,6 +573,10 @@ pub fn try_map_bound(self, f: F) -> Result, E> pub fn rebind(&self, value: U) -> EarlyBinder { EarlyBinder(value) } + + pub fn skip_binder(self) -> T { + self.0 + } } impl EarlyBinder> { diff --git a/compiler/rustc_trait_selection/src/lib.rs b/compiler/rustc_trait_selection/src/lib.rs index 975ff31a607..a30d1df4ede 100644 --- a/compiler/rustc_trait_selection/src/lib.rs +++ b/compiler/rustc_trait_selection/src/lib.rs @@ -19,6 +19,7 @@ #![feature(let_chains)] #![feature(if_let_guard)] #![feature(never_type)] +#![feature(result_option_inspect)] #![feature(type_alias_impl_trait)] #![recursion_limit = "512"] // For rustdoc @@ -37,4 +38,5 @@ pub mod autoderef; pub mod errors; pub mod infer; +pub mod solve; pub mod traits; diff --git a/compiler/rustc_trait_selection/src/solve/cache.rs b/compiler/rustc_trait_selection/src/solve/cache.rs new file mode 100644 index 00000000000..993b7989066 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/cache.rs @@ -0,0 +1,257 @@ +//! This module both handles the global cache which stores "finished" goals, +//! and the provisional cache which contains partially computed goals. +//! +//! The provisional cache is necessary when dealing with coinductive cycles. +//! +//! For more information about the provisional cache and coinduction in general, +//! check out the relevant section of the rustc-dev-guide. +//! +//! FIXME(@lcnr): Write that section, feel free to ping me if you need help here +//! before then or if I still haven't done that before January 2023. +use super::overflow::OverflowData; +use super::CanonicalGoal; +use super::{EvalCtxt, QueryResult}; + +use rustc_data_structures::fx::FxHashMap; +use rustc_middle::ty::TyCtxt; +use std::{cmp::Ordering, collections::hash_map::Entry}; + +#[derive(Debug, Clone)] +struct ProvisionalEntry<'tcx> { + // In case we have a coinductive cycle, this is the + // the currently least restrictive result of this goal. + response: QueryResult<'tcx>, + // The lowest element on the stack on which this result + // relies on. Starts out as just being the depth at which + // we've proven this obligation, but gets lowered to the + // depth of another goal if we rely on it in a cycle. + depth: usize, +} + +struct StackElem<'tcx> { + goal: CanonicalGoal<'tcx>, + has_been_used: bool, +} + +/// The cache used for goals which are currently in progress or which depend +/// on in progress results. +/// +/// Once we're done with a goal we can store it in the global trait solver +/// cache of the `TyCtxt`. For goals which we're currently proving, or which +/// have only been proven via a coinductive cycle using a goal still on our stack +/// we have to use this separate data structure. +/// +/// The current data structure is not perfect, so there may still be room for +/// improvement here. We have the following requirements: +/// +/// ## Is there is a provisional entry for the given goal: +/// +/// ```ignore (for syntax highlighting) +/// self.entries.get(goal) +/// ``` +/// +/// ## Get all goals on the stack involved in a cycle: +/// +/// ```ignore (for syntax highlighting) +/// let entry = self.entries.get(goal).unwrap(); +/// let involved_goals = self.stack.iter().skip(entry.depth); +/// ``` +/// +/// ## Capping the depth of all entries +/// +/// Needed whenever we encounter a cycle. The current implementation always +/// iterates over all entries instead of only the ones with a larger depth. +/// Changing this may result in notable performance improvements. +/// +/// ```ignore (for syntax highlighting) +/// let cycle_depth = self.entries.get(goal).unwrap().depth; +/// for e in &mut self.entries { +/// e.depth = e.depth.min(cycle_depth); +/// } +/// ``` +/// +/// ## Checking whether we have to rerun the current goal +/// +/// A goal has to be rerun if its provisional result was used in a cycle +/// and that result is different from its final result. We update +/// [StackElem::has_been_used] for the deepest stack element involved in a cycle. +/// +/// ## Moving all finished goals into the global cache +/// +/// If `stack_elem.has_been_used` is true, iterate over all entries, moving the ones +/// with equal depth. If not, simply move this single entry. +pub(super) struct ProvisionalCache<'tcx> { + stack: Vec>, + entries: FxHashMap, ProvisionalEntry<'tcx>>, +} + +impl<'tcx> ProvisionalCache<'tcx> { + pub(super) fn empty() -> ProvisionalCache<'tcx> { + ProvisionalCache { stack: Vec::new(), entries: Default::default() } + } + + pub(super) fn current_depth(&self) -> usize { + self.stack.len() + } +} + +impl<'tcx> EvalCtxt<'tcx> { + /// Tries putting the new goal on the stack, returning an error if it is already cached. + /// + /// This correctly updates the provisional cache if there is a cycle. + pub(super) fn try_push_stack( + &mut self, + goal: CanonicalGoal<'tcx>, + ) -> Result<(), QueryResult<'tcx>> { + // FIXME: start by checking the global cache + + // Look at the provisional cache to check for cycles. + let cache = &mut self.provisional_cache; + match cache.entries.entry(goal) { + // No entry, simply push this goal on the stack after dealing with overflow. + Entry::Vacant(v) => { + if self.overflow_data.has_overflow(cache.stack.len()) { + return Err(self.deal_with_overflow()); + } + + v.insert(ProvisionalEntry { + response: fixme_response_yes_no_constraints(), + depth: cache.stack.len(), + }); + cache.stack.push(StackElem { goal, has_been_used: false }); + Ok(()) + } + // We have a nested goal which relies on a goal `root` deeper in the stack. + // + // We first store that we may have to rerun `evaluate_goal` for `root` in case the + // provisional response is not equal to the final response. We also update the depth + // of all goals which recursively depend on our current goal to depend on `root` + // instead. + // + // Finally we can return either the provisional response for that goal if we have a + // coinductive cycle or an ambiguous result if the cycle is inductive. + Entry::Occupied(entry) => { + // FIXME: `ProvisionalEntry` should be `Copy`. + let entry = entry.get().clone(); + cache.stack[entry.depth].has_been_used = true; + for provisional_entry in cache.entries.values_mut() { + provisional_entry.depth = provisional_entry.depth.min(entry.depth); + } + + // NOTE: The goals on the stack aren't the only goals involved in this cycle. + // We can also depend on goals which aren't part of the stack but coinductively + // depend on the stack themselves. We already checked whether all the goals + // between these goals and their root on the stack. This means that as long as + // each goal in a cycle is checked for coinductivity by itself simply checking + // the stack is enough. + if cache.stack[entry.depth..] + .iter() + .all(|g| g.goal.value.predicate.is_coinductive(self.tcx)) + { + Err(entry.response) + } else { + Err(fixme_response_maybe_no_constraints()) + } + } + } + } + + /// We cannot simply store the result of [EvalCtxt::compute_goal] as we have to deal with + /// coinductive cycles. + /// + /// When we encounter a coinductive cycle, we have to prove the final result of that cycle + /// while we are still computing that result. Because of this we continously recompute the + /// cycle until the result of the previous iteration is equal to the final result, at which + /// point we are done. + /// + /// This function returns `true` if we were able to finalize the goal and `false` if it has + /// updated the provisional cache and we have to recompute the current goal. + /// + /// FIXME: Refer to the rustc-dev-guide entry once it exists. + pub(super) fn try_finalize_goal( + &mut self, + actual_goal: CanonicalGoal<'tcx>, + response: QueryResult<'tcx>, + ) -> bool { + let cache = &mut self.provisional_cache; + let StackElem { goal, has_been_used } = cache.stack.pop().unwrap(); + assert_eq!(goal, actual_goal); + + let provisional_entry = cache.entries.get_mut(&goal).unwrap(); + // Check whether the current stack entry is the root of a cycle. + // + // If so, we either move all participants of that cycle to the global cache + // or, in case the provisional response used in the cycle is not equal to the + // final response, have to recompute the goal after updating the provisional + // response to the final response of this iteration. + if has_been_used { + if provisional_entry.response == response { + // We simply drop all entries according to an immutable condition, so + // query instability is not a concern here. + #[allow(rustc::potential_query_instability)] + cache.entries.retain(|goal, entry| match entry.depth.cmp(&cache.stack.len()) { + Ordering::Less => true, + Ordering::Equal => { + Self::try_move_finished_goal_to_global_cache( + self.tcx, + &mut self.overflow_data, + &cache.stack, + // FIXME: these should be `Copy` :( + goal.clone(), + entry.response.clone(), + ); + false + } + Ordering::Greater => bug!("entry with greater depth than the current leaf"), + }); + + true + } else { + provisional_entry.response = response; + cache.stack.push(StackElem { goal, has_been_used: false }); + false + } + } else { + Self::try_move_finished_goal_to_global_cache( + self.tcx, + &mut self.overflow_data, + &cache.stack, + goal, + response, + ); + cache.entries.remove(&goal); + true + } + } + + fn try_move_finished_goal_to_global_cache( + tcx: TyCtxt<'tcx>, + overflow_data: &mut OverflowData, + stack: &[StackElem<'tcx>], + goal: CanonicalGoal<'tcx>, + response: QueryResult<'tcx>, + ) { + // We move goals to the global cache if we either did not hit an overflow or if it's + // the root goal as that will now always hit the same overflow limit. + // + // NOTE: We cannot move any non-root goals to the global cache even if their final result + // isn't impacted by the overflow as that goal still has unstable query dependencies + // because it didn't go its full depth. + // + // FIXME(@lcnr): We could still cache subtrees which are not impacted by overflow though. + // Tracking that info correctly isn't trivial, so I haven't implemented it for now. + let should_cache_globally = !overflow_data.did_overflow() || stack.is_empty(); + if should_cache_globally { + // FIXME: move the provisional entry to the global cache. + let _ = (tcx, goal, response); + } + } +} + +fn fixme_response_yes_no_constraints<'tcx>() -> QueryResult<'tcx> { + unimplemented!() +} + +fn fixme_response_maybe_no_constraints<'tcx>() -> QueryResult<'tcx> { + unimplemented!() +} diff --git a/compiler/rustc_trait_selection/src/solve/fulfill.rs b/compiler/rustc_trait_selection/src/solve/fulfill.rs new file mode 100644 index 00000000000..80115d78d88 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/fulfill.rs @@ -0,0 +1,92 @@ +use std::mem; + +use rustc_data_structures::fx::FxHashMap; +use rustc_infer::{ + infer::InferCtxt, + traits::{query::NoSolution, FulfillmentError, PredicateObligation, TraitEngine}, +}; +use rustc_middle::ty; + +use super::{Certainty, EvalCtxt}; + +/// A trait engine using the new trait solver. +/// +/// This is mostly identical to how `evaluate_all` works inside of the +/// solver, except that the requirements are slightly different. +/// +/// Unlike `evaluate_all` it is possible to add new obligations later on +/// and we also have to track diagnostics information by using `Obligation` +/// instead of `Goal`. +/// +/// It is also likely that we want to use slightly different datastructures +/// here as this will have to deal with far more root goals than `evaluate_all`. +pub struct FulfillmentCtxt<'tcx> { + obligations: Vec>, +} + +impl<'tcx> FulfillmentCtxt<'tcx> { + pub fn new() -> FulfillmentCtxt<'tcx> { + FulfillmentCtxt { obligations: Vec::new() } + } +} + +impl<'tcx> TraitEngine<'tcx> for FulfillmentCtxt<'tcx> { + fn register_predicate_obligation( + &mut self, + _infcx: &InferCtxt<'tcx>, + obligation: PredicateObligation<'tcx>, + ) { + self.obligations.push(obligation); + } + + fn select_all_or_error(&mut self, infcx: &InferCtxt<'tcx>) -> Vec> { + let errors = self.select_where_possible(infcx); + if !errors.is_empty() { + return errors; + } + + if self.obligations.is_empty() { + Vec::new() + } else { + unimplemented!("ambiguous obligations") + } + } + + fn select_where_possible(&mut self, infcx: &InferCtxt<'tcx>) -> Vec> { + let errors = Vec::new(); + for i in 0.. { + if !infcx.tcx.recursion_limit().value_within_limit(i) { + unimplemented!("overflow") + } + + let mut has_changed = false; + for o in mem::take(&mut self.obligations) { + let mut cx = EvalCtxt::new(infcx.tcx); + let (changed, certainty) = match cx.evaluate_goal(infcx, o.clone().into()) { + Ok(result) => result, + Err(NoSolution) => unimplemented!("error"), + }; + + has_changed |= changed; + match certainty { + Certainty::Yes => {} + Certainty::Maybe(_) => self.obligations.push(o), + } + } + + if !has_changed { + break; + } + } + + errors + } + + fn pending_obligations(&self) -> Vec> { + self.obligations.clone() + } + + fn relationships(&mut self) -> &mut FxHashMap { + unimplemented!("Should be moved out of `TraitEngine`") + } +} diff --git a/compiler/rustc_trait_selection/src/solve/infcx_ext.rs b/compiler/rustc_trait_selection/src/solve/infcx_ext.rs new file mode 100644 index 00000000000..436f4eea662 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/infcx_ext.rs @@ -0,0 +1,55 @@ +use rustc_infer::infer::canonical::CanonicalVarValues; +use rustc_infer::infer::type_variable::{TypeVariableOrigin, TypeVariableOriginKind}; +use rustc_infer::infer::InferCtxt; +use rustc_infer::traits::query::NoSolution; +use rustc_middle::ty::Ty; +use rustc_span::DUMMY_SP; + +use crate::solve::ExternalConstraints; + +use super::{Certainty, QueryResult, Response}; + +/// Methods used inside of the canonical queries of the solver. +pub(super) trait InferCtxtExt<'tcx> { + fn next_ty_infer(&self) -> Ty<'tcx>; + + fn make_canonical_response( + &self, + var_values: CanonicalVarValues<'tcx>, + certainty: Certainty, + ) -> QueryResult<'tcx>; +} + +impl<'tcx> InferCtxtExt<'tcx> for InferCtxt<'tcx> { + fn next_ty_infer(&self) -> Ty<'tcx> { + self.next_ty_var(TypeVariableOrigin { + kind: TypeVariableOriginKind::MiscVariable, + span: DUMMY_SP, + }) + } + + fn make_canonical_response( + &self, + var_values: CanonicalVarValues<'tcx>, + certainty: Certainty, + ) -> QueryResult<'tcx> { + let external_constraints = take_external_constraints(self)?; + + Ok(self.canonicalize_response(Response { var_values, external_constraints, certainty })) + } +} + +#[instrument(level = "debug", skip(infcx), ret)] +fn take_external_constraints<'tcx>( + infcx: &InferCtxt<'tcx>, +) -> Result, NoSolution> { + let region_obligations = infcx.take_registered_region_obligations(); + let opaque_types = infcx.take_opaque_types_for_query_response(); + Ok(ExternalConstraints { + // FIXME: Now that's definitely wrong :) + // + // Should also do the leak check here I think + regions: drop(region_obligations), + opaque_types, + }) +} diff --git a/compiler/rustc_trait_selection/src/solve/mod.rs b/compiler/rustc_trait_selection/src/solve/mod.rs new file mode 100644 index 00000000000..5d6529f8542 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/mod.rs @@ -0,0 +1,308 @@ +//! The new trait solver, currently still WIP. +//! +//! As a user of the trait system, you can use `TyCtxt::evaluate_goal` to +//! interact with this solver. +//! +//! For a high-level overview of how this solver works, check out the relevant +//! section of the rustc-dev-guide. +//! +//! FIXME(@lcnr): Write that section. If you read this before then ask me +//! about it on zulip. + +// FIXME: Instead of using `infcx.canonicalize_query` we have to add a new routine which +// preserves universes and creates a unique var (in the highest universe) for each +// appearance of a region. + +// FIXME: `CanonicalVarValues` should be interned and `Copy`. + +// FIXME: uses of `infcx.at` need to enable deferred projection equality once that's implemented. + +use std::mem; + +use rustc_infer::infer::canonical::OriginalQueryValues; +use rustc_infer::infer::{InferCtxt, TyCtxtInferExt}; +use rustc_infer::traits::query::NoSolution; +use rustc_infer::traits::Obligation; +use rustc_middle::infer::canonical::{Canonical, CanonicalVarValues}; +use rustc_middle::ty::{self, Ty, TyCtxt}; +use rustc_middle::ty::{RegionOutlivesPredicate, ToPredicate, TypeOutlivesPredicate}; +use rustc_span::DUMMY_SP; + +use self::infcx_ext::InferCtxtExt; + +mod cache; +mod fulfill; +mod infcx_ext; +mod overflow; +mod project_goals; +mod trait_goals; + +pub use fulfill::FulfillmentCtxt; + +/// A goal is a statement, i.e. `predicate`, we want to prove +/// given some assumptions, i.e. `param_env`. +/// +/// 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)] +pub struct Goal<'tcx, P> { + param_env: ty::ParamEnv<'tcx>, + predicate: P, +} + +impl<'tcx, P> Goal<'tcx, P> { + pub fn new( + tcx: TyCtxt<'tcx>, + param_env: ty::ParamEnv<'tcx>, + predicate: impl ToPredicate<'tcx, P>, + ) -> Goal<'tcx, P> { + Goal { param_env, predicate: predicate.to_predicate(tcx) } + } + + /// Updates the goal to one with a different `predicate` but the same `param_env`. + fn with(self, tcx: TyCtxt<'tcx>, predicate: impl ToPredicate<'tcx, Q>) -> Goal<'tcx, Q> { + Goal { param_env: self.param_env, predicate: predicate.to_predicate(tcx) } + } +} + +impl<'tcx, P> From> for Goal<'tcx, P> { + fn from(obligation: Obligation<'tcx, P>) -> Goal<'tcx, P> { + Goal { param_env: obligation.param_env, predicate: obligation.predicate } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, TypeFoldable, TypeVisitable)] +pub struct Response<'tcx> { + pub var_values: CanonicalVarValues<'tcx>, + /// Additional constraints returned by this query. + pub external_constraints: ExternalConstraints<'tcx>, + pub certainty: Certainty, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TypeFoldable, TypeVisitable)] +pub enum Certainty { + Yes, + Maybe(MaybeCause), +} + +impl Certainty { + /// When proving multiple goals using **AND**, e.g. nested obligations for an impl, + /// use this function to unify the certainty of these goals + pub fn unify_and(self, other: Certainty) -> Certainty { + match (self, other) { + (Certainty::Yes, Certainty::Yes) => Certainty::Yes, + (Certainty::Yes, Certainty::Maybe(_)) => other, + (Certainty::Maybe(_), Certainty::Yes) => self, + (Certainty::Maybe(MaybeCause::Overflow), Certainty::Maybe(MaybeCause::Overflow)) => { + Certainty::Maybe(MaybeCause::Overflow) + } + // If at least one of the goals is ambiguous, hide the overflow as the ambiguous goal + // may still result in failure. + (Certainty::Maybe(MaybeCause::Ambiguity), Certainty::Maybe(_)) + | (Certainty::Maybe(_), Certainty::Maybe(MaybeCause::Ambiguity)) => { + Certainty::Maybe(MaybeCause::Ambiguity) + } + } + } +} + +/// Why we failed to evaluate a goal. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, 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, + /// or we hit a case where we just don't bother, e.g. `?x: Trait` goals. + Ambiguity, + /// We gave up due to an overflow, most often by hitting the recursion limit. + Overflow, +} + +/// Additional constraints returned on success. +#[derive(Debug, PartialEq, Eq, Clone, Hash, TypeFoldable, TypeVisitable)] +pub struct ExternalConstraints<'tcx> { + // FIXME: implement this. + regions: (), + opaque_types: Vec<(Ty<'tcx>, Ty<'tcx>)>, +} + +type CanonicalGoal<'tcx, T = ty::Predicate<'tcx>> = Canonical<'tcx, Goal<'tcx, T>>; +type CanonicalResponse<'tcx> = Canonical<'tcx, Response<'tcx>>; +/// The result of evaluating a canonical query. +/// +/// FIXME: We use a different type than the existing canonical queries. This is because +/// we need to add a `Certainty` for `overflow` and may want to restructure this code without +/// having to worry about changes to currently used code. Once we've made progress on this +/// solver, merge the two responses again. +pub type QueryResult<'tcx> = Result, NoSolution>; + +pub trait TyCtxtExt<'tcx> { + fn evaluate_goal(self, goal: CanonicalGoal<'tcx>) -> QueryResult<'tcx>; +} + +impl<'tcx> TyCtxtExt<'tcx> for TyCtxt<'tcx> { + fn evaluate_goal(self, goal: CanonicalGoal<'tcx>) -> QueryResult<'tcx> { + let mut cx = EvalCtxt::new(self); + cx.evaluate_canonical_goal(goal) + } +} + +struct EvalCtxt<'tcx> { + tcx: TyCtxt<'tcx>, + + provisional_cache: cache::ProvisionalCache<'tcx>, + overflow_data: overflow::OverflowData, +} + +impl<'tcx> EvalCtxt<'tcx> { + fn new(tcx: TyCtxt<'tcx>) -> EvalCtxt<'tcx> { + EvalCtxt { + tcx, + provisional_cache: cache::ProvisionalCache::empty(), + overflow_data: overflow::OverflowData::new(tcx), + } + } + + /// Recursively evaluates `goal`, returning whether any inference vars have + /// been constrained and the certainty of the result. + fn evaluate_goal( + &mut self, + infcx: &InferCtxt<'tcx>, + goal: Goal<'tcx, ty::Predicate<'tcx>>, + ) -> Result<(bool, Certainty), NoSolution> { + let mut orig_values = OriginalQueryValues::default(); + let canonical_goal = infcx.canonicalize_query(goal, &mut orig_values); + let canonical_response = self.evaluate_canonical_goal(canonical_goal)?; + Ok(( + true, // FIXME: check whether `var_values` are an identity substitution. + fixme_instantiate_canonical_query_response(infcx, &orig_values, canonical_response), + )) + } + + fn evaluate_canonical_goal(&mut self, goal: CanonicalGoal<'tcx>) -> QueryResult<'tcx> { + match self.try_push_stack(goal) { + Ok(()) => {} + // Our goal is already on the stack, eager return. + Err(response) => return response, + } + + // We may have to repeatedly recompute the goal in case of coinductive cycles, + // check out the `cache` module for more information. + // + // FIXME: Similar to `evaluate_all`, this has to check for overflow. + loop { + let result = self.compute_goal(goal); + + // FIXME: `Response` should be `Copy` + if self.try_finalize_goal(goal, result.clone()) { + return result; + } + } + } + + fn compute_goal(&mut self, canonical_goal: CanonicalGoal<'tcx>) -> QueryResult<'tcx> { + // WARNING: We're looking at a canonical value without instantiating it here. + // + // We have to be incredibly careful to not change the order of bound variables or + // remove any. As we go from `Goal<'tcx, Predicate>` to `Goal` with the variants + // of `PredicateKind` this is the case and it is and faster than instantiating and + // recanonicalizing. + let Goal { param_env, predicate } = canonical_goal.value; + if let Some(kind) = predicate.kind().no_bound_vars() { + match kind { + ty::PredicateKind::Clause(ty::Clause::Trait(predicate)) => self.compute_trait_goal( + canonical_goal.unchecked_rebind(Goal { param_env, predicate }), + ), + ty::PredicateKind::Clause(ty::Clause::Projection(predicate)) => self + .compute_projection_goal( + canonical_goal.unchecked_rebind(Goal { param_env, predicate }), + ), + ty::PredicateKind::Clause(ty::Clause::TypeOutlives(predicate)) => self + .compute_type_outlives_goal( + canonical_goal.unchecked_rebind(Goal { param_env, predicate }), + ), + ty::PredicateKind::Clause(ty::Clause::RegionOutlives(predicate)) => self + .compute_region_outlives_goal( + canonical_goal.unchecked_rebind(Goal { param_env, predicate }), + ), + // FIXME: implement these predicates :) + ty::PredicateKind::WellFormed(_) + | ty::PredicateKind::ObjectSafe(_) + | ty::PredicateKind::ClosureKind(_, _, _) + | ty::PredicateKind::Subtype(_) + | ty::PredicateKind::Coerce(_) + | ty::PredicateKind::ConstEvaluatable(_) + | ty::PredicateKind::ConstEquate(_, _) + | ty::PredicateKind::TypeWellFormedFromEnv(_) + | ty::PredicateKind::Ambiguous => unimplemented!(), + } + } else { + let (infcx, goal, var_values) = + self.tcx.infer_ctxt().build_with_canonical(DUMMY_SP, &canonical_goal); + let kind = infcx.replace_bound_vars_with_placeholders(goal.predicate.kind()); + let goal = goal.with(self.tcx, ty::Binder::dummy(kind)); + let (_, certainty) = self.evaluate_goal(&infcx, goal)?; + infcx.make_canonical_response(var_values, certainty) + } + } + + fn compute_type_outlives_goal( + &mut self, + _goal: CanonicalGoal<'tcx, TypeOutlivesPredicate<'tcx>>, + ) -> QueryResult<'tcx> { + todo!() + } + + fn compute_region_outlives_goal( + &mut self, + _goal: CanonicalGoal<'tcx, RegionOutlivesPredicate<'tcx>>, + ) -> QueryResult<'tcx> { + todo!() + } +} + +impl<'tcx> EvalCtxt<'tcx> { + fn evaluate_all( + &mut self, + infcx: &InferCtxt<'tcx>, + mut goals: Vec>>, + ) -> Result { + let mut new_goals = Vec::new(); + self.repeat_while_none(|this| { + let mut has_changed = Err(Certainty::Yes); + for goal in goals.drain(..) { + let (changed, certainty) = match this.evaluate_goal(infcx, goal) { + Ok(result) => result, + Err(NoSolution) => return Some(Err(NoSolution)), + }; + + if changed { + has_changed = Ok(()); + } + + match certainty { + Certainty::Yes => {} + Certainty::Maybe(_) => { + new_goals.push(goal); + has_changed = has_changed.map_err(|c| c.unify_and(certainty)); + } + } + } + + match has_changed { + Ok(()) => { + mem::swap(&mut new_goals, &mut goals); + None + } + Err(certainty) => Some(Ok(certainty)), + } + }) + } +} + +fn fixme_instantiate_canonical_query_response<'tcx>( + _: &InferCtxt<'tcx>, + _: &OriginalQueryValues<'tcx>, + _: CanonicalResponse<'tcx>, +) -> Certainty { + unimplemented!() +} diff --git a/compiler/rustc_trait_selection/src/solve/overflow.rs b/compiler/rustc_trait_selection/src/solve/overflow.rs new file mode 100644 index 00000000000..8d73a83aec9 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/overflow.rs @@ -0,0 +1,80 @@ +use rustc_infer::traits::query::NoSolution; +use rustc_middle::ty::TyCtxt; +use rustc_session::Limit; + +use super::{Certainty, EvalCtxt, MaybeCause, QueryResult}; + +/// When detecting a solver overflow, we return ambiguity. Overflow can be +/// *hidden* by either a fatal error in an **AND** or a trivial success in an **OR**. +/// +/// This is in issue in case of exponential blowup, e.g. if each goal on the stack +/// has multiple nested (overflowing) candidates. To deal with this, we reduce the limit +/// used by the solver when hitting the default limit for the first time. +/// +/// FIXME: Get tests where always using the `default_limit` results in a hang and refer +/// to them here. We can also improve the overflow strategy if necessary. +pub(super) struct OverflowData { + default_limit: Limit, + current_limit: Limit, + /// When proving an **AND** we have to repeatedly iterate over the yet unproven goals. + /// + /// Because of this each iteration also increases the depth in addition to the stack + /// depth. + additional_depth: usize, +} + +impl OverflowData { + pub(super) fn new(tcx: TyCtxt<'_>) -> OverflowData { + let default_limit = tcx.recursion_limit(); + OverflowData { default_limit, current_limit: default_limit, additional_depth: 0 } + } + + #[inline] + pub(super) fn did_overflow(&self) -> bool { + self.default_limit.0 != self.current_limit.0 + } + + #[inline] + pub(super) fn has_overflow(&self, depth: usize) -> bool { + self.current_limit.value_within_limit(depth + self.additional_depth) + } + + /// Updating the current limit when hitting overflow. + fn deal_with_overflow(&mut self) { + // When first hitting overflow we reduce the overflow limit + // for all future goals to prevent hangs if there's an exponental + // blowup. + self.current_limit.0 = self.default_limit.0 / 8; + } +} + +impl<'tcx> EvalCtxt<'tcx> { + pub(super) fn deal_with_overflow(&mut self) -> QueryResult<'tcx> { + self.overflow_data.deal_with_overflow(); + fixme_response_overflow_no_constraints() + } + + /// A `while`-loop which tracks overflow. + pub(super) fn repeat_while_none( + &mut self, + mut loop_body: impl FnMut(&mut Self) -> Option>, + ) -> Result { + let start_depth = self.overflow_data.additional_depth; + let depth = self.provisional_cache.current_depth(); + while !self.overflow_data.has_overflow(depth) { + if let Some(result) = loop_body(self) { + self.overflow_data.additional_depth = start_depth; + return result; + } + + self.overflow_data.additional_depth += 1; + } + self.overflow_data.additional_depth = start_depth; + self.overflow_data.deal_with_overflow(); + Ok(Certainty::Maybe(MaybeCause::Overflow)) + } +} + +fn fixme_response_overflow_no_constraints<'tcx>() -> QueryResult<'tcx> { + unimplemented!() +} diff --git a/compiler/rustc_trait_selection/src/solve/project_goals.rs b/compiler/rustc_trait_selection/src/solve/project_goals.rs new file mode 100644 index 00000000000..47a8f59ceb7 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/project_goals.rs @@ -0,0 +1,324 @@ +use crate::traits::{specialization_graph, translate_substs}; + +use super::infcx_ext::InferCtxtExt; +use super::{ + fixme_instantiate_canonical_query_response, CanonicalGoal, CanonicalResponse, Certainty, + EvalCtxt, Goal, QueryResult, +}; +use rustc_errors::ErrorGuaranteed; +use rustc_hir::def::DefKind; +use rustc_hir::def_id::DefId; +use rustc_infer::infer::canonical::{CanonicalVarValues, OriginalQueryValues}; +use rustc_infer::infer::{InferCtxt, InferOk, TyCtxtInferExt}; +use rustc_infer::traits::query::NoSolution; +use rustc_infer::traits::specialization_graph::LeafDef; +use rustc_infer::traits::{ObligationCause, Reveal}; +use rustc_middle::ty; +use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams}; +use rustc_middle::ty::ProjectionPredicate; +use rustc_middle::ty::TypeVisitable; +use rustc_span::DUMMY_SP; +use std::iter; + +// FIXME: Deduplicate the candidate code between projection and trait goal. + +/// Similar to [super::trait_goals::Candidate] but for `Projection` goals. +#[derive(Debug, Clone)] +struct Candidate<'tcx> { + source: CandidateSource, + result: CanonicalResponse<'tcx>, +} + +#[allow(dead_code)] // FIXME: implement and use all variants. +#[derive(Debug, Clone, Copy)] +enum CandidateSource { + Impl(DefId), + ParamEnv(usize), + Builtin, +} + +impl<'tcx> EvalCtxt<'tcx> { + pub(super) fn compute_projection_goal( + &mut self, + goal: CanonicalGoal<'tcx, ProjectionPredicate<'tcx>>, + ) -> QueryResult<'tcx> { + let candidates = self.assemble_and_evaluate_project_candidates(goal); + self.merge_project_candidates(candidates) + } + + fn assemble_and_evaluate_project_candidates( + &mut self, + goal: CanonicalGoal<'tcx, ProjectionPredicate<'tcx>>, + ) -> Vec> { + let (ref infcx, goal, var_values) = + self.tcx.infer_ctxt().build_with_canonical(DUMMY_SP, &goal); + let mut acx = AssemblyCtxt { cx: self, infcx, var_values, candidates: Vec::new() }; + + acx.assemble_candidates_after_normalizing_self_ty(goal); + acx.assemble_impl_candidates(goal); + acx.candidates + } + + fn merge_project_candidates( + &mut self, + mut candidates: Vec>, + ) -> QueryResult<'tcx> { + match candidates.len() { + 0 => return Err(NoSolution), + 1 => return Ok(candidates.pop().unwrap().result), + _ => {} + } + + if candidates.len() > 1 { + let mut i = 0; + 'outer: while i < candidates.len() { + for j in (0..candidates.len()).filter(|&j| i != j) { + if self.project_candidate_should_be_dropped_in_favor_of( + &candidates[i], + &candidates[j], + ) { + debug!(candidate = ?candidates[i], "Dropping candidate #{}/{}", i, candidates.len()); + candidates.swap_remove(i); + continue 'outer; + } + } + + debug!(candidate = ?candidates[i], "Retaining candidate #{}/{}", i, candidates.len()); + // If there are *STILL* multiple candidates, give up + // and report ambiguity. + i += 1; + if i > 1 { + debug!("multiple matches, ambig"); + // FIXME: return overflow if all candidates overflow, otherwise return ambiguity. + unimplemented!(); + } + } + } + + Ok(candidates.pop().unwrap().result) + } + + fn project_candidate_should_be_dropped_in_favor_of( + &self, + candidate: &Candidate<'tcx>, + other: &Candidate<'tcx>, + ) -> bool { + // FIXME: implement this + match (candidate.source, other.source) { + (CandidateSource::Impl(_), _) + | (CandidateSource::ParamEnv(_), _) + | (CandidateSource::Builtin, _) => unimplemented!(), + } + } +} + +/// Similar to [super::trait_goals::AssemblyCtxt] but for `Projection` goals. +struct AssemblyCtxt<'a, 'tcx> { + cx: &'a mut EvalCtxt<'tcx>, + infcx: &'a InferCtxt<'tcx>, + var_values: CanonicalVarValues<'tcx>, + candidates: Vec>, +} + +impl<'tcx> AssemblyCtxt<'_, 'tcx> { + fn try_insert_candidate(&mut self, source: CandidateSource, certainty: Certainty) { + match self.infcx.make_canonical_response(self.var_values.clone(), certainty) { + Ok(result) => self.candidates.push(Candidate { source, result }), + Err(NoSolution) => debug!(?source, ?certainty, "failed leakcheck"), + } + } + + fn assemble_candidates_after_normalizing_self_ty( + &mut self, + goal: Goal<'tcx, ProjectionPredicate<'tcx>>, + ) { + let tcx = self.cx.tcx; + let &ty::Alias(ty::Projection, projection_ty) = goal.predicate.projection_ty.self_ty().kind() else { + return + }; + self.infcx.probe(|_| { + let normalized_ty = self.infcx.next_ty_infer(); + let normalizes_to_goal = goal.with( + tcx, + ty::Binder::dummy(ty::ProjectionPredicate { + projection_ty, + term: normalized_ty.into(), + }), + ); + let normalization_certainty = + match self.cx.evaluate_goal(&self.infcx, normalizes_to_goal) { + Ok((_, certainty)) => certainty, + Err(NoSolution) => return, + }; + + // NOTE: Alternatively we could call `evaluate_goal` here and only have a `Normalized` candidate. + // This doesn't work as long as we use `CandidateSource` in both winnowing and to resolve associated items. + let goal = goal.with(tcx, goal.predicate.with_self_ty(tcx, normalized_ty)); + let mut orig_values = OriginalQueryValues::default(); + let goal = self.infcx.canonicalize_query(goal, &mut orig_values); + let normalized_candidates = self.cx.assemble_and_evaluate_project_candidates(goal); + // Map each candidate from being canonical wrt the current inference context to being + // canonical wrt the caller. + for Candidate { source, result } in normalized_candidates { + self.infcx.probe(|_| { + let candidate_certainty = fixme_instantiate_canonical_query_response( + self.infcx, + &orig_values, + result, + ); + self.try_insert_candidate( + source, + normalization_certainty.unify_and(candidate_certainty), + ) + }) + } + }) + } + + fn assemble_impl_candidates(&mut self, goal: Goal<'tcx, ProjectionPredicate<'tcx>>) { + self.cx.tcx.for_each_relevant_impl( + goal.predicate.trait_def_id(self.cx.tcx), + goal.predicate.self_ty(), + |impl_def_id| self.consider_impl_candidate(goal, impl_def_id), + ); + } + + fn consider_impl_candidate( + &mut self, + goal: Goal<'tcx, ProjectionPredicate<'tcx>>, + impl_def_id: DefId, + ) { + let tcx = self.cx.tcx; + let goal_trait_ref = goal.predicate.projection_ty.trait_ref(tcx); + let impl_trait_ref = tcx.bound_impl_trait_ref(impl_def_id).unwrap(); + let drcx = DeepRejectCtxt { treat_obligation_params: TreatParams::AsPlaceholder }; + if iter::zip(goal_trait_ref.substs, impl_trait_ref.skip_binder().substs) + .any(|(goal, imp)| !drcx.generic_args_may_unify(goal, imp)) + { + return; + } + + self.infcx.probe(|_| { + let impl_substs = self.infcx.fresh_substs_for_item(DUMMY_SP, impl_def_id); + let impl_trait_ref = impl_trait_ref.subst(tcx, impl_substs); + + let Ok(InferOk { obligations, .. }) = self + .infcx + .at(&ObligationCause::dummy(), goal.param_env) + .define_opaque_types(false) + .eq(goal_trait_ref, impl_trait_ref) + .map_err(|e| debug!("failed to equate trait refs: {e:?}")) + else { + return + }; + + let nested_goals = obligations.into_iter().map(|o| o.into()).collect(); + let Ok(trait_ref_certainty) = self.cx.evaluate_all(self.infcx, nested_goals) else { return }; + + let Some(assoc_def) = self.fetch_eligible_assoc_item_def( + goal.param_env, + goal_trait_ref, + goal.predicate.def_id(), + impl_def_id + ) else { + return + }; + + if !assoc_def.item.defaultness(tcx).has_value() { + 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 ` as Trait>::Assoc` + // - the applicable impl `impl Trait for Vec` + // - and the impl which defines `Assoc` being `impl Trait for Vec` + // + // We first rebase the goal substs onto the impl, going from `[Vec, 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_trait_ref.substs, + ); + let substs = translate_substs( + self.infcx, + 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 is_const = matches!(tcx.def_kind(assoc_def.item.def_id), DefKind::AssocConst); + let ty = tcx.bound_type_of(assoc_def.item.def_id); + let term: ty::EarlyBinder> = if is_const { + let identity_substs = ty::InternalSubsts::identity_for_item(tcx, assoc_def.item.def_id); + let did = ty::WithOptConstParam::unknown(assoc_def.item.def_id); + let kind = + ty::ConstKind::Unevaluated(ty::UnevaluatedConst::new(did, identity_substs)); + ty.map_bound(|ty| tcx.mk_const(kind, ty).into()) + } else { + ty.map_bound(|ty| ty.into()) + }; + + let Ok(InferOk { obligations, .. }) = self + .infcx + .at(&ObligationCause::dummy(), goal.param_env) + .define_opaque_types(false) + .eq(goal.predicate.term, term.subst(tcx, substs)) + .map_err(|e| debug!("failed to equate trait refs: {e:?}")) + else { + return + }; + + let nested_goals = obligations.into_iter().map(|o| o.into()).collect(); + let Ok(rhs_certainty) = self.cx.evaluate_all(self.infcx, nested_goals) else { return }; + + let certainty = trait_ref_certainty.unify_and(rhs_certainty); + self.try_insert_candidate(CandidateSource::Impl(impl_def_id), certainty); + }) + } + + /// This behavior is also implemented in `rustc_ty_utils` and in the old `project` code. + /// + /// FIXME: We should merge these 3 implementations as it's likely that they otherwise + /// diverge. + #[instrument(level = "debug", skip(self, param_env), ret)] + fn fetch_eligible_assoc_item_def( + &self, + param_env: ty::ParamEnv<'tcx>, + goal_trait_ref: ty::TraitRef<'tcx>, + trait_assoc_def_id: DefId, + impl_def_id: DefId, + ) -> Option { + let node_item = + specialization_graph::assoc_def(self.cx.tcx, impl_def_id, trait_assoc_def_id) + .map_err(|ErrorGuaranteed { .. }| ()) + .ok()?; + + let eligible = if node_item.is_final() { + // Non-specializable items are always projectable. + true + } else { + // Only reveal a specializable default if we're past type-checking + // and the obligation is monomorphic, otherwise passes such as + // transmute checking and polymorphic MIR optimizations could + // get a result which isn't correct for all monomorphizations. + if param_env.reveal() == Reveal::All { + let poly_trait_ref = self.infcx.resolve_vars_if_possible(goal_trait_ref); + !poly_trait_ref.still_further_specializable() + } else { + debug!(?node_item.item.def_id, "not eligible due to default"); + false + } + }; + + if eligible { Some(node_item) } else { None } + } +} diff --git a/compiler/rustc_trait_selection/src/solve/trait_goals.rs b/compiler/rustc_trait_selection/src/solve/trait_goals.rs new file mode 100644 index 00000000000..4ea9081bf46 --- /dev/null +++ b/compiler/rustc_trait_selection/src/solve/trait_goals.rs @@ -0,0 +1,282 @@ +//! Dealing with trait goals, i.e. `T: Trait<'a, U>`. + +use std::iter; + +use super::infcx_ext::InferCtxtExt; +use super::{ + fixme_instantiate_canonical_query_response, CanonicalGoal, CanonicalResponse, Certainty, + EvalCtxt, Goal, QueryResult, +}; +use rustc_hir::def_id::DefId; +use rustc_infer::infer::canonical::{CanonicalVarValues, OriginalQueryValues}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_infer::infer::{InferCtxt, InferOk}; +use rustc_infer::traits::query::NoSolution; +use rustc_infer::traits::ObligationCause; +use rustc_middle::ty; +use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams}; +use rustc_middle::ty::TraitPredicate; +use rustc_span::DUMMY_SP; + +/// A candidate is a possible way to prove a goal. +/// +/// It consists of both the `source`, which describes how that goal +/// would be proven, and the `result` when using the given `source`. +/// +/// For the list of possible candidates, please look at the documentation +/// of [CandidateSource]. +#[derive(Debug, Clone)] +pub(super) struct Candidate<'tcx> { + source: CandidateSource, + result: CanonicalResponse<'tcx>, +} + +#[allow(dead_code)] // FIXME: implement and use all variants. +#[derive(Debug, Clone, Copy)] +pub(super) enum CandidateSource { + /// Some user-defined impl with the given `DefId`. + Impl(DefId), + /// The n-th caller bound in the `param_env` of our goal. + /// + /// This is pretty much always a bound from the `where`-clauses of the + /// currently checked item. + ParamEnv(usize), + /// A bound on the `self_ty` in case it is a projection or an opaque type. + /// + /// # Examples + /// + /// ```ignore (for syntax highlighting) + /// trait Trait { + /// type Assoc: OtherTrait; + /// } + /// ``` + /// + /// We know that `::Assoc: OtherTrait` holds by looking at + /// the bounds on `Trait::Assoc`. + AliasBound(usize), + /// A builtin implementation for some specific traits, used in cases + /// where we cannot rely an ordinary library implementations. + /// + /// The most notable examples are `Sized`, `Copy` and `Clone`. This is also + /// used for the `DiscriminantKind` and `Pointee` trait, both of which have + /// an associated type. + Builtin, + /// An automatic impl for an auto trait, e.g. `Send`. These impls recursively look + /// at the constituent types of the `self_ty` to check whether the auto trait + /// is implemented for those. + AutoImpl, +} + +struct AssemblyCtxt<'a, 'tcx> { + cx: &'a mut EvalCtxt<'tcx>, + infcx: &'a InferCtxt<'tcx>, + var_values: CanonicalVarValues<'tcx>, + candidates: Vec>, +} + +impl<'tcx> EvalCtxt<'tcx> { + pub(super) fn compute_trait_goal( + &mut self, + goal: CanonicalGoal<'tcx, TraitPredicate<'tcx>>, + ) -> QueryResult<'tcx> { + let candidates = self.assemble_and_evaluate_trait_candidates(goal); + self.merge_trait_candidates_discard_reservation_impls(candidates) + } + + pub(super) fn assemble_and_evaluate_trait_candidates( + &mut self, + goal: CanonicalGoal<'tcx, TraitPredicate<'tcx>>, + ) -> Vec> { + let (ref infcx, goal, var_values) = + self.tcx.infer_ctxt().build_with_canonical(DUMMY_SP, &goal); + let mut acx = AssemblyCtxt { cx: self, infcx, var_values, candidates: Vec::new() }; + + acx.assemble_candidates_after_normalizing_self_ty(goal); + acx.assemble_impl_candidates(goal); + + // FIXME: Remaining candidates + acx.candidates + } + + #[instrument(level = "debug", skip(self), ret)] + pub(super) fn merge_trait_candidates_discard_reservation_impls( + &mut self, + mut candidates: Vec>, + ) -> QueryResult<'tcx> { + match candidates.len() { + 0 => return Err(NoSolution), + 1 => return Ok(self.discard_reservation_impl(candidates.pop().unwrap()).result), + _ => {} + } + + if candidates.len() > 1 { + let mut i = 0; + 'outer: while i < candidates.len() { + for j in (0..candidates.len()).filter(|&j| i != j) { + if self.trait_candidate_should_be_dropped_in_favor_of( + &candidates[i], + &candidates[j], + ) { + debug!(candidate = ?candidates[i], "Dropping candidate #{}/{}", i, candidates.len()); + candidates.swap_remove(i); + continue 'outer; + } + } + + debug!(candidate = ?candidates[i], "Retaining candidate #{}/{}", i, candidates.len()); + // If there are *STILL* multiple candidates, give up + // and report ambiguity. + i += 1; + if i > 1 { + debug!("multiple matches, ambig"); + // FIXME: return overflow if all candidates overflow, otherwise return ambiguity. + unimplemented!(); + } + } + } + + Ok(self.discard_reservation_impl(candidates.pop().unwrap()).result) + } + + fn trait_candidate_should_be_dropped_in_favor_of( + &self, + candidate: &Candidate<'tcx>, + other: &Candidate<'tcx>, + ) -> bool { + // FIXME: implement this + match (candidate.source, other.source) { + (CandidateSource::Impl(_), _) + | (CandidateSource::ParamEnv(_), _) + | (CandidateSource::AliasBound(_), _) + | (CandidateSource::Builtin, _) + | (CandidateSource::AutoImpl, _) => unimplemented!(), + } + } + + fn discard_reservation_impl(&self, candidate: Candidate<'tcx>) -> Candidate<'tcx> { + if let CandidateSource::Impl(def_id) = candidate.source { + if let ty::ImplPolarity::Reservation = self.tcx.impl_polarity(def_id) { + debug!("Selected reservation impl"); + // FIXME: reduce candidate to ambiguous + // FIXME: replace `var_values` with identity, yeet external constraints. + unimplemented!() + } + } + + candidate + } +} + +impl<'tcx> AssemblyCtxt<'_, 'tcx> { + /// Adds a new candidate using the current state of the inference context. + /// + /// This does require each assembly method to correctly use `probe` to not taint + /// the results of other candidates. + fn try_insert_candidate(&mut self, source: CandidateSource, certainty: Certainty) { + match self.infcx.make_canonical_response(self.var_values.clone(), certainty) { + Ok(result) => self.candidates.push(Candidate { source, result }), + Err(NoSolution) => debug!(?source, ?certainty, "failed leakcheck"), + } + } + + /// If the self type of a trait goal is a projection, computing the relevant candidates is difficult. + /// + /// To deal with this, we first try to normalize the self type and add the candidates for the normalized + /// self type to the list of candidates in case that succeeds. Note that we can't just eagerly return in + /// this case as projections as self types add ` + fn assemble_candidates_after_normalizing_self_ty( + &mut self, + goal: Goal<'tcx, TraitPredicate<'tcx>>, + ) { + let tcx = self.cx.tcx; + // FIXME: We also have to normalize opaque types, not sure where to best fit that in. + let &ty::Alias(ty::Projection, projection_ty) = goal.predicate.self_ty().kind() else { + return + }; + self.infcx.probe(|_| { + let normalized_ty = self.infcx.next_ty_infer(); + let normalizes_to_goal = goal.with( + tcx, + ty::Binder::dummy(ty::ProjectionPredicate { + projection_ty, + term: normalized_ty.into(), + }), + ); + let normalization_certainty = + match self.cx.evaluate_goal(&self.infcx, normalizes_to_goal) { + Ok((_, certainty)) => certainty, + Err(NoSolution) => return, + }; + + // NOTE: Alternatively we could call `evaluate_goal` here and only have a `Normalized` candidate. + // This doesn't work as long as we use `CandidateSource` in both winnowing and to resolve associated items. + let goal = goal.with(tcx, goal.predicate.with_self_type(tcx, normalized_ty)); + let mut orig_values = OriginalQueryValues::default(); + let goal = self.infcx.canonicalize_query(goal, &mut orig_values); + let normalized_candidates = self.cx.assemble_and_evaluate_trait_candidates(goal); + + // Map each candidate from being canonical wrt the current inference context to being + // canonical wrt the caller. + for Candidate { source, result } in normalized_candidates { + self.infcx.probe(|_| { + let candidate_certainty = fixme_instantiate_canonical_query_response( + self.infcx, + &orig_values, + result, + ); + + // FIXME: This is a bit scary if the `normalizes_to_goal` overflows. + // + // If we have an ambiguous candidate it hides that normalization + // caused an overflow which may cause issues. + self.try_insert_candidate( + source, + normalization_certainty.unify_and(candidate_certainty), + ) + }) + } + }) + } + + fn assemble_impl_candidates(&mut self, goal: Goal<'tcx, TraitPredicate<'tcx>>) { + self.cx.tcx.for_each_relevant_impl( + goal.predicate.def_id(), + goal.predicate.self_ty(), + |impl_def_id| self.consider_impl_candidate(goal, impl_def_id), + ); + } + + fn consider_impl_candidate( + &mut self, + goal: Goal<'tcx, TraitPredicate<'tcx>>, + impl_def_id: DefId, + ) { + let impl_trait_ref = self.cx.tcx.bound_impl_trait_ref(impl_def_id).unwrap(); + let drcx = DeepRejectCtxt { treat_obligation_params: TreatParams::AsPlaceholder }; + if iter::zip(goal.predicate.trait_ref.substs, impl_trait_ref.skip_binder().substs) + .any(|(goal, imp)| !drcx.generic_args_may_unify(goal, imp)) + { + return; + } + + self.infcx.probe(|_| { + let impl_substs = self.infcx.fresh_substs_for_item(DUMMY_SP, impl_def_id); + let impl_trait_ref = impl_trait_ref.subst(self.cx.tcx, impl_substs); + + let Ok(InferOk { obligations, .. }) = self + .infcx + .at(&ObligationCause::dummy(), goal.param_env) + .define_opaque_types(false) + .eq(goal.predicate.trait_ref, impl_trait_ref) + .map_err(|e| debug!("failed to equate trait refs: {e:?}")) + else { + return + }; + + let nested_goals = obligations.into_iter().map(|o| o.into()).collect(); + + let Ok(certainty) = self.cx.evaluate_all(self.infcx, nested_goals) else { return }; + self.try_insert_candidate(CandidateSource::Impl(impl_def_id), certainty); + }) + } +} diff --git a/compiler/rustc_trait_selection/src/traits/project.rs b/compiler/rustc_trait_selection/src/traits/project.rs index 84d7244c1db..5276da2e49c 100644 --- a/compiler/rustc_trait_selection/src/traits/project.rs +++ b/compiler/rustc_trait_selection/src/traits/project.rs @@ -25,7 +25,6 @@ use rustc_data_structures::stack::ensure_sufficient_stack; use rustc_errors::ErrorGuaranteed; use rustc_hir::def::DefKind; -use rustc_hir::def_id::DefId; use rustc_hir::lang_items::LangItem; use rustc_infer::infer::at::At; use rustc_infer::infer::resolve::OpportunisticRegionResolver; @@ -1553,7 +1552,7 @@ fn assemble_candidates_from_impls<'cx, 'tcx>( // NOTE: This should be kept in sync with the similar code in // `rustc_ty_utils::instance::resolve_associated_item()`. let node_item = - assoc_def(selcx, impl_data.impl_def_id, obligation.predicate.def_id) + specialization_graph::assoc_def(selcx.tcx(), impl_data.impl_def_id, obligation.predicate.def_id) .map_err(|ErrorGuaranteed { .. }| ())?; if node_item.is_final() { @@ -2113,7 +2112,7 @@ fn confirm_impl_candidate<'cx, 'tcx>( let trait_def_id = tcx.trait_id_of_impl(impl_def_id).unwrap(); let param_env = obligation.param_env; - let Ok(assoc_ty) = assoc_def(selcx, impl_def_id, assoc_item_id) else { + let Ok(assoc_ty) = specialization_graph::assoc_def(tcx, impl_def_id, assoc_item_id) else { return Progress { term: tcx.ty_error().into(), obligations: nested }; }; @@ -2210,7 +2209,7 @@ fn confirm_impl_trait_in_trait_candidate<'tcx>( let mut obligations = data.nested; let trait_fn_def_id = tcx.impl_trait_in_trait_parent(obligation.predicate.def_id); - let Ok(leaf_def) = assoc_def(selcx, data.impl_def_id, trait_fn_def_id) else { + let Ok(leaf_def) = specialization_graph::assoc_def(tcx, data.impl_def_id, trait_fn_def_id) else { return Progress { term: tcx.ty_error().into(), obligations }; }; if !leaf_def.item.defaultness(tcx).has_value() { @@ -2347,58 +2346,6 @@ fn assoc_ty_own_obligations<'cx, 'tcx>( } } -/// Locate the definition of an associated type in the specialization hierarchy, -/// starting from the given impl. -/// -/// Based on the "projection mode", this lookup may in fact only examine the -/// topmost impl. See the comments for `Reveal` for more details. -fn assoc_def( - selcx: &SelectionContext<'_, '_>, - impl_def_id: DefId, - assoc_def_id: DefId, -) -> Result { - let tcx = selcx.tcx(); - let trait_def_id = tcx.impl_trait_ref(impl_def_id).unwrap().def_id; - let trait_def = tcx.trait_def(trait_def_id); - - // This function may be called while we are still building the - // specialization graph that is queried below (via TraitDef::ancestors()), - // so, in order to avoid unnecessary infinite recursion, we manually look - // for the associated item at the given impl. - // If there is no such item in that impl, this function will fail with a - // cycle error if the specialization graph is currently being built. - if let Some(&impl_item_id) = tcx.impl_item_implementor_ids(impl_def_id).get(&assoc_def_id) { - let item = tcx.associated_item(impl_item_id); - let impl_node = specialization_graph::Node::Impl(impl_def_id); - return Ok(specialization_graph::LeafDef { - item: *item, - defining_node: impl_node, - finalizing_node: if item.defaultness(tcx).is_default() { - None - } else { - Some(impl_node) - }, - }); - } - - let ancestors = trait_def.ancestors(tcx, impl_def_id)?; - if let Some(assoc_item) = ancestors.leaf_def(tcx, assoc_def_id) { - Ok(assoc_item) - } else { - // This is saying that neither the trait nor - // the impl contain a definition for this - // associated type. Normally this situation - // could only arise through a compiler bug -- - // if the user wrote a bad item name, it - // should have failed in astconv. - bug!( - "No associated type `{}` for {}", - tcx.item_name(assoc_def_id), - tcx.def_path_str(impl_def_id) - ) - } -} - pub(crate) trait ProjectionCacheKeyExt<'cx, 'tcx>: Sized { fn from_poly_projection_predicate( selcx: &mut SelectionContext<'cx, 'tcx>, diff --git a/compiler/rustc_trait_selection/src/traits/select/mod.rs b/compiler/rustc_trait_selection/src/traits/select/mod.rs index 792933096b1..81e8f9e914c 100644 --- a/compiler/rustc_trait_selection/src/traits/select/mod.rs +++ b/compiler/rustc_trait_selection/src/traits/select/mod.rs @@ -1171,19 +1171,7 @@ pub(crate) fn coinductive_match(&mut self, mut cycle: I) -> bool where I: Iterator>, { - cycle.all(|predicate| self.coinductive_predicate(predicate)) - } - - fn coinductive_predicate(&self, predicate: ty::Predicate<'tcx>) -> bool { - let result = match predicate.kind().skip_binder() { - ty::PredicateKind::Clause(ty::Clause::Trait(ref data)) => { - self.tcx().trait_is_coinductive(data.def_id()) - } - ty::PredicateKind::WellFormed(_) => true, - _ => false, - }; - debug!(?predicate, ?result, "coinductive_predicate"); - result + cycle.all(|predicate| predicate.is_coinductive(self.tcx())) } /// Further evaluates `candidate` to decide whether all type parameters match and whether nested diff --git a/compiler/rustc_trait_selection/src/traits/specialize/specialization_graph.rs b/compiler/rustc_trait_selection/src/traits/specialize/specialization_graph.rs index 4546c953393..02b06677740 100644 --- a/compiler/rustc_trait_selection/src/traits/specialize/specialization_graph.rs +++ b/compiler/rustc_trait_selection/src/traits/specialize/specialization_graph.rs @@ -1,6 +1,7 @@ use super::OverlapError; use crate::traits; +use rustc_errors::ErrorGuaranteed; use rustc_hir::def_id::DefId; use rustc_middle::ty::fast_reject::{self, SimplifiedType, TreatParams}; use rustc_middle::ty::{self, TyCtxt, TypeVisitable}; @@ -379,3 +380,51 @@ fn record_impl_from_cstore(&mut self, tcx: TyCtxt<'tcx>, parent: DefId, child: D self.children.entry(parent).or_default().insert_blindly(tcx, child); } } + +/// Locate the definition of an associated type in the specialization hierarchy, +/// starting from the given impl. +pub(crate) fn assoc_def( + tcx: TyCtxt<'_>, + impl_def_id: DefId, + assoc_def_id: DefId, +) -> Result { + let trait_def_id = tcx.impl_trait_ref(impl_def_id).unwrap().def_id; + let trait_def = tcx.trait_def(trait_def_id); + + // This function may be called while we are still building the + // specialization graph that is queried below (via TraitDef::ancestors()), + // so, in order to avoid unnecessary infinite recursion, we manually look + // for the associated item at the given impl. + // If there is no such item in that impl, this function will fail with a + // cycle error if the specialization graph is currently being built. + if let Some(&impl_item_id) = tcx.impl_item_implementor_ids(impl_def_id).get(&assoc_def_id) { + let &item = tcx.associated_item(impl_item_id); + let impl_node = Node::Impl(impl_def_id); + return Ok(LeafDef { + item, + defining_node: impl_node, + finalizing_node: if item.defaultness(tcx).is_default() { + None + } else { + Some(impl_node) + }, + }); + } + + let ancestors = trait_def.ancestors(tcx, impl_def_id)?; + if let Some(assoc_item) = ancestors.leaf_def(tcx, assoc_def_id) { + Ok(assoc_item) + } else { + // This is saying that neither the trait nor + // the impl contain a definition for this + // associated type. Normally this situation + // could only arise through a compiler bug -- + // if the user wrote a bad item name, it + // should have failed in astconv. + bug!( + "No associated type `{}` for {}", + tcx.item_name(assoc_def_id), + tcx.def_path_str(impl_def_id) + ) + } +} diff --git a/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.rs b/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.rs new file mode 100644 index 00000000000..a3bb76d7e3b --- /dev/null +++ b/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.rs @@ -0,0 +1,28 @@ +// known-bug + +// This should compile but fails with the current solver. +// +// This checks that the new solver uses `Ambiguous` when hitting the +// inductive cycle here when proving `exists<^0, ^1> (): Trait<^0, ^1>` +// which requires proving `Trait` but that has the same +// canonical representation. +trait Trait {} + +impl Trait for () +where + (): Trait, + T: OtherTrait, +{} + +trait OtherTrait {} +impl OtherTrait for u32 {} + +fn require_trait() +where + (): Trait +{} + +fn main() { + require_trait::<_, _>(); + //~^ ERROR overflow evaluating +} diff --git a/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.stderr b/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.stderr new file mode 100644 index 00000000000..e4b84e07822 --- /dev/null +++ b/src/test/ui/traits/solver-cycles/inductive-canonical-cycle.stderr @@ -0,0 +1,26 @@ +error[E0275]: overflow evaluating the requirement `_: Sized` + --> $DIR/inductive-canonical-cycle.rs:26:5 + | +LL | require_trait::<_, _>(); + | ^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider increasing the recursion limit by adding a `#![recursion_limit = "256"]` attribute to your crate (`inductive_canonical_cycle`) +note: required for `()` to implement `Trait<_, _>` + --> $DIR/inductive-canonical-cycle.rs:11:12 + | +LL | impl Trait for () + | ^^^^^^^^^^^ ^^ + = note: 128 redundant requirements hidden + = note: required for `()` to implement `Trait<_, _>` +note: required by a bound in `require_trait` + --> $DIR/inductive-canonical-cycle.rs:22:9 + | +LL | fn require_trait() + | ------------- required by a bound in this +LL | where +LL | (): Trait + | ^^^^^^^^^^^ required by this bound in `require_trait` + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0275`. diff --git a/triagebot.toml b/triagebot.toml index 46a3bab42a1..c7158a51861 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -342,6 +342,10 @@ cc = ["@BoxyUwU"] message = "Some changes occured in `rustc_ty_utils::consts.rs`" cc = ["@BoxyUwU"] +[mentions."compiler/rustc_trait_selection/src/solve] +message = "Some changes occurred to the core trait solver" +cc = ["@lcnr"] + [mentions."compiler/rustc_trait_selection/src/traits/engine.rs"] message = """ Some changes occurred in engine.rs, potentially modifying the public API \