Skip to content

Commit 9dc06fb

Browse files
authored
Merge pull request #488 from nikomatsakis/recursive-solver-document
document recursive solver
2 parents 656be2d + 6a1b090 commit 9dc06fb

File tree

9 files changed

+536
-1
lines changed

9 files changed

+536
-1
lines changed

book/src/SUMMARY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
- [Logic](./engine/logic.md)
2727
- [Coinduction](./engine/logic/coinduction.md)
2828
- [SLG Solver](./engine/slg.md)
29+
- [Chalk recursive solver](./recursive.md)
30+
- [The stack](./recursive/stack.md)
31+
- [Inductive cycles](./recursive/inductive_cycles.md)
32+
- [The search graph and caching](./recursive/search_graph.md)
33+
- [Coinduction](./recursive/coinduction.md)
2934

3035
---
3136

book/src/recursive.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Chalk recursive solver
2+
3+
The recursive solver, as its name suggests, is a logic solver that works
4+
"recursively". In particular, its basic structure is a function like:
5+
6+
```rust,ignore
7+
fn(Goal) -> Solution
8+
```
9+
10+
where the Goal is some [canonical goal](./canonical_queries.md) and
11+
the Solution is a result like:
12+
13+
* Provable(S): meaning the goal is provable and it is provably exactly (and
14+
only) for the substitution S. S is a set of values for the inference variables
15+
that appear in the goal. So if we had a goal like `Vec<?X>: Foo`, and we
16+
returned `Provable(?X = u32)`, it would mean that only `Vec<u32>: Foo` and not
17+
any other sort of vector (e.g., `Vec<u64>: Foo` does not hold).
18+
* Ambiguous(S): meaning that we can't prove whether or not the goal is true.
19+
This can sometimes come with a substitution S, which offers suggested values
20+
for the inference variables that might make it provable.
21+
* Error: the goal cannot be proven.
22+
23+
## Recursion: pros and cons
24+
25+
The recursive solver is so-called because, in the process of solving one goal,
26+
it will "recurse" to solve another. Consider an example like this:
27+
28+
```rust,ignore
29+
trait A { }
30+
impl<T: A> A for Vec<T> { }
31+
impl A for u32 { }
32+
impl A for i32 { }
33+
```
34+
35+
which results in program clauses like:
36+
37+
```notrust
38+
forall<T> { Implemented(Vec<T>: A) :- Implemented(T: MyTrait) }
39+
Implemented(u32: A)
40+
Implemented(i32: A)
41+
```
42+
43+
First, suppose that we have a goal like `Implemented(Vec<u64>: A)`. This would
44+
proceed like so:
45+
46+
* `Solve(Implemented(Vec<u64>: A))`
47+
* `Solve(Implemented(u64: A))`
48+
* returns `Error`
49+
* returns `Error`
50+
51+
In other words, the recursive solver would start by applying the first rule,
52+
which would cause us recursively try to solve `Implemented(u64: A)`. This would
53+
yield an Error result, because there are no applicable rules, and that error
54+
would propagate back up, causing the entire attempt at proving things to fail.
55+
56+
Next, consider `Implemented(Vec<u32>: A)`. This would proceed like so:
57+
58+
* `Solve(Implemented(Vec<u32>: A))`
59+
* `Solve(Implemented(u32: A))`
60+
* returns `Provable` with no substitution (no variables)
61+
* returns `Provable`
62+
63+
Finally, consider `Implemented(Vec<?X>: A)`. This is more interesting because it
64+
has a variable:
65+
66+
* `Solve(Implemented(Vec<?X>: A))`
67+
* `Solve(Implemented(?X: A))`
68+
* finds two viable solutions, returns `Ambiguous`
69+
* returns `Ambiguous`
70+
71+
## Recursion and completeness
72+
73+
One side-effect of the recursive solver's structure is that it
74+
cannot solve find solutions in some cases where a traditional
75+
Prolog solver would be successful. Consider this example:
76+
77+
```rust
78+
trait A { }
79+
trait B { }
80+
81+
impl<T: A + B> A for Vec<T> { }
82+
83+
impl A for u32 { }
84+
impl B for u32 { }
85+
86+
impl A for i32 { }
87+
```
88+
89+
In the recursive solver, with a goal of `Implemented(Vec<?X>: A)`, we
90+
recursively try to prove `Implemented(?X: A)` and get ambiguity, and we get
91+
stuck there.
92+
93+
The [SLG solver] in contrast starts by exploring `?X = u32` and finds
94+
that it works, and then later tries to explore `?X = i32` and finds that it
95+
fails (because `i32: B` is not true).
96+
97+
[SLG solver]: ./engine.md

