Obtaining Capability to a Place
The operation to obtain capability to a place in a PCG proceeds as follows.
When performing this operation to satisfy the place capability required for a statement, the analysis guarantees that no live mutable borrows conflicting with 1. At a high level, capability to is obtained by first collapsing the owned places in to
Step 1 - Label dereferences of shared borrows stored in
Reborrows of shared references derived from (i.e. from any postfix of ) will survive even if is moved or mutated. Therefore, if permits the function to mutate (i.e. ), then the analysis labels all places that project from shared references derived from .
Formally:
If , then for each place in where:
- is a strict postfix of , and
- is a shared reference, and
- is in
The analysis labels every postfix of in .
Step 2 - Collapse
Implementation
This operation is implemented as PlaceObtainer::obtain
The obtain(p, o) operation reorganizes a PCG state to a new state in where the PCG node for a place is present in the graph. The parameter o is an Obtain Type which describes the reason for the expansion. The reason is either:
- To obtain a specified capability to the place
- To access the place because it is the borrowed place of a two-phase borrow
An Obtain Type is associated with a result capability which is either
the specified capability (in the first case), or Read
(in the case of access
for two-phase borrows).
The "two-phase" borrow case is likely unnecessary: we can use the borrow-checker to detect if the place is also the borrowed place of a two-phase borrow reserved at the current location. In fact the current implementation make similar queries as part of the expand step.
Note that a place node for is not necessarily present in the graph before this occurs.
This proceeds as follows:
Step 1 - Label dereferences of shared borrows stored in
(Same as high-level description)
Step 2 - Upgrading Closest Ancestor From R to E
This step is included to handle a relatively uncommon case (see the Rationale section below).
If the obtain operation is called with permission or and the closest ancestor to , that is, the longest prefix of for which there exists a node in the graph, has capability, we upgrade 's capability to in exchange for removing capability to all pre- and postfix places of in the graph (excluding itself).
This is sound because if we need to obtain non-read capability to place
, and
there are any ancestors of place
in the graph with R capability, one such
ancestor originally had E capability was subsequently downgraded. This function
finds such an ancestor (if one exists), and performs the capability exchange.
Perhaps it would be better to explicitly track downgrades in the analysis so that they can be upgraded later? This will make the soundness argument more convincing.
Rationale
It's possible that we want to obtain exclusive or write permission to a field that we currently only have read access for. For example, consider the following case:
- There is an existing shared borrow of
(*c).f1
- Therefore we have read permission to *c, (*c).f1, and (*c).f2
- Then, we want to create a mutable borrow of (*c).f2
- This requires obtaining exclusive permission to (*c).f2
We can upgrade capability of (*c).f2 from R to E by downgrading all other pre-and postfix places of (*c).f2 to None (in this case c and *c). In the example, (*c).f2 is actually the closest read ancestor, but this is not always the case (e.g. if we wanted to obtain (*c).f2.f3 instead)
Step 3 - Collapse
Then, if a node for exists in the graph and 's capability is not at least as strong as , collapse the subgraph rooted at (and obtain capability for ) by performing the collapse(p, c) operation.
collapse
The collapse(p) operation is implemented as follows:
- For each such that is a prefix of (from longest to shortest) and there is a node for in the graph:
- perform the Collapse Repack Operation on .
- For each lifetime in the type of :
- Create a new lifetime projection node
- For each lifetime projection node where is an expansion of :
- Label
- Create a new
BorrowFlow
edge
Step 4 - Labelling
At this point, if is , we know that a subsequent operation will mutate .
As a result, if there exists a lifetime projection node for (for example, if stores a borrow that has since been reborrowed), it will no longer be tied to the current value of .
So, we label with reason ReAssign
.
Step 5 - Expand
At this point there should be a node for some prefix of in the graph such that .
We expand the graph to (and obtain the capability for ) by performing the expandTo(p, o) operation.
expandTo
The expandTo operation is implemented as follows:
- For each strict prefix of (from shortest to longest):
- If expanding one level adds new edges to the graph, then
- We expand the lifetime projections of one level
- If expanding one level adds new edges to the graph, then
The operation to expand a place one level is the expandPlaceOneLevel operation, and the operation to expand the lifetime projections one level is expandLifetimeProjectionsOneLevel.
expandLifetimeProjectionsOneLevel
expandLifetimeProjectionsOneLevel is defined with three parameters:
- : The place to expand
- : The target expansion of
- : The Obtain Type
The operation is implemented as follows:
- Let be the expansion of using
- For each lifetime projection of :
- Let be the set of lifetime projections in with lifetime
- If is nonempty2:
- We identify the base lifetime projection as follows:
- Let be the current snapshot location
- If is not an obtain for capability R:
- Otherwise, if is blocked by a two-phase borrow live at :
- Let be the reservation location of the conflicting borrow
- Otherwise,
- Create a new Borrow PCG Expansion hyperedge
- We identify the base lifetime projection as follows: