Function Shapes
Background
The PCG generates a "shape" for a function call - a bipartite graph indicating where borrows could flow as a result of the call. In particular this shape represents (1) reborrows that are returned from the function (w.r.t borrows passed in the arguments), and (2) the effects of the nested borrows passed in the operands.
We model the first case by introducing abstraction edges between lifetime projections in the operands and those in the result place: each lifetime projection in the operands is connected to the corresponding lifetime projections that it outlives in the result place. The (compiler-checked) outlives constraint captures whether borrows could be assigned in this way, according to the type system.
For (2) To directly model the potentially changed sets of borrows relevant to these concerns, our analysis of function calls introduces lifetime projections to represent the post-state of each lifetime projection in the operands. Each lifetime projection in the operands is connected with abstraction edges to its corresponding post-state projection as well as the post-state nested lifetime projections that it outlives (analogously to sets of borrows explicitly returned).
Creating Function Shapes
Function Shapes
A function shape source base takes the form . A function shape target base is either or .
A function shape source node is a pair where is the region index of the node. Function shape target nodes are defined analogously.
A function shape edge is a pair , and a function shape is a set of edges.
A shape permits more borrowing than a shape iff ; likewise permits less borrowing than iff .
Signature Shape
The corresponding node of a generalized lifetime projection is .
The corresponding generalized lifetime projection of a node is the generalized lifetime projection such that .
A generalized lifetime outlives a generalized lifetime in the signature of (denoted iff:
- , or
- is a generalized type and is a region and
Note that represents all regions that could occur in . More generally, outlives when all regions in outlive all corresponding regions in . The current implementation handles the case where (reflexivity); other cases may be added in the future.
The signature shape for a function instantiation is defined as follows:
For each then add to if both:
- , and
- is , or is a region that is invariant in .
Call Shape
The corresponding node of a lifetime projection is .
The corresponding node of a lifetime projection is .
The call shape for a function call is defined as follows:
For each then add to if both:
- at according to the borrow checker, and
- is , or is invariant in .
Type Aliases and Normalization
An alias type is a type of the form where is a type constructor. The function returns a type where alias types in may possibly be replaced with other types. This normalisation is idempotent, e.g. .
Signature-Derived Call Shape
For a call at ), the signature-derived call shape is obtained as follows:
Let be the normalized signature shape, e.g the one obtained by replacing each in with .
If is the operand in , the corresponding normalized type is the type of the argument in . Likewise, if , then is the output type of. Then, the corresponding normalized region of a lifetime projection is the region in that corresponds to in .
For each :
- Let , be the corresponding lifetime projections
- Then, let and be the corresponding normalized regions of and respectively.
- If outlives in , then add to
Using shapes for function calls in the PCG
If the call is to a defined function, then the signature-derived call shape is used. Otherwise, the call shape is used.
More Background
For a function , there are three types of shapes to consider:
- Signature Shapes: The shape of an instantiation of generated from its signature
- Call Shapes: A shape used to represent call to an instantiation of
- Implementation Shapes: A shape representing 's body, which connects remote lifetime projection nodes to the the result.
These different shape types are relevant for Prusti, as:
- Signature shapes define the shape of a magic wand
- Call shapes are used for magic wands that will be applied
- Implementation shapes define magic wands that will be packaged
The call shape and implementation shapes are necessarily related to the signature shape; the former can contain more edges while the latter can contain less.
The shape for a function call must necessarily have corresponding edges that appear in the shape for the function signature. The reverse is not necessarily the case. For example, consider the following:
#![allow(unused)] fn main() { fn caller<'e, 'f: 'e>(x: T<'e>, y: U<'e>, z: T<'f>) { let r: W<'e> = f(x, y, z); } }
Using the borrow-checker to generate the shape of the call will result in an edge from z|'f to r|'e: the borrow-checker reflects that 'f: 'e. However, the definition of f do not allow borrows to flow from z to r (which is reflected in the function call shape).
Therefore, to build more precise graphs at function call sites. We want to use the shape of the function to determine the shape of the call. The procedure is as follows:
- Generate the shape of the function.
- Build a map from
Def Projections toCall Projectionsby comparing the types of the arg places and formal args (analogously to the results). By construction, will contain all projections in the function def (even if they don't appear in the shape).
Here's an example of how its computed:
If the type of the 'th place is &'?1 mut U<'?2> and the type of the 'th formal arg is &'a mut U<'b>, then the visitor will first compare at the top level, add arg_i |'a -> p_i '?1 to the map and continue by comparing U<'?2> to U<'b>
- Then, for each edge in the fn shape, the edge is replaced with the corresponding projections of the call. If the fn shape has an edge
arg 1|'a -> result|'bfor example, then for the call shape it will lookup corresponding call projections and add an edge between them.
Reasoning about Associated Types
Consider the following code:
#![allow(unused)] fn main() { trait MyTrait<'a> { type Assoc<'b> where 'a: 'b; fn get_assoc<'slf, 'b>(&'slf mut self) -> Self::Assoc<'b> { todo!() } } }
The full signature for get_assoc is:
#![allow(unused)] fn main() { fn(&'slf mut Self) -> <Self as MyTrait<'a>>::Assoc<'b> }
we observe that the fn sig has the following lifetime projections:
argidx 0|'slfresult 0|'aresult 0|'b
And the shape for get_assoc contains the single edge:
argidx 0|'slf -> result|'b
For the body, self has type &'8 mut Self/#0
and result has Alias(Projection, AliasTy { args: [Self/#0, '?6, '?7], def_id: DefId(0:5 ~ input[9b88]::MyTrait::Assoc), .. }),
So we unify &'slf mut Self with &'?8 mut Self
adding argidx 0|'slf -> _1|'?8 to
We unify <Self as MyTrait<'a>::Assoc<'b> with
<Self as MyTrait<'?6>::Assoc<'?7>
adding
result|'a -> result|'?6toresult|'b -> result|'?7to
Then applying the substitutions our shape is _1|'?8 -> result|'?7