Skip to content

Commit f860873

Browse files
committed
split provisional cache and stack lookup
this makes it easier to maintain and modify going forward. There may be a small performance cost as we now need to access the provisional cache *and* walk through the stack to detect cycles. However, the provisional cache should be mostly empty and the stack should only have a few elements so the performance impact is likely minimal. Given the complexity of the search graph maintainability trumps linear performance improvements.
1 parent 0c75c08 commit f860873

File tree

2 files changed

+104
-118
lines changed

2 files changed

+104
-118
lines changed

compiler/rustc_type_ir/src/search_graph/mod.rs

+101-103
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ struct DetachedEntry<X: Cx> {
224224
result: X::Result,
225225
}
226226

227-
/// Stores the stack depth of a currently evaluated goal *and* already
228-
/// computed results for goals which depend on other goals still on the stack.
227+
/// Stores the provisional result of already computed results for goals which
228+
/// depend on other goals still on the stack.
229229
///
230230
/// The provisional result may depend on whether the stack above it is inductive
231231
/// or coinductive. Because of this, we store separate provisional results for
@@ -238,16 +238,13 @@ struct DetachedEntry<X: Cx> {
238238
/// see tests/ui/traits/next-solver/cycles/provisional-cache-impacts-behavior.rs.
239239
#[derive_where(Default; X: Cx)]
240240
struct ProvisionalCacheEntry<X: Cx> {
241-
stack_depth: Option<StackDepth>,
242241
with_inductive_stack: Option<DetachedEntry<X>>,
243242
with_coinductive_stack: Option<DetachedEntry<X>>,
244243
}
245244

246245
impl<X: Cx> ProvisionalCacheEntry<X> {
247246
fn is_empty(&self) -> bool {
248-
self.stack_depth.is_none()
249-
&& self.with_inductive_stack.is_none()
250-
&& self.with_coinductive_stack.is_none()
247+
self.with_inductive_stack.is_none() && self.with_coinductive_stack.is_none()
251248
}
252249
}
253250

@@ -378,71 +375,26 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
378375
None
379376
};
380377

381-
// Check whether the goal is in the provisional cache.
382-
// The provisional result may rely on the path to its cycle roots,
383-
// so we have to check the path of the current goal matches that of
384-
// the cache entry.
385-
let cache_entry = self.provisional_cache.entry(input).or_default();
386-
if let Some(entry) = cache_entry
387-
.with_coinductive_stack
388-
.as_ref()
389-
.filter(|p| Self::stack_coinductive_from(cx, &self.stack, p.head))
390-
.or_else(|| {
391-
cache_entry
392-
.with_inductive_stack
393-
.as_ref()
394-
.filter(|p| !Self::stack_coinductive_from(cx, &self.stack, p.head))
395-
})
396-
{
397-
debug!("provisional cache hit");
398-
// We have a nested goal which is already in the provisional cache, use
399-
// its result. We do not provide any usage kind as that should have been
400-
// already set correctly while computing the cache entry.
401-
debug_assert!(self.stack[entry.head].has_been_used.is_some());
402-
Self::tag_cycle_participants(&mut self.stack, entry.head);
403-
return entry.result;
404-
} else if let Some(stack_depth) = cache_entry.stack_depth {
405-
debug!("encountered cycle with depth {stack_depth:?}");
406-
// We have a nested goal which directly relies on a goal deeper in the stack.
407-
//
408-
// We start by tagging all cycle participants, as that's necessary for caching.
409-
//
410-
// Finally we can return either the provisional response or the initial response
411-
// in case we're in the first fixpoint iteration for this goal.
412-
let is_coinductive_cycle = Self::stack_coinductive_from(cx, &self.stack, stack_depth);
413-
let cycle_kind =
414-
if is_coinductive_cycle { CycleKind::Coinductive } else { CycleKind::Inductive };
415-
let usage_kind = UsageKind::Single(cycle_kind);
416-
self.stack[stack_depth].has_been_used = Some(
417-
self.stack[stack_depth]
418-
.has_been_used
419-
.map_or(usage_kind, |prev| prev.merge(usage_kind)),
420-
);
421-
Self::tag_cycle_participants(&mut self.stack, stack_depth);
422-
423-
// Return the provisional result or, if we're in the first iteration,
424-
// start with no constraints.
425-
return if let Some(result) = self.stack[stack_depth].provisional_result {
426-
result
427-
} else {
428-
D::initial_provisional_result(cx, cycle_kind, input)
429-
};
430-
} else {
431-
// No entry, we push this goal on the stack and try to prove it.
432-
let depth = self.stack.next_index();
433-
let entry = StackEntry {
434-
input,
435-
available_depth,
436-
reached_depth: depth,
437-
non_root_cycle_participant: None,
438-
encountered_overflow: false,
439-
has_been_used: None,
440-
nested_goals: Default::default(),
441-
provisional_result: None,
442-
};
443-
assert_eq!(self.stack.push(entry), depth);
444-
cache_entry.stack_depth = Some(depth);
378+
if let Some(result) = self.lookup_provisional_cache(cx, input) {
379+
return result;
380+
}
381+
382+
if let Some(result) = self.check_cycle_on_stack(cx, input) {
383+
return result;
384+
}
385+
386+
let depth = self.stack.next_index();
387+
let entry = StackEntry {
388+
input,
389+
available_depth,
390+
reached_depth: depth,
391+
non_root_cycle_participant: None,
392+
encountered_overflow: false,
393+
has_been_used: None,
394+
nested_goals: Default::default(),
395+
provisional_result: None,
445396
};
397+
assert_eq!(self.stack.push(entry), depth);
446398

447399
// This is for global caching, so we properly track query dependencies.
448400
// Everything that affects the `result` should be performed within this
@@ -474,8 +426,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
474426
debug_assert!(validate_cache.is_none());
475427
let coinductive_stack = Self::stack_coinductive_from(cx, &self.stack, head);
476428

477-
let entry = self.provisional_cache.get_mut(&input).unwrap();
478-
entry.stack_depth = None;
429+
let entry = self.provisional_cache.entry(input).or_default();
479430
if coinductive_stack {
480431
entry.with_coinductive_stack = Some(DetachedEntry { head, result });
481432
} else {
@@ -534,35 +485,52 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
534485
})
535486
}
536487

