blob: 517ca253b4da37f0a731a10a7c3f6ef5ad4523e3 [file] [log] [blame]
use std::fmt::Debug;
use std::hash::Hash;
use tracing::debug;
use tracing::{info, instrument};
mod cache;
mod search_graph;
mod stack;
pub use cache::Cache;
use search_graph::{DepthFirstNumber, SearchGraph};
use stack::{Stack, StackDepth};
pub(super) struct RecursiveContext<K, V>
where
K: Hash + Eq + Debug + Clone,
V: Debug + Clone,
{
stack: Stack,
/// The "search graph" stores "in-progress results" that are still being
/// solved.
search_graph: SearchGraph<K, V>,
/// The "cache" stores results for goals that we have completely solved.
/// Things are added to the cache when we have completely processed their
/// result.
cache: Option<Cache<K, V>>,
/// The maximum size for goals.
max_size: usize,
}
pub(super) trait SolverStuff<K, V>: Copy
where
K: Hash + Eq + Debug + Clone,
V: Debug + Clone,
{
fn is_coinductive_goal(self, goal: &K) -> bool;
fn initial_value(self, goal: &K, coinductive_goal: bool) -> V;
fn solve_iteration(
self,
context: &mut RecursiveContext<K, V>,
goal: &K,
minimums: &mut Minimums,
should_continue: impl std::ops::Fn() -> bool + Clone,
) -> V;
fn reached_fixed_point(self, old_value: &V, new_value: &V) -> bool;
fn error_value(self) -> V;
}
/// The `minimums` struct is used while solving to track whether we encountered
/// any cycles in the process.
#[derive(Copy, Clone, Debug)]
pub(super) struct Minimums {
positive: DepthFirstNumber,
}
impl Minimums {
pub fn new() -> Self {
Minimums {
positive: DepthFirstNumber::MAX,
}
}
pub fn update_from(&mut self, minimums: Minimums) {
self.positive = ::std::cmp::min(self.positive, minimums.positive);
}
}
impl<K, V> RecursiveContext<K, V>
where
K: Hash + Eq + Debug + Clone,
V: Debug + Clone,
{
pub fn new(overflow_depth: usize, max_size: usize, cache: Option<Cache<K, V>>) -> Self {
RecursiveContext {
stack: Stack::new(overflow_depth),
search_graph: SearchGraph::new(),
cache,
max_size,
}
}
pub fn max_size(&self) -> usize {
self.max_size
}
/// Solves a canonical goal. The substitution returned in the
/// solution will be for the fully decomposed goal. For example, given the
/// program
///
/// ```ignore
/// struct u8 { }
/// struct SomeType<T> { }
/// trait Foo<T> { }
/// impl<U> Foo<u8> for SomeType<U> { }
/// ```
///
/// and the goal `exists<V> { forall<U> { SomeType<U>: Foo<V> }
/// }`, `into_peeled_goal` can be used to create a canonical goal
/// `SomeType<!1>: Foo<?0>`. This function will then return a
/// solution with the substitution `?0 := u8`.
pub fn solve_root_goal(
&mut self,
canonical_goal: &K,
solver_stuff: impl SolverStuff<K, V>,
should_continue: impl std::ops::Fn() -> bool + Clone,
) -> V {
debug!("solve_root_goal(canonical_goal={:?})", canonical_goal);
assert!(self.stack.is_empty());
let minimums = &mut Minimums::new();
self.solve_goal(canonical_goal, minimums, solver_stuff, should_continue)
}
/// Attempt to solve a goal that has been fully broken down into leaf form
/// and canonicalized. This is where the action really happens, and is the
/// place where we would perform caching in rustc (and may eventually do in Chalk).
#[instrument(level = "info", skip(self, minimums, solver_stuff, should_continue))]
pub fn solve_goal(
&mut self,
goal: &K,
minimums: &mut Minimums,
solver_stuff: impl SolverStuff<K, V>,
should_continue: impl std::ops::Fn() -> bool + Clone,
) -> V {
// First check the cache.
if let Some(cache) = &self.cache {
if let Some(value) = cache.get(goal) {
debug!("solve_reduced_goal: cache hit, value={:?}", value);
return value;
}
}
// Next, check if the goal is in the search tree already.
if let Some(dfn) = self.search_graph.lookup(goal) {
// Check if this table is still on the stack.
if let Some(depth) = self.search_graph[dfn].stack_depth {
self.stack[depth].flag_cycle();
// Mixed cycles are not allowed. For more information about this
// see the corresponding section in the coinduction chapter:
// https://rust-lang.github.io/chalk/book/recursive/coinduction.html#mixed-co-inductive-and-inductive-cycles
if self.stack.mixed_inductive_coinductive_cycle_from(depth) {
return solver_stuff.error_value();
}
}
minimums.update_from(self.search_graph[dfn].links);
// Return the solution from the table.
let previous_solution = self.search_graph[dfn].solution.clone();
info!(
"solve_goal: cycle detected, previous solution {:?}",
previous_solution,
);
previous_solution
} else {
// Otherwise, push the goal onto the stack and create a table.
// The initial result for this table depends on whether the goal is coinductive.
let coinductive_goal = solver_stuff.is_coinductive_goal(goal);
let initial_solution = solver_stuff.initial_value(goal, coinductive_goal);
let depth = self.stack.push(coinductive_goal);
let dfn = self.search_graph.insert(goal, depth, initial_solution);
let subgoal_minimums =
self.solve_new_subgoal(goal, depth, dfn, solver_stuff, should_continue);
self.search_graph[dfn].links = subgoal_minimums;
self.search_graph[dfn].stack_depth = None;
self.stack.pop(depth);
minimums.update_from(subgoal_minimums);
// Read final result from table.
let result = self.search_graph[dfn].solution.clone();
// If processing this subgoal did not involve anything
// outside of its subtree, then we can promote it to the
// cache now. This is a sort of hack to alleviate the
// worst of the repeated work that we do during tabling.
if subgoal_minimums.positive >= dfn {
if let Some(cache) = &mut self.cache {
self.search_graph.move_to_cache(dfn, cache);
debug!("solve_reduced_goal: SCC head encountered, moving to cache");
} else {
debug!(
"solve_reduced_goal: SCC head encountered, rolling back as caching disabled"
);
self.search_graph.rollback_to(dfn);
}
}
info!("solve_goal: solution = {:?}", result);
result
}
}
#[instrument(level = "debug", skip(self, solver_stuff, should_continue))]
fn solve_new_subgoal(
&mut self,
canonical_goal: &K,
depth: StackDepth,
dfn: DepthFirstNumber,
solver_stuff: impl SolverStuff<K, V>,
should_continue: impl std::ops::Fn() -> bool + Clone,
) -> Minimums {
// We start with `answer = None` and try to solve the goal. At the end of the iteration,
// `answer` will be updated with the result of the solving process. If we detect a cycle
// during the solving process, we cache `answer` and try to solve the goal again. We repeat
// until we reach a fixed point for `answer`.
// Considering the partial order:
// - None < Some(Unique) < Some(Ambiguous)
// - None < Some(CannotProve)
// the function which maps the loop iteration to `answer` is a nondecreasing function
// so this function will eventually be constant and the loop terminates.
loop {
let minimums = &mut Minimums::new();
let current_answer = solver_stuff.solve_iteration(
self,
canonical_goal,
minimums,
should_continue.clone(), // Note: cloning required as workaround for https://github.com/rust-lang/rust/issues/95734
);
debug!(
"solve_new_subgoal: loop iteration result = {:?} with minimums {:?}",
current_answer, minimums
);
if !self.stack[depth].read_and_reset_cycle_flag() {
// None of our subgoals depended on us directly.
// We can return.
self.search_graph[dfn].solution = current_answer;
return *minimums;
}
let old_answer =
std::mem::replace(&mut self.search_graph[dfn].solution, current_answer);
if solver_stuff.reached_fixed_point(&old_answer, &self.search_graph[dfn].solution) {
return *minimums;
}
// Otherwise: rollback the search tree and try again.
self.search_graph.rollback_to(dfn + 1);
}
}
}