Skip to content

Commit 897d0df

Browse files
add corresponding documentation in book
add solution0 document solution0 and remove unecessary test cases simplify according to review
1 parent b6c75fa commit 897d0df

File tree

6 files changed

+231
-136
lines changed

6 files changed

+231
-136
lines changed

book/src/recursive/coinduction.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,73 @@
11
# Coinduction
22

3-
TBD
3+
This sub-chapter is meant to describe the current handling of coinductive goals in the recursive solver rather than providing an extensive overview over the theoretical backgrounds and ideas.
4+
It follows the description in [this GitHub comment](https://github.com/rust-lang/chalk/issues/399#issuecomment-643420016) and the Zulip topic linked there.
5+
6+
## General Idea
7+
The general idea for the handling of coinductive cycles in the recursive solver is to start by assuming the goal is provable and then try to find evidence that it is not.
8+
This search for a disproof is done by the standard recursive solving process described in the sub-chapters before.
9+
10+
Albeit this approach would allow for the handling of mixed inductive/co-inductive cycles, these are actually handled as errors to prevent propagation of the assumed provability outside of the coinductive cycle.
11+
This propagation of the assumed solution might also happen in pure coinductive cycles and can potentially lead to invalid results.
12+
13+
## Prevention of Invalid Results
14+
The problem of invalid results propagated outside of the coinductive cycle is also described in the [Coinduction chapter](../engine/logic/coinduction.md) for the SLG solver alongside the rather complex handling used with it.
15+
16+
### The Problem
17+
The problem arises if a solution that is purely based on the positive starting value for the coinductive cycle is cached and as such propagated to other goals that are possibly reliant on this. An example may look like this (cf. the test case `coinduction::coinductive_unsound`):
18+
19+
```notrust
20+
C :- C1.
21+
C :- C2
22+
C1 :- C2, C3.
23+
C2 :- C1.
24+
```
25+
26+
Here `C` may be proved by either showing `C1` or `C2`.
27+
Assuming the solver starts evaluating the branch with `C1` first, it then recursively tries to prove `C2` and `C3`.
28+
For proving `C2` it needs to show `C1` again, the coinductive cycle becomes evident.
29+
Therefore, `C1` is assumed to be provable and the solver proves `C2` with this information.
30+
Assuming, the solve does not handle this case specifically, the solution for `C2` is cached.
31+
Now it tries solving `C3` but fails due to the lack of information about it.
32+
As such, `C1` can also not be proven for this program.
33+
The recursive solver will now attempt to prove the initial goal `C` by solving `C2`.
34+
Unfortunately, it finds the invalidly cached solution and returns it as proof for `C`.
35+
36+
By visualizing this path of computation, it becomes evident, where the problem lies:
37+
* Start proving `C` with `C1`:
38+
* For `C1` prove `C2` and `C3`:
39+
* For `C2` prove `C1`:
40+
* This is a coinductive cycle. Assume that `C1` holds.
41+
* Thus `C2` also holds. Store this result about `C2` in the cache.
42+
* There is no way to prove `C3`. Lift this failure up.
43+
* Due to the failure of `C3` there is also no solution for `C1`.
44+
* Try proving `C` with `C2`:
45+
* Find the cached result that `C2` has a solution and return it as the solution for `C`.
46+
* Stop with the invalid result for `C`.
47+
48+
### The Solution
49+
The above example should make it evident that the caching of found solutions in coinductive cycles can lead to invalid results and should therefore be prevented.
50+
This can be achieved by delaying the caching of all results inside the coinductive cycle until it is clear whether the start of the cycle (i.e. `C1` in the example above) is provable.
51+
If the start of the cycle can be proven by the results of the cycle and related subgoals then the assumption about it was correct and thus all results for goals inside the cycle are also valid.
52+
If, however, the start of the cycle can not be proved, i.e. the initial assumption was false, then a subset of the found solutions for the coinductive cycle may be invalid (i.e. the solution for `C2` in the example).
53+
54+
To remove such invalid results, the cycle is restarted with a negative result for the cycle start.
55+
With this approach, it is possible to remove all invalid result that would otherwise depend on the disproved cycle assumption.
56+
To allow for the cycle to be restarted correctly, all nodes in the search graph after the cycle start are deleted.
57+
58+
With this procedure, the example is handled as follows:
59+
* Start proving `C` with `C1`:
60+
* For `C1` prove `C2` and `C3`:
61+
* For `C2` prove `C1`:
62+
* This is a coinductive cycle. Assume that `C1` holds.
63+
* Thus `C2` also holds. Delay the caching of the result about `C2`.
64+
* There is no way to prove `C3`. Cache this result and lift the failure up.
65+
* Due to the failure of `C3` there is also no solution for `C1`. Set `C1` to a negative result and restart the cycle.
66+
* For `C2` prove `C1`:
67+
* `C1` has now a negative result.
68+
* Thus, `C2` also has a negative result which is not yet cached.
69+
* The result for `C3` is already cached.
70+
* Nothing changed regarding `C1` (this would indicate a negative cycle which is currently not allowed) and the negative result for `C1` and `C2` are cached. Lift negative result for `C1`.
71+
* Start proving `C` with `C2`:
72+
* Find negative cached result for `C2`. Lift the result back up.
73+
* Neither `C1` nor `C2` have a positive result. Stop with the valid disproof of `C`.

chalk-recursive/src/combine.rs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,7 @@ use chalk_solve::Solution;
22
use tracing::debug;
33

44
use chalk_ir::interner::Interner;
5-
use chalk_ir::{ClausePriority, DomainGoal, Fallible, GenericArg, Goal, GoalData};
6-
7-
pub(crate) fn with_priorities_for_goal<I: Interner>(
8-
interner: &I,
9-
goal: &Goal<I>,
10-
a: Fallible<Solution<I>>,
11-
prio_a: ClausePriority,
12-
b: Fallible<Solution<I>>,
13-
prio_b: ClausePriority,
14-
) -> (Fallible<Solution<I>>, ClausePriority) {
15-
let domain_goal = match goal.data(interner) {
16-
GoalData::DomainGoal(domain_goal) => domain_goal,
17-
_ => {
18-
// non-domain goals currently have no priorities, so we always take the new solution here
19-
return (b, prio_b);
20-
}
21-
};
22-
match (a, b) {
23-
(Ok(a), Ok(b)) => {
24-
let (solution, prio) = with_priorities(interner, domain_goal, a, prio_a, b, prio_b);
25-
(Ok(solution), prio)
26-
}
27-
(Ok(solution), Err(_)) => (Ok(solution), prio_a),
28-
(Err(_), Ok(solution)) => (Ok(solution), prio_b),
29-
(Err(_), Err(e)) => (Err(e), prio_b),
30-
}
31-
}
5+
use chalk_ir::{ClausePriority, DomainGoal, GenericArg};
326

337
pub(super) fn with_priorities<I: Interner>(
348
interner: &I,

chalk-recursive/src/recursive.rs

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use crate::search_graph::DepthFirstNumber;
22
use crate::search_graph::SearchGraph;
33
use crate::solve::{SolveDatabase, SolveIteration};
44
use crate::stack::{Stack, StackDepth};
5-
use crate::{combine, Minimums, UCanonicalGoal};
6-
use chalk_ir::interner::Interner;
5+
use crate::{Minimums, UCanonicalGoal};
76
use chalk_ir::Fallible;
8-
use chalk_ir::{Canonical, ConstrainedSubst, Constraints, Goal, InEnvironment, UCanonical};
7+
use chalk_ir::{interner::Interner, NoSolution};
8+
use chalk_ir::{Canonical, ConstrainedSubst, Goal, InEnvironment, UCanonical};
99
use chalk_solve::{coinductive_goal::IsCoinductive, RustIrDatabase, Solution};
1010
use rustc_hash::FxHashMap;
1111
use std::fmt;
@@ -163,18 +163,6 @@ impl<'me, I: Interner> Solver<'me, I> {
163163
return *minimums;
164164
}
165165

166-
let old_answer = &self.context.search_graph[dfn].solution;
167-
let old_prio = self.context.search_graph[dfn].solution_priority;
168-
169-
let (current_answer, current_prio) = combine::with_priorities_for_goal(
170-
self.program.interner(),
171-
&canonical_goal.canonical.value.goal,
172-
old_answer.clone(),
173-
old_prio,
174-
current_answer,
175-
current_prio,
176-
);
177-
178166
// Some of our subgoals depended on us. We need to re-run
179167
// with the current answer.
180168
if self.context.search_graph[dfn].solution == current_answer {
@@ -223,37 +211,15 @@ impl<'me, I: Interner> SolveDatabase<I> for Solver<'me, I> {
223211
if let Some(dfn) = self.context.search_graph.lookup(&goal) {
224212
// Check if this table is still on the stack.
225213
if let Some(depth) = self.context.search_graph[dfn].stack_depth {
226-
// Is this a coinductive goal? If so, that is success,
227-
// so we can return and set the minimum to its DFN.
228-
// Note that this return is not tabled. And so are
229-
// all other solutions in the cycle until the cycle
230-
// start is finished. This avoids prematurely cached
231-
// false positives.
232-
if self.context.stack.coinductive_cycle_from(depth) {
233-
let value = ConstrainedSubst {
234-
subst: goal.trivial_substitution(self.program.interner()),
235-
constraints: Constraints::empty(self.program.interner()),
236-
};
237-
let trivial_solution = Ok(Solution::Unique(Canonical {
238-
value,
239-
binders: goal.canonical.binders,
240-
}));
241-
242-
debug!("applying coinductive semantics");
243-
244-
// Set minimum to first occurrence of cyclic goal to prevent premature caching of possibly false solutions
245-
minimums.update_from(self.context.search_graph[dfn].links);
246-
247-
// Store trivial solution to start coinductive reasoning from this node
248-
self.context.search_graph[dfn].solution = trivial_solution.clone();
249-
self.context.search_graph[dfn].solution_priority =
250-
chalk_ir::ClausePriority::Low;
251-
self.context.search_graph[dfn].coinductive_start = true;
252-
253-
return trivial_solution;
254-
}
255-
256214
self.context.stack[depth].flag_cycle();
215+
// Mixed cycles are not allowed.
216+
if self
217+
.context
218+
.stack
219+
.mixed_inductive_coinductive_cycle_from(depth)
220+
{
221+
return Err(NoSolution);
222+
}
257223
}
258224

259225
minimums.update_from(self.context.search_graph[dfn].links);
@@ -268,10 +234,15 @@ impl<'me, I: Interner> SolveDatabase<I> for Solver<'me, I> {
268234
previous_solution
269235
} else {
270236
// Otherwise, push the goal onto the stack and create a table.
271-
// The initial result for this table is error.
237+
// The initial result for this table depends on whether the goal is coinductive.
272238
let coinductive_goal = goal.is_coinductive(self.program);
273239
let depth = self.context.stack.push(coinductive_goal);
274-
let dfn = self.context.search_graph.insert(&goal, depth);
240+
let dfn = self.context.search_graph.insert(
241+
&goal,
242+
depth,
243+
coinductive_goal,
244+
self.program.interner(),
245+
);
275246
let subgoal_minimums = self.solve_new_subgoal(goal, depth, dfn);
276247
self.context.search_graph[dfn].links = subgoal_minimums;
277248
self.context.search_graph[dfn].stack_depth = None;
@@ -288,11 +259,6 @@ impl<'me, I: Interner> SolveDatabase<I> for Solver<'me, I> {
288259
// worst of the repeated work that we do during tabling.
289260
if subgoal_minimums.positive >= dfn {
290261
if self.context.caching_enabled {
291-
// Remove possible false positives from coinductive cycle
292-
if self.context.search_graph[dfn].coinductive_start && result.is_err() {
293-
self.context.search_graph.remove_false_positives_after(dfn);
294-
}
295-
296262
self.context
297263
.search_graph
298264
.move_to_cache(dfn, &mut self.context.cache);

chalk-recursive/src/search_graph.rs

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use std::usize;
55

66
use super::stack::StackDepth;
77
use crate::{Minimums, UCanonicalGoal};
8-
use chalk_ir::{interner::Interner, ClausePriority, Fallible, NoSolution};
8+
use chalk_ir::{
9+
interner::Interner, Canonical, ClausePriority, ConstrainedSubst, Constraints, Fallible,
10+
NoSolution,
11+
};
912
use chalk_solve::Solution;
1013
use rustc_hash::FxHashMap;
1114
use tracing::{debug, instrument};
@@ -37,11 +40,6 @@ pub(super) struct Node<I: Interner> {
3740
/// from the stack, it contains the DFN of the minimal ancestor
3841
/// that the table reached (or MAX if no cycle was encountered).
3942
pub(crate) links: Minimums,
40-
41-
/// If this is true, the node is the start of coinductive cycle.
42-
/// Thus, some cleanup has to be done before its result can be
43-
/// cached to rule out false positives.
44-
pub(crate) coinductive_start: bool,
4543
}
4644

4745
impl<I: Interner> SearchGraph<I> {
@@ -61,22 +59,35 @@ impl<I: Interner> SearchGraph<I> {
6159
///
6260
/// - stack depth as given
6361
/// - links set to its own DFN
64-
/// - solution is initially `NoSolution`
62+
/// - solution is initially an identity substitution for coinductive goals
63+
/// or `NoSolution` for other goals
6564
pub(crate) fn insert(
6665
&mut self,
6766
goal: &UCanonicalGoal<I>,
6867
stack_depth: StackDepth,
68+
coinductive: bool,
69+
interner: &I,
6970
) -> DepthFirstNumber {
7071
let dfn = DepthFirstNumber {
7172
index: self.nodes.len(),
7273
};
74+
let solution = if coinductive {
75+
Ok(Solution::Unique(Canonical {
76+
value: ConstrainedSubst {
77+
subst: goal.trivial_substitution(interner),
78+
constraints: Constraints::empty(interner),
79+
},
80+
binders: goal.canonical.binders.clone(),
81+
}))
82+
} else {
83+
Err(NoSolution)
84+
};
7385
let node = Node {
7486
goal: goal.clone(),
75-
solution: Err(NoSolution),
87+
solution,
7688
solution_priority: ClausePriority::High,
7789
stack_depth: Some(stack_depth),
7890
links: Minimums { positive: dfn },
79-
coinductive_start: false,
8091
};
8192
self.nodes.push(node);
8293
let previous_index = self.indices.insert(goal.clone(), dfn);
@@ -107,32 +118,6 @@ impl<I: Interner> SearchGraph<I> {
107118
cache.insert(node.goal, node.solution);
108119
}
109120
}
110-
111-
/// Removes all nodes that are part of a coinductive cycle and
112-
/// have a solution as they might be false positives due to
113-
/// coinductive reasoning.
114-
#[instrument(level = "debug", skip(self))]
115-
pub(crate) fn remove_false_positives_after(&mut self, dfn: DepthFirstNumber) {
116-
let mut false_positive_indices = vec![];
117-
118-
// Find all possible false positives in the graph below the
119-
// start of the coinductive cycle
120-
for (index, node) in self.nodes[dfn.index + 1..].iter().enumerate() {
121-
if node.solution.is_ok() {
122-
false_positive_indices.push(index + dfn.index + 1);
123-
}
124-
}
125-
126-
// Remove the potential false positives from the indices
127-
self.indices
128-
.retain(|_key, value| !false_positive_indices.contains(&value.index));
129-
130-
// Remove the potential false positives from the nodes
131-
// in descending order to avoid unnecessary shifts
132-
for false_positive_index in false_positive_indices.into_iter().rev() {
133-
self.nodes.remove(false_positive_index);
134-
}
135-
}
136121
}
137122

138123
impl<I: Interner> Index<DepthFirstNumber> for SearchGraph<I> {

chalk-recursive/src/stack.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,20 @@ impl Stack {
6767
self.entries.pop();
6868
}
6969

70-
/// True if all the goals from the top of the stack down to (and
71-
/// including) the given depth are coinductive.
72-
pub(crate) fn coinductive_cycle_from(&self, depth: StackDepth) -> bool {
73-
self.entries[depth.depth..]
74-
.iter()
75-
.all(|entry| entry.coinductive_goal)
70+
/// True if either all the goals from the top of the stack down to (and
71+
/// including) the given depth are coinductive or if all goals are inductive
72+
/// (i.e. not coinductive).
73+
pub(crate) fn mixed_inductive_coinductive_cycle_from(&self, depth: StackDepth) -> bool {
74+
let (inductive, coinductive) =
75+
self.entries[depth.depth..]
76+
.iter()
77+
.fold((false, false), |(ind, coind), entry| {
78+
(
79+
ind || !entry.coinductive_goal,
80+
coind || entry.coinductive_goal,
81+
)
82+
});
83+
inductive && coinductive
7684
}
7785
}
7886

0 commit comments

Comments
 (0)