537-
/// When encountering a cycle, both inductive and coinductive, we only
538-
/// move the root into the global cache. We also store all other cycle
539-
/// participants involved.
540-
///
541-
/// We must not use the global cache entry of a root goal if a cycle
542-
/// participant is on the stack. This is necessary to prevent unstable
543-
/// results. See the comment of `StackEntry::nested_goals` for
544-
/// more details.
545-
fn insert_global_cache(
546-
&mut self,
547-
cx: X,
548-
input: X::Input,
549-
final_entry: StackEntry<X>,
550-
result: X::Result,
551-
dep_node: X::DepNodeIndex,
552-
) {
553-
let additional_depth = final_entry.reached_depth.as_usize() - self.stack.len();
554-
debug!(?final_entry, ?result, "insert global cache");
555-
cx.with_global_cache(self.mode, |cache| {
556-
cache.insert(
557-
cx,
558-
input,
559-
result,
560-
dep_node,
561-
additional_depth,
562-
final_entry.encountered_overflow,
563-
&final_entry.nested_goals,
564-
)
565-
})
488+
fn lookup_provisional_cache(&mut self, cx: X, input: X::Input) -> Option<X::Result> {
489+
let cache_entry = self.provisional_cache.get(&input)?;
490+
let &DetachedEntry { head, result } = cache_entry
491+
.with_coinductive_stack
492+
.as_ref()
493+
.filter(|p| Self::stack_coinductive_from(cx, &self.stack, p.head))
494+
.or_else(|| {
495+
cache_entry
496+
.with_inductive_stack
497+
.as_ref()
498+
.filter(|p| !Self::stack_coinductive_from(cx, &self.stack, p.head))
499+
})?;
500+
501+
debug!("provisional cache hit");
502+
// We have a nested goal which is already in the provisional cache, use
503+
// its result. We do not provide any usage kind as that should have been
504+
// already set correctly while computing the cache entry.
505+
Self::tag_cycle_participants(&mut self.stack, head);
506+
debug_assert!(self.stack[head].has_been_used.is_some());
507+
Some(result)
508+
}
509+
510+
fn check_cycle_on_stack(&mut self, cx: X, input: X::Input) -> Option<X::Result> {
511+
let (head, _stack_entry) = self.stack.iter_enumerated().find(|(_, e)| e.input == input)?;
512+
debug!("encountered cycle with depth {head:?}");
513+
// We have a nested goal which directly relies on a goal deeper in the stack.
514+
//
515+
// We start by tagging all cycle participants, as that's necessary for caching.
516+
//
517+
// Finally we can return either the provisional response or the initial response
518+
// in case we're in the first fixpoint iteration for this goal.
519+
let is_coinductive_cycle = Self::stack_coinductive_from(cx, &self.stack, head);
520+
let cycle_kind =
521+
if is_coinductive_cycle { CycleKind::Coinductive } else { CycleKind::Inductive };
522+
let usage_kind = UsageKind::Single(cycle_kind);
523+
self.stack[head].has_been_used =
524+
Some(self.stack[head].has_been_used.map_or(usage_kind, |prev| prev.merge(usage_kind)));
525+
Self::tag_cycle_participants(&mut self.stack, head);
526+
527+
// Return the provisional result or, if we're in the first iteration,
528+
// start with no constraints.
529+
if let Some(result) = self.stack[head].provisional_result {
530+
Some(result)
531+
} else {
532+
Some(D::initial_provisional_result(cx, cycle_kind, input))
533+
}
566534
}
567535

568536
/// When we encounter a coinductive cycle, we have to fetch the
@@ -613,13 +581,43 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
613581
StepResult::Done(stack_entry, result)
614582
} else {
615583
debug!(?result, "fixpoint changed provisional results");
616-
let depth = self.stack.push(StackEntry {
584+
self.stack.push(StackEntry {
617585
has_been_used: None,
618586
provisional_result: Some(result),
619587
..stack_entry
620588
});
621-
debug_assert_eq!(self.provisional_cache[&input].stack_depth, Some(depth));
622589
StepResult::HasChanged
623590
}
624591
}
592+
593+
/// When encountering a cycle, both inductive and coinductive, we only
594+
/// move the root into the global cache. We also store all other cycle
595+
/// participants involved.
596+
///
597+
/// We must not use the global cache entry of a root goal if a cycle
598+
/// participant is on the stack. This is necessary to prevent unstable
599+
/// results. See the comment of `StackEntry::nested_goals` for
600+
/// more details.
601+
fn insert_global_cache(
602+
&mut self,
603+
cx: X,
604+
input: X::Input,
605+
final_entry: StackEntry<X>,
606+
result: X::Result,
607+
dep_node: X::DepNodeIndex,
608+
) {
609+
let additional_depth = final_entry.reached_depth.as_usize() - self.stack.len();
610+
debug!(?final_entry, ?result, "insert global cache");
611+
cx.with_global_cache(self.mode, |cache| {
612+
cache.insert(
613+
cx,
614+
input,
615+
result,
616+
dep_node,
617+
additional_depth,
618+
final_entry.encountered_overflow,
619+
&final_entry.nested_goals,
620+
)
621+
})
622+
}
625623
}

