Lecture 18: Resource Algebras
Lecture 18: Resource Algebras
Learning Outcomes
- Explain how fractional permissions are implemented.
- Understand how a resource algebra connects to separation logic.
Motivation
When we saw sequential separation logic, we said hProp = heap β Prop
where heap = gmap loc val
(a partial map from locations to values). This allowed us to give a definition of (points-to) and (separating conjunction). Separation in this context meant disjoint memory addresses.
However, we also saw fractional permissions with (a "half" and thus read-only points-to) and (a persistent read-only points-to). These seems unrelated to the definitions above, so how do they connect?
Remember that fractional and persistent permissions are useful, especially with concurrency: remember that when we spawn a thread , we have to split up our permissions into (which the spawned thread gets) and (which the rest of the code gets). If the only splitting possible were disjoint locations, it would be hard to verify any useful concurrent code: threads would need to be completely independent.
In this lecture we'll generalize separation logic by changing the definition of hProp
and what it means to split. However, we won't throw everything away: all the separation logic rules will remain exactly the same. In this lecture we'll be focused on the assertions of separation logic; nothing will change about program proofs.
Model of separation logic
In order to generalize separation logic, it helps to lay out some terminology for what we're doing.
The logic consists of the syntax (things like and ) and rules (like and ). This part won't change.
We can use a logic abstractly, following the rules to prove for various propositions.
A model of the logic is an interpretation or definition for the propositions and syntax where the rules can be proven as theorems. We introduced separation logic alongside a model where propositions are predicates over heaps, and separating conjunction holds when and hold over disjoint subsets and of .
Notice that I said a model and not the model. The same logic can have more than one model! That's exactly what we'll do today.
As an example, you might be familiar with comes from geometry. Euclid's axioms include a parallel postulate: if you have a line and a point not on the line, there is exactly one line parallel to the given line which goes through the given point. If you remove that axiom, then there are multiple possible models of Euclid's axioms: Euclidean geometry, hyperbolic geometry, and elliptic geometry.
Algebraic model of separation logic
Let's try to sketch out the ingredients of this generalization. Instead of ownership over one location and one value specifically, we'll want ownership to be more flexible. Wanting to stay as general as possible, let's say we'll pick a type and own elements , which we'll refer to as resources. An will represent a set of resources ; it's a set because separation logic propositions can describe one of several possible worlds, just like before an hProp could be true in zero or several heaps.
We need a way to represent ownership of one specific resource: we'll write it for the resource . The heap model was a special case: the resources were heaplets, and we had special syntax for the resource that represents ownership over a single location and its value. Now we'd write that as .
One of the things we need for separation logic is to split and combine resources, to implement the separation in the name of the logic. Combining is easier: the assertion should hold for some resource which combines the two sides separately. We have some arbitrary type , but now we'll assume we can combine two resources in , which we'll write ("x compose y"). Splitting will essentially be the reverse, where we have .
There's still one thing missing, which is the idea if disjointness in . The way we'll incorporate this is a bit indirectly: we'll define ("valid x") to say that some elements are valid, and others are not. will only be true for a resource if and . We'll make sure will only be provable when , and when the heaps overlap will be defined to produce something invalid.
What we've described is the beginning of a resource algebra : it defines some resources, a way to split and combine them, and a subset of valid ones to define when we're allowed to combine them.
Aside on algebra
We're about to define more formally what a resource algebra is using an algebraic structure There are many algebraic structures (such as fields, monoids, groups, and vector spaces). If you've never seen this setup (say, from an abstract algebra class), the setup for a resource algebra might seem strange to you.
It might help to see the rules for a different structure, a monoid, which are simpler than RAs.
A monoid is a structure with a carrier type (of elements "in the monoid") and a single operation that adds or "composes" two elements. The addition operation must be associative; that is .
Our first example of a monoid is the integers. The carrier is and addition is the usual addition operation.
Our second example is the monoid of lists with elements of type (this needs to be fixed but the specifics don't matter), where is list concatenation. Notice that this operation is associative but not commutative, which is fine.
Notice that there are three levels here: a monoid is the collection of all types with these properties, then we have a specific type like list which is a monoid, and then we have a specific element like [2; 5; 3]
of the list monoid.
Resource algebra definition
A resource algebra (RA) is a collection of "resources" that can be combined and split. It will help to keep in mind two concrete examples we've already seen. The first is our core example of heaps, which can be split and combined into disjoint subsets and was our original model of separation logic. The other is a slightly odd example of how we combine and split fractions , which we saw when we split and combined for some fixed location and value . That example is odd in that the resource algebra will only manipulate the fraction and we won't worry about multiple locations yet.
Formally, a resource algebra (RA) is an algebraic structure . It has these components:
component | type | description |
---|---|---|
Type | carrier; an is a "resource" | |
resource composition | ||
valid resources |
- is a type called the carrier of the resource algebra. In our examples this is a heap and a fraction respectively. For the heap RA we'll need to add an "error" value for invalid compositions.
- combines two resources. The expression can be pronounced "x compose y" (or "x plus y"). For the heap RA, is disjoint union. In that case, we will have if and overlap.
- says which elements are valid. We pronounced as "valid x". For the heap RA, all resources are valid except . For fractions, .
Let's walk through each example:
Heap resource algebra: The resources of the heap resource algebra are heaplets, subsets of the heap, or an error value . Two heaps can be combined if they are disjoint; otherwise they produce the error value. Any heaplet is valid (but not ).
Fractional resource algebra: The resources of the fractional resource are fractions . Any two fractions can be combined by addition, but only fractions are valid. No fraction has a core.
RAs in the logic
Resource algebras are defined this way so that for a given RA with carrier , we can use as the model of a separation logic proposition, a type I'll call iProp (though the Iris model, the actualy definition of iProp, is much more complicated to address a number of other concerns). The core ownership proposition is , which asserts ownership of a single resource .
Two key properties ownership will satisfy:
In this model, we can interpret the various parts of separation logic. Note that for each of these definitions we give them as a function, just like the definitions with hProp were all functions of the heap.
The definition of separating conjunction gives us , but the reverse doesn't follow from these definitions directly; it will actually require some additional properties about and .
Exercise: confirm that the definition of with defined as matches the definition you would expect from our original heap model of separation logic assertions prior to this lecture. What happens if under the two definitions?
Exercise: prove .
For the above rules to make sense, we actually can't have just any RA with the signatures above. There is a bit more to a valid resource algebra, namely some laws (properties) that the components need to satisfy. Here are the laws:
- (commutativity)
- (associativity)
- (valid-op)
These rules are needed so that ownership follows all the separation logic rules we had earlier; without them, we would have contradictions in the logic which would render the whole thing useless. For example, we need to be the same as (separating conjunction is commutative); since these two are equivalent to and respectively using our earlier rule about ownership splitting, we actually need or we run into problems (specifically, the logic becomes unsound).
The last rule might look oddly asymmetric. The reason we only need this one rule is that (due to commutativity of composition), and thus we can prove from the above that . This rule is what proves ; ownership of the two resources together implies validity of , and this is enough to guarantee validity of the subresources.
An RA for fractional permissions
We can now build up the specific RA used to implement . We'll do this in two stages: we'll first define an RA for a one-location heap with fractions, then we'll extend it to multiple locations (this part is mostly straightforward).
Fractions for one location
We'll define ; this is an RA parameterized by an arbitrary type of values . Making it general is intended to help you understand the definition - no part of this definition will care what specific values are used, although eventually they'll be the of our programming language.
The carrier type will be ; that is, the elements of this RA will be of the form (a fraction and a value ), or a special error value used for invalid compositions.
The resource composition operation will be defined as:
- (notice these are the same value)
- if
- just for completeness, , as you might expect
Validity is defined as , and is not valid.
The partial core is never defined.
Notice how fractions are divided and combined as before, and we make explicit in this definition that combining requires that the values are the same.
The full fractional heap RA
We'll now define a parameterized by a type of addresses or locations and values . Again, the specific types won't matter for the definition, and we'll use and for our actual programming language.
The carrier is a partial map from to the carrier of . It's common to refer to an algebraic structure like directly but actually means its carrier (a type); remember technically the RA is a tuple with the carrier and the composition and validity functions.
We'll see that the behavior of this RA mostly defers to how works, which is why we defined that RA first.
The composition will be defined pointwise: take the union of the two maps, and if and , the composed heap will have the value using the composition of the fracRA resource algebra.
A heap is valid if all of its values are valid, according to the validity of fracRA.
The "primitive" resources in this RA have the shape : single-location heaplets, with values that are tuples of a fraction and a value. These heaplets are exactly those that we will abbreviate .
Exercise: confirm that this makes sense; that is, this definition of splits and combines the way you expect for fractions given the general rules for .
Persistence
We omitted discussion of persistence above. With the definitions above, we can already have resources that can be duplicated: if in an RA we had a resource that satisfied , then . The persistent points-to is defined using an RA that extends fractions to have exactly such an element. Let's first see that construction, which we call discardable fractions, before talking about persistence.
Discardable fractions
The carrier is a type with three possibilities: a fraction, a marker that the fraction has been discarded, or a combination of both. We'll write the three possibilities DfracOwn(), DfracDiscarded, and DfracBoth(). You can get a good intuition for this RA by thinking of DfracDiscarded as an extremely small fraction and DfracBoth() as .
Here are some examples of composition. You can predict the rules with the intuition above, formally treating .
The valid elements are the following:
The purpose of discardable fractions is to have an element which is duplicable: . However, we haven't actually explained how to obtain such an element; that will be the subject of the next lecture on ghost state.
Partial core
Iris has a definition of a Persistent P
where P: iProp
, and l β¦[uint64T]β‘ #x
is an example of such a persistence proposition. However, Persistent P
is not defined as P β’ P β P
; it has a slightly stronger definition which gives persistence some additional properties (we won't get into them here in detail). A key feature of the Iris definition is that it also allows defining the persistently modality, which is used to define Hoare triples and implement the Iris Proof Mode's persistent context.
The way persistence is defined requires adding another operation (partial core) to the RA structure that every RA must also implement, in addition to composition and validity.
If you want the full details on the partial core, its laws, examples of its definition, and how it is used to implement the persistently modality, and you're generally interested in how Iris is implemented rather than how to use it, you should read Iris from the ground up.
Here is a very short description, only to pique your interest: For an RA with carrier type , has the type . The intuition behind the partial core is that if , then is the "shareable", persistent part of ; the operation is partial so that means there is no shareable component of . A key law for partial cores is that if , then . Notice the special case of ; in this case and . We can now define the persistently modality in terms of a resource algebra model: .