{ P } e { Q }
e1 ≼ e2
Separation logic is particularly well suited for programs with mutable state/references and concurrency.
There are many extensions of separation logic. For example, Rust’s borrows, effects and handlers, probabilistic programming, time and space bounds, distributed systems, session types, etc.
Assertion language to describe resources (particularly, heaps):
P, Q := True | False | P ∧ Q | P ∨ Q | P → Q | ∀ x:A. P | ∃ x:A. P
emp | l ↦ v | P ∗ Q | P -∗ Q
A version of Hoare logic based on this assertion language:
[ P ] e [ v . Q ]: If the heap satisfies P beforehand, then execution of e is will terminate with return value v such that the final heap satisfies Q v.{ P } e { v. Q }: If the heap satisfies P beforehand, then execution of e is safe (no memory errors such as use after free), and if e terminates, then the final heap satisfies Q v for the return value v.In this lecture, we discuss total program correctness (but many tools, e.g., Iris focus on partial correctness).
Let us consider a very simple program and try to specify it:
void foo (int *l, int *k)
{
int x = *l;
int y = x + 2;
*k = y;
}
Intuitive specification:
l has value n and location k has value ml has value n and location k has value n+2Problems:
l and k are the same location? I.e., they alias.
l is overwritten with n+2.l and k, what is guaranteed to happen to them?
While the syntax of separation logic appears similar to ordinary (first-order or higher-order) logic, the semantics of propositions of separation logic is very different. In ordinary logic, propositions describe truth or knowledge, i.e., if a proposition holds, it will hold forever, i.e., persistently. Propositions of separation logic describe ownership, they describe which parts of the heap we own. Let us take a look at the intuitive semantics of the new connectives to get a better idea of that:
l ↦ v.
This connective describes that the heap contains exactly one location l with value v and expresses that we we have unique ownership of that location in the heap.P ∗ Q.
This connective describes that the heap can be split into two disjoint parts, so that the first satisfies P, and the second satisfies Q.emp.
This connective describes that the heap is empty, i.e., we do not own any locations.Using these connectives, one can give very precise descriptions of heaps, for example:
l₁ ↦ 6 ∗ l₂ ↦ 8∃ v. l₁ ↦ v ∗ l₂ ↦ (2 * v)The first example describes a heap that consists of exactly two locations l₁ and l₂ that respectively contain the values 6 and 8. Since the separation conjunction P ∗ Q ensures that the parts of the heap described by P and Q are disjoint, we know that l₁ and l₂ are different (i.e., they do not alias). This makes separating conjunction very different from conjunction P ∧ Q, which says that P and Q both hold for the same heap.
The second example describes the set of heaps that contain two different locations l₁ and l₂, so that the value of l₂ is twice that of l₁.
Let us take a look at two other separation logic propositions:
l₁ ↦ 6 ∗ l₂ ↦ 8 ∗ True∃ x. l₁ ↦ 6 ∧ l₂ ↦ xThe first example describes a heap that consists of at least two locations l₁ and l₂, which respectively contain the values 6 and 8. The ∗ True describes that the heap may contain an arbitrary number of other locations.
The second example uses the ordinary conjunction P ∧ Q, which states that P and Q should both hold. As such this proposition describes a heap that contains exactly one location l₁, which is equal to l₂, and contains the value 6.
Circling back to our running example:
void foo (int *l, int *k)
{
int x = *l;
int y = x + 2;
*k = y;
}
Its specification is:
∀ l k i j.
[ l ↦ i ∗ k ↦ j ] foo (l, k) [ vret. vret = k ∗ l ↦ i ∗ k ↦ (i + 2) ]
It is implicitly captured by the ∗ that the locations l and k are disjoint. Note that this will scale to any number of locations, e.g., l₁ ↦ v₁ ∗ l₂ ↦ v₂ ∗ ... lₙ ↦ vₙ implicitly describes that lᵢ ≠ lₖ for any i ≠ k.
Exercise Let us consider another program:
int* bar (int x)
{
int *p = malloc (sizeof (int));
*p = x;
*p = *p + *p;
return p;
}
What would be the separation logic spec of this function?
Answer
∀ n.
[ emp ] bar(x) [ vret. vret ↦ (x + x) ]
The first question that we need to answer is: what will be the semantics of separation logic assertions? There are many different ways to give a semantics to separation logic (i.e., there are many different models). We now consider the simplest model: the heap model. The propositions in this model describe sets of heaps. The natural way to describe the propositions sepProp of separation logic is by considering them to be predicates over heaps:
heap := loc -fin→ val
sepProp := heap → Prop
Recall, in Rocq predicates are functions to Prop. So to define predicates, we should use a λ-abstraction.
(l ↦ v) := λ h. h = {[ l := v ]}
(P ∗ Q) := λ h. ∃ h₁ h₂. h₁ ## h₂ ∧ h = h₁ ∪ h₂ ∧ P h₁ ∧ Q h₂
emp := λ h. h = ∅
(x = y) := λ h. h = ∅ ∧ (x = y)
The above definitions make the intuitive semantics formal:
l ↦ v describes the heaps h that are equal to {[ l := v ]}.P ∗ Q describes the heaps h that can be split into disjoint parts h1 and h2, such that P holds in h1 and Q holds in h2.emp describes the heaps h that are equal to ∅, i.e., it describes the empty heap.Note Our semantics of the equality x = y implicitly captures that the heap should be empty (it contains m = ∅). An alternative semantics, that is often used in the literature, is (x = y) := λ h. (x = y). Our version has the upshot that it can easily be used in specifications, e.g., (x = y) ∗ (l ↦ x) describes heaps that contain exactly one cell. With the alternative semantics, you need to write (x = y ∧ emp) ∗ (l ↦ x) or (x = y) ∧ (l ↦ x).
The semantics of the other connectives is straightforward, we lift the connectives on Prop to those on sepProp.
True := λ h. True
False := λ h. False
(P → Q) := λ h. P h → Q h
(P ∧ Q) := λ h. P h ∧ Q h
(P ∨ Q) := λ h. P h ∨ Q h
(∀ x:A. P) := λ h. ∀ x:A. P h
(∃ x:A. P) := λ h. ∃ x:A. P h
Important Keep in mind that the logical connectives on the left-hand side are those of the shallowly embedded object logic (i.e., separation logic), and the ones on the right hand side are those of our meta logic (i.e., Rocq). On paper we typically overload the syntax, but when mechanizing separation logic in Rocq we will be very explicit about the difference between the two.
Separation logic would not be a logic if there were no proof rules. To express the proof rules of separation logic, we need a notion of entailment P ⊢ Q, which says that Q follows from P:
P ⊢ Q := ∀ h. P h → Q h
The entailment P ⊢ Q expresses that P implies Q for any heap h.
The quintessential proof rules of separation logic are the following:
P1 ⊢ P2 and Q1 ⊢ Q2 then P1 ∗ Q1 ⊢ P2 ∗ Q2.P ∗ (Q ∗ R) ⊢ (P ∗ Q) ∗ RP ∗ Q ⊢ Q ∗ PP ∗ emp ⊢ P and P ⊢ P ∗ empIn addition, separation logic enjoys the usual rules of first-order logic for introduction and elimination of the ordinary logical connectives. For example:
R ⊢ P R ⊢ Q
---------------∧I -----------∧El -----------∧Er
R ⊢ P ∧ Q P ∧ Q ⊢ P P ∧ Q ⊢ Q
Now we will give a semantics to Hoare triples for partial program correctness. For that, we will not consider the C language, but a version of lambda calculus with operations for references (in the style of ML, but with C-like free):
ref v returns an unused location on the heap, in which the value v is stored.! l returns the value of the location l on the heapl ← v stores the value of v on location l on the heap.free l removes the location l from the heap.Further assume that we have a standard small-step operational semantics:
(e, h) => (e',h')
Where the es are the expressions of our language, and the hs are heaps.
The semantics of the Hoare triple is:
[ P ] e [ vret. Q ] := ∀ h hf.
h ## hf →
P h →
∃ v h'.
(e, h ∪ hf) ⇓ (v, h' ∪ hf) ∧ h' ## hf ∧ Q[vret:=v] h'
This definition is quite a mouthful, so let us go over it step by step. Hoare triples express the following:
h,P h holds beforehand, then,hf called the frame that disjoint to h (notation h ## hf),e in h ∪ hf in is guaranteed to terminate with a value v in a final heap h' ∪ hf with h' ## hf,Q[vret:=v] h' holds.With the semantics of Hoare triples at hand, we will now look at the proof rules.
Let us start with the proof rules for the load/store/free/alloc:
[ emp ] ref v [ l. l ↦ v ]
[ l ↦ v ] !l [ vret. (vret = v) ∗ l ↦ v ]
[ ∃ v', l ↦ v' ] l ← v [ vret. (vret = ()) ∗ l ↦ v ]
[ l ↦ v ] free l [ vret. (vret = ()) ]
(Our store and free operations return the unit value (), akin to return; for a void function in C).
While concise and intuitive, these Hoare rules are not too useful on their own because:
To address issue (1), separation logic comes with the frame rule, which allows one to extend the pre- and postcondition with a proposition R that describes the remaining part of the heap:
[ P ] e [ vret. Q ]
———————————————————————————
[ P ∗ R ] e [ vret. Q ∗ R ]
For example, using the frame rule we can derive the following Hoare triple:
[ l ↦ v ∗ k ↦ u ] !l [ wret. (wret = v) ∗ l ↦ v ∗ k ↦ u ]
The frame rule is the key feature of separation logic. It allows for so-called small-footprint specifications that only mention the parts of the heap that are relevant for the programs in question, instead of having to quantify over the rest of the heap in every specification.
Another important rule is the sequencing rule:
[ P ] e₁ [ v. Q ] v ∉ FV(Q) [ Q ] e₂ [ vret. R ]
———————————————————————————————————————————————————————
[ P ] e₁; e₂ [ vret. R ]
Since sequencing e₁; e₂ ignores the return value of e₁, it is crucial that v does not appear in the postcondition Q of e₁.
The rule for let-expressions generalizes the rule for sequencing by taking the return value of e₁ into account. In the second premise we consider the program e₂[x:=v] for any value v that satisfies the postcondition Q of e₁.
[ P ] e₁ [ v. Q ] (∀ v. [ Q ] e₂[x:=v] [ vret. R ])
———————————————————————————————————————---——————————————
[ P ] let x := e₁ in e₂ [ vret. R ]
The last rule we consider is the rule of consequence, which allows one to weaken the pre- and postconditions according to logical entailment ⊢:
P ⊢ P' [ P' ] e [ vret. Q' ] ∀ vret. Q' ⊢ Q
————————————————————————————————————————————————
[ P ] e [ vret. Q ]
In the remainder of this lecture we will do some exercises with separation logic in Iris. Iris is a framework for separation logic, implemented and proved sound using the Rocq prover. Iris comes with a number of parts:
⊢ using tactics like iIntros and iApply, which behave much like Rocq’s native tactics, but are tailored to separation logic.iProp and an program logic for partial program correctness.The type of propositions iProp of Iris is different in a number of ways compared to sepProp defined in this lecture:
P ∗ Q ⊢ P, and particularly l ↦ v ∗ k ↦ w ⊢ l ↦ v. This allows us to “forget” about locations that we do not use instead of having to free them explicitly.iProp Σ has a parameter Σ which describes what resoures can be used. For most our examples, we just need the heap (in Rocq you will therefore see boilerplate like Arguments {!heapGS Σ} to express that).▷), “persistently” (□), update (|==>). We will mostly ignore these modalities, but shed some light on them during the next two lectures. (If you like Haskell or category theory, these modalities are “just” an applicative, co-monod, and monad, respectively :).)Finally, Iris makes use of weakest preconditions instead of Hoare triples. A weakest precondition WP e { Φ } is like a Hoare triple without precondition. You can see more about that in the Rocq demo and exercises.