@@ -4,16 +4,33 @@ use std::sync::Arc;
44use super :: * ;
55use solve:: fulfill:: Fulfill ;
66
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+
725/// A Solver is the basic context in which you can propose goals for a given
826/// program. **All questions posed to the solver are in canonical, closed form,
927/// so that each question is answered with effectively a "clean slate"**. This
1028/// 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.
1330pub struct Solver {
1431 pub ( super ) program : Arc < ProgramEnvironment > ,
15- overflow_depth : usize ,
16- stack : Vec < FullyReducedGoal > ,
32+ stack : Vec < StackSlot > ,
33+ cycle_strategy : CycleStrategy ,
1734}
1835
1936/// An extension trait for merging `Result`s
@@ -35,11 +52,11 @@ impl<T> MergeWith<T> for Result<T> {
3552}
3653
3754impl Solver {
38- pub fn new ( program : & Arc < ProgramEnvironment > , overflow_depth : usize ) -> Self {
55+ pub fn new ( program : & Arc < ProgramEnvironment > , cycle_strategy : CycleStrategy ) -> Self {
3956 Solver {
4057 program : program. clone ( ) ,
4158 stack : vec ! [ ] ,
42- overflow_depth ,
59+ cycle_strategy ,
4360 }
4461 }
4562
@@ -88,67 +105,94 @@ impl Solver {
88105 pub fn solve_reduced_goal ( & mut self , goal : FullyReducedGoal ) -> Result < Solution > {
89106 debug_heading ! ( "Solver::solve({:?})" , goal) ;
90107
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 ( ) ) ;
99115 }
100- self . stack . push ( goal. clone ( ) ) ;
101116
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+ } ) ;
147133
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:
149146
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+ }
152196 }
153197
154198 fn solve_via_unification (
0 commit comments