Skip to content

Commit ad32300

Browse files
authored
Merge pull request #47 from scalexm/cycles
Implement a tabling strategy for handling cycles
2 parents 8e5b301 + 9fb7d49 commit ad32300

File tree

4 files changed

+165
-86
lines changed

4 files changed

+165
-86
lines changed

src/bin/chalki.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::Arc;
1111

1212
use chalk::ir;
1313
use chalk::lower::*;
14-
use chalk::solve::solver::Solver;
14+
use chalk::solve::solver::{Solver, CycleStrategy};
1515

1616
use rustyline::error::ReadlineError;
1717

@@ -118,8 +118,7 @@ fn read_program(rl: &mut rustyline::Editor<()>) -> Result<String> {
118118

119119
fn goal(text: &str, prog: &Program) -> Result<()> {
120120
let goal = chalk_parse::parse_goal(text)?.lower(&*prog.ir)?;
121-
let overflow_depth = 10;
122-
let mut solver = Solver::new(&prog.env, overflow_depth);
121+
let mut solver = Solver::new(&prog.env, CycleStrategy::Tabling);
123122
let goal = ir::InEnvironment::new(&ir::Environment::new(), *goal);
124123
match solver.solve_closed_goal(goal) {
125124
Ok(v) => println!("{}\n", v),

src/overlap.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ use itertools::Itertools;
44

55
use errors::*;
66
use ir::*;
7-
use solve::solver::Solver;
7+
use solve::solver::{Solver, CycleStrategy};
88

99
impl Program {
1010
pub fn check_overlapping_impls(&self) -> Result<()> {
11-
let mut solver = Solver::new(&Arc::new(self.environment()), 10);
11+
let mut solver = Solver::new(&Arc::new(self.environment()), CycleStrategy::Tabling);
1212

1313
// Create a vector of references to impl datums, sorted by trait ref
1414
let impl_data = self.impl_data.values().sorted_by(|lhs, rhs| {
@@ -80,7 +80,7 @@ fn intersection_of(lhs: &ImplDatum, rhs: &ImplDatum) -> InEnvironment<Goal> {
8080

8181
let lhs_len = lhs.binders.len();
8282

83-
// Join the two impls' binders together
83+
// Join the two impls' binders together
8484
let mut binders = lhs.binders.binders.clone();
8585
binders.extend(rhs.binders.binders.clone());
8686

@@ -100,7 +100,7 @@ fn intersection_of(lhs: &ImplDatum, rhs: &ImplDatum) -> InEnvironment<Goal> {
100100
// Create a goal for each clause in both where clauses
101101
let wc_goals = lhs_where_clauses.chain(rhs_where_clauses)
102102
.map(|wc| Goal::Leaf(LeafGoal::DomainGoal(wc)));
103-
103+
104104
// Join all the goals we've created together with And, then quantify them
105105
// over the joined binders. This is our query.
106106
let goal = params_goals.chain(wc_goals)

src/solve/solver.rs

+107-63
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,33 @@ use std::sync::Arc;
44
use super::*;
55
use 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.
1330
pub 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

3754
impl 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(

src/solve/test.rs

+52-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use chalk_parse;
22
use errors::*;
33
use ir;
44
use lower::*;
5-
use solve::solver::Solver;
5+
use solve::solver::{Solver, CycleStrategy};
66
use std::sync::Arc;
77

88
fn parse_and_lower_program(text: &str) -> Result<ir::Program> {
@@ -35,11 +35,7 @@ fn solve_goal(program_text: &str,
3535
assert!(goal_text.ends_with("}"));
3636
let goal = parse_and_lower_goal(&program, &goal_text[1..goal_text.len()-1]).unwrap();
3737

38-
// Pick a low overflow depth just because the existing
39-
// tests don't require a higher one.
40-
let overflow_depth = 3;
41-
42-
let mut solver = Solver::new(&env, overflow_depth);
38+
let mut solver = Solver::new(&env, CycleStrategy::Tabling);
4339
let goal = ir::InEnvironment::new(&ir::Environment::new(), *goal);
4440
let result = match solver.solve_closed_goal(goal) {
4541
Ok(v) => format!("{}", v),
@@ -136,6 +132,8 @@ fn prove_forall() {
136132
impl<T> Marker for Vec<T> { }
137133

138134
trait Clone { }
135+
impl Clone for Foo { }
136+
139137
impl<T> Clone for Vec<T> where T: Clone { }
140138
}
141139

@@ -158,7 +156,7 @@ fn prove_forall() {
158156
"Unique; substitution [], lifetime constraints []"
159157
}
160158

161-
// We don't have know to anything about `T` to know that
159+
// We don't have to know anything about `T` to know that
162160
// `Vec<T>: Marker`.
163161
goal {
164162
forall<T> { Vec<T>: Marker }
@@ -229,28 +227,66 @@ fn ordering() {
229227
}
230228
}
231229

232-
/// This test forces the solver into an overflow scenario: `Foo` is
233-
/// only implemented for `S<S<S<...>>>` ad infinitum. So when asked to
234-
/// compute the type for which `Foo` is implemented, we wind up
235-
/// recursing for a while before we overflow. You can see that our
236-
/// final result is "Maybe" (i.e., either multiple proof trees or an
237-
/// infinite proof tree) and that we do conclude that, if a definite
238-
/// proof tree exists, it must begin with `S<S<S<S<...>>>>`.
239230
#[test]
240-
fn max_depth() {
231+
fn cycle_no_solution() {
232+
test! {
233+
program {
234+
trait Foo { }
235+
struct S<T> { }
236+
impl<T> Foo for S<T> where T: Foo { }
237+
}
238+
239+
// only solution: infinite type S<S<S<...
240+
goal {
241+
exists<T> {
242+
T: Foo
243+
}
244+
} yields {
245+
"No possible solution: no applicable candidates"
246+
}
247+
}
248+
}
249+
250+
#[test]
251+
fn cycle_many_solutions() {
241252
test! {
242253
program {
243254
trait Foo { }
244255
struct S<T> { }
256+
struct i32 { }
245257
impl<T> Foo for S<T> where T: Foo { }
258+
impl Foo for i32 { }
259+
}
260+
261+
// infinite family of solutions: {i32, S<i32>, S<S<i32>>, ... }
262+
goal {
263+
exists<T> {
264+
T: Foo
265+
}
266+
} yields {
267+
"Ambiguous; no inference guidance"
268+
}
269+
}
270+
}
271+
272+
#[test]
273+
fn cycle_unique_solution() {
274+
test! {
275+
program {
276+
trait Foo { }
277+
trait Bar { }
278+
struct S<T> { }
279+
struct i32 { }
280+
impl<T> Foo for S<T> where T: Foo, T: Bar { }
281+
impl Foo for i32 { }
246282
}
247283

248284
goal {
249285
exists<T> {
250286
T: Foo
251287
}
252288
} yields {
253-
"Ambiguous; definite substitution [?0 := S<S<S<S<?0>>>>]"
289+
"Unique; substitution [?0 := i32]"
254290
}
255291
}
256292
}

0 commit comments

Comments
 (0)