Skip to content

Commit 242633f

Browse files
committed
add comments and tests
1 parent 88271de commit 242633f

File tree

6 files changed

+231
-36
lines changed

6 files changed

+231
-36
lines changed

compiler/rustc_trait_selection/src/solve/search_graph.rs

+58-36
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ rustc_index::newtype_index! {
1919
}
2020

2121
bitflags::bitflags! {
22+
/// Whether and how this goal has been used as the root of a
23+
/// cycle. We track the kind of cycle as we're otherwise forced
24+
/// to always rerun at least once.
2225
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2326
struct HasBeenUsed: u8 {
2427
const INDUCTIVE_CYCLE = 1 << 0;
@@ -29,23 +32,30 @@ bitflags::bitflags! {
2932
#[derive(Debug)]
3033
struct StackEntry<'tcx> {
3134
input: CanonicalInput<'tcx>,
35+
3236
available_depth: Limit,
37+
3338
/// The maximum depth reached by this stack entry, only up-to date
3439
/// for the top of the stack and lazily updated for the rest.
3540
reached_depth: StackDepth,
36-
/// Whether this entry is a cycle participant which is not a root.
41+
42+
/// Whether this entry is a non-root cycle participant.
3743
///
38-
/// If so, it must not be moved to the global cache. See
39-
/// [SearchGraph::cycle_participants] for more details.
44+
/// We must not move the result of non-root cycle participants to the
45+
/// global cache. See [SearchGraph::cycle_participants] for more details.
46+
/// We store the highest stack depth of a head of a cycle this goal is involved
47+
/// in. This necessary to soundly cache its provisional result.
4048
non_root_cycle_participant: Option<StackDepth>,
4149

4250
encountered_overflow: bool,
51+
4352
has_been_used: HasBeenUsed,
4453
/// Starts out as `None` and gets set when rerunning this
4554
/// goal in case we encounter a cycle.
4655
provisional_result: Option<QueryResult<'tcx>>,
4756
}
4857

58+
/// The provisional result for a goal which is not on the stack.
4959
struct DetachedEntry<'tcx> {
5060
/// The head of the smallest non-trivial cycle involving this entry.
5161
///
@@ -59,6 +69,18 @@ struct DetachedEntry<'tcx> {
5969
result: QueryResult<'tcx>,
6070
}
6171

72+
/// Stores the stack depth of a currently evaluated goal *and* already
73+
/// computed results for goals which depend on other goals still on the stack.
74+
///
75+
/// The provisional result may depend on whether the stack above it is inductive
76+
/// or coinductive. Because of this, we store separate provisional results for
77+
/// each case. If an provisional entry is not applicable, it may be the case
78+
/// that we already have provisional result while computing a goal. In this case
79+
/// we prefer the provisional result to potentially avoid fixpoint iterations.
80+
/// See tests/ui/traits/next-solver/cycles/mixed-cycles-2.rs for an example.
81+
///
82+
/// The provisional cache can theoretically result in changes to the observable behavior,
83+
/// see tests/ui/traits/next-solver/cycles/provisional-cache-impacts-behavior.rs.
6284
#[derive(Default)]
6385
struct ProvisionalCacheEntry<'tcx> {
6486
stack_depth: Option<StackDepth>,
@@ -200,6 +222,16 @@ impl<'tcx> SearchGraph<'tcx> {
200222
.all(|entry| entry.input.value.goal.predicate.is_coinductive(tcx))
201223
}
202224

225+
// When encountering a solver cycle, the result of the current goal
226+
// depends on goals lower on the stack.
227+
//
228+
// We have to therefore be careful when caching goals. Only the final result
229+
// of the cycle root, i.e. the lowest goal on the stack involved in this cycle,
230+
// is moved to the global cache while all others are stored in a provisional cache.
231+
//
232+
// We update both the head of this cycle to rerun its evaluation until
233+
// we reach a fixpoint and all other cycle participants to make sure that
234+
// their result does not get moved to the global cache.
203235
fn tag_cycle_participants(
204236
stack: &mut IndexVec<StackDepth, StackEntry<'tcx>>,
205237
cycle_participants: &mut FxHashSet<CanonicalInput<'tcx>>,
@@ -281,24 +313,20 @@ impl<'tcx> SearchGraph<'tcx> {
281313
}
282314

283315
// Check whether the goal is in the provisional cache.
316+
// The provisional result may rely on the path to its cycle roots,
317+
// so we have to check the path of the current goal matches that of
318+
// the cache entry.
284319
let cache_entry = self.provisional_cache.entry(input).or_default();
285-
if let Some(with_coinductive_stack) = &cache_entry.with_coinductive_stack
286-
&& Self::stack_coinductive_from(tcx, &self.stack, with_coinductive_stack.head)
287-
{
288-
// We have a nested goal which is already in the provisional cache, use
289-
// its result. We do not provide any usage kind as that should have been
290-
// already set correctly while computing the cache entry.
291-
inspect
292-
.goal_evaluation_kind(inspect::WipCanonicalGoalEvaluationKind::ProvisionalCacheHit);
293-
Self::tag_cycle_participants(
294-
&mut self.stack,
295-
&mut self.cycle_participants,
296-
HasBeenUsed::empty(),
297-
with_coinductive_stack.head,
298-
);
299-
return with_coinductive_stack.result;
300-
} else if let Some(with_inductive_stack) = &cache_entry.with_inductive_stack
301-
&& !Self::stack_coinductive_from(tcx, &self.stack, with_inductive_stack.head)
320+
if let Some(entry) = cache_entry
321+
.with_coinductive_stack
322+
.as_ref()
323+
.filter(|p| Self::stack_coinductive_from(tcx, &self.stack, p.head))
324+
.or_else(|| {
325+
cache_entry
326+
.with_inductive_stack
327+
.as_ref()
328+
.filter(|p| !Self::stack_coinductive_from(tcx, &self.stack, p.head))
329+
})
302330
{
303331
// We have a nested goal which is already in the provisional cache, use
304332
// its result. We do not provide any usage kind as that should have been
@@ -309,20 +337,17 @@ impl<'tcx> SearchGraph<'tcx> {
309337
&mut self.stack,
310338
&mut self.cycle_participants,
311339
HasBeenUsed::empty(),
312-
with_inductive_stack.head,
340+
entry.head,
313341
);
314-
return with_inductive_stack.result;
342+
return entry.result;
315343
} else if let Some(stack_depth) = cache_entry.stack_depth {
316344
debug!("encountered cycle with depth {stack_depth:?}");
317-
// We have a nested goal which relies on a goal `root` deeper in the stack.
345+
// We have a nested goal which directly relies on a goal deeper in the stack.
318346
//
319-
// We first store that we may have to reprove `root` in case the provisional
320-
// response is not equal to the final response. We also update the depth of all
321-
// goals which recursively depend on our current goal to depend on `root`
322-
// instead.
347+
// We start by tagging all cycle participants, as that's necessary for caching.
323348
//
324-
// Finally we can return either the provisional response for that goal if we have a
325-
// coinductive cycle or an ambiguous result if the cycle is inductive.
349+
// Finally we can return either the provisional response or the initial response
350+
// in case we're in the first fixpoint iteration for this goal.
326351
inspect.goal_evaluation_kind(inspect::WipCanonicalGoalEvaluationKind::CycleInStack);
327352
let is_coinductive_cycle = Self::stack_coinductive_from(tcx, &self.stack, stack_depth);
328353
let usage_kind = if is_coinductive_cycle {
@@ -410,10 +435,10 @@ impl<'tcx> SearchGraph<'tcx> {
410435
false
411436
};
412437

438+
// If we did not reach a fixpoint, update the provisional result and reevaluate.
413439
if reached_fixpoint {
414440
return (stack_entry, result);
415441
} else {
416-
// Did not reach a fixpoint, update the provisional result and reevaluate.
417442
let depth = self.stack.push(StackEntry {
418443
has_been_used: HasBeenUsed::empty(),
419444
provisional_result: Some(result),
@@ -435,9 +460,6 @@ impl<'tcx> SearchGraph<'tcx> {
435460
// We're now done with this goal. In case this goal is involved in a larger cycle
436461
// do not remove it from the provisional cache and update its provisional result.
437462
// We only add the root of cycles to the global cache.
438-
//
439-
// It is not possible for any nested goal to depend on something deeper on the
440-
// stack, as this would have also updated the depth of the current goal.
441463
if let Some(head) = final_entry.non_root_cycle_participant {
442464
let coinductive_stack = Self::stack_coinductive_from(tcx, &self.stack, head);
443465

@@ -449,6 +471,9 @@ impl<'tcx> SearchGraph<'tcx> {
449471
entry.with_inductive_stack = Some(DetachedEntry { head, result });
450472
}
451473
} else {
474+
self.provisional_cache.remove(&input);
475+
let reached_depth = final_entry.reached_depth.as_usize() - self.stack.len();
476+
let cycle_participants = mem::take(&mut self.cycle_participants);
452477
// When encountering a cycle, both inductive and coinductive, we only
453478
// move the root into the global cache. We also store all other cycle
454479
// participants involved.
@@ -457,9 +482,6 @@ impl<'tcx> SearchGraph<'tcx> {
457482
// participant is on the stack. This is necessary to prevent unstable
458483
// results. See the comment of `SearchGraph::cycle_participants` for
459484
// more details.
460-
self.provisional_cache.remove(&input);
461-
let reached_depth = final_entry.reached_depth.as_usize() - self.stack.len();
462-
let cycle_participants = mem::take(&mut self.cycle_participants);
463485
self.global_cache(tcx).insert(
464486
tcx,
465487
input,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// compile-flags: -Znext-solver
2+
#![feature(rustc_attrs)]
3+
4+
// A test intended to check how we handle provisional results
5+
// for a goal computed with an inductive and a coinductive stack.
6+
//
7+
// Unfortunately this doesn't really detect whether we've done
8+
// something wrong but instead only showcases that we thought of
9+
// this.
10+
//
11+
// FIXME(-Znext-solver=coinductive): With the new coinduction approach
12+
// the same goal stack can be both inductive and coinductive, depending
13+
// on why we're proving a specific nested goal. Rewrite this test
14+
// at that point instead of relying on `BInd`.
15+
16+
17+
#[rustc_coinductive]
18+
trait A {}
19+
20+
#[rustc_coinductive]
21+
trait B {}
22+
trait BInd {}
23+
impl<T: ?Sized + B> BInd for T {}
24+
25+
#[rustc_coinductive]
26+
trait C {}
27+
trait CInd {}
28+
impl<T: ?Sized + C> CInd for T {}
29+
30+
impl<T: ?Sized + BInd + C> A for T {}
31+
impl<T: ?Sized + CInd + C> B for T {}
32+
impl<T: ?Sized + B + A> C for T {}
33+
34+
fn impls_a<T: A>() {}
35+
36+
fn main() {
37+
impls_a::<()>();
38+
//~^ ERROR overflow evaluating the requirement `(): A`
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
error[E0275]: overflow evaluating the requirement `(): A`
2+
--> $DIR/mixed-cycles-1.rs:37:15
3+
|
4+
LL | impls_a::<()>();
5+
| ^^
6+
|
7+
= help: consider increasing the recursion limit by adding a `#![recursion_limit = "256"]` attribute to your crate (`mixed_cycles_1`)
8+
note: required by a bound in `impls_a`
9+
--> $DIR/mixed-cycles-1.rs:34:15
10+
|
11+
LL | fn impls_a<T: A>() {}
12+
| ^ required by this bound in `impls_a`
13+
14+
error: aborting due to 1 previous error
15+
16+
For more information about this error, try `rustc --explain E0275`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// compile-flags: -Znext-solver
2+
#![feature(rustc_attrs)]
3+
4+
// A test showcasing that the solver may need to
5+
// compute a goal which is already in the provisional
6+
// cache.
7+
//
8+
// However, given that `(): BInd` and `(): B` are currently distinct
9+
// goals, this is actually not possible right now.
10+
//
11+
// FIXME(-Znext-solver=coinductive): With the new coinduction approach
12+
// the same goal stack can be both inductive and coinductive, depending
13+
// on why we're proving a specific nested goal. Rewrite this test
14+
// at that point.
15+
16+
#[rustc_coinductive]
17+
trait A {}
18+
19+
#[rustc_coinductive]
20+
trait B {}
21+
trait BInd {}
22+
impl<T: ?Sized + B> BInd for T {}
23+
24+
impl<T: ?Sized + BInd + B> A for T {}
25+
impl<T: ?Sized + BInd> B for T {}
26+
27+
fn impls_a<T: A>() {}
28+
29+
fn main() {
30+
impls_a::<()>();
31+
//~^ ERROR overflow evaluating the requirement `(): A`
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
error[E0275]: overflow evaluating the requirement `(): A`
2+
--> $DIR/mixed-cycles-2.rs:30:15
3+
|
4+
LL | impls_a::<()>();
5+
| ^^
6+
|
7+
= help: consider increasing the recursion limit by adding a `#![recursion_limit = "256"]` attribute to your crate (`mixed_cycles_2`)
8+
note: required by a bound in `impls_a`
9+
--> $DIR/mixed-cycles-2.rs:27:15
10+
|
11+
LL | fn impls_a<T: A>() {}
12+
| ^ required by this bound in `impls_a`
13+
14+
error: aborting due to 1 previous error
15+
16+
For more information about this error, try `rustc --explain E0275`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// compile-flags: -Znext-solver
2+
// check-pass
3+
#![feature(rustc_attrs)]
4+
5+
// A test showcasing that using a provisional cache can differ
6+
// from only tracking stack entries.
7+
//
8+
// Without a provisional cache, we have the following proof tree:
9+
//
10+
// - (): A
11+
// - (): B
12+
// - (): A (coinductive cycle)
13+
// - (): C
14+
// - (): B (coinductive cycle)
15+
// - (): C
16+
// - (): B
17+
// - (): A (coinductive cycle)
18+
// - (): C (coinductive cycle)
19+
//
20+
// While with the current provisional cache implementation we get:
21+
//
22+
// - (): A
23+
// - (): B
24+
// - (): A (coinductive cycle)
25+
// - (): C
26+
// - (): B (coinductive cycle)
27+
// - (): C
28+
// - (): B (provisional cache hit)
29+
//
30+
// Note that if even if we were to expand the provisional cache hit,
31+
// the proof tree would still be different:
32+
//
33+
// - (): A
34+
// - (): B
35+
// - (): A (coinductive cycle)
36+
// - (): C
37+
// - (): B (coinductive cycle)
38+
// - (): C
39+
// - (): B (provisional cache hit, expanded)
40+
// - (): A (coinductive cycle)
41+
// - (): C
42+
// - (): B (coinductive cycle)
43+
//
44+
// Theoretically, this can result in observable behavior differences
45+
// due to incompleteness. However, this would require a very convoluted
46+
// example and would still be sound. The difference is determinstic
47+
// and can not be observed outside of the cycle itself as we don't move
48+
// non-root cycle participants into the global cache.
49+
//
50+
// For an example of how incompleteness could impact the observable behavior here, see
51+
//
52+
// tests/ui/traits/next-solver/cycles/coinduction/incompleteness-unstable-result.rs
53+
#[rustc_coinductive]
54+
trait A {}
55+
56+
#[rustc_coinductive]
57+
trait B {}
58+
59+
#[rustc_coinductive]
60+
trait C {}
61+
62+
impl<T: ?Sized + B + C> A for T {}
63+
impl<T: ?Sized + A + C> B for T {}
64+
impl<T: ?Sized + B> C for T {}
65+
66+
fn impls_a<T: A>() {}
67+
68+
fn main() {
69+
impls_a::<()>();
70+
}

0 commit comments

Comments
 (0)