@@ -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 < ' 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+ }
0 commit comments