Scholarly article on topic 'Step-Indexed Kripke Model of Separation Logic for Storable Locks'

Step-Indexed Kripke Model of Separation Logic for Storable Locks Academic research paper on "Computer and information sciences"

CC BY-NC-ND
0
0
Share paper
Keywords
{Semantics / Concurrency / "Separation Logic" / Step-Indexing}

Abstract of research paper on Computer and information sciences, author of scientific article — Alexandre Buisse, Lars Birkedal, Kristian Støvring

Abstract We present a version of separation logic for modular reasoning about concurrent programs with dynamically allocated storable locks and dynamic thread creation. The assertions of the program logic are modelled by a Kripke model over a recursively de. ned set of worlds and the program logic is proved sound through a Kripke relation to the standard operational semantics. This constitutes an elegant solution to the circularity issue arising from lock resource invariants depending on worlds containing lock resource invariants.

Academic research paper on topic "Step-Indexed Kripke Model of Separation Logic for Storable Locks"

Available online at www.sciencedirect.com

SciVerse ScienceDirect

Electronic Notes in Theoretical Computer Science 276 (2011) 121-143

www.elsevier.com/locate/entcs

A Step-Indexed Kripke Model of Separation Logic for Storable Locks

Alexandre Buisse1 Lars Birkedal2

IT University of Copenhagen, Denmark

Kristian St0vring3

DIKU, University of Copenhagen, Denmark

Abstract

We present a version of separation logic for modular reasoning about concurrent programs with dynamically allocated storable locks and dynamic thread creation. The assertions of the program logic are modelled by a Kripke model over a recursively defined set of worlds and the program logic is proved sound through a Kripke relation to the standard operational semantics. This constitutes an elegant solution to the circularity issue arising from lock resource invariants depending on worlds containing lock resource invariants.

Keywords: Semantics, Concurrency, Separation Logic, Step-Indexing

1 Introduction

We present a version of separation program logic for modular reasoning about shared-variable concurrent imperative programs with dynamically allocated storable locks and dynamic thread creation. Following earlier work [8,9,11], the idea of the logic is that a lock can be used to protect a resource invariant which, conceptually speaking, is transferred from one thread to another. The resource invariant follows the movements of the lock itself, changing ownership through acquire and release operations. Since the resource invariants associated with locks describe properties of heaps while locks are themselves (dynamically allocated and) stored in the heap, there is a tricky form of circularity that needs to be resolved in order to develop a model to show soundness of the logic. More specifically, since locks are dynamically

1 abui@itu.dk

2 birkedal@itu.dk

3 stovring@diku.dk

1571-0661/$ - see front matter © 2011 Elsevier B.V. All rights reserved. doi:10.1016/j.entcs.2011.09.018

allocated, it is natural to use a Kripke model of assertions, such that the semantics of an assertion is a world-indexed predicate on the set of program heaps, and the set of worlds describes the predicates that the allocated locks protect. In summary, we end up with recursive semantics equations of roughly the following form:

Pred = W ~^V(H) W = Loc Pred

In earlier work, Gotsman et al. [8] evaded this circularity by restricting the logic and using a static number of classes of locks, so that the circularity could be broken using an indirection through a syntactic name for the class of the lock, and by instrumenting the operational semantics to include such syntactic names of classes of locks in the program heap. In [10,9], Hobor et al. addressed the circularity using step-indexing and instrumenting the operational semantics. The instrumentation of the operational semantics means in both cases that some additional work is required to relate the semantics to the standard operational semantics, as was recently done for the setup used by Hobor et al. [12].

Here instead we (1) solve (a version of) the above recursive equation in a category of complete bounded ultrametric spaces, following ideas that we have used earlier to model higher-order programming languages with dynamically allocated reference cells [5,4]. This gives a real semantic status to predicates, allowing for instance recursive definitions via a fixpoint operator. Moreover (2), we prove the soundness of the logic by giving a Kripke relation to the standard (uninstrumented) operational semantics. This approach to proving soundness is related to logical relation proofs for soundness of models of type systems (e.g., [4]) and was proposed for concurrency by Vafeiadis [13,7]. Here we show how to extend and apply the technique in a richer language with dynamic thread creation and storable locks.

To sum up, we believe our new approach to the semantics and soundness is both semantically simple and useful since it solves the circularity issues directly and soundness is proved w.r.t. standard operational semantics. We demonstrate our approach on a simple but illustrative language and also include an example of using the resulting logic.

2 The language

2.1 Definition

We will use a small, low-level imperative language supporting minimal heap manipulation commands, simple lock operations and concurrency via dynamic thread creation and synchronisation. Its syntax is defined as follows:

Var ::= l,x,y,...

E,F ::= nil | Var | E + F | ...

B ::= E = F IE = F

C ::=let x = new in C | let x = E in C |

Variables Expressions Boolean Expressions

let x = [E] in C | [E] = F |

if B then C else C' | while [E] do C | skip | C;C' | makelock_P(E) | acquire(E) | release(E) | let x = fork(f) in C| join(E)

Commands

Expressions include integer constants and arithmetic operators, which we will not detail here.

A program consists of a static definition of sub-procedures, f0 to fn, followed by a main command, C:

let f0 = CO and f1 = C1 and ... and fn = Cn in C

Here the main command C can create new instances of the sub-procedures via calls to fork.

We have made several choices in the syntax, trying to simplify the language whenever features were orthogonal to the problems we are interested in. In particular, we use immutable stack variables via continuation passing style commands (let x = new in C and let x = E in C for allocation and lookup, respectively), following [6]. Any command of the form let x = ... in C uses a fresh variable x, obtained if necessary via alpha-conversion.

Also, in order to keep things relatively simple, the language does not support any freeing of resources, be it regular memory cells or locks. The latter permits us to omit the entire system of fractional permissions found in previous works (e.g. [8] and [9]).

2.2 Operational semantics

We will use a simple non-deterministic operational semantics for the language, close to what would actually be executed by a real machine. Our abstract locks behave similarly to spinlocks in that an acquire instruction on a lock that is not available will simply "hang" and will be allowed to try to acquire the lock again after another thread took an execution step.

A state in our semantics is a triple (h, s,tp) where h is a heap in H = Loc ^fin Val, s a stack in Var ^ Val and tp a thread pool mapping each thread identifier to the code it should execute, i.e. tp : {1,...,n} ^ Cmd for some n.

It is important to note that this operational semantics is very low-level and doesn't deal with some of the more "semantic" features. In particular, neither the heap nor the stack are split between threads and there is no notion of private or shared space. Similarly, locks are implemented in a very simple fashion: as an integer which contains 0 if the lock is available, and k if it is currently owned by the thread with identifier k. Since there is no notion of world, nothing really

distinguishes a lock or a thread handle from a regular memory cell, and an incorrect program could easily overwrite lock values.

Definition 2.1 The general form of a reduction in our operational semantics is (h,s,t) —j (h',s',t'). However, to improve readability, we will write (c,h,s,t) —j (c' ,h',s',t) as a shorthand notation for "(h, s,t) —j (h',s',t') and t(j) = c and t'(j) = c'".

