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}