Skip to content

Commit b6c75fa

Browse files
add correct handling of false positives in coinductive cycles
add new tests for coinduction handling
1 parent e2e88a6 commit b6c75fa

File tree

3 files changed

+156
-21
lines changed

3 files changed

+156
-21
lines changed

chalk-recursive/src/recursive.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,33 @@ impl<'me, I: Interner> SolveDatabase<I> for Solver<'me, I> {
224224
// Check if this table is still on the stack.
225225
if let Some(depth) = self.context.search_graph[dfn].stack_depth {
226226
// Is this a coinductive goal? If so, that is success,
227-
// so we can return normally. Note that this return is
228-
// not tabled.
229-
//
230-
// XXX how does caching with coinduction work?
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.
231232
if self.context.stack.coinductive_cycle_from(depth) {
232233
let value = ConstrainedSubst {
233234
subst: goal.trivial_substitution(self.program.interner()),
234235
constraints: Constraints::empty(self.program.interner()),
235236
};
236-
debug!("applying coinductive semantics");
237-
return Ok(Solution::Unique(Canonical {
237+
let trivial_solution = Ok(Solution::Unique(Canonical {
238238
value,
239239
binders: goal.canonical.binders,
240240
}));
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;
241254
}
242255

243256
self.context.stack[depth].flag_cycle();
@@ -275,6 +288,11 @@ impl<'me, I: Interner> SolveDatabase<I> for Solver<'me, I> {
275288
// worst of the repeated work that we do during tabling.
276289
if subgoal_minimums.positive >= dfn {
277290
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+
278296
self.context
279297
.search_graph
280298
.move_to_cache(dfn, &mut self.context.cache);

chalk-recursive/src/search_graph.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ pub(super) struct Node<I: Interner> {
3737
/// from the stack, it contains the DFN of the minimal ancestor
3838
/// that the table reached (or MAX if no cycle was encountered).
3939
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,
4045
}
4146

4247
impl<I: Interner> SearchGraph<I> {
@@ -71,6 +76,7 @@ impl<I: Interner> SearchGraph<I> {
7176
solution_priority: ClausePriority::High,
7277
stack_depth: Some(stack_depth),
7378
links: Minimums { positive: dfn },
79+
coinductive_start: false,
7480
};
7581
self.nodes.push(node);
7682
let previous_index = self.indices.insert(goal.clone(), dfn);
@@ -101,6 +107,32 @@ impl<I: Interner> SearchGraph<I> {
101107
cache.insert(node.goal, node.solution);
102108
}
103109
}
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+
}
104136
}
105137

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

tests/test/coinduction.rs

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,23 +213,58 @@ fn coinductive_unsound1() {
213213

214214
goal {
215215
forall<X> { X: C1orC2 }
216-
} yields[SolverChoice::slg(3, None)] {
216+
} yields {
217217
"No possible solution"
218218
}
219+
}
220+
}
221+
222+
/// The only difference between this test and `coinductive_unsound1`
223+
/// is the order of the final `forall` clauses.
224+
#[test]
225+
fn coinductive_unsound2() {
226+
test! {
227+
program {
228+
trait C1orC2 { }
229+
230+
#[coinductive]
231+
trait C1 { }
232+
233+
#[coinductive]
234+
trait C2 { }
235+
236+
#[coinductive]
237+
trait C3 { }
238+
239+
forall<T> {
240+
T: C1 if T: C2, T: C3
241+
}
242+
243+
forall<T> {
244+
T: C2 if T: C1
245+
}
246+
247+
forall<T> {
248+
T: C1orC2 if T: C2
249+
}
250+
251+
forall<T> {
252+
T: C1orC2 if T: C1
253+
}
254+
}
219255

220256
goal {
221257
forall<X> { X: C1orC2 }
222-
} yields[SolverChoice::recursive_default()] {
223-
// FIXME(chalk#399) recursive solver doesn't handle coinduction correctly
224-
"Unique; substitution [], lifetime constraints []"
258+
} yields {
259+
"No possible solution"
225260
}
226261
}
227262
}
228263

229-
/// The only difference between this test and `coinductive_unsound1`
230-
/// is the order of the final `forall` clauses.
264+
/// Same as the two before but needs to show T: C2 in both
265+
// branches of T: C1.
231266
#[test]
232-
fn coinductive_unsound2() {
267+
fn coinductive_unsound3() {
233268
test! {
234269
program {
235270
trait C1orC2 { }
@@ -243,6 +278,13 @@ fn coinductive_unsound2() {
243278
#[coinductive]
244279
trait C3 { }
245280

281+
#[coinductive]
282+
trait C4 { }
283+
284+
forall<T> {
285+
T: C3 if T: C2, T: C4
286+
}
287+
246288
forall<T> {
247289
T: C1 if T: C2, T: C3
248290
}
@@ -251,13 +293,62 @@ fn coinductive_unsound2() {
251293
T: C2 if T: C1
252294
}
253295

296+
forall<T> {
297+
T: C1orC2 if T: C1
298+
}
299+
254300
forall<T> {
255301
T: C1orC2 if T: C2
256302
}
303+
}
304+
305+
goal {
306+
forall<X> { X: C1orC2 }
307+
} yields {
308+
"No possible solution"
309+
}
310+
}
311+
}
312+
313+
/// Tests whether a nested coinductive cycle
314+
/// that is also unsound is handled correctly.
315+
#[test]
316+
fn coinductive_unsound4() {
317+
test! {
318+
program {
319+
trait C1orC2 { }
320+
321+
#[coinductive]
322+
trait C1 { }
323+
324+
#[coinductive]
325+
trait C2 { }
326+
327+
#[coinductive]
328+
trait C3 { }
329+
330+
#[coinductive]
331+
trait C4 { }
332+
333+
forall<T> {
334+
T: C4 if T:C2, T: C3
335+
}
336+
337+
forall<T> {
338+
T: C1 if T: C2, T: C3
339+
}
340+
341+
forall<T> {
342+
T: C2 if T: C1, T: C4
343+
}
257344

258345
forall<T> {
259346
T: C1orC2 if T: C1
260347
}
348+
349+
forall<T> {
350+
T: C1orC2 if T: C2
351+
}
261352
}
262353

263354
goal {
@@ -450,14 +541,8 @@ fn coinductive_multicycle4() {
450541

451542
goal {
452543
forall<X> { X: Any }
453-
} yields_all[SolverChoice::slg(3, None)] {
454-
}
455-
456-
goal {
457-
forall<X> { X: Any }
458-
} yields[SolverChoice::recursive_default()] {
459-
// FIXME(chalk#399) recursive solver doesn't handle coinduction correctly
460-
"Unique; substitution [], lifetime constraints []"
544+
} yields {
545+
"No possible solution"
461546
}
462547
}
463548
}

0 commit comments

Comments
 (0)