Furthermore, a state (h, s,t) can also be .stuck with respect to thread j, written (h,s,t) —j, or lead to an execution fault, written (h,s,t) —j abort. In order to save space, we do not list all the conditions for aborting states: rather, we define an aborting state to be one that is neither stuck nor reduces to another state.

(let x = new in c, h, s, t) -yj (c, h[v ^ 0], s[x ^ v], t[j ^ c])

where v E dom(h)

(let x = [E] in c,h,s,t) -j (c,h,s[x ^ v],t[j ^ c]), where h(|E]s) = v

(let x = E in c,h,s,t) -!•j (c, h,s[x ^ IE]s],t[j ^ c])

([E] = F, h, s, t) -!•j (skip, h[|E]s ^ |F]s],s,t[j ^ skip])

(if E then c else c' ,h, s, t) -y j (c, h, s, t[j ^ c]), if |E]s = True

(if E then c else c',h,s,t) -yj (c',h,s,t[j ^ c']), if |E]s = False

(while [E] do c,h,s,t) -yj (skip,h, s, t[j ^ skip]), if h(|E]s) = False

(while [E] do c,h,s,t) -j (c;while [E] do c,h,s,t[j ^ c;while [E] do c])

if h(|E]s) = True

(skip;c, h, s, t) -yj (c, h, s, t[j ^ c])

(c;c' ,h, s, t) -i- j (c'';c', h', s', t'[j ^ c'';c']),

if (c,h,s,t) -j (c'',h',s',t')

(skip, h, s, t) ^j

(make_lock_P(E),h,s,t) —j (skip,h[|E]s ^ j],s,t[j ^ skip]), if h(|E]s) = -(acquire(E), h, s, t) —i-j (skip,h[|E]s ^ j],s,t[j ^ skip]), if h(|E]s) = 0 (acquire(E), h, s, t) if h(|E]s) > 0

(release(E), h, s, t) —j (skip,h[|E]s ^ 0],s,t[j ^ skip]), if h(|E]s) = j

(let x = fork(f) in c,h,s,t) -yj (c,h[v ^ k],s[x ^ v] ,t[k ^ c]),

if v E dom(h), k E dom(t) and f : c E r

(join(E),h,s,t) -j (skip,h|dom(h)-JE]s ,s,t[j ^ skip] | dom(t)—{hy),

if h(|E]s) = k and t(k) = skip (join(E) ,h, s, t) ^j

if h(|E]s) = k and t(k) = skip (h,s,t) -yj abort in all other cases

2.3 Assertion language

Our assertion language is built upon separation logic, with extra assertions dealing with locks and dynamic thread creation and synchronisation. In addition, the

intrinsic circularity of storable locks is expressed via a fixpoint operator ¡.

PredVar ::= p,q,r,...

P ::= Ei = E2 | Ei ^ E2 | P A P | P V P | P * P | emp | Locked(E, P) | Ex(E, P) | tid(f, E) | r (E) | (¡r.Xx.P)(E) (*) Val = Loc W Z W (Val x Val)

Notice that we do not include separating implication. This is a limitation of our current model; see Proposition 4.6 below.

In order to express lock properties, we use two predicates: Ex(E, P) and Locked(E,P). The first affirms that the interpretation of expression E points to a lock in the heap, with resource invariant P. No hypothesis is made as to whether the lock is available or not. Locked(E, P) is similar, with the additional affirmation that the current thread owns the lock.

It is necessary for a thread to know about the existence of a lock before it can try to acquire it, but not to know that the lock is available. Similarly, the postcondition of a lock release simply says that the lock still exists, but not that it is available, in order to protect against interferences from other threads, which can now successfully acquire the lock.

The side condition (*) stipulates that all occurrences of the predicate variable r in P are guarded, i.e. that they only appear under a Locked or Ex predicate. More formally, using the notation that P[ ] is a formula with a hole which can be filled by a predicate Q, written P [Q]:

Definition 2.2 A predicate variable r is guarded in P if 3Pi,E,Q such that P = Pi[Ex(E,Q)] or P = Pi[Locked(E, Q)] and r fv(Pi).

Additionally, we also have the usual intuitionistic logic rules and some additional rules for the assertion logic, such as

Locked(E, P) ^ Locked(E, P) * Ex(E, P) (¡r.Xx.P)(E) ^ P[(¡r.Xx.P)/r, E/x]

2.4 Selected proof rules

We now present some of the proof rules for the specification logic, choosing to omit most of the structural rules. We need the following definitions: a predicate P is thread-independent if it does not contain any sub-predicates of the form Locked(E,P'). Intuitively, the meaning of such a predicate will be the same for all threads in a given configuration, whereas, e.g., a Locked(E,P') predicate is true for at most one of the threads. A predicate is mobile if it is thread-indepedent and precise.

{P * x ^ 0}c{Q} {P}let x = new in c{Q}

x £ fv(P) U fv(Q)

{P * E ^ x}c{Q}

{3x,P * E ^ x}let x = [E] in c{Q}

x £ fv(Q)

{P A x = E}c{Q} {P}let x = E in c{Q}

x £ fv(Q)

{E = true A P}c{Q} {E = false A P}c'{Q} {P }if E then c else c'{Q}

{P }skip{P }

{P }c{Q} {Q}c'{R} {P }c;c'{R}

{[E ^ -]}[E] = F{[E ^ F]}

{P A E ^ true}c{P A E ^ _}

{P A E ^ _}while [E] do c{P A E ^ false}

{Ex(E,P)}acquire(E) {Locked(E, P) * P} r, {R}f{S} h {P * tid(f, x)}c{Q}

{E ^ _}makeloc^P(E) {Locked(E, P)}

{Locked(E, P) * P}release(E) {Ex(E, P)}

r, {fl}f{5} h {P * fl}let x = fork(f) in c{Q} w r, {fl}f {5} h {tid(f, E)}join(E) {5}

f : {R}c'{S}er r h{R}c'{S} r h {P}c{Q}

h let f : {R}c'{S} in {P}c{Q} ( )

The rules marked (*) have the following general provisos:

• All predicates P occurring in Locked(E, P) or Ex(E, P) in the rules are mobile.

• For every triple {R}f{S} in the assumptions (i.e., on the left of "H"), R is mobile while S is thread-indepedent.

The precision requirements for the locks' resource invariants and the preconditions of forkable procedures are necessary to prove soundness of, respectively, the release case and the frame rule, since there are two operations, release and fork, which perform heap splitting of the private space of the active thread, about which we need to be able to reason.

The predicates which are required to be thread-independent are precisely those describing parts of the heap that, conceptually speaking, is transfered between different threads. The meaning of these predicates needs to be the same before and after such a transfer.

3 Example: Lock coupling

The lock coupling algorithm can be considered the seminal example for storable locks and is treated in a very similar fashion by Gotsman et al. [8]. The idea is to allow multiple threads to simultaneously add and remove elements from an ordered singly linked list. Instead of having a single general lock for the entire list, which would require all threads to wait for the active one to finish its operation, there is one lock per node. By requiring threads to hold the lock for both the current node and the previous one whenever they search through the list or try to modify it, we can ensure that the data structures are never in an incoherent state, while the fine-grained locks allow for a great gain in efficiency.

