@@ -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 < ' tcx > ,
247
259
mut prove_goal : impl FnMut ( & mut Self , & mut ProofTreeBuilder < ' 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 ( ) {
@@ -281,12 +294,7 @@ impl<'tcx> SearchGraph<'tcx> {
281
294
// already set correctly while computing the cache entry.
282
295
inspect
283
296
. 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 ) ;
290
298
return entry. result ;
291
299
} else if let Some ( stack_depth) = cache_entry. stack_depth {
292
300
debug ! ( "encountered cycle with depth {stack_depth:?}" ) ;
@@ -303,12 +311,7 @@ impl<'tcx> SearchGraph<'tcx> {
303
311
} else {
304
312
HasBeenUsed :: INDUCTIVE_CYCLE
305
313
} ;
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) ;
312
315
313
316
// Return the provisional result or, if we're in the first iteration,
314
317
// start with no constraints.
@@ -329,6 +332,7 @@ impl<'tcx> SearchGraph<'tcx> {
329
332
non_root_cycle_participant : None ,
330
333
encountered_overflow : false ,
331
334
has_been_used : HasBeenUsed :: empty ( ) ,
335
+ cycle_participants : Default :: default ( ) ,
332
336
provisional_result : None ,
333
337
} ;
334
338
assert_eq ! ( self . stack. push( entry) , depth) ;
@@ -375,27 +379,28 @@ impl<'tcx> SearchGraph<'tcx> {
375
379
} else {
376
380
self . provisional_cache . remove ( & input) ;
377
381
let reached_depth = final_entry. reached_depth . as_usize ( ) - self . stack . len ( ) ;
378
- let cycle_participants = mem:: take ( & mut self . cycle_participants ) ;
379
382
// When encountering a cycle, both inductive and coinductive, we only
380
383
// move the root into the global cache. We also store all other cycle
381
384
// participants involved.
382
385
//
383
386
// We must not use the global cache entry of a root goal if a cycle
384
387
// 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
386
389
// more details.
387
390
self . global_cache ( tcx) . insert (
388
391
tcx,
389
392
input,
390
393
proof_tree,
391
394
reached_depth,
392
395
final_entry. encountered_overflow ,
393
- cycle_participants,
396
+ final_entry . cycle_participants ,
394
397
dep_node,
395
398
result,
396
399
)
397
400
}
398
401
402
+ self . check_invariants ( ) ;
403
+
399
404
result
400
405
}
401
406
@@ -519,3 +524,77 @@ impl<'tcx> SearchGraph<'tcx> {
519
524
Ok ( super :: response_no_constraints_raw ( tcx, goal. max_universe , goal. variables , certainty) )
520
525
}
521
526
}
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