compiler/rustc_type_ir/src/search_graph/validate.rs

+3-15
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
2323
ref nested_goals,
2424
provisional_result,
2525
} = *entry;
26-
let cache_entry = provisional_cache.get(&entry.input).unwrap();
27-
assert_eq!(cache_entry.stack_depth, Some(depth));
2826
if let Some(head) = non_root_cycle_participant {
2927
assert!(head < depth);
3028
assert!(nested_goals.is_empty());
@@ -45,19 +43,9 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
4543
}
4644
}
4745

48-
for (&input, entry) in &self.provisional_cache {
49-
let ProvisionalCacheEntry { stack_depth, with_coinductive_stack, with_inductive_stack } =
50-
entry;
51-
assert!(
52-
stack_depth.is_some()
53-
|| with_coinductive_stack.is_some()
54-
|| with_inductive_stack.is_some()
55-
);
56-
57-
if let &Some(stack_depth) = stack_depth {
58-
assert_eq!(stack[stack_depth].input, input);
59-
}
60-
46+
for (&_input, entry) in &self.provisional_cache {
47+
let ProvisionalCacheEntry { with_coinductive_stack, with_inductive_stack } = entry;
48+
assert!(with_coinductive_stack.is_some() || with_inductive_stack.is_some());
6149
let check_detached = |detached_entry: &DetachedEntry<X>| {
6250
let DetachedEntry { head, result: _ } = *detached_entry;
6351
assert_ne!(stack[head].has_been_used, None);

0 commit comments

Comments
 (0)