book/src/recursive/coinduction.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Coinduction
2+
3+
TBD
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Inductive cycles
2+
3+
Recursive solving without cycles is easy. Solving with cycles is rather more
4+
complicated. Before we get into the details of the implementation,
5+
let's talk a bit about what behavior we actually *expect* in the face
6+
of possible cycles.
7+
8+
## Inductive cycles
9+
10+
By default, Rust trait solving is **inductive**. What that means is that, roughly
11+
speaking, you have to prove something is true without any cycles (i.e., you
12+
can't say "it's true because it's true"!).
13+
14+
For our purpose, a "cycle" means that, in the course of proving some canonical
15+
goal G, we had to prove that same goal G again.
16+
17+
Consider this Rust program:
18+
19+
```rust
20+
trait A { }
21+
impl<T: A> A for Vec<T> { }
22+
impl A for u32 { }
23+
```
24+
25+
Whether or not we hit a cycle will depend on the goal we are trying
26+
to solve. If for example we are trying to prove `Implemented(Vec<u32>: A)`,
27+
then we don't hit any cycle:
28+
29+
* `Implemented(Vec<u32>: A) :- Implemented(u32: A)` // from the first impl
30+
* `Implemented(u32: A)` // from the second impl
31+
32+
But what if we are trying to prove `Implemented(?X: A)`? This is a bit
33+
more interesting. Because we don't know what `?X` is, both impls are
34+
actually potentially applicable, so we wind up with two ways to
35+
prove our goal. We will try them out one after the other.
36+
37+
One possible execution might be:
38+
39+
* Prove `Implemented(?X: A)`
40+
* we find the program clause `forall<T> { Implemented(Vec<T>: A) :- Implemented(T: A) }` from the first impl
41+
* we create the variable `?Y` to represent `T` and unify `?X = Vec<?Y>`.
42+
* after unification, we have the subgoal `Implemented(?Y: A)`
43+
* when we go to recursively prove this impl, however, we find that it is already on the stack
44+
* this is because the [canonical form] of `Implemented(?X: A)` and `Implemented(?Y: A)` is the same
45+
46+
[canonical form]: ../canonical_queries.md
47+
48+
## What happens if we treat inductive cycles as errors?
49+
50+
So, what do we do when we hit an inductive cycle? Given that we told you that an
51+
inductive proof cannot contain cycles, you might imagine that we can just treat
52+
such a cycle as an error. But this won't give us the correct result.
53+
54+
Consider our previous example. If we just treat that cycle as an error, then we
55+
will conclude that the impl for `Vec<T>` doesn't apply to `?X: A`, and we'll
56+
proceed to try the impl for `u32`. This will let us reason that `?X: A` is
57+
provable if `?X = u32`. This is, in fact, correct: `?X = u32` *is* a possible
58+
answer. The problem is, it's not the only one!
59+
60+
In fact, `Implemented(?X: A)` has an **infinite** number of answers. It is true
61+
for `?X = u32`. It is true for `?X = Vec<u32>`. It is also true for
62+
`Vec<Vec<u32>>` and `Vec<Vec<Vec<u32>>>` and so on.
63+
64+
Given this, the correct result for our query is actually "ambiguous" -- in
65+
particular, there is no unique substitution that we can give that would make the
66+
query provable.
67+
68+
## How we solve cycles: loop and try again
69+
70+
The way we actually handle cycles is by iterating until we reach a fixed point
71+
(or ambiguity). We start out by assuming that all cycles are errors and we try
72+
to find some solution S. If we succeed, then we can do a loop and iterate again
73+
-- this time, for each cycle, we assume the result is S. This may yield some new
74+
solution, S1. The key point here is that we now have **two possible solutions**
75+
to the same goal, S and S1. This implies two possibilities:
76+
77+
* If S == S1, then in fact there is a unique solution, so we can return `Provable(S)`.
78+
* If S != S1, then we know there are two solutions, which means that there is
79+
not one unique solution, and hence the correct result is **ambiguous**,
80+
and in fact we can just stop and return right now.
81+
82+
This technique is very similar to the traditional Prolog technique of handling
83+
cycles, which is called **tabling**. The difference between our approach and
84+
tabling is that we are always looking for a unique solution, whereas Prolog
85+
(like the [SLG solver]) tries to enumerate all solutions (i.e., in Prolog,
86+
solving a goal is not a function but an iterator that yields solutions, and
87+
hence it would yield up S first, and then S1, and then any further answers we
88+
might get).
89+
90+
[SLG solver]: ../engine.md
91+
92+
Intuitively, what is happening here is that we're building bigger and bigger
93+
"proof trees" (i.e., trees of impl applications). In the first iteration, where
94+
we assumed that all recursive calls were errors, we would find exactly one
95+
solution, `u32: A` -- this is the root tree. In the next iteration, we can use
96+
this result to build a tree for `Vec<u32>: A` and so forth.
97+
98+
## Inductive cycles with no base case
99+
100+
It is interesting to look at what happens without the base case. Consider this
101+
program:
102+
103+
```rust
104+
trait B { }
105+
impl<T: B> B for Vec<T> { }
106+
```
107+
108+
In this case, there is no base case -- this means that in fact there are no
109+
solutions at all to the query `?X: B`. The reason is that the only type that
110+
could match would be a type of infinite size like `Vec<Vec<Vec<...>>>: B`, where
111+
the chain of `Vec` never terminates.
112+
113+
In our solver, this will work out just fine. We will wind up recursing
114+
and encountering a cycle. This will be treated as an error in the first
115+
iteration -- and then, at the end, we'll still have an error. This means
116+
that we've reached a fixed point, and we can stop.
117+
118+
119+
## Inductive cycles: when do we ever terminate
120+
121+
You might be wondering whether there are any examples of inductive cycles that
122+
actually terminate successfully and without ambiguity. In fact, there are very
123+
few, but you can construct an example like this:
124+
125+
```rust
126+
trait C { }
127+
impl<T: C + D> C for Vec<T> { }
128+
impl C for u32 { }
129+
130+
trait D { }
131+
```
132+
133+
In this case, the only valid result of `Implemented(?X: C)` is `?X = u32`. It can't
134+
be `Vec<u32>` because `Implemented(u32: D)` is not true.
135+
136+
How does this work out with the recursive solver? In the first iteration,
137+
we wind up with `?X = u32`, but we do encounter a cycle:
138+
139+
* proving `Implemented(?X: C)` has two possibilities...
140+
* `?X = Vec<?Y>` and `Implemented(?Y: C)`, which is a cycle (error, at least in this iteration)
141+
* `?X = u32`, succeeds
142+
143+
So then we try the next iteration:
144+
145+
* proving `Implemented(?X: C)` has two possibilities...
146+
* `?X = Vec<?Y>` and `Implemented(?Y: C)`, which is a cycle, so we use our previous result of `?Y = u32`
147+
* we then have to prove `Implemented(u32: D)`, which fails
148+
* `?X = u32`, succeeds

0 commit comments

Comments
 (0)