rustc_type_ir/search_graph/
mod.rs

1//! The search graph is responsible for caching and cycle detection in the trait
2//! solver. Making sure that caching doesn't result in soundness bugs or unstable
3//! query results is very challenging and makes this one of the most-involved
4//! self-contained components of the compiler.
5//!
6//! We added fuzzing support to test its correctness. The fuzzers used to verify
7//! the current implementation can be found in <https://github.com/lcnr/search_graph_fuzz>.
8//!
9//! This is just a quick overview of the general design, please check out the relevant
10//! [rustc-dev-guide chapter](https://rustc-dev-guide.rust-lang.org/solve/caching.html) for
11//! more details. Caching is split between a global cache and the per-cycle `provisional_cache`.
12//! The global cache has to be completely unobservable, while the per-cycle cache may impact
13//! behavior as long as the resulting behavior is still correct.
14use std::cmp::Ordering;
15use std::collections::BTreeMap;
16use std::collections::hash_map::Entry;
17use std::fmt::Debug;
18use std::hash::Hash;
19use std::marker::PhantomData;
20
21use derive_where::derive_where;
22#[cfg(feature = "nightly")]
23use rustc_macros::{Decodable_NoContext, Encodable_NoContext, HashStable_NoContext};
24use tracing::debug;
25
26use crate::data_structures::HashMap;
27
28mod stack;
29use stack::{Stack, StackDepth, StackEntry};
30mod global_cache;
31use global_cache::CacheData;
32pub use global_cache::GlobalCache;
33
34/// The search graph does not simply use `Interner` directly
35/// to enable its fuzzing without having to stub the rest of
36/// the interner. We don't make this a super trait of `Interner`
37/// as users of the shared type library shouldn't have to care
38/// about `Input` and `Result` as they are implementation details
39/// of the search graph.
40pub trait Cx: Copy {
41    type Input: Debug + Eq + Hash + Copy;
42    type Result: Debug + Eq + Hash + Copy;
43
44    type DepNodeIndex;
45    type Tracked<T: Debug + Clone>: Debug;
46    fn mk_tracked<T: Debug + Clone>(
47        self,
48        data: T,
49        dep_node_index: Self::DepNodeIndex,
50    ) -> Self::Tracked<T>;
51    fn get_tracked<T: Debug + Clone>(self, tracked: &Self::Tracked<T>) -> T;
52    fn with_cached_task<T>(self, task: impl FnOnce() -> T) -> (T, Self::DepNodeIndex);
53
54    fn with_global_cache<R>(self, f: impl FnOnce(&mut GlobalCache<Self>) -> R) -> R;
55
56    fn evaluation_is_concurrent(&self) -> bool;
57}
58
59pub trait Delegate {
60    type Cx: Cx;
61    /// Whether to use the provisional cache. Set to `false` by a fuzzer when
62    /// validating the search graph.
63    const ENABLE_PROVISIONAL_CACHE: bool;
64    type ValidationScope;
65    /// Returning `Some` disables the global cache for the current goal.
66    ///
67    /// The `ValidationScope` is used when fuzzing the search graph to track
68    /// for which goals the global cache has been disabled. This is necessary
69    /// as we may otherwise ignore the global cache entry for some goal `G`
70    /// only to later use it, failing to detect a cycle goal and potentially
71    /// changing the result.
72    fn enter_validation_scope(
73        cx: Self::Cx,
74        input: <Self::Cx as Cx>::Input,
75    ) -> Option<Self::ValidationScope>;
76
77    const FIXPOINT_STEP_LIMIT: usize;
78
79    type ProofTreeBuilder;
80    fn inspect_is_noop(inspect: &mut Self::ProofTreeBuilder) -> bool;
81
82    const DIVIDE_AVAILABLE_DEPTH_ON_OVERFLOW: usize;
83
84    fn initial_provisional_result(
85        cx: Self::Cx,
86        kind: PathKind,
87        input: <Self::Cx as Cx>::Input,
88    ) -> <Self::Cx as Cx>::Result;
89    fn is_initial_provisional_result(
90        cx: Self::Cx,
91        kind: PathKind,
92        input: <Self::Cx as Cx>::Input,
93        result: <Self::Cx as Cx>::Result,
94    ) -> bool;
95    fn on_stack_overflow(
96        cx: Self::Cx,
97        inspect: &mut Self::ProofTreeBuilder,
98        input: <Self::Cx as Cx>::Input,
99    ) -> <Self::Cx as Cx>::Result;
100    fn on_fixpoint_overflow(
101        cx: Self::Cx,
102        input: <Self::Cx as Cx>::Input,
103    ) -> <Self::Cx as Cx>::Result;
104
105    fn is_ambiguous_result(result: <Self::Cx as Cx>::Result) -> bool;
106    fn propagate_ambiguity(
107        cx: Self::Cx,
108        for_input: <Self::Cx as Cx>::Input,
109        from_result: <Self::Cx as Cx>::Result,
110    ) -> <Self::Cx as Cx>::Result;
111}
112
113/// In the initial iteration of a cycle, we do not yet have a provisional
114/// result. In the case we return an initial provisional result depending
115/// on the kind of cycle.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117#[cfg_attr(
118    feature = "nightly",
119    derive(Decodable_NoContext, Encodable_NoContext, HashStable_NoContext)
120)]
121pub enum PathKind {
122    /// A path consisting of only inductive/unproductive steps. Their initial
123    /// provisional result is `Err(NoSolution)`. We currently treat them as
124    /// `PathKind::Unknown` during coherence until we're fully confident in
125    /// our approach.
126    Inductive,
127    /// A path which is not be coinductive right now but we may want
128    /// to change of them to be so in the future. We return an ambiguous
129    /// result in this case to prevent people from relying on this.
130    Unknown,
131    /// A path with at least one coinductive step. Such cycles hold.
132    Coinductive,
133}
134
135impl PathKind {
136    /// Returns the path kind when merging `self` with `rest`.
137    ///
138    /// Given an inductive path `self` and a coinductive path `rest`,
139    /// the path `self -> rest` would be coinductive.
140    ///
141    /// This operation represents an ordering and would be equivalent
142    /// to `max(self, rest)`.
143    fn extend(self, rest: PathKind) -> PathKind {
144        match (self, rest) {
145            (PathKind::Coinductive, _) | (_, PathKind::Coinductive) => PathKind::Coinductive,
146            (PathKind::Unknown, _) | (_, PathKind::Unknown) => PathKind::Unknown,
147            (PathKind::Inductive, PathKind::Inductive) => PathKind::Inductive,
148        }
149    }
150}
151
152/// The kinds of cycles a cycle head was involved in.
153///
154/// This is used to avoid rerunning a cycle if there's
155/// just a single usage kind and the final result matches
156/// its provisional result.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum UsageKind {
159    Single(PathKind),
160    Mixed,
161}
162impl From<PathKind> for UsageKind {
163    fn from(path: PathKind) -> UsageKind {
164        UsageKind::Single(path)
165    }
166}
167impl UsageKind {
168    #[must_use]
169    fn merge(self, other: impl Into<Self>) -> Self {
170        match (self, other.into()) {
171            (UsageKind::Mixed, _) | (_, UsageKind::Mixed) => UsageKind::Mixed,
172            (UsageKind::Single(lhs), UsageKind::Single(rhs)) => {
173                if lhs == rhs {
174                    UsageKind::Single(lhs)
175                } else {
176                    UsageKind::Mixed
177                }
178            }
179        }
180    }
181}
182
183/// For each goal we track whether the paths from this goal
184/// to its cycle heads are coinductive.
185///
186/// This is a necessary condition to rebase provisional cache
187/// entries.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum AllPathsToHeadCoinductive {
190    Yes,
191    No,
192}
193impl From<PathKind> for AllPathsToHeadCoinductive {
194    fn from(path: PathKind) -> AllPathsToHeadCoinductive {
195        match path {
196            PathKind::Coinductive => AllPathsToHeadCoinductive::Yes,
197            _ => AllPathsToHeadCoinductive::No,
198        }
199    }
200}
201impl AllPathsToHeadCoinductive {
202    #[must_use]
203    fn merge(self, other: impl Into<Self>) -> Self {
204        match (self, other.into()) {
205            (AllPathsToHeadCoinductive::Yes, AllPathsToHeadCoinductive::Yes) => {
206                AllPathsToHeadCoinductive::Yes
207            }
208            (AllPathsToHeadCoinductive::No, _) | (_, AllPathsToHeadCoinductive::No) => {
209                AllPathsToHeadCoinductive::No
210            }
211        }
212    }
213    fn and_merge(&mut self, other: impl Into<Self>) {
214        *self = self.merge(other);
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219struct AvailableDepth(usize);
220impl AvailableDepth {
221    /// Returns the remaining depth allowed for nested goals.
222    ///
223    /// This is generally simply one less than the current depth.
224    /// However, if we encountered overflow, we significantly reduce
225    /// the remaining depth of all nested goals to prevent hangs
226    /// in case there is exponential blowup.
227    fn allowed_depth_for_nested<D: Delegate>(
228        root_depth: AvailableDepth,
229        stack: &Stack<D::Cx>,
230    ) -> Option<AvailableDepth> {
231        if let Some(last) = stack.last() {
232            if last.available_depth.0 == 0 {
233                return None;
234            }
235
236            Some(if last.encountered_overflow {
237                AvailableDepth(last.available_depth.0 / D::DIVIDE_AVAILABLE_DEPTH_ON_OVERFLOW)
238            } else {
239                AvailableDepth(last.available_depth.0 - 1)
240            })
241        } else {
242            Some(root_depth)
243        }
244    }
245
246    /// Whether we're allowed to use a global cache entry which required
247    /// the given depth.
248    fn cache_entry_is_applicable(self, additional_depth: usize) -> bool {
249        self.0 >= additional_depth
250    }
251}
252
253/// All cycle heads a given goal depends on, ordered by their stack depth.
254///
255/// We also track all paths from this goal to that head. This is necessary
256/// when rebasing provisional cache results.
257#[derive(Clone, Debug, PartialEq, Eq, Default)]
258struct CycleHeads {
259    heads: BTreeMap<StackDepth, AllPathsToHeadCoinductive>,
260}
261
262impl CycleHeads {
263    fn is_empty(&self) -> bool {
264        self.heads.is_empty()
265    }
266
267    fn highest_cycle_head(&self) -> StackDepth {
268        self.opt_highest_cycle_head().unwrap()
269    }
270
271    fn opt_highest_cycle_head(&self) -> Option<StackDepth> {
272        self.heads.last_key_value().map(|(k, _)| *k)
273    }
274
275    fn opt_lowest_cycle_head(&self) -> Option<StackDepth> {
276        self.heads.first_key_value().map(|(k, _)| *k)
277    }
278
279    fn remove_highest_cycle_head(&mut self) {
280        let last = self.heads.pop_last();
281        debug_assert_ne!(last, None);
282    }
283
284    fn insert(
285        &mut self,
286        head: StackDepth,
287        path_from_entry: impl Into<AllPathsToHeadCoinductive> + Copy,
288    ) {
289        self.heads.entry(head).or_insert(path_from_entry.into()).and_merge(path_from_entry);
290    }
291
292    fn merge(&mut self, heads: &CycleHeads) {
293        for (&head, &path_from_entry) in heads.heads.iter() {
294            self.insert(head, path_from_entry);
295            debug_assert!(matches!(self.heads[&head], AllPathsToHeadCoinductive::Yes));
296        }
297    }
298
299    fn iter(&self) -> impl Iterator<Item = (StackDepth, AllPathsToHeadCoinductive)> + '_ {
300        self.heads.iter().map(|(k, v)| (*k, *v))
301    }
302
303    /// Update the cycle heads of a goal at depth `this` given the cycle heads
304    /// of a nested goal. This merges the heads after filtering the parent goal
305    /// itself.
306    fn extend_from_child(&mut self, this: StackDepth, step_kind: PathKind, child: &CycleHeads) {
307        for (&head, &path_from_entry) in child.heads.iter() {
308            match head.cmp(&this) {
309                Ordering::Less => {}
310                Ordering::Equal => continue,
311                Ordering::Greater => unreachable!(),
312            }
313
314            let path_from_entry = match step_kind {
315                PathKind::Coinductive => AllPathsToHeadCoinductive::Yes,
316                PathKind::Unknown | PathKind::Inductive => path_from_entry,
317            };
318
319            self.insert(head, path_from_entry);
320        }
321    }
322}
323
324bitflags::bitflags! {
325    /// Tracks how nested goals have been accessed. This is necessary to disable
326    /// global cache entries if computing them would otherwise result in a cycle or
327    /// access a provisional cache entry.
328    #[derive(Debug, Clone, Copy)]
329    pub struct PathsToNested: u8 {
330        /// The initial value when adding a goal to its own nested goals.
331        const EMPTY                      = 1 << 0;
332        const INDUCTIVE                  = 1 << 1;
333        const UNKNOWN                    = 1 << 2;
334        const COINDUCTIVE                = 1 << 3;
335    }
336}
337impl From<PathKind> for PathsToNested {
338    fn from(path: PathKind) -> PathsToNested {
339        match path {
340            PathKind::Inductive => PathsToNested::INDUCTIVE,
341            PathKind::Unknown => PathsToNested::UNKNOWN,
342            PathKind::Coinductive => PathsToNested::COINDUCTIVE,
343        }
344    }
345}
346impl PathsToNested {
347    /// The implementation of this function is kind of ugly. We check whether
348    /// there currently exist 'weaker' paths in the set, if so we upgrade these
349    /// paths to at least `path`.
350    #[must_use]
351    fn extend_with(mut self, path: PathKind) -> Self {
352        match path {
353            PathKind::Inductive => {
354                if self.intersects(PathsToNested::EMPTY) {
355                    self.remove(PathsToNested::EMPTY);
356                    self.insert(PathsToNested::INDUCTIVE);
357                }
358            }
359            PathKind::Unknown => {
360                if self.intersects(PathsToNested::EMPTY | PathsToNested::INDUCTIVE) {
361                    self.remove(PathsToNested::EMPTY | PathsToNested::INDUCTIVE);
362                    self.insert(PathsToNested::UNKNOWN);
363                }
364            }
365            PathKind::Coinductive => {
366                if self.intersects(
367                    PathsToNested::EMPTY | PathsToNested::INDUCTIVE | PathsToNested::UNKNOWN,
368                ) {
369                    self.remove(
370                        PathsToNested::EMPTY | PathsToNested::INDUCTIVE | PathsToNested::UNKNOWN,
371                    );
372                    self.insert(PathsToNested::COINDUCTIVE);
373                }
374            }
375        }
376
377        self
378    }
379}
380
381/// The nested goals of each stack entry and the path from the
382/// stack entry to that nested goal.
383///
384/// They are used when checking whether reevaluating a global cache
385/// would encounter a cycle or use a provisional cache entry given the
386/// currentl search graph state. We need to disable the global cache
387/// in this case as it could otherwise result in behaviorial differences.
388/// Cycles can impact behavior. The cycle ABA may have different final
389/// results from a the cycle BAB depending on the cycle root.
390///
391/// We only start tracking nested goals once we've either encountered
392/// overflow or a solver cycle. This is a performance optimization to
393/// avoid tracking nested goals on the happy path.
394#[derive_where(Debug, Default, Clone; X: Cx)]
395struct NestedGoals<X: Cx> {
396    nested_goals: HashMap<X::Input, PathsToNested>,
397}
398impl<X: Cx> NestedGoals<X> {
399    fn is_empty(&self) -> bool {
400        self.nested_goals.is_empty()
401    }
402
403    fn insert(&mut self, input: X::Input, paths_to_nested: PathsToNested) {
404        match self.nested_goals.entry(input) {
405            Entry::Occupied(mut entry) => *entry.get_mut() |= paths_to_nested,
406            Entry::Vacant(entry) => drop(entry.insert(paths_to_nested)),
407        }
408    }
409
410    /// Adds the nested goals of a nested goal, given that the path `step_kind` from this goal
411    /// to the parent goal.
412    ///
413    /// If the path from this goal to the nested goal is inductive, the paths from this goal
414    /// to all nested goals of that nested goal are also inductive. Otherwise the paths are
415    /// the same as for the child.
416    fn extend_from_child(&mut self, step_kind: PathKind, nested_goals: &NestedGoals<X>) {
417        #[allow(rustc::potential_query_instability)]
418        for (input, paths_to_nested) in nested_goals.iter() {
419            let paths_to_nested = paths_to_nested.extend_with(step_kind);
420            self.insert(input, paths_to_nested);
421        }
422    }
423
424    #[cfg_attr(feature = "nightly", rustc_lint_query_instability)]
425    #[allow(rustc::potential_query_instability)]
426    fn iter(&self) -> impl Iterator<Item = (X::Input, PathsToNested)> + '_ {
427        self.nested_goals.iter().map(|(i, p)| (*i, *p))
428    }
429
430    fn contains(&self, input: X::Input) -> bool {
431        self.nested_goals.contains_key(&input)
432    }
433}
434
435/// A provisional result of an already computed goals which depends on other
436/// goals still on the stack.
437#[derive_where(Debug; X: Cx)]
438struct ProvisionalCacheEntry<X: Cx> {
439    /// Whether evaluating the goal encountered overflow. This is used to
440    /// disable the cache entry except if the last goal on the stack is
441    /// already involved in this cycle.
442    encountered_overflow: bool,
443    /// All cycle heads this cache entry depends on.
444    heads: CycleHeads,
445    /// The path from the highest cycle head to this goal. This differs from
446    /// `heads` which tracks the path to the cycle head *from* this goal.
447    path_from_head: PathKind,
448    result: X::Result,
449}
450
451/// The final result of evaluating a goal.
452///
453/// We reset `encountered_overflow` when reevaluating a goal,
454/// but need to track whether we've hit the recursion limit at
455/// all for correctness.
456///
457/// We've previously simply returned the final `StackEntry` but this
458/// made it easy to accidentally drop information from the previous
459/// evaluation.
460#[derive_where(Debug; X: Cx)]
461struct EvaluationResult<X: Cx> {
462    encountered_overflow: bool,
463    required_depth: usize,
464    heads: CycleHeads,
465    nested_goals: NestedGoals<X>,
466    result: X::Result,
467}
468
469impl<X: Cx> EvaluationResult<X> {
470    fn finalize(
471        final_entry: StackEntry<X>,
472        encountered_overflow: bool,
473        result: X::Result,
474    ) -> EvaluationResult<X> {
475        EvaluationResult {
476            encountered_overflow,
477            // Unlike `encountered_overflow`, we share `heads`, `required_depth`,
478            // and `nested_goals` between evaluations.
479            required_depth: final_entry.required_depth,
480            heads: final_entry.heads,
481            nested_goals: final_entry.nested_goals,
482            // We only care about the final result.
483            result,
484        }
485    }
486}
487
488pub struct SearchGraph<D: Delegate<Cx = X>, X: Cx = <D as Delegate>::Cx> {
489    root_depth: AvailableDepth,
490    /// The stack of goals currently being computed.
491    ///
492    /// An element is *deeper* in the stack if its index is *lower*.
493    stack: Stack<X>,
494    /// The provisional cache contains entries for already computed goals which
495    /// still depend on goals higher-up in the stack. We don't move them to the
496    /// global cache and track them locally instead. A provisional cache entry
497    /// is only valid until the result of one of its cycle heads changes.
498    provisional_cache: HashMap<X::Input, Vec<ProvisionalCacheEntry<X>>>,
499
500    _marker: PhantomData<D>,
501}
502
503/// While [`SearchGraph::update_parent_goal`] can be mostly shared between
504/// ordinary nested goals/global cache hits and provisional cache hits,
505/// using the provisional cache should not add any nested goals.
506///
507/// `nested_goals` are only used when checking whether global cache entries
508/// are applicable. This only cares about whether a goal is actually accessed.
509/// Given that the usage of the provisional cache is fully determinstic, we
510/// don't need to track the nested goals used while computing a provisional
511/// cache entry.
512enum UpdateParentGoalCtxt<'a, X: Cx> {
513    Ordinary(&'a NestedGoals<X>),
514    ProvisionalCacheHit,
515}
516
517impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
518    pub fn new(root_depth: usize) -> SearchGraph<D> {
519        Self {
520            root_depth: AvailableDepth(root_depth),
521            stack: Default::default(),
522            provisional_cache: Default::default(),
523            _marker: PhantomData,
524        }
525    }
526
527    /// Lazily update the stack entry for the parent goal.
528    /// This behavior is shared between actually evaluating goals
529    /// and using existing global cache entries to make sure they
530    /// have the same impact on the remaining evaluation.
531    fn update_parent_goal(
532        stack: &mut Stack<X>,
533        step_kind_from_parent: PathKind,
534        required_depth_for_nested: usize,
535        heads: &CycleHeads,
536        encountered_overflow: bool,
537        context: UpdateParentGoalCtxt<'_, X>,
538    ) {
539        if let Some(parent_index) = stack.last_index() {
540            let parent = &mut stack[parent_index];
541            parent.required_depth = parent.required_depth.max(required_depth_for_nested + 1);
542            parent.encountered_overflow |= encountered_overflow;
543
544            parent.heads.extend_from_child(parent_index, step_kind_from_parent, heads);
545            let parent_depends_on_cycle = match context {
546                UpdateParentGoalCtxt::Ordinary(nested_goals) => {
547                    parent.nested_goals.extend_from_child(step_kind_from_parent, nested_goals);
548                    !nested_goals.is_empty()
549                }
550                UpdateParentGoalCtxt::ProvisionalCacheHit => true,
551            };
552            // Once we've got goals which encountered overflow or a cycle,
553            // we track all goals whose behavior may depend depend on these
554            // goals as this change may cause them to now depend on additional
555            // goals, resulting in new cycles. See the dev-guide for examples.
556            if parent_depends_on_cycle {
557                parent.nested_goals.insert(parent.input, PathsToNested::EMPTY);
558            }
559        }
560    }
561
562    pub fn is_empty(&self) -> bool {
563        if self.stack.is_empty() {
564            debug_assert!(self.provisional_cache.is_empty());
565            true
566        } else {
567            false
568        }
569    }
570
571    /// The number of goals currently in the search graph. This should only be
572    /// used for debugging purposes.
573    pub fn debug_current_depth(&self) -> usize {
574        self.stack.len()
575    }
576
577    /// Whether the path from `head` to the current stack entry is inductive or coinductive.
578    ///
579    /// The `step_kind_to_head` is used to add a single additional path segment to the path on
580    /// the stack which completes the cycle. This given an inductive step AB which then cycles
581    /// coinductively with A, we need to treat this cycle as coinductive.
582    fn cycle_path_kind(
583        stack: &Stack<X>,
584        step_kind_to_head: PathKind,
585        head: StackDepth,
586    ) -> PathKind {
587        stack.cycle_step_kinds(head).fold(step_kind_to_head, |curr, step| curr.extend(step))
588    }
589
590    /// Probably the most involved method of the whole solver.
591    ///
592    /// Given some goal which is proven via the `prove_goal` closure, this
593    /// handles caching, overflow, and coinductive cycles.
594    pub fn with_new_goal(
595        &mut self,
596        cx: X,
597        input: X::Input,
598        step_kind_from_parent: PathKind,
599        inspect: &mut D::ProofTreeBuilder,
600        evaluate_goal: impl Fn(&mut Self, X, X::Input, &mut D::ProofTreeBuilder) -> X::Result + Copy,
601    ) -> X::Result {
602        let Some(available_depth) =
603            AvailableDepth::allowed_depth_for_nested::<D>(self.root_depth, &self.stack)
604        else {
605            return self.handle_overflow(cx, input, inspect);
606        };
607
608        // We check the provisional cache before checking the global cache. This simplifies
609        // the implementation as we can avoid worrying about cases where both the global and
610        // provisional cache may apply, e.g. consider the following example
611        //
612        // - xxBA overflow
613        // - A
614        //     - BA cycle
615        //     - CB :x:
616        if let Some(result) = self.lookup_provisional_cache(input, step_kind_from_parent) {
617            return result;
618        }
619
620        // Lookup the global cache unless we're building proof trees or are currently
621        // fuzzing.
622        let validate_cache = if !D::inspect_is_noop(inspect) {
623            None
624        } else if let Some(scope) = D::enter_validation_scope(cx, input) {
625            // When validating the global cache we need to track the goals for which the
626            // global cache has been disabled as it may otherwise change the result for
627            // cyclic goals. We don't care about goals which are not on the current stack
628            // so it's fine to drop their scope eagerly.
629            self.lookup_global_cache_untracked(cx, input, step_kind_from_parent, available_depth)
630                .inspect(|expected| debug!(?expected, "validate cache entry"))
631                .map(|r| (scope, r))
632        } else if let Some(result) =
633            self.lookup_global_cache(cx, input, step_kind_from_parent, available_depth)
634        {
635            return result;
636        } else {
637            None
638        };
639
640        // Detect cycles on the stack. We do this after the global cache lookup to
641        // avoid iterating over the stack in case a goal has already been computed.
642        // This may not have an actual performance impact and we could reorder them
643        // as it may reduce the number of `nested_goals` we need to track.
644        if let Some(result) = self.check_cycle_on_stack(cx, input, step_kind_from_parent) {
645            debug_assert!(validate_cache.is_none(), "global cache and cycle on stack: {input:?}");
646            return result;
647        }
648
649        // Unfortunate, it looks like we actually have to compute this goal.
650        self.stack.push(StackEntry {
651            input,
652            step_kind_from_parent,
653            available_depth,
654            provisional_result: None,
655            required_depth: 0,
656            heads: Default::default(),
657            encountered_overflow: false,
658            has_been_used: None,
659            nested_goals: Default::default(),
660        });
661
662        // This is for global caching, so we properly track query dependencies.
663        // Everything that affects the `result` should be performed within this
664        // `with_cached_task` closure. If computing this goal depends on something
665        // not tracked by the cache key and from outside of this anon task, it
666        // must not be added to the global cache. Notably, this is the case for
667        // trait solver cycles participants.
668        let (evaluation_result, dep_node) =
669            cx.with_cached_task(|| self.evaluate_goal_in_task(cx, input, inspect, evaluate_goal));
670
671        // We've finished computing the goal and have popped it from the stack,
672        // lazily update its parent goal.
673        Self::update_parent_goal(
674            &mut self.stack,
675            step_kind_from_parent,
676            evaluation_result.required_depth,
677            &evaluation_result.heads,
678            evaluation_result.encountered_overflow,
679            UpdateParentGoalCtxt::Ordinary(&evaluation_result.nested_goals),
680        );
681        let result = evaluation_result.result;
682
683        // We're now done with this goal. We only add the root of cycles to the global cache.
684        // In case this goal is involved in a larger cycle add it to the provisional cache.
685        if evaluation_result.heads.is_empty() {
686            if let Some((_scope, expected)) = validate_cache {
687                // Do not try to move a goal into the cache again if we're testing
688                // the global cache.
689                assert_eq!(evaluation_result.result, expected, "input={input:?}");
690            } else if D::inspect_is_noop(inspect) {
691                self.insert_global_cache(cx, input, evaluation_result, dep_node)
692            }
693        } else if D::ENABLE_PROVISIONAL_CACHE {
694            debug_assert!(validate_cache.is_none(), "unexpected non-root: {input:?}");
695            let entry = self.provisional_cache.entry(input).or_default();
696            let EvaluationResult {
697                encountered_overflow,
698                required_depth: _,
699                heads,
700                nested_goals: _,
701                result,
702            } = evaluation_result;
703            let path_from_head = Self::cycle_path_kind(
704                &self.stack,
705                step_kind_from_parent,
706                heads.highest_cycle_head(),
707            );
708            let provisional_cache_entry =
709                ProvisionalCacheEntry { encountered_overflow, heads, path_from_head, result };
710            debug!(?provisional_cache_entry);
711            entry.push(provisional_cache_entry);
712        } else {
713            debug_assert!(validate_cache.is_none(), "unexpected non-root: {input:?}");
714        }
715
716        result
717    }
718
719    fn handle_overflow(
720        &mut self,
721        cx: X,
722        input: X::Input,
723        inspect: &mut D::ProofTreeBuilder,
724    ) -> X::Result {
725        if let Some(last) = self.stack.last_mut() {
726            last.encountered_overflow = true;
727            // If computing a goal `B` depends on another goal `A` and
728            // `A` has a nested goal which overflows, then computing `B`
729            // at the same depth, but with `A` already on the stack,
730            // would encounter a solver cycle instead, potentially
731            // changing the result.
732            //
733            // We must therefore not use the global cache entry for `B` in that case.
734            // See tests/ui/traits/next-solver/cycles/hidden-by-overflow.rs
735            last.nested_goals.insert(last.input, PathsToNested::EMPTY);
736        }
737
738        debug!("encountered stack overflow");
739        D::on_stack_overflow(cx, inspect, input)
740    }
741
742    /// When reevaluating a goal with a changed provisional result, all provisional cache entry
743    /// which depend on this goal get invalidated.
744    fn clear_dependent_provisional_results(&mut self) {
745        let head = self.stack.next_index();
746        #[allow(rustc::potential_query_instability)]
747        self.provisional_cache.retain(|_, entries| {
748            entries.retain(|entry| entry.heads.highest_cycle_head() != head);
749            !entries.is_empty()
750        });
751    }
752
753    /// A necessary optimization to handle complex solver cycles. A provisional cache entry
754    /// relies on a set of cycle heads and the path towards these heads. When popping a cycle
755    /// head from the stack after we've finished computing it, we can't be sure that the
756    /// provisional cache entry is still applicable. We need to keep the cache entries to
757    /// prevent hangs.
758    ///
759    /// What we therefore do is check whether the cycle kind of all cycles the goal of a
760    /// provisional cache entry is involved in would stay the same when computing the
761    /// goal without its cycle head on the stack. For more details, see the relevant
762    /// [rustc-dev-guide chapter](https://rustc-dev-guide.rust-lang.org/solve/caching.html).
763    ///
764    /// This can be thought of rotating the sub-tree of this provisional result and changing
765    /// its entry point while making sure that all paths through this sub-tree stay the same.
766    ///
767    /// In case the popped cycle head failed to reach a fixpoint anything which depends on
768    /// its provisional result is invalid. Actually discarding provisional cache entries in
769    /// this case would cause hangs, so we instead change the result of dependant provisional
770    /// cache entries to also be ambiguous. This causes some undesirable ambiguity for nested
771    /// goals whose result doesn't actually depend on this cycle head, but that's acceptable
772    /// to me.
773    fn rebase_provisional_cache_entries(
774        &mut self,
775        stack_entry: &StackEntry<X>,
776        mut mutate_result: impl FnMut(X::Input, X::Result) -> X::Result,
777    ) {
778        let head = self.stack.next_index();
779        #[allow(rustc::potential_query_instability)]
780        self.provisional_cache.retain(|&input, entries| {
781            entries.retain_mut(|entry| {
782                let ProvisionalCacheEntry {
783                    encountered_overflow: _,
784                    heads,
785                    path_from_head,
786                    result,
787                } = entry;
788                if heads.highest_cycle_head() == head {
789                    heads.remove_highest_cycle_head()
790                } else {
791                    return true;
792                }
793
794                // We only try to rebase if all paths from the cache entry
795                // to its heads are coinductive. In this case these cycle
796                // kinds won't change, no matter the goals between these
797                // heads and the provisional cache entry.
798                if heads.iter().any(|(_, p)| matches!(p, AllPathsToHeadCoinductive::No)) {
799                    return false;
800                }
801
802                // The same for nested goals of the cycle head.
803                if stack_entry.heads.iter().any(|(_, p)| matches!(p, AllPathsToHeadCoinductive::No))
804                {
805                    return false;
806                }
807
808                // Merge the cycle heads of the provisional cache entry and the
809                // popped head. If the popped cycle head was a root, discard all
810                // provisional cache entries which depend on it.
811                heads.merge(&stack_entry.heads);
812                let Some(head) = heads.opt_highest_cycle_head() else {
813                    return false;
814                };
815
816                // We now care about the path from the next highest cycle head to the
817                // provisional cache entry.
818                *path_from_head = path_from_head.extend(Self::cycle_path_kind(
819                    &self.stack,
820                    stack_entry.step_kind_from_parent,
821                    head,
822                ));
823                // Mutate the result of the provisional cache entry in case we did
824                // not reach a fixpoint.
825                *result = mutate_result(input, *result);
826                true
827            });
828            !entries.is_empty()
829        });
830    }
831
832    fn lookup_provisional_cache(
833        &mut self,
834        input: X::Input,
835        step_kind_from_parent: PathKind,
836    ) -> Option<X::Result> {
837        if !D::ENABLE_PROVISIONAL_CACHE {
838            return None;
839        }
840
841        let entries = self.provisional_cache.get(&input)?;
842        for &ProvisionalCacheEntry { encountered_overflow, ref heads, path_from_head, result } in
843            entries
844        {
845            let head = heads.highest_cycle_head();
846            if encountered_overflow {
847                // This check is overly strict and very subtle. We need to make sure that if
848                // a global cache entry depends on some goal without adding it to its
849                // `nested_goals`, that goal must never have an applicable provisional
850                // cache entry to avoid incorrectly applying the cache entry.
851                //
852                // As we'd have to otherwise track literally all nested goals, we only
853                // apply provisional cache entries which encountered overflow once the
854                // current goal is already part of the same cycle. This check could be
855                // improved but seems to be good enough for now.
856                let last = self.stack.last().unwrap();
857                if last.heads.opt_lowest_cycle_head().is_none_or(|lowest| lowest > head) {
858                    continue;
859                }
860            }
861
862            // A provisional cache entry is only valid if the current path from its
863            // highest cycle head to the goal is the same.
864            if path_from_head == Self::cycle_path_kind(&self.stack, step_kind_from_parent, head) {
865                Self::update_parent_goal(
866                    &mut self.stack,
867                    step_kind_from_parent,
868                    0,
869                    heads,
870                    encountered_overflow,
871                    UpdateParentGoalCtxt::ProvisionalCacheHit,
872                );
873                debug_assert!(self.stack[head].has_been_used.is_some());
874                debug!(?head, ?path_from_head, "provisional cache hit");
875                return Some(result);
876            }
877        }
878
879        None
880    }
881
882    /// Even if there is a global cache entry for a given goal, we need to make sure
883    /// evaluating this entry would not have ended up depending on either a goal
884    /// already on the stack or a provisional cache entry.
885    fn candidate_is_applicable(
886        &self,
887        step_kind_from_parent: PathKind,
888        nested_goals: &NestedGoals<X>,
889    ) -> bool {
890        // If the global cache entry didn't depend on any nested goals, it always
891        // applies.
892        if nested_goals.is_empty() {
893            return true;
894        }
895
896        // If a nested goal of the global cache entry is on the stack, we would
897        // definitely encounter a cycle.
898        if self.stack.iter().any(|e| nested_goals.contains(e.input)) {
899            debug!("cache entry not applicable due to stack");
900            return false;
901        }
902
903        // The global cache entry is also invalid if there's a provisional cache entry
904        // would apply for any of its nested goals.
905        #[allow(rustc::potential_query_instability)]
906        for (input, path_from_global_entry) in nested_goals.iter() {
907            let Some(entries) = self.provisional_cache.get(&input) else {
908                continue;
909            };
910
911            debug!(?input, ?path_from_global_entry, ?entries, "candidate_is_applicable");
912            // A provisional cache entry is applicable if the path to
913            // its highest cycle head is equal to the expected path.
914            for &ProvisionalCacheEntry {
915                encountered_overflow,
916                ref heads,
917                path_from_head: head_to_provisional,
918                result: _,
919            } in entries.iter()
920            {
921                // We don't have to worry about provisional cache entries which encountered
922                // overflow, see the relevant comment in `lookup_provisional_cache`.
923                if encountered_overflow {
924                    continue;
925                }
926
927                // A provisional cache entry only applies if the path from its highest head
928                // matches the path when encountering the goal.
929                //
930                // We check if any of the paths taken while computing the global goal
931                // would end up with an applicable provisional cache entry.
932                let head = heads.highest_cycle_head();
933                let head_to_curr = Self::cycle_path_kind(&self.stack, step_kind_from_parent, head);
934                let full_paths = path_from_global_entry.extend_with(head_to_curr);
935                if full_paths.contains(head_to_provisional.into()) {
936                    debug!(
937                        ?full_paths,
938                        ?head_to_provisional,
939                        "cache entry not applicable due to matching paths"
940                    );
941                    return false;
942                }
943            }
944        }
945
946        true
947    }
948
949    /// Used when fuzzing the global cache. Accesses the global cache without
950    /// updating the state of the search graph.
951    fn lookup_global_cache_untracked(
952        &self,
953        cx: X,
954        input: X::Input,
955        step_kind_from_parent: PathKind,
956        available_depth: AvailableDepth,
957    ) -> Option<X::Result> {
958        cx.with_global_cache(|cache| {
959            cache
960                .get(cx, input, available_depth, |nested_goals| {
961                    self.candidate_is_applicable(step_kind_from_parent, nested_goals)
962                })
963                .map(|c| c.result)
964        })
965    }
966
967    /// Try to fetch a previously computed result from the global cache,
968    /// making sure to only do so if it would match the result of reevaluating
969    /// this goal.
970    fn lookup_global_cache(
971        &mut self,
972        cx: X,
973        input: X::Input,
974        step_kind_from_parent: PathKind,
975        available_depth: AvailableDepth,
976    ) -> Option<X::Result> {
977        cx.with_global_cache(|cache| {
978            let CacheData { result, required_depth, encountered_overflow, nested_goals } = cache
979                .get(cx, input, available_depth, |nested_goals| {
980                    self.candidate_is_applicable(step_kind_from_parent, nested_goals)
981                })?;
982
983            // We don't move cycle participants to the global cache, so the
984            // cycle heads are always empty.
985            let heads = Default::default();
986            Self::update_parent_goal(
987                &mut self.stack,
988                step_kind_from_parent,
989                required_depth,
990                &heads,
991                encountered_overflow,
992                UpdateParentGoalCtxt::Ordinary(nested_goals),
993            );
994
995            debug!(?required_depth, "global cache hit");
996            Some(result)
997        })
998    }
999
1000    fn check_cycle_on_stack(
1001        &mut self,
1002        cx: X,
1003        input: X::Input,
1004        step_kind_from_parent: PathKind,
1005    ) -> Option<X::Result> {
1006        let head = self.stack.find(input)?;
1007        // We have a nested goal which directly relies on a goal deeper in the stack.
1008        //
1009        // We start by tagging all cycle participants, as that's necessary for caching.
1010        //
1011        // Finally we can return either the provisional response or the initial response
1012        // in case we're in the first fixpoint iteration for this goal.
1013        let path_kind = Self::cycle_path_kind(&self.stack, step_kind_from_parent, head);
1014        debug!(?path_kind, "encountered cycle with depth {head:?}");
1015        let usage_kind = UsageKind::Single(path_kind);
1016        self.stack[head].has_been_used =
1017            Some(self.stack[head].has_been_used.map_or(usage_kind, |prev| prev.merge(usage_kind)));
1018
1019        // Subtle: when encountering a cyclic goal, we still first checked for overflow,
1020        // so we have to update the reached depth.
1021        let last_index = self.stack.last_index().unwrap();
1022        let last = &mut self.stack[last_index];
1023        last.required_depth = last.required_depth.max(1);
1024
1025        last.nested_goals.insert(input, step_kind_from_parent.into());
1026        last.nested_goals.insert(last.input, PathsToNested::EMPTY);
1027        if last_index != head {
1028            last.heads.insert(head, step_kind_from_parent);
1029        }
1030
1031        // Return the provisional result or, if we're in the first iteration,
1032        // start with no constraints.
1033        if let Some(result) = self.stack[head].provisional_result {
1034            Some(result)
1035        } else {
1036            Some(D::initial_provisional_result(cx, path_kind, input))
1037        }
1038    }
1039
1040    /// Whether we've reached a fixpoint when evaluating a cycle head.
1041    fn reached_fixpoint(
1042        &mut self,
1043        cx: X,
1044        stack_entry: &StackEntry<X>,
1045        usage_kind: UsageKind,
1046        result: X::Result,
1047    ) -> bool {
1048        if let Some(prev) = stack_entry.provisional_result {
1049            prev == result
1050        } else if let UsageKind::Single(kind) = usage_kind {
1051            D::is_initial_provisional_result(cx, kind, stack_entry.input, result)
1052        } else {
1053            false
1054        }
1055    }
1056
1057    /// When we encounter a coinductive cycle, we have to fetch the
1058    /// result of that cycle while we are still computing it. Because
1059    /// of this we continuously recompute the cycle until the result
1060    /// of the previous iteration is equal to the final result, at which
1061    /// point we are done.
1062    fn evaluate_goal_in_task(
1063        &mut self,
1064        cx: X,
1065        input: X::Input,
1066        inspect: &mut D::ProofTreeBuilder,
1067        evaluate_goal: impl Fn(&mut Self, X, X::Input, &mut D::ProofTreeBuilder) -> X::Result + Copy,
1068    ) -> EvaluationResult<X> {
1069        // We reset `encountered_overflow` each time we rerun this goal
1070        // but need to make sure we currently propagate it to the global
1071        // cache even if only some of the evaluations actually reach the
1072        // recursion limit.
1073        let mut encountered_overflow = false;
1074        let mut i = 0;
1075        loop {
1076            let result = evaluate_goal(self, cx, input, inspect);
1077            let stack_entry = self.stack.pop();
1078            encountered_overflow |= stack_entry.encountered_overflow;
1079            debug_assert_eq!(stack_entry.input, input);
1080
1081            // If the current goal is not the root of a cycle, we are done.
1082            //
1083            // There are no provisional cache entries which depend on this goal.
1084            let Some(usage_kind) = stack_entry.has_been_used else {
1085                return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
1086            };
1087
1088            // If it is a cycle head, we have to keep trying to prove it until
1089            // we reach a fixpoint. We need to do so for all cycle heads,
1090            // not only for the root.
1091            //
1092            // See tests/ui/traits/next-solver/cycles/fixpoint-rerun-all-cycle-heads.rs
1093            // for an example.
1094            //
1095            // Check whether we reached a fixpoint, either because the final result
1096            // is equal to the provisional result of the previous iteration, or because
1097            // this was only the root of either coinductive or inductive cycles, and the
1098            // final result is equal to the initial response for that case.
1099            if self.reached_fixpoint(cx, &stack_entry, usage_kind, result) {
1100                self.rebase_provisional_cache_entries(&stack_entry, |_, result| result);
1101                return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
1102            }
1103
1104            // If computing this goal results in ambiguity with no constraints,
1105            // we do not rerun it. It's incredibly difficult to get a different
1106            // response in the next iteration in this case. These changes would
1107            // likely either be caused by incompleteness or can change the maybe
1108            // cause from ambiguity to overflow. Returning ambiguity always
1109            // preserves soundness and completeness even if the goal is be known
1110            // to succeed or fail.
1111            //
1112            // This prevents exponential blowup affecting multiple major crates.
1113            // As we only get to this branch if we haven't yet reached a fixpoint,
1114            // we also taint all provisional cache entries which depend on the
1115            // current goal.
1116            if D::is_ambiguous_result(result) {
1117                self.rebase_provisional_cache_entries(&stack_entry, |input, _| {
1118                    D::propagate_ambiguity(cx, input, result)
1119                });
1120                return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
1121            };
1122
1123            // If we've reached the fixpoint step limit, we bail with overflow and taint all
1124            // provisional cache entries which depend on the current goal.
1125            i += 1;
1126            if i >= D::FIXPOINT_STEP_LIMIT {
1127                debug!("canonical cycle overflow");
1128                let result = D::on_fixpoint_overflow(cx, input);
1129                self.rebase_provisional_cache_entries(&stack_entry, |input, _| {
1130                    D::on_fixpoint_overflow(cx, input)
1131                });
1132                return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
1133            }
1134
1135            // Clear all provisional cache entries which depend on a previous provisional
1136            // result of this goal and rerun.
1137            self.clear_dependent_provisional_results();
1138
1139            debug!(?result, "fixpoint changed provisional results");
1140            self.stack.push(StackEntry {
1141                input,
1142                step_kind_from_parent: stack_entry.step_kind_from_parent,
1143                available_depth: stack_entry.available_depth,
1144                provisional_result: Some(result),
1145                // We can keep these goals from previous iterations as they are only
1146                // ever read after finalizing this evaluation.
1147                required_depth: stack_entry.required_depth,
1148                heads: stack_entry.heads,
1149                nested_goals: stack_entry.nested_goals,
1150                // We reset these two fields when rerunning this goal. We could
1151                // keep `encountered_overflow` as it's only used as a performance
1152                // optimization. However, given that the proof tree will likely look
1153                // similar to the previous iterations when reevaluating, it's better
1154                // for caching if the reevaluation also starts out with `false`.
1155                encountered_overflow: false,
1156                has_been_used: None,
1157            });
1158        }
1159    }
1160
1161    /// When encountering a cycle, both inductive and coinductive, we only
1162    /// move the root into the global cache. We also store all other cycle
1163    /// participants involved.
1164    ///
1165    /// We must not use the global cache entry of a root goal if a cycle
1166    /// participant is on the stack. This is necessary to prevent unstable
1167    /// results. See the comment of `StackEntry::nested_goals` for
1168    /// more details.
1169    fn insert_global_cache(
1170        &mut self,
1171        cx: X,
1172        input: X::Input,
1173        evaluation_result: EvaluationResult<X>,
1174        dep_node: X::DepNodeIndex,
1175    ) {
1176        debug!(?evaluation_result, "insert global cache");
1177        cx.with_global_cache(|cache| cache.insert(cx, input, evaluation_result, dep_node))
1178    }
1179}