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