Skip to content

Commit 1114458

Browse files
committed
track cycle participants per entry
1 parent c0b0295 commit 1114458

File tree

4 files changed

+150
-43
lines changed

4 files changed

+150
-43
lines changed

compiler/rustc_trait_selection/src/solve/search_graph.rs

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,31 @@ struct StackEntry<'tcx> {
5151
encountered_overflow: bool,
5252

5353
has_been_used: HasBeenUsed,
54+
55+
/// We put only the root goal of a coinductive cycle into the global cache.
56+
///
57+
/// If we were to use that result when later trying to prove another cycle
58+
/// participant, we can end up with unstable query results.
59+
///
60+
/// See tests/ui/next-solver/coinduction/incompleteness-unstable-result.rs for
61+
/// an example of where this is needed.
62+
///
63+
/// There can be multiple roots on the same stack, so we need to track
64+
/// cycle participants per root:
65+
/// ```text
66+
/// A :- B
67+
/// B :- A, C
68+
/// C :- D
69+
/// D :- C
70+
/// ```
71+
cycle_participants: FxHashSet<CanonicalInput<'tcx>>,
5472
/// Starts out as `None` and gets set when rerunning this
5573
/// goal in case we encounter a cycle.
5674
provisional_result: Option<QueryResult<'tcx>>,
5775
}
5876

5977
/// The provisional result for a goal which is not on the stack.
78+
#[derive(Debug)]
6079
struct DetachedEntry<'tcx> {
6180
/// The head of the smallest non-trivial cycle involving this entry.
6281
///
@@ -83,7 +102,7 @@ struct DetachedEntry<'tcx> {
83102
///
84103
/// The provisional cache can theoretically result in changes to the observable behavior,
85104
/// see tests/ui/traits/next-solver/cycles/provisional-cache-impacts-behavior.rs.
86-
#[derive(Default)]
105+
#[derive(Debug, Default)]
87106
struct ProvisionalCacheEntry<'tcx> {
88107
stack_depth: Option<StackDepth>,
89108
with_inductive_stack: Option<DetachedEntry<'tcx>>,
@@ -98,31 +117,19 @@ impl<'tcx> ProvisionalCacheEntry<'tcx> {
98117
}
99118
}
100119

120+
#[derive(Debug)]
101121
pub(super) struct SearchGraph<'tcx> {
102122
mode: SolverMode,
103123
/// The stack of goals currently being computed.
104124
///
105125
/// An element is *deeper* in the stack if its index is *lower*.
106126
stack: IndexVec<StackDepth, StackEntry<'tcx>>,
107127
provisional_cache: FxHashMap<CanonicalInput<'tcx>, ProvisionalCacheEntry<'tcx>>,
108-
/// We put only the root goal of a coinductive cycle into the global cache.
109-
///
110-
/// If we were to use that result when later trying to prove another cycle
111-
/// participant, we can end up with unstable query results.
112-
///
113-
/// See tests/ui/next-solver/coinduction/incompleteness-unstable-result.rs for
114-
/// an example of where this is needed.
115-
cycle_participants: FxHashSet<CanonicalInput<'tcx>>,
116128
}
117129

118130
impl<'tcx> SearchGraph<'tcx> {
119131
pub(super) fn new(mode: SolverMode) -> SearchGraph<'tcx> {
120-
Self {
121-
mode,
122-
stack: Default::default(),
123-
provisional_cache: Default::default(),
124-
cycle_participants: Default::default(),
125-
}
132+
Self { mode, stack: Default::default(), provisional_cache: Default::default() }
126133
}
127134

