@@ -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 ) ]
6079struct 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 ) ]
87106struct 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 ) ]
101121pub ( 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
118130impl < ' 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 < TyCtxt < ' tcx > > ,
247259 mut prove_goal : impl FnMut ( & mut Self , & mut ProofTreeBuilder < TyCtxt < ' 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 ( ) {
@@ -282,12 +295,7 @@ impl<'tcx> SearchGraph<'tcx> {
282295 // already set correctly while computing the cache entry.
283296 inspect
284297 . goal_evaluation_kind ( inspect:: WipCanonicalGoalEvaluationKind :: ProvisionalCacheHit ) ;
285- Self :: tag_cycle_participants (
286- & mut self . stack ,
287- & mut self . cycle_participants ,
288- HasBeenUsed :: empty ( ) ,
289- entry. head ,
290- ) ;
298+ Self :: tag_cycle_participants ( & mut self . stack , HasBeenUsed :: empty ( ) , entry. head ) ;
291299 return entry. result ;
292300 } else if let Some ( stack_depth) = cache_entry. stack_depth {
293301 debug ! ( "encountered cycle with depth {stack_depth:?}" ) ;
@@ -304,12 +312,7 @@ impl<'tcx> SearchGraph<'tcx> {
304312 } else {
305313 HasBeenUsed :: INDUCTIVE_CYCLE
306314 } ;
307- Self :: tag_cycle_participants (
308- & mut self . stack ,
309- & mut self . cycle_participants ,
310- usage_kind,
311- stack_depth,
312- ) ;
315+ Self :: tag_cycle_participants ( & mut self . stack , usage_kind, stack_depth) ;
313316
314317 // Return the provisional result or, if we're in the first iteration,
315318 // start with no constraints.
@@ -330,6 +333,7 @@ impl<'tcx> SearchGraph<'tcx> {
330333 non_root_cycle_participant : None ,
331334 encountered_overflow : false ,
332335 has_been_used : HasBeenUsed :: empty ( ) ,
336+ cycle_participants : Default :: default ( ) ,
333337 provisional_result : None ,
334338 } ;
335339 assert_eq ! ( self . stack. push( entry) , depth) ;
@@ -376,27 +380,28 @@ impl<'tcx> SearchGraph<'tcx> {
376380 } else {
377381 self . provisional_cache . remove ( & input) ;
378382 let reached_depth = final_entry. reached_depth . as_usize ( ) - self . stack . len ( ) ;
379- let cycle_participants = mem:: take ( & mut self . cycle_participants ) ;
380383 // When encountering a cycle, both inductive and coinductive, we only
381384 // move the root into the global cache. We also store all other cycle
382385 // participants involved.
383386 //
384387 // We must not use the global cache entry of a root goal if a cycle
385388 // participant is on the stack. This is necessary to prevent unstable
386- // results. See the comment of `SearchGraph ::cycle_participants` for
389+ // results. See the comment of `StackEntry ::cycle_participants` for
387390 // more details.
388391 self . global_cache ( tcx) . insert (
389392 tcx,
390393 input,
391394 proof_tree,
392395 reached_depth,
393396 final_entry. encountered_overflow ,
394- cycle_participants,
397+ final_entry . cycle_participants ,
395398 dep_node,
396399 result,
397400 )
398401 }
399402
403+ self . check_invariants ( ) ;
404+
400405 result
401406 }
402407
@@ -520,3 +525,77 @@ impl<'tcx> SearchGraph<'tcx> {
520525 Ok ( super :: response_no_constraints_raw ( tcx, goal. max_universe , goal. variables , certainty) )
521526 }
522527}
528+
529+ impl < ' tcx > SearchGraph < ' tcx > {
530+ #[ allow( rustc:: potential_query_instability) ]
531+ fn check_invariants ( & self ) {
532+ if !cfg ! ( debug_assertions) {
533+ return ;
534+ }
535+
536+ let SearchGraph { mode : _, stack, provisional_cache } = self ;
537+ if stack. is_empty ( ) {
538+ assert ! ( provisional_cache. is_empty( ) ) ;
539+ }
540+
541+ for ( depth, entry) in stack. iter_enumerated ( ) {
542+ let StackEntry {
543+ input,
544+ available_depth : _,
545+ reached_depth : _,
546+ non_root_cycle_participant,
547+ encountered_overflow : _,
548+ has_been_used,
549+ ref cycle_participants,
550+ provisional_result,
551+ } = * entry;
552+ let cache_entry = provisional_cache. get ( & entry. input ) . unwrap ( ) ;
553+ assert_eq ! ( cache_entry. stack_depth, Some ( depth) ) ;
554+ if let Some ( head) = non_root_cycle_participant {
555+ assert ! ( head < depth) ;
556+ assert ! ( cycle_participants. is_empty( ) ) ;
557+ assert_ne ! ( stack[ head] . has_been_used, HasBeenUsed :: empty( ) ) ;
558+
559+ let mut current_root = head;
560+ while let Some ( parent) = stack[ current_root] . non_root_cycle_participant {
561+ current_root = parent;
562+ }
563+ assert ! ( stack[ current_root] . cycle_participants. contains( & input) ) ;
564+ }
565+
566+ if !cycle_participants. is_empty ( ) {
567+ assert ! ( provisional_result. is_some( ) || !has_been_used. is_empty( ) ) ;
568+ for entry in stack. iter ( ) . take ( depth. as_usize ( ) ) {
569+ assert_eq ! ( cycle_participants. get( & entry. input) , None ) ;
570+ }
571+ }
572+ }
573+
574+ for ( & input, entry) in & self . provisional_cache {
575+ let ProvisionalCacheEntry { stack_depth, with_coinductive_stack, with_inductive_stack } =
576+ entry;
577+ assert ! (
578+ stack_depth. is_some( )
579+ || with_coinductive_stack. is_some( )
580+ || with_inductive_stack. is_some( )
581+ ) ;
582+
583+ if let & Some ( stack_depth) = stack_depth {
584+ assert_eq ! ( stack[ stack_depth] . input, input) ;
585+ }
586+
587+ let check_detached = |detached_entry : & DetachedEntry < ' tcx > | {
588+ let DetachedEntry { head, result : _ } = * detached_entry;
589+ assert_ne ! ( stack[ head] . has_been_used, HasBeenUsed :: empty( ) ) ;
590+ } ;
591+
592+ if let Some ( with_coinductive_stack) = with_coinductive_stack {
593+ check_detached ( with_coinductive_stack) ;
594+ }
595+
596+ if let Some ( with_inductive_stack) = with_inductive_stack {
597+ check_detached ( with_inductive_stack) ;
598+ }
599+ }
600+ }
601+ }
0 commit comments