Each node contains three fields: a lock protecting the entire node, with resource invariant P, a data field val containing an integer and a pointer to the next node

or nil. We also use the convention that the head of the list always contains the value and the tail We present here code for initializing the list and for

adding a new node with value e in the correct location in the heap. A procedure to remove a node with value e would be very similar.

Since the only way to know about the existence of a particular lock is by acquiring its predecessor in the list, the invariant itself enforces the particular locking procedure for iterating through a list. We present below a simplified version of the annotated proof for add.

The resource invariant is a recursively defined predicate:

P(n, e) = (ßr.X(n, e). [(n.val ^ e A e = œ)V

(3x, ê ,n.val ^ e * n.next ^ x * Ex(x.lock, r(x, ê)) A e < e'))])(n, e)

initialize () {

let tail = new NODE in [tail.val] = +to ; [tail.next] = NULL; makelockp(tail,+™) (tail .lock); release(tail.lock); let head = new NODE in [head.val] = — to; [head.next] = tail; makelockp(head,-™) (head . lock) ; release(head.lock);

add (e) {

Ex(head.lock, P(head, —to)) let prev = head in acquire(prev.lock);

Locked(prev.lock, P(prev, —to)) * P(prev, —to) let curr = prev.next in acquire (curr.lock) ;

3v', Locked(prev.lock, P(prev, —to))* prev.val ^ —to * prev.next ^ curr* Locked(curr.lock, P(curr,v')) * P(curr, v') A —to < v'

while (curr.val < e) do { 3x, v, v',v'', Locked(prev.lock, P(prev, v))* Locked(curr.lock, P(curr, v')) * prev.val ^ v* prev.next ^ curr * curr.val ^ v' * (v' = toV (curr.next ^ x * Ex(x.lock, P(x,v")) A v' < v'') Av' < e A v < v'

release(prev.lock); let prev = curr in let curr = curr.next in acquire(curr.lock);

3x, v, v',v", Locked(prev.lock, P(prev, v))* Locked(curr.lock, P(curr,v')) * prev.val ^ v* prev.next ^ curr * curr.val ^ v' * (v' = toV (curr.next ^ x * Ex(x.lock, P(x, v'')) A v' < v'') Av < e < v'

if (curr.val != e) { let n = new NODE in [n.val] = e; [n.next] = curr; makelockp(n,e) (n. lock) ; 3x, v, v', Locked(prev.lock, P(prev, v))* Locked(n.lock, P(n, e))* Locked(curr.lock, P(curr, v'))* prev.val ^ v * n.val ^ e * n.next ^ curr* curr.val ^ v' * Av < e < v' release(n.lock); [prev.next] = n;

release(prev.lock); release(curr.lock) ;

3v, v', Ex(prev.lock, P(prev, v))* Ex(curr.lock, P(curr, v')) * ((v' = e A v < v')

V(3n, Ex(n.lock, P(n, e) A v < e < v'))) }

As mentioned before, this example was treated already in [8]. Though our model removes the limitation that lock sorts have to be statically defined before the program is run, this increased generality is not needed in this particular example.

We conjecture that the additional flexibility will be useful for making proofs that are generic in the resource invariant predicate.

128 A. Buisse et al. /Electronic Notes in Theoretical Computer Science 276 (2011) 121—143

4 The model

4-1 The Recursive Domain Equations

As described in Section 1, the main issue with storable locks originates from the fact that the resource invariants depend on "worlds" which contain locks and resource invariants. The worlds can be seen as a semantic layer above the heap, containing additional information about the state of the machine. In our case, in order to be able to formulate soundness of the logic, we want to keep track of three different kinds of information:

• Whenever a memory cell is a lock, we want access to its resource invariant.

• Whenever a memory cell is a thread handle (the value returned by a call to fork), we want to remember the name of the procedure we are executing, so that we can look up its pre and post-conditions in the context.

• If a memory cell is "regular" (i.e. not a lock or a thread handle), we don't need to remember any other information than the fact that it does exist.

This is formulated in the following equation:

W = Loc —fin (Fun + Cell + Pred) (1)

Here Loc = N represents locations in the heap, Fun is a set of procedure names, and Cell is a singleton set. For clarity, we will use the notations T(f) and Lock(Q) to refer to the first and third components of the Fun + Cell + Pred sum type.

Heaps in our semantic layer will be identical to the concrete ones used in the operational semantics, with one exception: we will occasionally use an unknown value U to denote that no assumption is made as to the exact state of a lock.

H = Loc ^fin Val U {U}

This allows us to define a special * operation designed to split a heap into several private sub-heaps. Because the memory cells corresponding to locks are shared by several threads, the regular separating conjunction is not well defined, forcing us to use a variant where starred heaps are allowed to overlap.

Concretely, hi * h2 is well defined if and only if for all l £ dom(hi) n dom(h2) we have that hi(l) * h2(l) is well defined. Here we define * on individual memory cells by U*U = U and U*j = j *U = j for any j £ Val, while j* k is undefined. We then take (hi * h2)(l) = hi(l) * h2(l) when l £ dom(hi) n dom(h2), while (hi * h2)(l) is defined as for regular separating conjuction when l £ dom(hi) n dom(h2). We also use an operation to merge a tuple of heaps into a single one: hp = *iedorn(hp)hp(i).

Since, because of interferences, threads can only be certain of the state of a lock when they actually own it, there is a simple way to map full abstract heaps to concrete ones: simply replace all unknown values by 0, as the absence of any claims to hold the lock means that it is available.

\.\ : H — H

[ 0 ifh(x) = U \h\ = Ax. I V '

h(x) otherwise

This is a departure from the way heap sharing has been handled in concurrent separation logic. Instead of having a general shared space that is not private to any thread and that everybody can manipulate, we include duplicate values of the shared memory cells in relevant private spaces. This reflects more accurately the scope of the lock variables, and permits much finer granularity, with locks being known only to a limited number of threads.

The one remaining set to define is Pred, the type of lock resource invariants. A predicate should be a function that takes a world (containing semantic information about the current state) and returns a set of heaps, i.e., those heaps that "satisfy the predicate." Roughly, we want something like:

Pred = W — T(H). (2)

(We also require that if a heap h £ H satisfies a predicate in a world w £ W, then dom(h) C dom(w); see below.) Notice the circularity between W and Pred in (1) and (2): the two equations do not in themselves form a good definition of the sets of worlds and heaps.

4-2 Complete, Bounded Ultrametric Spaces

We solve this system of equations using Complete, Bounded, Ultrametric spaces, objects in the category CBUlt, following the methods used in [5]. The first step is to define a proper distance on the various spaces we are manipulating, and in particular P (H), which we turn into the set of uniform predicates, UPred, by combining heaps with downwards closed integer indices.

Definition 4.1

UPred (Hi) = {P £ P(H x N) \Vh £ H, Vk £ N, Vj < k,P (h,k) ^ P (h,j)} We define the following distance on UPred (H):

f 0 if P = Q

d(P,Q)= „ Q

[ min{2-n\Vk < n, Vh £ H, P(h, k) Q(h, k)} otherwise.

For any distance d and natural number n, we define "equality up to n", denoted P =n Q, as d(P,Q) < 2-n.

Let T be a fixed set. We want to solve the following equation in CBUlt:

X ^ 2 ((Z -fin (T + x)) —n UPred(£)) (3)

Here the function space consists of those functions p which are non-expansive (in the metric-space sense) and satisfy the following property: If (h, n) £ p(w), then dom(h) C dom(w).

In order to do solve this equation, we must first specify which metric is used on the right-hand side, assuming that (X,dx) is a given object of CBUlt.4 This metric is obtained from the following building blocks. We equip the set T with the discrete metric, i.e., every pair of distinct elements has distance 1. The operators x and are given by the cartesian closed structure of CBUlt (here is a closed subspace of the exponential), while + is the coproduct (which gives elements from different summands the maximal distance, 1). More details about these operators can be found in, e.g., [5]. It only remains to explain the "finite partial function space" operator -fin. Concretely, the distance function d on Z -fin (T + X) is given by

d(p Q) — J 1 if dom(P) — dom(Q)

d(P, Q) — N

[ maxiedam(P)(dT+x(P(i),Q(i))) otherwise.

Using well-known existence theorems for recursive metric-space equations [1], we can then find a solution X to (3). If we rename Z to Loc, T to T(Fun) + Cell and X to Pred, we obtain a solution to the following equation:

W — Loc —fin (T(Fun) + Cell + Lock(Pred,))) Pred — W ^n UPred(H)

Pred — 2 Pred

Let App be the isomorphism above, and let Abs be its inverse:

App : Pred ^ ^ Pred Abs Pred ^ pred.

World composition (of elements w and w' of W) is defined as follows: w * w' is defined if the domains of w and w' do not overlap, and in this case w * w' is the union of the two partial functions w and w'.

We can then prove that Pred models the usual separation logic operations, which is summarized by saying it is equipped with a complete BI-algebra structure.5 First, for every w £ W we define UPredw(#) to be the subset of UPred(H) where all heaps have domain contained in dom(w). This set is given a BI-algebra structure as follows. We define hi±wh2 to hold if hi * h2 is defined, and if furthermore

4 Actually, we must show that the right-hand side of the equation is a contravariant functor in the metric space X. We omit this argument here; details of a similar case can be found in [5].

5 Although we do not include separating implication in the syntactic assertation language, the BI-algebra structure on Pred does include a "separating implication" operator —See the discussion after Proposition 4.6.

w(l) = Lock(—) for all l £ dom(hi) n dom(h2). Then we define *w on heaps in the same way as *, but with additional requirement that hi *w h2 is only defined if hi±wh2. (The intuition is that only values which correspond to locks can be shared between heaps.) Now the BI-algebra structure on UPredw(H) is given as follows: 0 is L, {h \ dom(h) C dom(w)} x N is T, and set-theoretic union and intersection are, respectively, join and meet. The remaining three operations are defined by:

(h,n) £ P — Q = Vk < n, (h, k) £ P ^ (h,k) £ Q

(h,n) £ P * Q = 3hi,h2,h = hi *w h2 A (hi,n) £ P A (h2,n) £ Q (h,n) £ P — Q = Vk < n, Vh'Lwh, (h',k) £ P ^ (h *w h',k) £ Q

These operations are then extended pointwise to Pred, using the fact that, by construction, every element of Pred maps any world w into UPredw(H) (and not just into UPred(H)).

Lemma 4.2 Pred equipped with these operations is a complete BI-algebra [3] on Set.

4-3 Predicate interpretation

Having solved the recursive domain equations, we can now define interpretations for our assertion language. The interpretation of a predicate P, written \ Ptakes as arguments a stack s and the identifier k of the current thread, returning an object in Pred.

Stacks can store either plain values or predicates with an optional argument:

Stack = (Var Val) W (PredVar (Val — Pred)) Definition 4.3

1 Ex(E, P)] k = Aw.{(h, n) \ 3Q. w(l E]s) = Lock(Q) A h = [\E]s — U] A

Vw'.App(Q)(w') =n-i \PLV)} \Locked(E, P)]k = Aw.{(h, n) \ 3Q. w(\ Ey = Lock(Q) A h = [\ E]s — k] A

Vw'.App(Q)(w') =n-i \ P]k(w')} \tid(f, E)]k = Aw.{(h, n) \ w(\E]s) = T(f) A 3j. h = [\ E]s — j]}

\r (E) Lk = s(r)(\ E]s) \(¡ir.Ax.P)(E) ]k = fix (APVal^Pred Av. \ P]k[r(\E]s) \P * Q]k = Aw.{(h, n) \ 3hi,h2. h = hi *w h2 A

(hi,n) £ \P]k(w) A (h2,n) £\Q]k(w)} \Ei — E2]k = Aw.{(h,n) \ w(\ Ei]s) = Cell A h = [\ Ei]s — \E2]s]}

