Expand description

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 obligations (register_obligation).
  2. Process the pending obligations (process_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 T, 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 per-tree state T. The callback should process the obligation O that it is given and return a ProcessResult:

  • 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 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.
  • 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 Outcome, which includes two 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 Changed(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.

Upon completion, 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.

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 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.

Modules

graphviz 🔒

Structs

Enums

NodeState 🔒
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.
The result type used by process_obligation.

Traits

This trait allows us to have two different Outcome types:

Type Definitions