@@ -51,12 +51,31 @@ struct StackEntry<'tcx> {
51
51
encountered_overflow : bool ,
52
52
53
53
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 > > ,
54
72
/// Starts out as `None` and gets set when rerunning this
55
73
/// goal in case we encounter a cycle.
56
74
provisional_result : Option < QueryResult < ' tcx > > ,
57
75
}
58
76
59
77
/// The provisional result for a goal which is not on the stack.
78
+ #[ derive( Debug ) ]
60
79
struct DetachedEntry < ' tcx > {
61
80
/// The head of the smallest non-trivial cycle involving this entry.
62
81
///
@@ -83,7 +102,7 @@ struct DetachedEntry<'tcx> {
83
102
///
84
103
/// The provisional cache can theoretically result in changes to the observable behavior,
85
104
/// see tests/ui/traits/next-solver/cycles/provisional-cache-impacts-behavior.rs.
86
- #[ derive( Default ) ]
105
+ #[ derive( Debug , Default ) ]
87
106
struct ProvisionalCacheEntry < ' tcx > {
88
107
stack_depth : Option < StackDepth > ,
89
108
with_inductive_stack : Option < DetachedEntry < ' tcx > > ,
@@ -98,31 +117,19 @@ impl<'tcx> ProvisionalCacheEntry<'tcx> {
98
117
}
99
118
}
100
119
120
+ #[ derive( Debug ) ]
101
121
pub ( super ) struct SearchGraph < ' tcx > {
102
122
mode : SolverMode ,
103
123
/// The stack of goals currently being computed.
104
124
///
105
125
/// An element is *deeper* in the stack if its index is *lower*.
106
126
stack : IndexVec < StackDepth , StackEntry < ' tcx > > ,
107
127
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 > > ,
116
128
}
117
129
118
130
impl < ' tcx > SearchGraph < ' tcx > {
119
131
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 ( ) }
126
133
}
127
134
128
135
pub ( super ) fn solver_mode ( & self ) -> SolverMode {
@@ -155,13 +162,7 @@ impl<'tcx> SearchGraph<'tcx> {
155
162
}
156
163
157
164
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 ( )
165
166
}
166
167
167
168
/// Returns the remaining depth allowed for nested goals.
@@ -211,15 +212,26 @@ impl<'tcx> SearchGraph<'tcx> {
211
212
// their result does not get moved to the global cache.
212
213
fn tag_cycle_participants (
213
214
stack : & mut IndexVec < StackDepth , StackEntry < ' tcx > > ,
214
- cycle_participants : & mut FxHashSet < CanonicalInput < ' tcx > > ,
215
215
usage_kind : HasBeenUsed ,
216
216
head : StackDepth ,
217
217
) {
218
218
stack[ head] . has_been_used |= usage_kind;
219
219
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 {
221
232
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 ) ) ;
223
235
}
224
236
}
225
237
@@ -246,6 +258,7 @@ impl<'tcx> SearchGraph<'tcx> {
246
258
inspect : & mut ProofTreeBuilder < TyCtxt < ' tcx > > ,
247
259
mut prove_goal : impl FnMut ( & mut Self , & mut ProofTreeBuilder < TyCtxt < ' tcx > > ) -> QueryResult < ' tcx > ,
248
260
) -> QueryResult < ' tcx > {
261
+ self . check_invariants ( ) ;
249
262
// Check for overflow.
250
263
let Some ( available_depth) = Self :: allowed_depth_for_nested ( tcx, & self . stack ) else {
251
264
if let Some ( last) = self . stack . raw . last_mut ( ) {
@@ -282,12 +295,7 @@ impl<'tcx> SearchGraph<'tcx> {
282
295
// already set correctly while computing the cache entry.
283
296
inspect
284
297
. 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 ) ;
291
299
return entry. result ;
292
300
} else if let Some ( stack_depth) = cache_entry. stack_depth {
293
301
debug ! ( "encountered cycle with depth {stack_depth:?}" ) ;
@@ -304,12 +312,7 @@ impl<'tcx> SearchGraph<'tcx> {
304
312
} else {
305
313
HasBeenUsed :: INDUCTIVE_CYCLE
306
314
} ;
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) ;
313
316
314
317
// Return the provisional result or, if we're in the first iteration,
315
318
// start with no constraints.
@@ -330,6 +333,7 @@ impl<'tcx> SearchGraph<'tcx> {
330
333
non_root_cycle_participant : None ,
331
334
encountered_overflow : false ,
332
335
has_been_used : HasBeenUsed :: empty ( ) ,
336
+ cycle_participants : Default :: default ( ) ,
333
337
provisional_result : None ,
334
338
} ;
335
339
assert_eq ! ( self . stack. push( entry) , depth) ;
@@ -376,27 +380,28 @@ impl<'tcx> SearchGraph<'tcx> {
376
380
} else {
377
381
self . provisional_cache . remove ( & input) ;
378
382
let reached_depth = final_entry. reached_depth . as_usize ( ) - self . stack . len ( ) ;
379
- let cycle_participants = mem:: take ( & mut self . cycle_participants ) ;
380
383
// When encountering a cycle, both inductive and coinductive, we only
381
384
// move the root into the global cache. We also store all other cycle
382
385
// participants involved.
383
386
//
384
387
// We must not use the global cache entry of a root goal if a cycle
385
388
// 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
387
390
// more details.
388
391
self . global_cache ( tcx) . insert (
389
392
tcx,
390
393
input,
391
394
proof_tree,
392
395
reached_depth,
393
396
final_entry. encountered_overflow ,
394
- cycle_participants,
397
+ final_entry . cycle_participants ,
395
398
dep_node,
396
399
result,
397
400
)
398
401
}
399
402
403
+ self . check_invariants ( ) ;
404
+
400
405
result
401
406
}
402
407
@@ -520,3 +525,77 @@ impl<'tcx> SearchGraph<'tcx> {
520
525
Ok ( super :: response_no_constraints_raw ( tcx, goal. max_universe , goal. variables , certainty) )
521
526
}
522
527
}
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