Again, the intuition behind the use of *w is that only values which correspond to locks can be shared between heaps.

The most interesting point to note is how the interpretation of the two lock predicates Ex and Locked refers to (n — 1)-equality of predicates instead of "true" equality. This is necessary by the non-expansiveness requirement of W UPred(H) in the domain equation, while the —1 part simply reflects the 2 factor in the final isomorphism.

Theorem 4.4 For all P, s, and k, the predicate [P]k is well-defined.

Proof Using the fact that recursion can only happen in guarded environments, the interpretation of (^r.Xx.P)(E) uses the fixpoint of a contractive function on Val ^ Pred, which is in CBUlt if we view Val as a discrete metric space. □

Semantic predicates which are definable by syntactic predicates satisfy a certain property which is crucial for our soundness proof:

Definition 4.5 A predicate p £ Pred is local if for all h, n, and w,

(h,n) £ p(w) ^^ (h,n) £ p(w\dom(h)).

Proposition 4.6 For every syntactic predicate P containing no free predicate variables, the semantic predicate [P]k is local.

Proposition 4.7 If p is local, then p is monotone: (h,n) £ p(w) implies (h,n) £ p(w * w') whenever w * w' is defined.

Proposition 4.6 does not hold if the assertion language includes separating implication, and this is the reason we have excluded that operator. A counterexample is given by the predicate [0 ^ 1] —*± which would hold for the empty heap in the empty world, but not in a world which maps 0 to Cell. An attempt at avoiding the problem would be to construct a model where all semantic predicates are "local by construction": this could perhaps be done by requiring that (h, n) £ p(w) implies dom(h) — dom(w) (instead of just dom(h) C dom(w)). Then, in the definition of separating conjunction, one would split worlds as well as heaps. We have not yet investigated this approach in detail.

5 Soundness

5.1 Soundness definitions

Having defined how to interpret assertions, we can now define what it means for a Hoare triple to be semantically valid. Using a similar method to the one developed in [13], we use as our main building brick a safety predicate, which stipulates that, for a given number of steps, the current machine state will not lead to an execution fault and will satisfy relevant assertions.

Intuitively, a Hoare triple {P}c{Q} is then said to be semantically valid to the jth level if we can add to any state safe to the jth level a thread executing c and a

heap satisfying | P|, and obtain a new state that is also safe to the jth level. The fact that the new thread, once finished executing, should satisfy | Q| is already part of the safety predicate and does not need to be specified separately.

This allows the complete abstraction of the interferences between different threads, where the non-deterministic character of the operational semantics reductions is confined to the safety predicate.

We now turn to the definitions. A configuration is a tuple (w,hp, s,tp,qp) where for some n G N we have w G W, hp : {1,...,n, Q} ^ H, s G Stack, tp : {1,...,n} ^ Cmd, and qp : {1,...,n} ^ Pred. The intuition is the following. In order to have a notion of ownership, allowing us to reason with resource invariants, we split the heap in many private sub-heaps hp, one for each thread in tp, forbidding anyone to manipulate a memory cell not in its private space. There is also an additional space, denoted with the special thread id Q and corresponding to the heaps described by the invariants of available locks. A successful lock acquisition will then correspond to the transfer of a sub-heap owned by Q into the private space of the new owner thread, with release being the reverse operation. Similar heap swaps occur with fork and join operations. Finally, the intended semantic properties of the total heap are described by the world w, and each predicate qp(j) is a postcondition that the private heap of thread j must satisfy when that thread is done executing.

Definition 5.1 Assume that w G W and h G H satisfy dom(w) = dom(h). The free predicate of (w, h) is defined as the separating conjunction of all lock invariants of available locks: free(w, h) = ^App(pi) where l\,...,lm are all the locations in dom(w) such that w(li) = Lock(-) and h(li) = 0, and where w(li) = Lockp) for each i = 1,...,m.

