//! Code for projecting associated types out of trait references. use rustc_data_structures::snapshot_map::{self, SnapshotMapRef, SnapshotMapStorage}; use rustc_data_structures::undo_log::Rollback; use rustc_middle::traits::EvaluationResult; use rustc_middle::ty; use tracing::{debug, info}; use super::PredicateObligations; use crate::infer::snapshot::undo_log::InferCtxtUndoLogs; pub(crate) type UndoLog<'tcx> = snapshot_map::UndoLog, ProjectionCacheEntry<'tcx>>; #[derive(Clone)] pub struct MismatchedProjectionTypes<'tcx> { pub err: ty::error::TypeError<'tcx>, } #[derive(Clone)] pub struct Normalized<'tcx, T> { pub value: T, pub obligations: PredicateObligations<'tcx>, } pub type NormalizedTerm<'tcx> = Normalized<'tcx, ty::Term<'tcx>>; impl<'tcx, T> Normalized<'tcx, T> { pub fn with(self, value: U) -> Normalized<'tcx, U> { Normalized { value, obligations: self.obligations } } } // # Cache /// The projection cache. Unlike the standard caches, this can include /// infcx-dependent type variables, therefore we have to roll the /// cache back each time we roll a snapshot back, to avoid assumptions /// on yet-unresolved inference variables. Types with placeholder /// regions also have to be removed when the respective snapshot ends. /// /// Because of that, projection cache entries can be "stranded" and left /// inaccessible when type variables inside the key are resolved. We make no /// attempt to recover or remove "stranded" entries, but rather let them be /// (for the lifetime of the infcx). /// /// Entries in the projection cache might contain inference variables /// that will be resolved by obligations on the projection cache entry (e.g., /// when a type parameter in the associated type is constrained through /// an "RFC 447" projection on the impl). /// /// When working with a fulfillment context, the derived obligations of each /// projection cache entry will be registered on the fulfillcx, so any users /// that can wait for a fulfillcx fixed point need not care about this. However, /// users that don't wait for a fixed point (e.g., trait evaluation) have to /// resolve the obligations themselves to make sure the projected result is /// ok and avoid issues like #43132. /// /// If that is done, after evaluation the obligations, it is a good idea to /// call `ProjectionCache::complete` to make sure the obligations won't be /// re-evaluated and avoid an exponential worst-case. // // FIXME: we probably also want some sort of cross-infcx cache here to // reduce the amount of duplication. Let's see what we get with the Chalk reforms. pub struct ProjectionCache<'a, 'tcx> { map: &'a mut SnapshotMapStorage, ProjectionCacheEntry<'tcx>>, undo_log: &'a mut InferCtxtUndoLogs<'tcx>, } #[derive(Clone, Default)] pub struct ProjectionCacheStorage<'tcx> { map: SnapshotMapStorage, ProjectionCacheEntry<'tcx>>, } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub struct ProjectionCacheKey<'tcx> { term: ty::AliasTerm<'tcx>, param_env: ty::ParamEnv<'tcx>, } impl<'tcx> ProjectionCacheKey<'tcx> { pub fn new(term: ty::AliasTerm<'tcx>, param_env: ty::ParamEnv<'tcx>) -> Self { Self { term, param_env } } } #[derive(Clone, Debug)] pub enum ProjectionCacheEntry<'tcx> { InProgress, Ambiguous, Recur, Error, NormalizedTerm { ty: NormalizedTerm<'tcx>, /// If we were able to successfully evaluate the corresponding cache /// entry key during predicate evaluation, then this field stores the /// final result obtained from evaluating all of the projection /// sub-obligations. During evaluation, we will skip evaluating the /// cached sub-obligations in `ty` if this field is set. Evaluation /// only cares about the final result, so we don't care about any /// region constraint side-effects produced by evaluating the /// sub-obligations. /// /// Additionally, we will clear out the sub-obligations entirely if we /// ever evaluate the cache entry (along with all its sub obligations) /// to `EvaluatedToOk`. This affects all users of the cache, not just /// evaluation. Since a result of `EvaluatedToOk` means that there were /// no region obligations that need to be tracked, it's fine to forget /// about the sub-obligations - they don't provide any additional /// information. However, we do *not* discard any obligations when we /// see `EvaluatedToOkModuloRegions` - we don't know which /// sub-obligations may introduce region constraints, so we keep them /// all to be safe. /// /// When we are not performing evaluation (e.g. in /// `FulfillmentContext`), we ignore this field, and always re-process /// the cached sub-obligations (which may have been cleared out - see /// the above paragraph). This ensures that we do not lose any regions /// constraints that arise from processing the sub-obligations. complete: Option, }, } impl<'tcx> ProjectionCacheStorage<'tcx> { #[inline] pub(crate) fn with_log<'a>( &'a mut self, undo_log: &'a mut InferCtxtUndoLogs<'tcx>, ) -> ProjectionCache<'a, 'tcx> { ProjectionCache { map: &mut self.map, undo_log } } } impl<'tcx> ProjectionCache<'_, 'tcx> { #[inline] fn map( &mut self, ) -> SnapshotMapRef< '_, ProjectionCacheKey<'tcx>, ProjectionCacheEntry<'tcx>, InferCtxtUndoLogs<'tcx>, > { self.map.with_log(self.undo_log) } pub fn clear(&mut self) { self.map().clear(); } /// Try to start normalize `key`; returns an error if /// normalization already occurred (this error corresponds to a /// cache hit, so it's actually a good thing). pub fn try_start( &mut self, key: ProjectionCacheKey<'tcx>, ) -> Result<(), ProjectionCacheEntry<'tcx>> { let mut map = self.map(); if let Some(entry) = map.get(&key) { return Err(entry.clone()); } map.insert(key, ProjectionCacheEntry::InProgress); Ok(()) } /// Indicates that `key` was normalized to `value`. pub fn insert_term(&mut self, key: ProjectionCacheKey<'tcx>, value: NormalizedTerm<'tcx>) { debug!( "ProjectionCacheEntry::insert_ty: adding cache entry: key={:?}, value={:?}", key, value ); let mut map = self.map(); if let Some(ProjectionCacheEntry::Recur) = map.get(&key) { debug!("Not overwriting Recur"); return; } let fresh_key = map.insert(key, ProjectionCacheEntry::NormalizedTerm { ty: value, complete: None }); assert!(!fresh_key, "never started projecting `{key:?}`"); } /// Mark the relevant projection cache key as having its derived obligations /// complete, so they won't have to be re-computed (this is OK to do in a /// snapshot - if the snapshot is rolled back, the obligations will be /// marked as incomplete again). pub fn complete(&mut self, key: ProjectionCacheKey<'tcx>, result: EvaluationResult) { let mut map = self.map(); match map.get(&key) { Some(ProjectionCacheEntry::NormalizedTerm { ty, complete: _ }) => { info!("ProjectionCacheEntry::complete({:?}) - completing {:?}", key, ty); let mut ty = ty.clone(); if result.must_apply_considering_regions() { ty.obligations = PredicateObligations::new(); } map.insert( key, ProjectionCacheEntry::NormalizedTerm { ty, complete: Some(result) }, ); } ref value => { // Type inference could "strand behind" old cache entries. Leave // them alone for now. info!("ProjectionCacheEntry::complete({:?}) - ignoring {:?}", key, value); } }; } pub fn is_complete(&mut self, key: ProjectionCacheKey<'tcx>) -> Option { self.map().get(&key).and_then(|res| match res { ProjectionCacheEntry::NormalizedTerm { ty: _, complete } => *complete, _ => None, }) } /// Indicates that trying to normalize `key` resulted in /// ambiguity. No point in trying it again then until we gain more /// type information (in which case, the "fully resolved" key will /// be different). pub fn ambiguous(&mut self, key: ProjectionCacheKey<'tcx>) { let fresh = self.map().insert(key, ProjectionCacheEntry::Ambiguous); assert!(!fresh, "never started projecting `{key:?}`"); } /// Indicates that while trying to normalize `key`, `key` was required to /// be normalized again. Selection or evaluation should eventually report /// an error here. pub fn recur(&mut self, key: ProjectionCacheKey<'tcx>) { let fresh = self.map().insert(key, ProjectionCacheEntry::Recur); assert!(!fresh, "never started projecting `{key:?}`"); } /// Indicates that trying to normalize `key` resulted in /// error. pub fn error(&mut self, key: ProjectionCacheKey<'tcx>) { let fresh = self.map().insert(key, ProjectionCacheEntry::Error); assert!(!fresh, "never started projecting `{key:?}`"); } } impl<'tcx> Rollback> for ProjectionCacheStorage<'tcx> { fn reverse(&mut self, undo: UndoLog<'tcx>) { self.map.reverse(undo); } }