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}