Document ObligationForest better.

This commit is contained in:
Niko Matsakis 2016-01-16 05:21:01 -05:00
parent 0723d6c6ed
commit 8c14649ce8
4 changed files with 179 additions and 50 deletions
src
librustc/middle/traits
librustc_data_structures/obligation_forest

@ -281,7 +281,7 @@ impl<'tcx> FulfillmentContext<'tcx> {
debug!("select_where_possible: outcome={:?}", outcome);
// these are obligations that were proven to be true.
for pending_obligation in outcome.successful {
for pending_obligation in outcome.completed {
let predicate = &pending_obligation.obligation.predicate;
if predicate.is_global() {
selcx.tcx().fulfilled_predicates.borrow_mut()

@ -0,0 +1,80 @@
The `ObligationForest` is a utility data structure used in trait
matching to track the set of outstanding obligations (those not yet
resolved to success or error). It also tracks the "backtrace" of each
pending obligation (why we are trying to figure this out in the first
place).
### External view
`ObligationForest` supports two main public operations (there are a
few others not discussed here):
1. Add a new root obligation (`push_root`).
2. Process the pending obligations (`process_obligations`).
When a new obligation `N` is added, it becomes the root of an
obligation tree. This tree is a singleton to start, so `N` is both the
root and the only leaf. Each time the `process_obligations` method is
called, it will invoke its callback with every pending obligation (so
that will include `N`, the first time). The callback shoud process the
obligation `O` that it is given and return one of three results:
- `Ok(None)` -> ambiguous result. Obligation was neither a success
nor a failure. It is assumed that further attempts to process the
obligation will yield the same result unless something in the
surrounding environment changes.
- `Ok(Some(C))` - the obligation was *shallowly successful*. The
vector `C` is a list of subobligations. The meaning of this is that
`O` was successful on the assumption that all the obligations in `C`
are also successful. Therefore, `O` is only considered a "true"
success if `C` is empty. Otherwise, `O` is put into a suspended
state and the obligations in `C` become the new pending
obligations. They will be processed the next time you call
`process_obligations`.
- `Err(E)` -> obligation failed with error `E`. We will collect this
error and return it from `process_obligations`, along with the
"backtrace" of obligations (that is, the list of obligations up to
and including the root of the failed obligation). No further
obligations from that same tree will be processed, since the tree is
now considered to be in error.
When the call to `process_obligations` completes, you get back an `Outcome`,
which includes three bits of information:
- `completed`: a list of obligations where processing was fully
completed without error (meaning that all transitive subobligations
have also been completed). So, for example, if the callback from
`process_obligations` returns `Ok(Some(C))` for some obligation `O`,
then `O` will be considered completed right away if `C` is the
empty vector. Otherwise it will only be considered completed once
all the obligations in `C` have been found completed.
- `errors`: a list of errors that occurred and associated backtraces
at the time of error, which can be used to give context to the user.
- `stalled`: if true, then none of the existing obligations were
*shallowly successful* (that is, no callback returned `Ok(Some(_))`).
This implies that all obligations were either errors or returned an
ambiguous result, which means that any further calls to
`process_obligations` would simply yield back further ambiguous
results. This is used by the `FulfillmentContext` to decide when it
has reached a steady state.
#### Snapshots
The `ObligationForest` supports a limited form of snapshots; see
`start_snapshot`; `commit_snapshot`; and `rollback_snapshot`. In
particular, you can use a snapshot to roll back new root
obligations. However, it is an error to attempt to
`process_obligations` during a snapshot.
### Implementation details
For the most part, comments specific to the implementation are in the
code. This file only contains a very high-level overview. Basically,
the forest is stored in a vector. Each element of the vector is a node
in some tree. Each node in the vector has the index of an (optional)
parent and (for convenience) its root (which may be itself). It also
has a current state, described by `NodeState`. After each
processing step, we compress the vector to remove completed and error
nodes, which aren't needed anymore.

@ -8,6 +8,13 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! The `ObligationForest` is a utility data structure used in trait
//! matching to track the set of outstanding obligations (those not
//! yet resolved to success or error). It also tracks the "backtrace"
//! of each pending obligation (why we are trying to figure this out
//! in the first place). See README.md for a general overview of how
//! to use this class.
use std::fmt::Debug;
use std::mem;
@ -17,6 +24,18 @@ mod node_index;
mod test;
pub struct ObligationForest<O> {
/// The list of obligations. In between calls to
/// `process_obligations`, this list only contains nodes in the
/// `Pending` or `Success` state (with a non-zero number of
/// incomplete children). During processing, some of those nodes
/// may be changed to the error state, or we may find that they
/// are completed (That is, `num_incomplete_children` drops to 0).
/// At the end of processing, those nodes will be removed by a
/// call to `compress`.
///
/// At all times we maintain the invariant that every node appears
/// at a higher index than its parent. This is needed by the
/// backtrace iterator (which uses `split_at`).
nodes: Vec<Node<O>>,
snapshots: Vec<usize>
}
@ -33,10 +52,26 @@ struct Node<O> {
root: NodeIndex, // points to the root, which may be the current node
}
/// The state of one node in some tree within the forest. This
/// represents the current state of processing for the obligation (of
/// type `O`) associated with this node.
#[derive(Debug)]
enum NodeState<O> {
Leaf { obligation: O },
Success { obligation: O, num_children: usize },
/// Obligation not yet resolved to success or error.
Pending { obligation: O },
/// Obligation resolved to success; `num_incomplete_children`
/// indicates the number of children still in an "incomplete"
/// state. Incomplete means that either the child is still
/// pending, or it has children which are incomplete. (Basically,
/// there is pending work somewhere in the subtree of the child.)
///
/// Once all children have completed, success nodes are removed
/// from the vector by the compression step.
Success { obligation: O, num_incomplete_children: usize },
/// This obligation was resolved to an error. Error nodes are
/// removed from the vector by the compression step.
Error,
}
@ -44,17 +79,17 @@ enum NodeState<O> {
pub struct Outcome<O,E> {
/// Obligations that were completely evaluated, including all
/// (transitive) subobligations.
pub successful: Vec<O>,
pub completed: Vec<O>,
/// Backtrace of obligations that were found to be in error.
pub errors: Vec<Error<O,E>>,
/// If true, then we saw no successful obligations, which means
/// there is no point in further iteration. This is based on the
/// assumption that `Err` and `Ok(None)` results do not affect
/// environmental inference state. (Note that if we invoke
/// `process_obligations` with no pending obligations, stalled
/// will be true.)
/// assumption that when trait matching returns `Err` or
/// `Ok(None)`, those results do not affect environmental
/// inference state. (Note that if we invoke `process_obligations`
/// with no pending obligations, stalled will be true.)
pub stalled: bool,
}
@ -90,13 +125,15 @@ impl<O: Debug> ObligationForest<O> {
}
pub fn rollback_snapshot(&mut self, snapshot: Snapshot) {
// check that we are obeying stack discipline
// Check that we are obeying stack discipline.
assert_eq!(snapshot.len, self.snapshots.len());
let nodes_len = self.snapshots.pop().unwrap();
// the only action permitted while in a snapshot is to push new roots
// The only action permitted while in a snapshot is to push
// new root obligations. Because no processing will have been
// done, those roots should still be in the pending state.
debug_assert!(self.nodes[nodes_len..].iter().all(|n| match n.state {
NodeState::Leaf { .. } => true,
NodeState::Pending { .. } => true,
_ => false,
}));
@ -116,12 +153,15 @@ impl<O: Debug> ObligationForest<O> {
}
/// Convert all remaining obligations to the given error.
///
/// This cannot be done during a snapshot.
pub fn to_errors<E:Clone>(&mut self, error: E) -> Vec<Error<O,E>> {
assert!(!self.in_snapshot());
let mut errors = vec![];
for index in 0..self.nodes.len() {
debug_assert!(!self.nodes[index].is_popped());
self.inherit_error(index);
if let NodeState::Leaf { .. } = self.nodes[index].state {
if let NodeState::Pending { .. } = self.nodes[index].state {
let backtrace = self.backtrace(index);
errors.push(Error { error: error.clone(), backtrace: backtrace });
}
@ -131,11 +171,11 @@ impl<O: Debug> ObligationForest<O> {
errors
}
/// Convert all remaining obligations to the given error.
/// Returns the set of obligations that are in a pending state.
pub fn pending_obligations(&self) -> Vec<O> where O: Clone {
self.nodes.iter()
.filter_map(|n| match n.state {
NodeState::Leaf { ref obligation } => Some(obligation),
NodeState::Pending { ref obligation } => Some(obligation),
_ => None,
})
.cloned()
@ -174,9 +214,11 @@ impl<O: Debug> ObligationForest<O> {
let (prefix, suffix) = self.nodes.split_at_mut(index);
let backtrace = Backtrace::new(prefix, parent);
match suffix[0].state {
NodeState::Error => continue,
NodeState::Success { .. } => continue,
NodeState::Leaf { ref mut obligation } => action(obligation, backtrace),
NodeState::Error |
NodeState::Success { .. } =>
continue,
NodeState::Pending { ref mut obligation } =>
action(obligation, backtrace),
}
};
@ -204,7 +246,7 @@ impl<O: Debug> ObligationForest<O> {
debug!("process_obligations: complete");
Outcome {
successful: successful_obligations,
completed: successful_obligations,
errors: errors,
stalled: stalled,
}
@ -219,9 +261,9 @@ impl<O: Debug> ObligationForest<O> {
fn success(&mut self, index: usize, children: Vec<O>) {
debug!("success(index={}, children={:?})", index, children);
let num_children = children.len();
let num_incomplete_children = children.len();
if num_children == 0 {
if num_incomplete_children == 0 {
// if there is no work left to be done, decrement parent's ref count
self.update_parent(index);
} else {
@ -233,13 +275,14 @@ impl<O: Debug> ObligationForest<O> {
.map(|o| Node::new(root_index, Some(node_index), o)));
}
// change state from `Leaf` to `Success`, temporarily swapping in `Error`
// change state from `Pending` to `Success`, temporarily swapping in `Error`
let state = mem::replace(&mut self.nodes[index].state, NodeState::Error);
self.nodes[index].state = match state {
NodeState::Leaf { obligation } =>
NodeState::Pending { obligation } =>
NodeState::Success { obligation: obligation,
num_children: num_children },
NodeState::Success { .. } | NodeState::Error =>
num_incomplete_children: num_incomplete_children },
NodeState::Success { .. } |
NodeState::Error =>
unreachable!()
};
}
@ -251,9 +294,9 @@ impl<O: Debug> ObligationForest<O> {
if let Some(parent) = self.nodes[child].parent {
let parent = parent.get();
match self.nodes[parent].state {
NodeState::Success { ref mut num_children, .. } => {
*num_children -= 1;
if *num_children > 0 {
NodeState::Success { ref mut num_incomplete_children, .. } => {
*num_incomplete_children -= 1;
if *num_incomplete_children > 0 {
return;
}
}
@ -263,8 +306,10 @@ impl<O: Debug> ObligationForest<O> {
}
}
/// If the root of `child` is in an error error, places `child`
/// into an error state.
/// If the root of `child` is in an error state, places `child`
/// into an error state. This is used during processing so that we
/// skip the remaining obligations from a tree once some other
/// node in the tree is found to be in error.
fn inherit_error(&mut self, child: usize) {
let root = self.nodes[child].root.get();
if let NodeState::Error = self.nodes[root].state {
@ -274,12 +319,15 @@ impl<O: Debug> ObligationForest<O> {
/// Returns a vector of obligations for `p` and all of its
/// ancestors, putting them into the error state in the process.
/// The fact that the root is now marked as an error is used by
/// `inherit_error` above to propagate the error state to the
/// remainder of the tree.
fn backtrace(&mut self, mut p: usize) -> Vec<O> {
let mut trace = vec![];
loop {
let state = mem::replace(&mut self.nodes[p].state, NodeState::Error);
match state {
NodeState::Leaf { obligation } |
NodeState::Pending { obligation } |
NodeState::Success { obligation, .. } => {
trace.push(obligation);
}
@ -338,9 +386,9 @@ impl<O: Debug> ObligationForest<O> {
(0 .. dead).map(|_| self.nodes.pop().unwrap())
.flat_map(|node| match node.state {
NodeState::Error => None,
NodeState::Leaf { .. } => unreachable!(),
NodeState::Success { obligation, num_children } => {
assert_eq!(num_children, 0);
NodeState::Pending { .. } => unreachable!(),
NodeState::Success { obligation, num_incomplete_children } => {
assert_eq!(num_incomplete_children, 0);
Some(obligation)
}
})
@ -365,15 +413,15 @@ impl<O> Node<O> {
fn new(root: NodeIndex, parent: Option<NodeIndex>, obligation: O) -> Node<O> {
Node {
parent: parent,
state: NodeState::Leaf { obligation: obligation },
state: NodeState::Pending { obligation: obligation },
root: root
}
}
fn is_popped(&self) -> bool {
match self.state {
NodeState::Leaf { .. } => false,
NodeState::Success { num_children, .. } => num_children == 0,
NodeState::Pending { .. } => false,
NodeState::Success { num_incomplete_children, .. } => num_incomplete_children == 0,
NodeState::Error => true,
}
}
@ -399,7 +447,8 @@ impl<'b, O> Iterator for Backtrace<'b, O> {
if let Some(p) = self.pointer {
self.pointer = self.nodes[p.get()].parent;
match self.nodes[p.get()].state {
NodeState::Leaf { ref obligation } | NodeState::Success { ref obligation, .. } => {
NodeState::Pending { ref obligation } |
NodeState::Success { ref obligation, .. } => {
Some(obligation)
}
NodeState::Error => {

@ -21,7 +21,7 @@ fn push_pop() {
// A |-> A.1
// |-> A.2
// |-> A.3
let Outcome { successful: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
let Outcome { completed: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
match *obligation {
"A" => Ok(Some(vec!["A.1", "A.2", "A.3"])),
"B" => Err("B is for broken"),
@ -40,7 +40,7 @@ fn push_pop() {
// D |-> D.1
// |-> D.2
forest.push_root("D");
let Outcome { successful: ok, errors: err, .. }: Outcome<&'static str, ()> =
let Outcome { completed: ok, errors: err, .. }: Outcome<&'static str, ()> =
forest.process_obligations(|obligation, _| {
match *obligation {
"A.1" => Ok(None),
@ -58,7 +58,7 @@ fn push_pop() {
// propagates to A.3.i, but not D.1 or D.2.
// D |-> D.1 |-> D.1.i
// |-> D.2 |-> D.2.i
let Outcome { successful: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
let Outcome { completed: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
match *obligation {
"A.1" => Ok(Some(vec![])),
"A.2" => Err("A is for apple"),
@ -72,7 +72,7 @@ fn push_pop() {
backtrace: vec!["A.2", "A"] }]);
// fourth round: error in D.1.i that should propagate to D.2.i
let Outcome { successful: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
let Outcome { completed: ok, errors: err, .. } = forest.process_obligations(|obligation, _| {
match *obligation {
"D.1.i" => Err("D is for dumb"),
_ => panic!("unexpected obligation {:?}", obligation),
@ -96,7 +96,7 @@ fn success_in_grandchildren() {
let mut forest = ObligationForest::new();
forest.push_root("A");
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, _| {
match *obligation {
"A" => Ok(Some(vec!["A.1", "A.2", "A.3"])),
@ -106,7 +106,7 @@ fn success_in_grandchildren() {
assert!(ok.is_empty());
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, _| {
match *obligation {
"A.1" => Ok(Some(vec![])),
@ -118,7 +118,7 @@ fn success_in_grandchildren() {
assert_eq!(ok, vec!["A.3", "A.1"]);
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, _| {
match *obligation {
"A.2.i" => Ok(Some(vec!["A.2.i.a"])),
@ -129,7 +129,7 @@ fn success_in_grandchildren() {
assert_eq!(ok, vec!["A.2.ii"]);
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, _| {
match *obligation {
"A.2.i.a" => Ok(Some(vec![])),
@ -139,7 +139,7 @@ fn success_in_grandchildren() {
assert_eq!(ok, vec!["A.2.i.a", "A.2.i", "A.2", "A"]);
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|_, _| unreachable!());
assert!(ok.is_empty());
assert!(err.is_empty());
@ -151,7 +151,7 @@ fn to_errors_no_throw() {
// only yields one of them (and does not panic, in particular).
let mut forest = ObligationForest::new();
forest.push_root("A");
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, _| {
match *obligation {
"A" => Ok(Some(vec!["A.1", "A.2", "A.3"])),
@ -170,7 +170,7 @@ fn backtrace() {
// only yields one of them (and does not panic, in particular).
let mut forest: ObligationForest<&'static str> = ObligationForest::new();
forest.push_root("A");
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, mut backtrace| {
assert!(backtrace.next().is_none());
match *obligation {
@ -180,7 +180,7 @@ fn backtrace() {
});
assert!(ok.is_empty());
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, mut backtrace| {
assert!(backtrace.next().unwrap() == &"A");
assert!(backtrace.next().is_none());
@ -191,7 +191,7 @@ fn backtrace() {
});
assert!(ok.is_empty());
assert!(err.is_empty());
let Outcome { successful: ok, errors: err, .. } =
let Outcome { completed: ok, errors: err, .. } =
forest.process_obligations::<(),_>(|obligation, mut backtrace| {
assert!(backtrace.next().unwrap() == &"A.1");
assert!(backtrace.next().unwrap() == &"A");