Definition 5.2 A configuration (w, hp, s,tp, qp) is consistent, which is written cons(w, hp, s,tp, qp), if the following properties hold:

• dom(w) =dom(hp).

• For every l and all j = k, if l G dom(hp(j)) n dom(hp(k)), then w(l) = Lock(-).

• For every j, the predicate qp(j) is local.

• For every l, if w(l) = Cell or w(l) = T(fi), then l G dom(hp(j)) implies hp(j)(l) = U.

• For every l, if w(l) = Lock(p), then the predicate p is local. Furthermore, if l G dom(hp(j)) then hp(j)(l) = 0.

A consistent configuration is called complete if, intuitively, all threads are present and hp(Q) satisfies the invariants of free locks:

Definition 5.3 A configuration (w,hp, s,tp,qp) is n-complete, which is written compn(w, hp, s, tp, qp), if the following properties hold:

• (w, hp, s, tp, qp) is consistent.

• For every l, if w(l) = T(fi), then there exist j and k such that l G dom(hp(j))

and hp(j)(l) = k and qp(k) = \ Qi]k. Here {Pi}fi{Qi} comes from the global environment r.

• (hp(Q),n) £ free(w, \hp\)(w).

We next define a transition relation on consistent configurations. For complete configurations, this relation serves as a semantic layer on top of the standard operational semantics. For configurations which are not complete, however, we add non-deterministic reductions that mimic the "missing parts."

Definition 5.4 The relation

(w, hp, s, tp, qp) —j (w',hp',s',tp',qp')

holds if there exists a reduction ( \hp\ ,s,tp) —j (h',s',tp') such that \hp\ = h' and hp'(k) = hp(k) for all k £ {j, Q}. Furthermore, letting c = tp(j):

• If c is not a reference allocation, lock operation, or thread operation, then w' = w, qp' = qp, and hp'(Q) = hp(Q).

• If c is let x = new in C', then w' = w[v — Cell], qp' = qp, and hp'(Q) = hp(Q).

• If c is make_lock_P(E), then w(\ Ey = Cell, w' = w[\ E— Lock(Abs([P j))], qp' = qp, and hp'(Q) = hp(Q).

• If c is acquire(E), then w(\ E]s) = Lock(Q) for some Q, and hp(j)(\ E]s) = U. Furthermore, w' = w, qp' = qp, and there exists (h0,n) £ App(Q)(w) such that hp(Q) = ho *w hi while hp'(Q) = hi and hp'(j) = h0 *w (hp(j)[\ E]s — j]). (Notice that here the superscript "n" on the relation symbol is used.)

• If c is release(E), then w(\E]s) = Lock(Q) for some Q, and hp(j)(\ E]s) = j. Furthermore, w' = w, qp' = qp, and there exists (h0,n) £ App(Q)(w) such that hp(j) = h0 *w hi while hp'(j) = hi[\ E]s — U] and hp'(Q) = h0 *w hp(Q).

• If c is let x = fork(f) in M, then w' = w[v — T(f)] where s' = s[x — v]. Furthermore, qp' = qp[k — \ Q]k] where {P}f {Q} £ r and k £ dom(tp') \ dom(tp). Also, hp'(Q) = hp(Q), and there exist hi,h2, such that hp(j) = hi *w h2 and (h2,n) £ \ P]k(w), while hp'(j) = hi[v — k] and hp'(k) = h2.

• Finally, if c is join(E), then w(\ E]s) = T(f) for some f. Also, hp(j) = hi *w [\ E]s — k] where k £ dom(tp) \ dom(tp'). Furthermore, w' = w\dom(w)\{lEls}, hp'(Q) = hp(Q), and hp'(j) = hi *w hp(k).

Furthermore, the following reductions hold:

(w, hp[j — h * [l —U]],s, tp[j — acquire(E)],qp) —j

(w, hp[j — hi * [l — j] * ho],s, tp[j — skip],qp),

if w(l) = Lock(Q), \ E]s = l, hp(l) = U, (h0,n) £ App(Q)(w), and there is no

A. Buisse etal. /Electronic Notes in Theoretical Computer Science 276 (2011) 121—143 135

sub-heap h'0 of hp(fi) such that (h'0,n) £ App(Q)(w).

(w,hp[j 4 hi * [l 4 k]],s,tp[j 4 join(E)],qp) 4j

(w, hp[j 4 hi * ho],s, tp[j 4 skip],qp),

