@@ -4,16 +4,33 @@ use std::sync::Arc;
4
4
use super :: * ;
5
5
use solve:: fulfill:: Fulfill ;
6
6
7
+ /// We use a stack for detecting cycles. Each stack slot contains:
8
+ /// - a goal which is being processed
9
+ /// - a flag indicating the presence of a cycle during the processing of this goal
10
+ /// - in case a cycle has been found, the latest previous answer to the same goal
11
+ #[ derive( Debug ) ]
12
+ struct StackSlot {
13
+ goal : FullyReducedGoal ,
14
+ cycle : bool ,
15
+ answer : Option < Solution > ,
16
+ }
17
+
18
+ /// For debugging purpose only: choose whether to apply a tabling strategy for cycles or
19
+ /// treat them as hard errors (the latter can possibly reduce debug output)
20
+ pub enum CycleStrategy {
21
+ Tabling ,
22
+ Error ,
23
+ }
24
+
7
25
/// A Solver is the basic context in which you can propose goals for a given
8
26
/// program. **All questions posed to the solver are in canonical, closed form,
9
27
/// so that each question is answered with effectively a "clean slate"**. This
10
28
/// allows for better caching, and simplifies management of the inference
11
- /// context. Solvers do, however, maintain a stack of questions being posed, so
12
- /// as to avoid unbounded search.
29
+ /// context.
13
30
pub struct Solver {
14
31
pub ( super ) program : Arc < ProgramEnvironment > ,
15
- overflow_depth : usize ,
16
- stack : Vec < FullyReducedGoal > ,
32
+ stack : Vec < StackSlot > ,
33
+ cycle_strategy : CycleStrategy ,
17
34
}
18
35
19
36
/// An extension trait for merging `Result`s
@@ -35,11 +52,11 @@ impl<T> MergeWith<T> for Result<T> {
35
52
}
36
53
37
54
impl Solver {
38
- pub fn new ( program : & Arc < ProgramEnvironment > , overflow_depth : usize ) -> Self {
55
+ pub fn new ( program : & Arc < ProgramEnvironment > , cycle_strategy : CycleStrategy ) -> Self {
39
56
Solver {
40
57
program : program. clone ( ) ,
41
58
stack : vec ! [ ] ,
42
- overflow_depth ,
59
+ cycle_strategy ,
43
60
}
44
61
}
45
62
@@ -88,67 +105,94 @@ impl Solver {
88
105
pub fn solve_reduced_goal ( & mut self , goal : FullyReducedGoal ) -> Result < Solution > {
89
106
debug_heading ! ( "Solver::solve({:?})" , goal) ;
90
107
91
- // First we cut off runaway recursion
92
- if self . stack . contains ( & goal) || self . stack . len ( ) > self . overflow_depth {
93
- // Recursive invocation or overflow
94
- debug ! (
95
- "solve: {:?} already on the stack or overflowed max depth" ,
96
- goal
97
- ) ;
98
- return Ok ( Solution :: Ambig ( Guidance :: Unknown ) ) ;
108
+ // If the goal is already on the stack, we found a cycle and indicate it by setting
109
+ // `slot.cycle = true`. If there is no cached answer, we can't make any more progress
110
+ // and return `Err`. If there is one, use this answer.
111
+ if let Some ( slot) = self . stack . iter_mut ( ) . find ( |s| { s. goal == goal } ) {
112
+ slot. cycle = true ;
113
+ debug ! ( "cycle detected: previous solution {:?}" , slot. answer) ;
114
+ return slot. answer . clone ( ) . ok_or ( "cycle" . into ( ) ) ;
99
115
}
100
- self . stack . push ( goal. clone ( ) ) ;
101
116
102
- let result = match goal {
103
- FullyReducedGoal :: EqGoal ( g) => {
104
- // Equality goals are understood "natively" by the logic, via unification:
105
- self . solve_via_unification ( g)
106
- }
107
- FullyReducedGoal :: DomainGoal ( Canonical { value, binders } ) => {
108
- // "Domain" goals (i.e., leaf goals that are Rust-specific) are
109
- // always solved via some form of implication. We can either
110
- // apply assumptions from our environment (i.e. where clauses),
111
- // or from the lowered program, which includes fallback
112
- // clauses. We try each approach in turn:
113
-
114
- let env_clauses = value
115
- . environment
116
- . elaborated_clauses ( & self . program )
117
- . map ( DomainGoal :: into_program_clause) ;
118
- let env_solution = self . solve_from_clauses ( & binders, & value, env_clauses) ;
119
-
120
- let prog_clauses: Vec < _ > = self . program . program_clauses . iter ( )
121
- . cloned ( )
122
- . filter ( |clause| !clause. fallback_clause )
123
- . collect ( ) ;
124
- let prog_solution = self . solve_from_clauses ( & binders, & value, prog_clauses) ;
125
-
126
- // These fallback clauses are used when we're sure we'll never
127
- // reach Unique via another route
128
- let fallback: Vec < _ > = self . program . program_clauses . iter ( )
129
- . cloned ( )
130
- . filter ( |clause| clause. fallback_clause )
131
- . collect ( ) ;
132
- let fallback_solution = self . solve_from_clauses ( & binders, & value, fallback) ;
133
-
134
- // Now that we have all the outcomes, we attempt to combine
135
- // them. Here, we apply a heuristic (also found in rustc): if we
136
- // have possible solutions via both the environment *and* the
137
- // program, we favor the environment; this only impacts type
138
- // inference. The idea is that the assumptions you've explicitly
139
- // made in a given context are more likely to be relevant than
140
- // general `impl`s.
141
-
142
- env_solution
143
- . merge_with ( prog_solution, |env, prog| env. favor_over ( prog) )
144
- . merge_with ( fallback_solution, |merged, fallback| merged. fallback_to ( fallback) )
145
- }
146
- } ;
117
+ // We start with `answer = None` and try to solve the goal. At the end of the iteration,
118
+ // `answer` will be updated with the result of the solving process. If we detect a cycle
119
+ // during the solving process, we cache `answer` and try to solve the goal again. We repeat
120
+ // until we reach a fixed point for `answer`.
121
+ // Considering the partial order:
122
+ // - None < Some(Unique) < Some(Ambiguous)
123
+ // - None < Some(CannotProve)
124
+ // the function which maps the loop iteration to `answer` is a nondecreasing function
125
+ // so this function will eventually be constant and the loop terminates.
126
+ let mut answer = None ;
127
+ loop {
128
+ self . stack . push ( StackSlot {
129
+ goal : goal. clone ( ) ,
130
+ cycle : false ,
131
+ answer : answer. clone ( ) ,
132
+ } ) ;
147
133
148
- self . stack . pop ( ) . unwrap ( ) ;
134
+ debug ! ( "Solver::solve: new loop iteration" ) ;
135
+ let result = match goal. clone ( ) {
136
+ FullyReducedGoal :: EqGoal ( g) => {
137
+ // Equality goals are understood "natively" by the logic, via unification:
138
+ self . solve_via_unification ( g)
139
+ }
140
+ FullyReducedGoal :: DomainGoal ( Canonical { value, binders } ) => {
141
+ // "Domain" goals (i.e., leaf goals that are Rust-specific) are
142
+ // always solved via some form of implication. We can either
143
+ // apply assumptions from our environment (i.e. where clauses),
144
+ // or from the lowered program, which includes fallback
145
+ // clauses. We try each approach in turn:
149
146
150
- debug ! ( "Solver::solve: result={:?}" , result) ;
151
- result
147
+ let env_clauses = value
148
+ . environment
149
+ . elaborated_clauses ( & self . program )
150
+ . map ( DomainGoal :: into_program_clause) ;
151
+ let env_solution = self . solve_from_clauses ( & binders, & value, env_clauses) ;
152
+
153
+ let prog_clauses: Vec < _ > = self . program . program_clauses . iter ( )
154
+ . cloned ( )
155
+ . filter ( |clause| !clause. fallback_clause )
156
+ . collect ( ) ;
157
+ let prog_solution = self . solve_from_clauses ( & binders, & value, prog_clauses) ;
158
+
159
+ // These fallback clauses are used when we're sure we'll never
160
+ // reach Unique via another route
161
+ let fallback: Vec < _ > = self . program . program_clauses . iter ( )
162
+ . cloned ( )
163
+ . filter ( |clause| clause. fallback_clause )
164
+ . collect ( ) ;
165
+ let fallback_solution = self . solve_from_clauses ( & binders, & value, fallback) ;
166
+
167
+ // Now that we have all the outcomes, we attempt to combine
168
+ // them. Here, we apply a heuristic (also found in rustc): if we
169
+ // have possible solutions via both the environment *and* the
170
+ // program, we favor the environment; this only impacts type
171
+ // inference. The idea is that the assumptions you've explicitly
172
+ // made in a given context are more likely to be relevant than
173
+ // general `impl`s.
174
+
175
+ env_solution
176
+ . merge_with ( prog_solution, |env, prog| env. favor_over ( prog) )
177
+ . merge_with ( fallback_solution, |merged, fallback| merged. fallback_to ( fallback) )
178
+ }
179
+ } ;
180
+ debug ! ( "Solver::solve: loop iteration result = {:?}" , result) ;
181
+
182
+ let slot = self . stack . pop ( ) . unwrap ( ) ;
183
+ match self . cycle_strategy {
184
+ CycleStrategy :: Tabling if slot. cycle => {
185
+ let actual_answer = result. as_ref ( ) . ok ( ) . map ( |s| s. clone ( ) ) ;
186
+ if actual_answer == answer {
187
+ // Fixed point: break
188
+ return result;
189
+ } else {
190
+ answer = actual_answer;
191
+ }
192
+ }
193
+ _ => return result,
194
+ } ;
195
+ }
152
196
}
153
197
154
198
fn solve_via_unification (
0 commit comments