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
ObligationForest supports two main public operations (there are a
few others not discussed here):
- Add a new root obligations (
- Process the pending obligations (
When a new obligation
N is added, it becomes the root of an
obligation tree. This tree can also carry some per-tree state
which is given at the same time. 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 also receives a (mutable) reference to the
T. The callback should process the obligation
that it is given and return a
Unchanged-> 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.
Changed(C)- the obligation was shallowly successful. The vector
Cis a list of subobligations. The meaning of this is that
Owas successful on the assumption that all the obligations in
Care also successful. Therefore,
Ois only considered a “true” success if
Cis empty. Otherwise,
Ois put into a suspended state and the obligations in
Cbecome the new pending obligations. They will be processed the next time you call
Error(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
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
Changed(C)for some obligation
Owill be considered completed right away if
Cis the empty vector. Otherwise it will only be considered completed once all the obligations in
Chave 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
Changed(_)). This implies that all obligations were either errors or returned an ambiguous result, which means that any further calls to
process_obligationswould simply yield back further ambiguous results. This is used by the
FulfillmentContextto decide when it has reached a steady state.
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 its dependents,
including the first dependent which is known as the parent. 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.
The state of one node in some tree within the forest. This represents the
current state of processing for the obligation (of type
with this node.
The result type used by
This trait allows us to have two different Outcome types: