Computing Place Capabilities
This section describes a design that is currently being implemented and is subject to change.
In the updated design, place capabilities are computed from the owned state and the borrow state, rather than being tracked and updated via explicit rules. This eliminates a class of soundness issues (see below) and simplifies the capability accounting logic.
Motivation
The previous design maintained a map from places to capabilities that was updated by three mechanisms: statement evaluation, borrow expiry/activation, and control-flow joins. This led to three problems:
-
Unsoundness: the rule "when a mutable borrow expires, restore the place's capability to
E" is unsound when the place has been conditionally moved out. After a join, the move-out information is lost, and the borrow expiry incorrectly restoresEto a potentially uninitialised place. -
Insufficient annotations: per-place capability update annotations (e.g. "Restore
pair.1toE") do not explain the source of the capability. Edge-oriented annotations (see Edge Capabilities) address this. -
Complex rules: the rules for updating capabilities were numerous and difficult to justify, complicating an eventual soundness proof.
Computing Capabilities for Owned Places
The capability of an owned place is determined as follows:
-
Look up in the initialisation state. If is within a materialised extension, use the initialisation capability of the ancestor leaf from which the extension grows.
-
Based on the initialisation capability:
U(uninitialised): the place capability isW(write-only).S(shallow): the place capability ise(shallow exclusive).- Internal node: the place capability is
∅(none), because the place is only partially initialised. D(deep): proceed to check the borrow state below.
-
If the place is fully initialised (
D), consult the borrow state:- If the type of contains a region but the lifetime
projection does not exist in the borrow state:
the capability is
e(shallow exclusive). - If or any of its sub-places is blocked by a mutable borrow:
the capability is
∅(none). - If or any of its sub-places is blocked by a shared borrow:
the capability is
R(read). - Otherwise: the capability is
E(exclusive).
- If the type of contains a region but the lifetime
projection does not exist in the borrow state:
the capability is
Computing Capabilities for Borrowed Places
For borrowed places, the capability is determined by the borrow PCG:
- If the place projects a shared borrow (e.g.
*rwherer: &T):R(read). - Otherwise, if the place is a leaf in the borrow PCG:
E(exclusive). - Otherwise:
∅(none).
Example: Soundness Fix
The following example demonstrates how the computed capability approach avoids the unsoundness of the previous design:
#![allow(unused)] fn main() { fn conditional_move(choice: bool) { let mut p = String::new(); let mut p2 = String::new(); let mut rp: &mut String; // BB0: init {p: D, p2: D, rp: U} -> cap {p: E, p2: E, rp: W} if true { consume(p); rp = &mut p2; // BB1: init {p: U, p2: D, rp: D} -> cap {p: W, p2: ∅, rp: E} } else { rp = &mut p; // BB2: init {p: D, p2: D, rp: D} -> cap {p: ∅, p2: E, rp: E} } // BB3: join -> init {p: U, p2: D, rp: D} // -> cap {p: ∅, p2: ∅, rp: E} *rp = String::from("updated"); // After borrow expiry: // init {p: U, p2: D, rp: D} -> cap {p: W, p2: E, rp: e} }
In the previous design, borrow expiry would have incorrectly restored
p to E. In the computed design, p remains W after expiry because
the initialisation state still records p: U.