128135
pub(super) fn solver_mode(&self) -> SolverMode {
@@ -155,13 +162,7 @@ impl<'tcx> SearchGraph<'tcx> {
155162
}
156163

157164
pub(super) fn is_empty(&self) -> bool {
158-
if self.stack.is_empty() {
159-
debug_assert!(self.provisional_cache.is_empty());
160-
debug_assert!(self.cycle_participants.is_empty());
161-
true
162-
} else {
163-
false
164-
}
165+
self.stack.is_empty()
165166
}
166167

167168
/// Returns the remaining depth allowed for nested goals.
@@ -211,15 +212,26 @@ impl<'tcx> SearchGraph<'tcx> {
211212
// their result does not get moved to the global cache.
212213
fn tag_cycle_participants(
213214
stack: &mut IndexVec<StackDepth, StackEntry<'tcx>>,
214-
cycle_participants: &mut FxHashSet<CanonicalInput<'tcx>>,
215215
usage_kind: HasBeenUsed,
216216
head: StackDepth,
217217
) {
218218
stack[head].has_been_used |= usage_kind;
219219
debug_assert!(!stack[head].has_been_used.is_empty());
220-
for entry in &mut stack.raw[head.index() + 1..] {
220+
221+
// The current root of these cycles. Note that this may not be the final
222+
// root in case a later goal depends on a goal higher up the stack.
223+
let mut current_root = head;
224+
while let Some(parent) = stack[current_root].non_root_cycle_participant {
225+
current_root = parent;
226+
debug_assert!(!stack[current_root].has_been_used.is_empty());
227+
}
228+
229+
let (stack, cycle_participants) = stack.raw.split_at_mut(head.index() + 1);
230+
let current_cycle_root = &mut stack[current_root.as_usize()];
231+
for entry in cycle_participants {
221232
entry.non_root_cycle_participant = entry.non_root_cycle_participant.max(Some(head));
222-
cycle_participants.insert(entry.input);
233+
current_cycle_root.cycle_participants.insert(entry.input);
234+
current_cycle_root.cycle_participants.extend(mem::take(&mut entry.cycle_participants));
223235
}
224236
}
225237

@@ -246,6 +258,7 @@ impl<'tcx> SearchGraph<'tcx> {
246258
inspect: &mut ProofTreeBuilder<'tcx>,
247259
mut prove_goal: impl FnMut(&mut Self, &mut ProofTreeBuilder<'tcx>) -> QueryResult<'tcx>,
248260
) -> QueryResult<'tcx> {
261+
self.check_invariants();
249262
// Check for overflow.
250263
let Some(available_depth) = Self::allowed_depth_for_nested(tcx, &self.stack) else {
251264
if let Some(last) = self.stack.raw.last_mut() {
@@ -281,12 +294,7 @@ impl<'tcx> SearchGraph<'tcx> {
281294
// already set correctly while computing the cache entry.
282295
inspect
283296
.goal_evaluation_kind(inspect::WipCanonicalGoalEvaluationKind::ProvisionalCacheHit);
284-
Self::tag_cycle_participants(
285-
&mut self.stack,
286-
&mut self.cycle_participants,
287-
HasBeenUsed::empty(),
288-
entry.head,
289-
);
297+
Self::tag_cycle_participants(&mut self.stack, HasBeenUsed::empty(), entry.head);
290298
return entry.result;
291299
} else if let Some(stack_depth) = cache_entry.stack_depth {
292300
debug!("encountered cycle with depth {stack_depth:?}");
@@ -303,12 +311,7 @@ impl<'tcx> SearchGraph<'tcx> {
303311
} else {
304312
HasBeenUsed::INDUCTIVE_CYCLE
305313
};
306-
Self::tag_cycle_participants(
307-
&mut self.stack,
308-
&mut self.cycle_participants,
309-
usage_kind,
310-
stack_depth,
311-
);
314+
Self::tag_cycle_participants(&mut self.stack, usage_kind, stack_depth);
312315

313316
// Return the provisional result or, if we're in the first iteration,
314317
// start with no constraints.
@@ -329,6 +332,7 @@ impl<'tcx> SearchGraph<'tcx> {
329332
non_root_cycle_participant: None,
330333
encountered_overflow: false,
331334
has_been_used: HasBeenUsed::empty(),
335+
cycle_participants: Default::default(),
332336
provisional_result: None,
333337
};
334338
assert_eq!(self.stack.push(entry), depth);
@@ -375,27 +379,28 @@ impl<'tcx> SearchGraph<'tcx> {
375379
} else {
376380
self.provisional_cache.remove(&input);
377381
let reached_depth = final_entry.reached_depth.as_usize() - self.stack.len();
378-
let cycle_participants = mem::take(&mut self.cycle_participants);
379382
// When encountering a cycle, both inductive and coinductive, we only
380383
// move the root into the global cache. We also store all other cycle
381384
// participants involved.
382385
//
383386
// We must not use the global cache entry of a root goal if a cycle
384387
// participant is on the stack. This is necessary to prevent unstable
385-
// results. See the comment of `SearchGraph::cycle_participants` for
388+
// results. See the comment of `StackEntry::cycle_participants` for
386389
// more details.
387390
self.global_cache(tcx).insert(
388391
tcx,
389392
input,
390393
proof_tree,
391394
reached_depth,
392395
final_entry.encountered_overflow,
393-
cycle_participants,
396+
final_entry.cycle_participants,
394397
dep_node,
395398
result,
396399
)
397400
}
398401

402+
self.check_invariants();
403+
399404
result
400405
}
401406

@@ -519,3 +524,77 @@ impl<'tcx> SearchGraph<'tcx> {
519524
Ok(super::response_no_constraints_raw(tcx, goal.max_universe, goal.variables, certainty))
520525
}
521526
}
527+
528+
impl<'tcx> SearchGraph<'tcx> {
529+
#[allow(rustc::potential_query_instability)]
530+
fn check_invariants(&self) {
531+
if !cfg!(debug_assertions) {
532+
return;
533+
}
534+
535+
let SearchGraph { mode: _, stack, provisional_cache } = self;
536+
if stack.is_empty() {
537+
assert!(provisional_cache.is_empty());
538+
}
539+
540+
for (depth, entry) in stack.iter_enumerated() {
541+
let StackEntry {
542+
input,
543+
available_depth: _,
544+
reached_depth: _,
545+
non_root_cycle_participant,
546+
encountered_overflow: _,
547+
has_been_used,
548+
ref cycle_participants,
549+
provisional_result,
550+
} = *entry;
551+
let cache_entry = provisional_cache.get(&entry.input).unwrap();
552+
assert_eq!(cache_entry.stack_depth, Some(depth));
553+
if let Some(head) = non_root_cycle_participant {
554+
assert!(head < depth);
555+
assert!(cycle_participants.is_empty());
556+
assert_ne!(stack[head].has_been_used, HasBeenUsed::empty());
557+
558+
let mut current_root = head;
559+
while let Some(parent) = stack[current_root].non_root_cycle_participant {
560+
current_root = parent;
561+
}
562+
assert!(stack[current_root].cycle_participants.contains(&input));
563+
}
564+
565+
if !cycle_participants.is_empty() {
566+
assert!(provisional_result.is_some() || !has_been_used.is_empty());
567+
for entry in stack.iter().take(depth.as_usize()) {
568+
assert_eq!(cycle_participants.get(&entry.input), None);
569+
}
570+
}
571+
}
572+
573+
for (&input, entry) in &self.provisional_cache {
574+
let ProvisionalCacheEntry { stack_depth, with_coinductive_stack, with_inductive_stack } =
575+
entry;
576+
assert!(
577+
stack_depth.is_some()
578+
|| with_coinductive_stack.is_some()
579+
|| with_inductive_stack.is_some()
580+
);
581+
582+
if let &Some(stack_depth) = stack_depth {
583+
assert_eq!(stack[stack_depth].input, input);
584+
}
585+
586+
let check_detached = |detached_entry: &DetachedEntry<'tcx>| {
587+
let DetachedEntry { head, result: _ } = *detached_entry;
588+
assert_ne!(stack[head].has_been_used, HasBeenUsed::empty());
589+
};
590+
591+
if let Some(with_coinductive_stack) = with_coinductive_stack {
592+
check_detached(with_coinductive_stack);
593+
}
594+
595+
if let Some(with_inductive_stack) = with_inductive_stack {
596+
check_detached(with_inductive_stack);
597+
}
598+
}
599+
}
600+
}

tests/ui/traits/next-solver/cycles/coinduction/incompleteness-unstable-result.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//@ revisions: with without
12
//@ compile-flags: -Znext-solver
23
#![feature(rustc_attrs)]
34

@@ -56,6 +57,7 @@ where
5657
X: IncompleteGuidance<u32, i8>,
5758
X: IncompleteGuidance<u32, i16>,
5859
{
60+
#[cfg(with)]
5961
impls_trait::<B<X>, _, _, _>(); // entering the cycle from `B` works
6062

6163
// entering the cycle from `A` fails, but would work if we were to use the cache

tests/ui/traits/next-solver/cycles/coinduction/incompleteness-unstable-result.stderr renamed to tests/ui/traits/next-solver/cycles/coinduction/incompleteness-unstable-result.with.stderr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
error[E0277]: the trait bound `A<X>: Trait<_, _, _>` is not satisfied
2-
--> $DIR/incompleteness-unstable-result.rs:63:19
2+
--> $DIR/incompleteness-unstable-result.rs:65:19
33
|
44
LL | impls_trait::<A<X>, _, _, _>();
55
| ^^^^ the trait `Trait<_, _, _>` is not implemented for `A<X>`, which is required by `A<X>: Trait<_, _, _>`
66
|
77
= help: the trait `Trait<U, V, D>` is implemented for `A<T>`
88
note: required for `A<X>` to implement `Trait<_, _, _>`
9-
--> $DIR/incompleteness-unstable-result.rs:32:50
9+
--> $DIR/incompleteness-unstable-result.rs:33:50
1010
|
1111
LL | impl<T: ?Sized, U: ?Sized, V: ?Sized, D: ?Sized> Trait<U, V, D> for A<T>
1212
| ^^^^^^^^^^^^^^ ^^^^
@@ -16,7 +16,7 @@ LL | A<T>: Trait<U, D, V>,
1616
= note: 8 redundant requirements hidden
1717
= note: required for `A<X>` to implement `Trait<_, _, _>`
1818
note: required by a bound in `impls_trait`
19-
--> $DIR/incompleteness-unstable-result.rs:51:28
19+
--> $DIR/incompleteness-unstable-result.rs:52:28
2020
|
2121
LL | fn impls_trait<T: ?Sized + Trait<U, V, D>, U: ?Sized, V: ?Sized, D: ?Sized>() {}
2222
| ^^^^^^^^^^^^^^ required by this bound in `impls_trait`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
error[E0277]: the trait bound `A<X>: Trait<_, _, _>` is not satisfied
2+
--> $DIR/incompleteness-unstable-result.rs:65:19
3+
|
4+
LL | impls_trait::<A<X>, _, _, _>();
5+
| ^^^^ the trait `Trait<_, _, _>` is not implemented for `A<X>`, which is required by `A<X>: Trait<_, _, _>`
6+
|
7+
= help: the trait `Trait<U, V, D>` is implemented for `A<T>`
8+
note: required for `A<X>` to implement `Trait<_, _, _>`
9+
--> $DIR/incompleteness-unstable-result.rs:33:50
10+
|
11+
LL | impl<T: ?Sized, U: ?Sized, V: ?Sized, D: ?Sized> Trait<U, V, D> for A<T>
12+
| ^^^^^^^^^^^^^^ ^^^^
13+
...
14+
LL | A<T>: Trait<U, D, V>,
15+
| -------------- unsatisfied trait bound introduced here
16+
= note: 8 redundant requirements hidden
17+
= note: required for `A<X>` to implement `Trait<_, _, _>`
18+
note: required by a bound in `impls_trait`
19+
--> $DIR/incompleteness-unstable-result.rs:52:28
20+
|
21+
LL | fn impls_trait<T: ?Sized + Trait<U, V, D>, U: ?Sized, V: ?Sized, D: ?Sized>() {}
22+
| ^^^^^^^^^^^^^^ required by this bound in `impls_trait`
23+
24+
error: aborting due to 1 previous error
25+
26+
For more information about this error, try `rustc --explain E0277`.

0 commit comments

Comments
 (0)