if k £ dom(tp), w(l) — T(fi), [ E[a — l, and (ho,n) £ [ Qi¡1. (Here {Pi}fi{Qi} comes from the global environment.)

Notice that the last two reductions can only happen for tuples that are not n-complete. In the first of the two, the idea is that there is no sub-heap in hp(Q) which satisfies the lock invariant, so a sub-heap is chosen non-deterministically. In the second of the two, the idea is that the thread to be joined is not present, so a sub-heap satisfying the postcondition of the thread is chosen non-deterministically. In the following, let d range over configurations.

Proposition 5.5

(i) If cons(d) and d 4j d', then cons(d').

(ii) If compn(d) and d -nj d', then compn(d').

As promised, reduction of complete configurations can be viewed as a semantic layer on top of standard reduction:

Proposition 5.6 If (w,hp, s,tp,qp) is n-complete and (w,hp, s,tp,qp) --j (w',hp',s',tp',qp'), then (hp, s, tp) 4j (hp ,s',tp').

We are now ready to define the semantic safety predicate. The safety predicate stipulates that, up to a given number of steps, the configuration d it considers will be semantically correct. Intuitively, this means the following:

• d is consistent.

• d cannot reduce to abort in one step.

• The private heap of any thread which has finished execution (i.e. it only needs to execute skip) satisfies its post-condition.

• If d reduces to another configuration d', then d' is safe up to a smaller number of steps. ("Preservation")

• If d is complete, and if the configuration corresponding to d reduces in the standard operational semantics then d reduces. ("Progress")

Definition 5.7 Let d = (w,hp, s,tp,qp). The predicate safen(d) is defined by induction on n as follows. safe0(d) always holds, and safen+1(d) holds iff:

• d is consistent, i.e., cons(d) holds.

• Vj G dom(tp), Vh such that h±hp, there is no reduction of the form ( |hp * h I ,s,tp) —> j abort.

• Vj G dom(tp). tp(j) = skip (hp(j),n) G qp(j)(w)

• If d —j ^ then safemin(n,m)(d').

136 A. Buisse et al. /Electronic Notes in Theoretical Computer Science 276 (2011) 121-143

• If d is n-complete and ( \hp\,s,tp) —j (h',s',tp'), then d —j d' for some d'.

Next we define semantic validity of Hoare triples. Definition 5.8

r Nj {P}c{Q}

is defined as

Vm < j, Vw, hp, s, tp, qp, if (Vi £ dom(r), r Nm {Pi}c_i{Qi} A safem(w, hp, s, tp, qp)) then Vk £ dom(hp), Vw' such that w * w' is well defined, V(h,n) £ \P]k(w * w'), (h±hp A cons(w * w',hp[k — h],s,tp[k — c],qp[k — \Q]k])) ^

safemin(m,n)(w * w',hp[k — h],s tp[k — c],qp[k — \Q\k]) Definition 5.9

Nj let fi : {PJc_i{Qi} in {P}c{Q}

is defined as

Vi, r Nj {Pi}c_i{Qi} A r Nj {P}c{Q} Theorem 5.10 (Soundness)

h let fi : {Pi}c.i{Qi} in {P }c{Q}^Vj, Nj let fi : {Pi}c_i{Qi} in {P }c{Q} 5.2 The proof

We prove soundness separately for each proof rule, in each case using strong induction on the index of the safety predicate. The proofs are relatively straightforward, consisting mostly in unfolding various definitions, considering all reduction steps that can be taken from the initial state and then proving safety of the new state. The following result takes care of the cases where a reduction happens in a thread other than the one we have just added.

Proposition 5.11 Assume that safej(w,hp,s,tp,qp), cons(w * w',hp[k — h],s, tp[k — c],qp[k — q]) where k £ dom(tp), and

(w * w',hp[k — h],s, tp[k — c],qp[k — q]) —j d

for some j = k. Assume furthermore that c = skip. Then there exist w0, hp0, s0, tp0, and qp0 such that

d = (w0 * w',hp0[k — h],s0,tp0[k — c],qp0[k — q])

(w, hp, s, tp, qp) —j (w0,hp0,s0,tp0,qp0)•

Proof (sketch). By case analysis following the definition of the reduction relation. The locality requirements in the definition of consistent configurations are needed to obtain the reduction starting from (w, hp, s, tp, qp); intuitively, we need to ensure that the relevant predicates which hold in world w * w' already hold in world w. □

By this proposition, it is enough to consider reductions in the newly added thread k when proving safety. Here the case of the "fork" rule is the most challenging, since (informally speaking) after a "fork" reduction there are two threads that need to be added to the "base" configuration, while the definition of soundness only allows adding one at a time. By using the locality property (Proposition 4.6) of preconditions of forkable threads, one can add the newly forked thread first, and then the old thread. Using precision of the considered predicates, we can then prove that the heap splitting operated by the transition relation yields a new configuration suitable for using our induction hypotheses.

6 Conclusion

We have provided a new approach to models of program logics for concurrent programs with storable locks, using ultrametric spaces to solve directly the recursive domain equations that arise when resource invariants are allowed to describe heaps in which their associated locks reside. In addition, we have also proposed conceptually simple proof methods for soundness of our logic with respect to the standard operational semantics.

Future work includes extending the language with new features, for instance storable procedures, which we believe would be straightforward to add. The soundness proof also appears to lend itself particularly well to formalisation and automated machine proving, e.g. using the work by Benton et al. [2].

We thank Mike Dodds, Derek Dreyer, Jacob Thamsborg, and Viktor Vafeiadis for discussions about this work.

References

[1] P. America and J. J. M. M. Rutten. Solving reflexive domain equations in a category of complete metric spaces. J. Comput. Syst. Sci., 39(3):343—375, 1989.

[2] N. Benton, L. Birkedal, A. Kennedy, and C. Varming. Formalizing domains, ultrametric spaces and semantics of programming languages. Available from http://research. microsoft.com/en-us/um/people/nick/domultra.pdf, July 2010.

[3] B. Biering, L. Birkedal, and N. Torp-Smith. BI-hyperdoctrines, higher-order separation logic, and abstraction. ACM Transactions on Programming Languages and Systems, 2007.

[4] L. Birkedal, B. Reus, J. Schwinghammer, K. St0vring, J. Thamsborg, and H. Yang. Step-indexed Kripke models over recursive worlds. In POPL, 2011.

[5] L. Birkedal, K. St0vring, and J. Thamsborg. Realisability semantics of parametric polymorphism, general references and recursive types. Mathematical Structures in Computer Science, 20(4):655—703, 2010.

[6] L. Birkedal, N. Torp-Smith, and H. Yang. Semantics of separation-logic typing and higher-order frame rules for Algol-like languages. CoRR, 2006.

[7] T. Dinsdale-Young, M. Dodds, P. Gardner, M. J. Parkinson, and V. Vafeiadis. Concurrent abstract predicates. In ECOOP, pages 504-528, 2010.

[8] A. Gotsman, J. Berdine, B. Cook, N. Rinetzky, and M. Sagiv. Local reasoning for storable locks and threads. In APLAS, pages 19-37, 2007.

[9] A. Hobor. Oracle Semantics. PhD thesis, Princeton University, Department of Computer Science, Princeton, NJ, October 2008.

[10] A. Hobor, A. Appel, and F. Zappa-Nardelli. Oracle semantics for concurrent separation logic. In ESOP, 2008.

[11] P. W. O'Hearn. Resources, concurrency and local reasoning. In CONCUR, pages 49-67, 2004.

[12] G. Stewart and A. Appel. Local actions for a Curry-style operational semantics. In Proceedings of PLPV'11, 2011.

[13] V. Vafeiadis. Concurrent separation logic and operational semantics. In MFPS, 2011.

A Detailed soundness proof

A.1 Technical lemmas

In order for the soundness proofs to go through, we need a number of small results on the operation semantics:

Lemma A.1 If k = j and (if tp(j) = join(E) then h([E]s) = k), then (h, s, tp) —j abort ^ (h, s, tp[k — c]) —j abort

Lemma A.2 If (h * h',s,tp) —j abort, then (h,s,tp) —j abort. A.2 Sequential composition

Before proving soundness of sequential composition, we first need a technical lemma:

Lemma A.3 Vj, Vw, hp, s, tp, qp, k, Q, R, c1, c2, noting qpQ = qp[k — \ Q]k] and qpn = qp[k — \ R]k], if

• safej(w,hp, s,tp[k — cl],qpQ) (1)

• Nj {Q}c2{R}

then safe j (w, hp, s, tp[k — c1;c2] ,qpn).

Proof We proceed by induction on j. The only truly interesting case is when (w, hp, s, tp[k — c1; c2],qp#) —k (w',hp',s',tp[k — c1'; c2],qp#), for which we want to prove safemin(j-1,n)(w',hp',s',tp[k — ci;c2],qpfl).

From the operational semantics, we get that (w,hp, s,tp[k — cl],qpQ) —k (w',hp',s',tp[k — c1'],qpQ) which, in conjunction with (1), gives safemin(n,j-1)(w',hp',s',tp[k — c1'],qpQ). Since min(j — 1,n) < j, we can apply the induction hypothesis and conclude.

The proof rule for sequentical composition is

h {P}ci(Q} h {Q}c2{R} h {P}c1;c2{R}

Following the definition of soundness, we assume that Vj, \=j {P}cl{Q} (i) and Vj,\j {Q}c2{R} (2) and aim to prove that Vj,\j {P}c1;c2{R}.

Proof We proceed by induction on j. The case where j — 0 is trivial since the definition of \ relies on properties for some m < j.

Let's assume that Vk < j, \k {P}c1;c2{R} (3) and prove Nj+i {P}c1;c2{R}. Following the definition of we need to prove safety for all m < j + 1, but any m < j is obtained directly via (3), so we only need to consider the case where m — j.

Let w, hp, s, tp, qp be fixed such that safej (w, hp, s, tp, qp) (4) holds. Let now w' and k £ dom(hp) be fixed. We choose (h, n) £ [ P^(w * w') (5) such that h±hp and cons(q*w',hp[k 4 h],s, tp',qp') (6). We note tp' — tp[k 4 c1;c2] and qp' — qp[k 4 [ R]k]. Our goal is now to prove safemin(nj)(w * w',hp[k 4 h],s, tp',qp').

• cons(w * w',hp[k 4 h],s, tp',qp') is exactly (6).

• Let's assume there is a thread l such that ( [ hp * h[ ,s,tp') —>i abort. If l — k, we get from lemmas A.1 and A.2 that ( [hp[ ,s, tp) —>i abort, which is absurd by (4). If l — k, the operational semantics entails that ( [hp * h[, s, tp[k 4 c1]) —>i abort which contradicts (i).

• Vl £ dom(tp') such that tp'(l) — skip, we know from (4) that (hp(l),j — 1) £ qp(l)(w). Since tp'(k) — skip, we have that k — l and thus, qp'(l) — qp(l) and hp[k 4 h](l) — hp(l), hence that (hp[k 4 h](l),j — 1) £ qp'(l)(w). Since min(n,j) < j, we obtain (hp[k 4 h](l),min(n, j) — 1) £ qp'(l)(w). Finally, with proposition 4.7, we conclude that (hp[k 4 h](l),min(n,j) — 1) £ qp'(l)(w * w').

• If (w * w',hp[k 4 h],s,tp',qp') 4i (w0,hp0,s0,tp0,qp0), we want to show safemin(j-i, n)(w0,hp0,s0,tp0,qp0). If l — k, we conclude from lemma 5.11. If on the other hand, l — k, from (i), we obtain safemin(nj)(w * w',hp[k 4 h],s,tp[k 4 c1],qp[k 4 [ Q]) (7). There are then two possible situations:

• If c1 — skip, we get ( [hp * h[,s,tp') —>k ( [hp * h[,s,tp[k 4 c2]). From (7) and the safety definition, (h,min(n, j) — 1) £ [ Q]k(w * w'). From (2) with index

j — 1 we get safemin(j — i,min(n,j)-i)(w * w',hp[k 4 h],s, p 4 c2],qp'). Since

min(j — 1,min(n,j) — 1) — min(n — 1,j — 1), we can conclude.

• If c1 — skip and ( [hp * h[,s,tp[k 4 c1]) —>k ( [hp'[,s',tp[k 4 c1']), from (7) we get that for some j' < j and w'', safeji(w'',hp',s',tp[k 4 c1'],qp[k 4 [ Q]k]). Since the thread local operational semantics is deterministic, we want to show that safeji(w'',hp',s',tp[k 4 c1';c2],qp'), which is obtained directly by applying lemma A.3.

• If ( [hp*h[,s, tp') —>i (h0,s0,tp0), the fact that (w*w',hp[k 4 h],s, tp',qp') 4i d' for some d' follows from (4) and the definition of 4i.

A.3 Lock release

The proof rule for releasing a lock is

h {Locked(E, P) * P}release(E) {Ex(E, P)}

Let's assume that ik < j, \=j {Locked(E, P) * P}release(E) {Ex(E, P)} (1) and let's prove that this holds for j + 1.

We choose w,hp, s,tp,qp such that safe j (w,hp,s,tp,qp) (2) holds. We then choose k G dom(hp),w' such that w * w' is well defined. Let then (h,n) G | Locked(E,P) * P[^(w * w') (4),h±hp and cons(w * w',hp[k M h],s,tp[k M release(E)],qp[k M | Ex(E,P) ]k[) (5).

We note qp' = qp[k M | Ex(E,P)[k],tp' = tp[k M release(E)] and choose and want to prove safemin(n j)(w * w',hp[k M h],s, tp',qp').

Proof We unfold the safety definition:

• Consistency of the state is given by (5).

• If there was a reduction to abort in thread l, we could use the same argument than in the sequential composition proof to show that l = k is absurd. If l = k, the operational semantics tells us that h(| E]s) = k. By (4) and the definition of | Locked(E,P)k however, we have that h(| E]s) = k, which is absurd.

• Since the command to execute in thread k is not skip, we can conclude by (2) and proposition 4.7 that all threads done executing satisfy their postconditions in qp'.

• If there is a transition (w * w',hp[k M h],s, tp' ,qp') (wo,hpo,so,tpo, qpo), the case where l = k is handled by lemma 5.11.

If l = k, we know from the operational semantics and the definition of Mk that w0 = w*w',s0 = s, tpo = tp[k M skip],qpo = qp. Furthermore, from (4), we know that (w * w')(|E]s) = Lock(Q) for some Q, and there exists (h1,n') G App(Q)(w * w') (6) such that h = h1 *w^w' h2 (7) and hp0 = hp[k ^ h2[| E]s ^ U], Q ^ hp(Q) *w*w' hi] (7). We want to prove safemin(m, n,j-i)(w * w',hpo,s, tpo,qp').

From (4), 3h3,h4 such that (h3,n) G | Locked(E,P)]k(w * w') (8) and (h4,n) G | P]k (w * w') (9).

From (8) and the interpretation of Locked(E,P), we get that App(Q)(w * w') =n-i | P]k(w * w'). From (6), (h1,min(m, n - 1)) G | P]k(w * w').

From (9), (h4,min(m,n — 1)) G | P]k(w * w'). Since the resource invariants are required to be precise, we can conclude that h4 = h1, and further than h3 = h2.

Since now (h2,min(m,n — 1)) G | Locked(E,P) ^(w * w'), we get that (h2[| E ]a ^U],min(m,n — 1)) G | Ex(E, P ) ]k (w * w') (1o).

We can then unfold the safety definition one last time. All cases are trivial, with one exception: we now have tpo(k) = skip. From (1o), we get that (h2[| E]s U],min(m,n — 1,j — 1)) G | Ex(E,P)]k(w * w') = qp'(k). We also know that there will be no more reduction in thread k, so all reduction cases are handled by lemma 5.11, which concludes our proof.

A. Buisse etal. /Electronic Notes in Theoretical Computer Science 276 (2011) 121—143

A4 Lock creation

The proof rule for creating a lock is

h {E 4 _}make_lock_P(E) {Locked(E, P)}

Let's assume ik < j, \k {E 4 _}make_lock_P(E) {Locked(E,P)} (1) and let's prove this also holds for j + 1.

Let's choose w,hp, s,tp,qp such that .safej(w,hp, s,tp,qp) (2) holds. We pick k G dom(hp) and w' such that w * w' is well defined. Let then (h, n) G | E 4 _ (w * w') (3),h±hp and cons(w * w',hp[k 4 h],s,tp',qp'). We note qp' = qp[k 4 | Locked(E, P) [ ^[ and tp' = tp[k 4 make_lock_P(E)[ and want to prove safemin(nj)-i(w * w',hp[k 4 h],s, tp,qp').

• If there was a reduction to abort, it would have to be in thread k by the same argument than in the sequential composition proof. This would however contradict the operational semantics and (3), which implies there is no execution fault.

• All threads finished executing satisfy their postconditions follows directly from (2) and tp'(k) = skip.

• If (w * w',hp[k 4 h],s, tp',qp') 4k (w0,hp0,s0,tp0,qp0), we have from the definition of the transition relation that w0 = (w * w')[| E] s 4 Lock(Abs([P[ k))] ,hp0 = hp[k 4 h'],so = s,tp0 = tp[k 4 skip] and qp0 = qp', with the notation h' — h[| E[s 4 k[. We now want to show safemin(n-1 j-1 ,m(w0,hp[k 4 h'],s, tp[k 4 skip],qp').

The only interesting case when we unfold the safety definition comes from the fact that tp[k 4 skip](k) = skip, which means we need to prove (hp[k 4 h'](k),min(n — 2,j — 2,m — 1) = (h',min(n — 2,j — 2,m — 1) G qp'(k)(w0) = | Locked(E, P) ^(w0). | E]s is in the domains of both h' and w0, and h'(| E]s) = k. Furthermore, for any w'',p,App(AbsdP^))(w'') =p | P]k(w'').

A.5 Thread creation The proof rule for fork is

r, {R}f [S]h{P * tid(f,x)}c{Q} r, {R]f {S}h{P * R]let x = fork(f) in c{Q]

We assume Vj, r Nj {P * tid(f,x)}c{Q} (1) and {R}f : cf {Q} G r (2) (note that r here is not exactly the same as in the proof rule, as we "factor in" the triple for f ).

We now assume that ik < j, r \=k {P * R}let x = fork(f) in c{Q} (3) and want to show that r \j+1 {P * R}let x = fork(f) in c{Q}.

Let's fix w, hp, s, tp, qp such that safej(w, hp, s, tp, qp) (4) and r \=j {R}f {S} (5). We now choose k G dom(hp),w' such that w * w' is well defined, (h,n) G |P * R]k(w * w') (6),h±hp and cons(w * w',hp[k M h],s,tp',qp') (7). We note qp' = qp[k M | Q]k] and tp' = tp[k M let x = fork(f) in c] and now want to show ■safe j (w * w',hp[k M h],s, tp' ,qp').

Proof As in the other proof cases, most of the safety conditions follow directly from (4). The only difficult condition is the case where a reduction happens in thread k:

(w * w',hp[k M h],s,tp',qp') Mk (wo,hpo,so,tpo,qpo) for some i G dom(tp'), where so = s[x M v],wo = w[v M T(f)],qpo = qp'[i M | S]i],tpo = tp[k M c,i M cf]. Furthermore, there exists h1,h2 such that h = h1 *ww' h2,h2 G | Rp(w * w') and hpo = hp[k M h1[v M i],i M h2].

By (6), 3h3,h4 such that h = h3 *ww' h4, (h3,min(m, n — 1,j — 1)) G | P]k(w * w') (8) and (h4,min(m, j — 1,n — 1)) G | R]k(w * w') (9). Using precision of R, we can deduce that h1 = h3 and h2 = h4.

The key step is now to start from the "base state" (w,hp, s,tp,qp) and first add the thread i, which we can do thanks to (5), (4), (7) and (9), which gives us saf emin(m,j — 1 ,n-1)(wo ,hp[i M h2],so,tp[i M cf],qp[i M I S P]).

In order to be able to use (1) to add thread k and conclude, we first need to apply the locality property of proposition 4.7 to (8), which gives that h1[v M i] G | P * tid(f,x)]k0(wo). From there, using the soundness definition, we obtain the desired saf emin(m,n— 1,j—1)(wo,hpo,so,tpo,qpo).

A.6 Thread synchronisation The proof rule for join is

r {P}f {Q]h {tid(f,E)]join(E){Q}

We assume {P}f {Q} e r (1) and Vk < j, r \=j {tid(f, E)]join(E){Q] (2). Let w,hp, s,tp,qp be fixed such that safej(w,hp,s,tp,qp) (3). We choose k e dom(hp),w' such that w * w' is well defined. Let (h,n) e [ tid(f,E)]k(w * w') (4),h±hp and cons(w * w',hp[k ^ h],s, tp',qp') (5).

We note qp' = qp[k ^ fQ]k],tp' = tp[k ^ join(E)], and want to show safemin(j,n)(w * w',hp[k ^ h],s, tp,qp').

• Hypothesis (3) implies that if a thread reduces to abort, it has to be k. From (4) and (7), we know there is a i such that h([ E]s) = i and i e dom(tp). The

operational semantics allows us to conclude that thread k does not reduce into abort.

• We only show the proof for the first of the two kinds of "join" reductions. If (w * w',hp[k 4 h],s,tp',qp') ——k (w0,hp0,s0,tp0,qp0), we know from the definition of the reduction relation and (7) that there exists some i = h([ E] s) and that

w0 = (w * w') \ dom(w*w')\{\E\s},s0 = s,tp0 = tp[k — skip] I dom(tp')\{i} and qpo = qp'\dcmqp'\{i}. Furthermore, h = hi *ww [[ E]s — i] for some hi and hp0 =

hp[k — hp(k) *w*w' hi] \ domhp'\{i}.

We prove the desired safemin(m,n-i, j-i)(w0,hp0,s, tp0,qp0) directly by unfolding the safety definition. The only interesting case arises from tp0(k) = skip. Since there was a reduction, we know that tp(i) = skip, which from (i) and (3) gives that (hp(i),j - 1) e [ Q!S(w).

From the side condition on the join proof rule, we get (hp(i),j — 1) e [ Q[]£(w) (since the interpretation of Locked is the only one making use of the thread identifier). Using the locality property (proposition 4.7), (hp(i),j — 1) e [ Q¡k(w0).

Finally, we can weaken this into the desired (hp0(k),min(n, j) — 2) e [ Q¡k(w0).