Using The Temporal Logic of Actions: A Tutorial on TLA Verification

Peter B. Ladkin Universität Bielefeld, Germany ladkin@rvs.uni-bielefeld.de

June 1997

\begin{matrix}c \\ ◯ \end{matrix}P. B. Ladkin 1997

1 Contents

The purpose of these notes is to tell a story: to introduce the main ideas in verification using TLA, and some background to those ideas. Concepts are introduced dynamically (or ignored where possible!) by working through some examples. What doesn’t arise in the examples is generally not mentioned. For those wishing more in the way of reference manuals to and explanations of TLA, TLA+, and their use, they abound in [Lama].

On to the story. In this tutorial, I briefly :-)

• talk about change

• introduce a linguistic convention for describing some sorts of change

• note that computer systems essentially involve change

• explain why verification is needed

• explain what a TLA specification is: a state machine

• explain what a TLA verification is: a proof that one state machine simulates another • ..that proceeds by showing a logical implication

• define the logic of TLA; but mostly...

• ....work through an example

The example is taken directly from [Lad96a], in which the entire verification is laid out in hierarchical form. In these notes, the verification is broken up into levels, to illustrate how a proof may be intellectually decomposed. Once one understands the concept of proof level and how it looks, the textual form in [Lad96a] should be easier to survey than the form presented here.

I shall only introduce those concepts which seem to be needed for doing the proof. That means there are fundamental concepts of TLA which are omitted here – I make no pretence to completeness. I also wave my hands about TLA+ syntax, and about some semantic issues, in places where I think ‘getting a feel’ is more helpful than precision (um, did I really say that? Precision is, of course, of utmost importance in specification and verification!). The definitive source for further information is [Lama], which accumulates and points to all work on TLA.

Some may carp at my use of an outmoded syntax for TLA+ (namely, include -ing and import -ing of modules) and some extra keywords. I find that the explanation of import and include that I give here is straightforward to understand, even if these concepts themselves are not optimal; and I find the extra keywords helpful. All is explained in Section 5.1. Besides, this has to do with TLA+, the specification language that is an extension of TLA, and not TLA itself. The syntax of TLA itself has not changed. The ‘latest’ TLA+ syntax is available on-line at [Lama] and I don’t consider it hugely different from what I use here.

2 Change

The ideas of stasis and change have been at the center of philosophical inquiry for millenia. The discussion of being and becoming [Res95] has Heraclitus claiming that being is illusory; there is only becoming, and the Eleatics believing the opposite, along with McTaggart [McT27] and Bradley [Bra97]. Let’s summarise 3,000 years of discussion by saying there’s been arbitrage. McTaggart’s (in)famous argument for the unreality of time (in [PM93]) presages the modern syntax (the ‘A-series’) and semantics (the ‘Bseries’) of temporal logic.

Take a snapshot. The relations between objects that you observe in the snapshot are said to be ‘state predicates’. Take another. You may observe that some state predicates are no longer true: some relations in the new snapshot contradict the relations in the first. We can point out the differences. Let us call them ‘changes’. To describe them, we may conveniently refer to the first and second snapshots. This is the B-series way of conceiving of the passage of time.

We may know as we’re taking the first snapshot some features that will remain the same in our second, and some that will be different. We say: in the future, this will remain, this will change. When we take the second, we notice some more features: in the past, these were different. This is the A-series way, ‘looking’ from a continously-changing present into the future and the past.

Changes are noted by comparing two snapshots, identifying objects across the two snapshots as being the ‘same object’ or a different one. Sometimes, we may wish to remark on a change by trying to find a state predicate on which we believe that change is consequent: thus change of position is assimilated to a state property velocity. Velocity is in turn correlated with force, so that a constant force results in constant velocity (Aristotle). It takes a Galileo to point out that this ain’t true, and a Newton to iterate the process and relate constant force instead to constant change-of-velocity.

All we can see on our snapshot is position, but apparently there are more things in heaven and earth, Horatio, than we can see on our snapshots. Velocity and acceleration are state properties nowadays.

2.1 Some Notation for Change

Suppose we count the oranges in the first snapshot. There are three. Let’s introduce a name for the result of the counting operation: NumberOfOranges.

NumberOfOranges = 3

And then in the second snapshot, we perform the operation and find there are four.

NumberOfOranges = 4

That is the only difference we can see. But the two sentences are contradictory: NumberOfOranges cannot have both the value 3 and the value 4 at the same time, according to our normal logic and logical notation. We have to distinguish the reference of the name NumberOfOranges in the first snapshot from that in the second. But it’s not completely different: we’re counting in some sense the same thing. How can we make sense of this?

We also want to compare them; to say: the NumberOfOranges has changed, or the NumberOfOranges has increased, How may we say: everything else has stayed the same? (But of course, it hasn’t. We just can’t see the positions of the molecules to note the differences. We really mean: everything else we can notice has stayed the same.) So how can we note the difference between our two quantities and yet note that they are values for somehow the same feature?

Let me perform the comparison:

NumberOfOranges = 3 ∧ NumberOfOranges′ = 4

uses the meta-operator prime to talk about the-next-time-this-changes. We can also say that the NumberOfApples has not changed:

NumberOfApples′ = NumberOfApples

Pace McTaggart (who thought such a way of speaking essential to the idea of time, but that it wasn’t possible ultimately to make sense of this way of speaking), this is just syntax and it’s a conservative extension of FOL – I have, if you like, simply introduced a way of making new variables from old.

According to this view, then, remarking ‘change’ involves remarking a difference to the ‘same’ object or ‘same’ relation on two snapshots. We must be able to identify objects and relations across snapshots, but if we can do that, it suffices. Accordingly, let’s restrict ourselves to writing prime only once: NumberOfApples′′ is not well-formed.

2.2 Change Tokens and Types

Now I can classify occurences of change (which we can call change tokens) into types: A pair of snapshots shows a change in the NumberOfOranges if and only if

NumberOfOranges′ ≠ NumberOfOranges

A pair of snapshots shows a doubling in the NumberOfOranges if and only if

NumberOfOranges′ = 2 × NumberOfOranges And any doubling in the NumberOfOranges is also a change in the NumberOfOranges, because any pair of snapshots showing a doubling in the NumberOfOranges also shows a change: substitute

x ← NumberOfOranges′, y ← NumberOfOranges

in

⊢ _{PA} (x = 2 × y ⇒ x ≠ y)

The ‘classical’ thing to do would be thus to identify the type change in the NumberOfOranges with the set of pairs of snapshots (tokens) that exemplify it. Then the implication allows change-type-inclusion to be ‘reduced’ to setinclusion.

3 A Natural Deduction System

So what’s the logic of all this? Let’s formulate it in a natural deduction system. This is convenient, because the hierarchical proof method we shall use [Lam93, Lam95] is a form of (inverted) natural deduction. I’m going to assume the usual notions of logical syntax. In natural deduction, there are no axioms, but there are a lot of rules of inference: traditionally, one introduction and one elimination rule for each logical constant (sentential connectives, quantifiers, modal operators).

3.1 Assumptions in Natural Deduction

How assumptions and assumption sets function is essential for defining any natural-deduction system, because in a natural-deduction system, there need be no axioms. However, these notions will not play much of an explicit role in the example. Defining these notions in the context of the hierarchical proof method (which is an inverted form of standard natural deduction) is much easier than in more traditional natural deduction systems. I include an explanation here for completeness, but in fact it may be skipped without much pain by those wanting to go straight to the TLA verification example.

Instead of using axioms in natural deduction, one makes assumptions, which may be arbitrary formulas. Assumptions are accumulated in assumption sets as one proceeds with a proof, and ‘carried along with’ the proof. That is, every occurrence of every formula in a natural-deduction proof has an assumption set associated with it, and the definition of the assumption set of an occurrence of a formula is given recursively by the proof rule used to derive the formula. The assumption set starts out as ∅. Then:

• Make a new assumption: increment the assumption set with it.

The inference rules indicate how to modify the assumption set on which the conclusion is based, given the assumption set of each premise, by using set operations and by discharging certain assumptions (discharge being represented by square brackets).

• ‘Discharge’ an assumption: decrement the assumption set by it.

A theorem is the conclusion of a chain of inferences, such that all the assumptions made in the course of the proof have been discharged by the time this conclusion is reached: its assumption set is ∅. In rules, we write assumptions A that are discharged by application of the rule in square parentheses, thus: [A].

For example, the rule (⇒-intro):

\frac{{[A] \atop B}}{A ⇒ B}

says that if the set of assumptions on which the premise B is based is Λ, then the set of assumptions on which the conclusion is based is

Δ = Λ\{A}

Another example: (∨-elim)

A ∨ B  [A]  [B]

\frac{C C}{C}

says that if the set of assumptions on which the first premise is based is Λ, set of assumptions of the second premise is Γ and of the third, Θ, then the set of assumptions on which the conclusion is based is

Δ = Λ ∪ (Γ\{A}) ∪ (Θ\{B})

See [Pra65] for a precise definition and explanation of assumptions and discharging.

In the hierarchical proof method, assumption sets are easier to define. One makes assumptions at particular proof steps. The scope of an assumption is the set of all substeps that appear under it. The set of all assumptions of a proof step is the set of assumptions within whose scope that step lies.

3.2 Classical Rules

Let’s start with a Prawitz-type natural deduction system [Pra65] with the classical rules, presented in the traditional manner: proofs ‘move’ downwards towards their desired conclusion.

3.2.1 Propositional Rules

(&-intro) \frac{A B}{A ∧ B}  (&-elim) \frac{A ∧ B}{A} \frac{A ∧ B}{B}
(∨-intro) \frac{A}{A ∨ B} \frac{A}{A ∨ B}
(∨-elim) A ∨ B  [A]  [B]

\frac{C C}{C}

Note that these rules strictly speaking build proof trees, not simple sequences of formulas, since proof branches converge in certain rules. Branches converge as we move ‘downwards’ in the proof towards the desired conclusion. The root of the tree is the desired theorem, and the leaves are the initial assumptions.

Define (¬A) to be the formula (A ⇒ ⊥)

(⇒-intro) \frac{{[A] \atop B}}{A ⇒ B}  (⇒-elim) \frac{A A ⇒ B}{B}

(⊥l)  \frac{⊥}{A}  (⊥c)  \frac{\begin{matrix}[¬A] \\ ⊥ \end{matrix}}{A}

A restriction on both ⊥ rules is that A is different from ⊥. A restriction on ⊥c is that A is not of the form B ⇒ ⊥ (but this is only for convenience:

Exercise:- Show that if A has the form ¬B in ⊥c that there’s another way of deriving the conclusion). Note that ⊥c subsumes ⊥l.

The use of ⊥ is logically important: if ⊥c is used, the theorems are those of classical propositional logic; if ⊥l is used, the theorems are those of intuitionistic propositional logic. The formal difference between the two logics is thus reduced just to that of how negation is treated.

3.2.2 Quantifiers

Now let’s include the quantifiers:

(∀−intro) \frac{A}{∀x : A[a/x]} (∀−elim) \frac{∀x : A}{A[x/t]}

in which a does not occur in the assumption set of premise A, nor within the scope of a quantifier binding x in A; resp. no occurence of x in A occurs within the scope of a quantifier binding a variable of t (i.e. the standard logic stuff usually embodied in the definition of <term> is free for <variable> in <formula>).

The rules for the existential quantifier are

(∃−intro) \frac{A[x/t]}{∃x : A}
(∃−elim)  [A[a/x] ]

\frac{∃x : A B}{B}

with appropriate restrictions here too.

Now the modalities. First I define the notion of essentially modal formula:

• □A is essentially modal;

• ⊥ is essentially modal;

• the essentially modal formulas are closed under ∧, ∨, ∃

The rules are

(□−intro) \frac{A}{□A} (□−elim) \frac{□A}{A}

in which the assumption set of the premise of (□−intro) must include only essentially-modal formulas.

This set of rules defines a natural deduction system whose theorems are those of S4 [Pra65].

3.2.3 Cheating

Actually, we don’t use many of these rules in a TLA proof. When we come across a piece of purely propositional or predicate-logical reasoning, we normally cheat, which is defined in Section 3.5 as consisting of asserting its validity (if we do so wrongly, then we lie instead of cheating, as defined in the same section). The hard part of verifications is normally not the pure logic involved, but rather the mathematics that occurs. So in practice, we cheat on propositional and predicate logic, even in rigorous proofs. This has disadvantages of course, such as occasional lying, but that seems to be reasonably controllable, and tends to disappear with automated help. But now for rules which do appear in rigorous TLA proofs.

3.3 Temporal Rules

What proof rules do we need for the temporal part of the logic? First, Temporal Logic rules sufficient for ‘simple linear-time temporal logic’ (STL), as some computer scientists call it. It’s S4.3 plus a bit. While we could extend the Prawitz rules with axioms for S4.3 (axioms are inference rules without premises), we shall also need to be able to understand the existing TLA literature, including proofs, which is formulated somewhat differently by Lamport. Accordingly, let’s look briefly at the Lamport axioms for STL. They are introduced by Lamport [Lam94b] as follows (with STL1 cleaned up a bit):

STL1: \frac{F}{□F}

in which assumptions(F ) = ∅

STL2: ⊢ □F ⇒ F
STL3: ⊢ □□F ≡ □F
STL4: \frac{F ⇒ G}{□F ⇒ □G}
STL5: ⊢ □(F ∧ G) ≡ (□F ) ∧ (□G)
STL6: ⊢ (◇□F ) ∧ (◇□G) ≡ ◇□(F ∧ G)
STL7: ⊢ □◇□F ≡ ◇□F

Lamport does not use assumptions: the assumption set of each premise is ∅. Thus it should be clear that many of these rules are derived rules of the Prawitz system. Some hints:

• STL1: follows immediately from □−intro

• STL2: assume □F , apply □−elim then ⇒−intro

• STL3: assume □F , apply □−intro then ⇒ −intro; assume □□F ; apply □−elim then ⇒−intro; finally apply ∧−intro and definition of ≡

• STL4: assume □F , apply □−elim, ⇒−elim and □−intro to get □G, finally ⇒−intro to get □F ⇒ □G

• STL5.1: assume □(F ∧ G), apply □−elim then ∧−elim twice then □−intro twice, followed by ∧−intro to get (□F ∧ □G)

• STL5.2: assume □F ∧ □G), apply ∧−elim twice then □−elim twice then ∧−intro, followed by □−intro to get □(F ∧ G)

Bryan Olivier has pointed out that STL7 is derivable from classical propositional logic plus STL1-STL6 [Lam96], from which it follows that we only need to add STL6 to the Prawitz rules to obtain STL. Nevertheless, STL7 is useful when we wish to quote it as a derived rule in a proof.

Second, we add the Basic TLA Rules which specifically axiomatise ‘prime’:

TLA1. \frac{P ∧ (f′ = f ) ⇒ P′}{□P ≡ P ∧ □[P ⇒ P′]_{f}} TLA2. \frac{P ∧ [A]_{f} ⇒ Q ∧ [B]_{g}}{□P ∧ □[A]_{f} ⇒ □Q ∧ □[B]_{g}}

where the notation

[A]_{f} =^{Δ} A ∨ (f′ = f )

These rules were formulated and used by Lamport specifically for TLA, hence the name [Lam94b].

3.4 Behaviors and the Rules They Engender

At this point, we need to make a brief nod towards the semantics. We use more-or-less the standard ‘simple temporal logic’ semantics used in computer science [MP92, Gol92]. We have ‘snapshots’, i.e. classical model-theoretic worlds, and models of the modal logic will be linear chains of these snapshots, according to classical Kripke semantics with the S4.3 correspondence. We actually want our models, called ‘behaviors’ in computer-science parlance, to be ω−sequences of snapshots. That is, chains ordered as the natural numbers are ordered:

[figure: S1; S2; S3]

Hence we need to add an induction rule:

INV1: \frac{I ∧ [N]_{f} ⇒ I′}{I ∧ □[N]_{f} ⇒ □I}

in which N is an action formula, thus defining a relation between a given state and the next. We suppose also that f is the state function which is a sequence whose elements are the variables of N (TLA includes ZF, so sequences can be formed). The rule says intuitively: if it is provable that I is preserved under state-relation N, then it follows if N always holds and I holds, I will always hold. We also add the technical rule:

INV2: ⊢ □I ⇒ (□[N]_{f} ≡ □[N ∧ I ∧ I′]_{f} )

These two rules are known as the ‘Invariance Rules’. We hope that it is clear that these rules are sound. They suffice for many purposes, for example to prove safety properties of systems.

The Ontology. What’s a system? Let’s say: a state machine. What’s a state machine? A state machine can be identified with a set of behaviors. To some, that doesn’t yet clearly say what it is, but I won’t define it any other way. (It’s an extensional definition, which precludes there being a ‘requirements problem’: how to ensure that no unwanted system behavior is allowed by the system requirements specification. TLA’s extensional conception simply defines this problem away: the system is the set of behaviors.) To define a state machine, then, one must specify a set of behaviors, which we shall do in TLA specifications.

We have change, and this change is remarked through a syntax which allows names for quantities which may have a different value in each state. We assume that there is a collection of variables which take values in each state of a behavior. So these variables are interpreted by functions whose domain is the set of states in a behavior - so-called state functions. Such variables are called flexible variables, and are to be distinguished from normal mathematical variables, rigid variables, which take one value and one only per ‘model’ - that is, per behavior. Rigid variables take thus a constant value over a whole behavior, and so as far as the behavior is concerned, they act as a constant. What sorts of values can any variable take? Sets. TLA is based on Zermelo-Fraenkel set theory (ZF), the ‘standard’ set theory of mathematics. Thus all data structures are sets. This is no restriction - all of math, including the math of data structures, can be formulated in set theory, more-or-less.

Back to Syntax Briefly. Because there are two sorts of variables, there must be two sorts of quantifiers: ∃ for rigid variables and ∃ ∃ ∃ ∃ ∃ ∃ for flexible variables. Both quantifiers satisfy similar intro and elim rules.

The Definition of Truth is ‘standard’ (for applications of temporal logic in computer science): all formulas are interpreted in the first state of a behavior. This distinguishes somewhat this semantics from a raw Kripke semantics for a temporal logic. So any purely first-order formulas, not involving temporal operators or prime, are true in a behavior if and only if they are true in the first state of the behavior. Temporal formulas □A are true if and only if they are true in the first state; which is the case if and only if A is true in all states. The semantics of ◇ follows from the syntactic equivalence ◇A =^{Δ} ¬□¬A: ◇A is true if and only if it is true in the first state; which is the case if and only if A is true in some state (including possibly the first). Formulas including prime are evaluated on pairs of states: the values of the unprimed variables are taken from the first state, and the values of the primed variables are taken from the second state.

This explanation is of course crude, but I’m hoping it will suffice for our purposes. Those who wish for a precise account of the syntax and semantics of TLA may find one in [Lam94b].

3.5 Hierarchical Proofs

Before we consider proofs in detail, let’s revise the idea of how natural deduction proofs proceed from bottom-up to top-down [Lam93, Lam95]. Consider an (informal) procedure with polyadic output:

proof-procedure(T ): To prove T , enumerate a number of assertions, say A_{1}, …, A_{k} from which T 1. A ∧ (P ∧ Q ∧ R)

Proof:

1.1. A

Proof: I guess A just is true. 1.2. P ∧ Q ∧ R

Proof: 1.2.1. P ∧ Q Proof: 1.2.1.1. P

Proof: I guess P just is true. 1.2.1.2. Q

Proof: I guess Q just is true. 1.2.1.3. Q.E.D.

Proof: ∧−intro from 1.2.1.1 and 1.2.1.2. 1.2.2. R

Proof: I guess R just is true. 1.2.3. Q.E.D.

Proof: ∧−intro from 1.2.1 and 1.2.2. 1.3. Q.E.D.

Proof: ∧−intro from 1.1 and 1.2.

Figure 1: A simple hierarchical proof

logically follows by means of the rules. Then perform proof-procedure(A_{1}); … proof-procedure(A_{k})

To prove theorem A , invoke proof-procedure(A)

This describes a recursive procedure for writing natural-deduction style proofs, and its advantages have been explained in [Lam93, Lam95].

proof-procedure(T ) has output: where do we write the output? Say, in infix order: right where we invoke proof-procedure(T ). For good measure, we enumerate the nodes according to a path-numbering scheme: suppose T has number m. Then A_{1} is assigned number m.1, …, A_{k} is assigned number m.k. Consider a trivial example in Figure 1. Instead of using the full path numbering, it is often convenient to use just an indicator of the level of embedding, as in Figure 2. We can rewrite the natural-deduction proof-rules in this style as in Figure 3.

This may at first look tedious, but is in fact a very helpful way to keep ⟨1⟩1. A ∧ (P ∧ Q ∧ R)

Proof: ⟨2⟩1. A

Proof: I guess A just is true. ⟨2⟩2. P ∧ Q ∧ R

Proof: ⟨3⟩1. P ∧ Q

Proof:

⟨4⟩1. P

Proof: I guess P just is true. ⟨4⟩2. Q

Proof: I guess Q just is true. ⟨4⟩3. Q.E.D.

Proof: ∧−intro from ⟨4⟩1 and ⟨4⟩2. ⟨3⟩2. R

Proof: I guess R just is true. ⟨3⟩3. Q.E.D.

Proof: ∧−intro from ⟨3⟩1 and ⟨3⟩2. ⟨2⟩3. Q.E.D.

Proof: ∧−intro from ⟨2⟩1 and ⟨2⟩2.

Figure 2: A simple hierarchical proof with relative labelling (&-intro):

⟨111⟩1. A ∧B Proof: ⟨112⟩1. A ⟨112⟩2. B ⟨112⟩3. Q.E.D.

Proof:Follows from ⟨112⟩1 and ⟨112⟩2 by ∧−intro.

(&-elim):

⟨111⟩1. A Proof: ⟨112⟩1. A ∧B ⟨112⟩2. Q.E.D.

Proof:Follows from ⟨112⟩1 by ∧−elim.

(∨−elim):

⟨111⟩1. C Proof: ⟨112⟩1. A ∨ B

ASSUME A

⟨112⟩2. C

ASSUME B

⟨112⟩3. C ⟨112⟩4. Q.E.D.

Proof:Follows from ⟨112⟩1, ⟨112⟩2 and ⟨112⟩3 by ∧−elim.

The scope of the ASSUME is the immediately following step. The assumption set of a statement is then the set of all ASSUMEd formulas in whose scope it is.

Figure 3: Proof rules in hierarchical style, illustrating assumptions practical proofs going: one cannot lie, but one can cheat and steal:

lie =^{Δ} state a non-logically-true assertion as a proof step
cheat =^{Δ} accept the current statement as true

without further proof

steal =^{Δ} use an automated decision procedure to prove

the current statement

The notation =^{Δ} means ‘is defined to be’.

Hierarchical proofs may be terminated at a cheat step or a steal step. This is not really doable with the bottom-up approach.

4 TLA, State Machines, and Why Verify?

The Temporal Logic of Actions (TLA) was developed in the late 1980’s - early 1990’s by Leslie Lamport at DEC SRC in Palo Alto, CA to aid in the verification of concurrent computer systems and algorithms. It was also intended to facilitate hand proofs. TLA is a ‘standard’ linear-time temporal logic based on first-order Zermelo-Fraenkel set theory (ZF) with two sorts of variables (and hence two sorts of quantifiers) and a special operator “prime”.

4.1 State Machines

A TLA specification defines a ‘state machine’. A TLA verification consists in taking two TLA specifications, one that describes a more detailed machine (the ‘implementation’) and one that describes a simpler machine (the ‘specification’) and a rigorous proof that the implementation-machine simulates the specification-machine. This proof is accomplished in TLA by showing, modulo some functional correspondence between the variables, that a canonical specification of the implementation-machine logically implies a canonical specification of the specification-machine: more accurately, since the Deduction Theorem doesn’t hold in temporal logic, that

⊢ _{TLA}Impl.spec ⇒ f (Spec.spec)

where f is a syntactic substitution function that replaces the free variables in Spec.spec with terms in the free variables of Impl.spec (state functions). The function f is called a Refinement Mapping [AL91]. I shall overload the word ‘specification’, which I shall use for both TLA specification and for specification-machine.

So where do all these state machines come from? Consider a Central Processing Unit, a CPU. A CPU is a piece of hardware that executes an instruction. An instruction operates on data. Data may reside locally in registers, or in a cache elsewhere in the chip, or further away in memory, or even further away on disk. We have here certain data structures, and certain actions specified by instructions.

Actions: It adds, multiplies (unless it’s a Pentium :-) puts a value in memory, gets a value from memory.

Now consider yourself, the user. You save files, send an email message to a colleague

on another machine

compile a program

while reading your mail,

call the Web pages from a machine in California (say, to learn about TLA)

There are again two parts to these descriptions: an action and a data structure. Some of the actions are general operations on data structures common to all systems. Some are actions and structures specific to what you’re doing at the moment. Whether a given function is ‘system’ or ‘application’ really depends on convention, that is, how the structure of the system was conceived by its designers - although there is a lot of agreement. Think of a window system, for example. Is it a ‘system’ function (Windows NT) or ‘application software’ (Unix, MS-DOS)? (Logically, who cares?)

How is all this done? The fundamental concept is that of a machine. There are virtual machines, such as in the system and applications. There are real machines, such as the processor with limited capabilities. An operating system such as Unix gives interprocess and intermachine communication capabilities. That’s a more sophisticated virtual machine. The window system, which organises the user interface, lies ‘above’ that. One can view the various virtual machines as arranged in a hierarchy of levels of increasing functional sophistication, in which a virtual machine at Level n provides functions which are used as ‘primitives’ by Level (n + 1), and which itself uses the functions provided by Level (n − 1) as primitive.

Interestingly, such ideas have been used in the engineering domain of Man-Machine Interaction [VR92]. It turns out that it is cognitively easier for an operator to understand how a system works if it is presented to himher as an abstraction hierarchy, a hierarchical description such as we have just considered.

We should not be surprised that hierarchies of machines are fruitful for design, verification and construction of systems. TLA verification is based on the possibility of writing a system in a hierarchical description, as are other verification systems such as SRI’s EHDM [RvHO91] and PVS [Rus].

At the bottom of the hierarchy, the processor ‘knows’ how to:

get, add, multiply, store

bytes from memory and registers.

Data structures: bytes.

Near the top of the hierarchy, the networked machine ‘knows’: verify password, store files

(on another machine),

call procedure (from another machine), start applications (editor, mailserver), save state of application, swap one running program for another, kill application.

Data structures are: files, memory locations, byte streams, messages, control blocks, Ethernet addresses, address tables, ports, Internet addresses, passwords, login names,.......

Not only verification procedures, but many structures in computer engineering are based on this idea of a hierarchy of virtual machines. The ISO Open Systems Inteconnection standard for inter-machine communication is based on seven layers of protocols. The TCP/IP packet-switching protocols on which the Internet is based have fewer (four, at last count :-). The PSOS (Provably Secure Operating System) [Neu] and the SIFT OS for digital flight control [SRIa] (both SRI, 1970’s-80’s) were based on hierarchical design so that they could be verified (proved correct) by SRI’s EHDM [RvHO91] (Extended Hierarchical Development Method) verification system. Hierarchical decomposition is still the most fruitful way of proving algorithms and designs correct, and systems such as PVS (from SRI) [Rus] and TLA depend on it.

The lower-level Virtual Machine implements the higher-level Virtual Machine, by simulation:

The LLVM simulates the HLVM

by

• defining higher-level data structures ‘in terms of’ lower-level data structures: words in terms of bytes, arrays in terms of words sets in terms of arrays

• defining higher-level actions ‘in terms of’ lower-level actions: send-message in terms of a C program

A TLA program specification

• describes the possible actions in Formal Logic

• describes properties of the machine:

– Initial (Starting) Condition

– Safety (the only possible actions are those described)

– Liveness (if certain actions can happen, eventually they will happen)

Program specification and verification in TLA proceeds by

• specifying both the HLVM and LLVM,

• then proving using the TLA rules that

LLVM.Spec ⇒ f (HLVM.Spec)

in which f is a syntactic transformation, replacing the variables of HLVM.Spec by terms in the free variables of LLVM.Spec (the refinement mapping). The TLA proof system is supported by

• a specification-writing L^{A}TEX style file

• a proof-writing L^{A}TEX style file

• ...but not yet so far by automated provers. A first attempt was [Eng95]. Stefan Merz in Munich also has an implementation in Isabelle [Mer97], and Sara Kalvala at Warwick is currently implementing TLA in Isabelle [Kal]. So at the moment, stealing (Section 3.5) is harder than it will be....

4.1.1 A Typical Problem with Concurrent Programs

Interacting state machines have their own sets of problems which arise through the interactions. For some reason not very well understood, it seems to be cognitively hard to write concurrent programs and to understand how such programs really operate. Even mathematicians and computer scientists have been caught out by writing ‘proofs’ of correctness of concurrent algorithms which weren’t. Rigor is not only desirable, but seems to be required.

Here’s an example of a concurrency problem. Suppose Program 1 and Program 2 read a common variable turn, which can also be written by Program 3. We say that Program 1, Program 2 and Program 3 share the variable turn. For example, Program 3 may be the operating system, and turn is a variable controlling access to the printer. Just one process at a time may send something to the printer, and the processes can tell whose turn it is by reading the variable turn. Suppose turn has 3 bits; that the initial value of turn is 000; that value 001 corresponds to Process 1 ; and that value 101 corresponds to Process 2. All three processes access turn from right to left.

Suppose now that the processes access turn concurrently, reading its bits one at a time, in the following order:

P3 writes bit3 P2 reads bit3 P1 reads bit3 P3 writes bit2 P2 reads bit2 P1 reads bit2 P1 reads bit P3 writes bit1 P2 reads bit1

This looks as in Figure 4. P1 reads 001, P2 reads 101, and as a result, both ‘think’ they have exclusive access to the printer. The result is that both processes send their files to the printer at the same time. An artistic outcome, maybe, but not what is wanted. Imagine if this sort of thing were to happen in an airplane flight control system!

4.1.2 Testing Won’t Do It

Maybe occurrences of such problems may be found by thorough testing of such systems? Such high-integrity systems as those found in commercial

[figure: 0; 0; 0; P3; 0; 0; 1; P2, P3; 0; 0; 1; P1, P2, P3; 0; 0; 1; P3; P1, P2; 0; 0; 1; P2, P3; P1; 0; 0; 1; P1, P2, P3; 0; 0; 1; P1; P2, P3; 0; 0; 1; P1, P3; P2; 1; 0; 1; P1, P2, P3; 1; 0; 1; P1 has read 001; P2 has read 101]

Figure 4: A Concurrent Readers-Writer Mess aircraft flight control have to be certified to a reliability of 1 in 10^{−9}. That is, one expected failure every 10^{9} cycles (trips) [Req92]. There are roughly 500-1,000 aircraft of each common type, except the B737 of which there are about 2,000 as of this writing. Suppose that each makes 10 trips a day (longer-range aircraft that fly intercontinental, such as jumbos, usually make 2 or fewer). That’s one failure per 50,000 days for a B737 - one expected failure every 75 years in the whole fleet! That’s way longer than the expected life of the airplane type. You can’t test for this level of reliability. See [BF93] for frequentist arguments to this conclusion, [LS93] for Bayesian arguments.

By the way, it’s worth noting in this context that ‘bugs’ in computer software or hardware count as design errors in traditional engineering disciplines such as aerospace engineering. The term ‘failure’ means something breaks :-) It is thus often said that software itself doesn’t ‘fail’ - but that failures may be due to design errors such as occur in software. The use of this vocabulary is a little confusing and it’s wise to approach it with care, from either side.

The Bayesian argument is: there is no way to obtain a lower posterior probability (expectation of failure) than the prior probability (expectation of failure) through testing, at this very high level of required reliability. The only way known to obtain a very high prior is through formal verification - and then this high prior is 1! It follows that one must employ formal verification to ensure a high prior level of reliability. Engineers do not always accept this conclusion but I know of no way around the argument.

5 An Example Specification

Now to the example. I describe and then specify an abstract buffer with two operations:

                    push(something) and pop

Figure 5 shows the state of the buffer before each operation and the state after it. Figure 6 shows a concrete ‘implementation’ of the abstract buffer. A fixed-length data structure, an array, has been chose. Because some of the array elements will not necessarily contain data, we chose a special nullelement, ⊥, to indicate ‘nothing here’. The intended correspondence, ‘implementation’ to ‘specification’, is shown in Figure 7 for the data structures, and Figure 8 for the operations.

[figure: buffer; buffer′; < b,x,y,d >; pop; < x,y,d >; < b,x,y,d >; push(a); < b,x,y,d,a >]

Figure 5: An Abstract Buffer with Operations

How does this fit together? The concrete buffer simulates the abstract buffer, and we shall prove that. Simulation means that

• they start in ‘equivalent’ states

• every action of the concrete buffer corresponds either to an action or to a non-action of the abstract buffer

• when the concrete buffer is sufficiently ‘live’, then the abstract buffer actually does some desired action

This method of state machine simulation is common to many verification methods, for example

• TLA of Lamport

• the Input/Output machines of Tuttle, Lynch, Vaandrager (e.g. [Vaa])

• the method of Lam and Shankar (e.g. [LS84]) which is also TL-based

An alternative is to have actions only—then the operation of the system is an abstract machine simulation, but not a state machine simulation, since one doesn’t have state. This is the set-up in process algebra. But one ends up with state anyway - most process algebras have a way of defining state.

How does one specify the actions? We write one from the abstract buffer module in Figure 9. It uses the ‘bulleted list’ notation for conjunctions and

[figure: Buffer; Buffer′; ⊥; b; x; y; ⊥; d; ⊥; push(a); ⊥; b; x; y; ⊥; d; a; ⊥; b; x; y; ⊥; d; a; move(6); ⊥; b; x; y; d; ⊥; a; ⊥; b; x; y; d; ⊥; a; move(2); b; ⊥; x; y; d; ⊥; a; b; ⊥; x; y; d; ⊥; a; pop; ⊥; ⊥; x; y; d; ⊥; a]

⊥ is the symbol for ‘nothing here’ Figure 6: A Concrete Buffer, an Array, with Operations

[figure: ConcreteBuffer; AbstractBuffer; ⊥; b; x; y; ⊥; d; ⊥; < b,x,y,d >; ⊥; b; x; y; ⊥; d; a; < b,x,y,d,a >; ⊥; b; x; y; d; ⊥; a; < b,x,y,d,a >; b; ⊥; x; y; d; ⊥; a; < b,x,y,d,a >]

Figure 7: Correspondence of the Data Structures

[figure: Buffer; Buffer′; ⊥; b; x; y; ⊥; d; ⊥; push(a); ⊥; b; x; y; ⊥; d; a; < b,x,y,d >; push(a); < b,x,y,d,a >; ⊥; b; x; y; ⊥; d; a; move(6); ⊥; b; x; y; d; ⊥; a; < b,x,y,d,a >; no-op; < b,x,y,d,a >; ⊥; b; x; y; d; ⊥; a; move(2); b; ⊥; x; y; d; ⊥; a; < b,x,y,d,a >; no-op; < b,x,y,d,a >; b; ⊥; x; y; d; ⊥; a; pop; ⊥; ⊥; x; y; d; ⊥; a; < b,x,y,d,a >; pop; < x,y,d,a >]

Figure 8: Correspondence of the Operations

module AbstractBuffer

…

actions push(a) =^{Δ} ∧ a ∈ Data ∧ Len(buffer) < N ∧ buffer′ = buffer ∘ ⟨a ⟩

…

Recall the notation =^{Δ} means ‘is defined to be’.

Figure 9: An Action from the Abstract Buffer Module

disjunctions advocated in [Lam94a]. This notation enhances readability, as can be seen by comparing the readability of the definition in Figure 9 with that in Figure 10. If that doesn’t persuade you, try Figure 11.

5.1 Some TLA+ Syntax

Now we shall write the two specifications. There are some notions of TLA+ syntax which need to be clarified.

TLA+ is based on modules, which handle the scoping of names (variables, operators, definitions). Flexible and rigid variables must be declared before any use: thus we normally write the declarations first, under the keyword parameters. (Flexible variables have type variable and rigid variables type constant. ‘Data types’ aren’t declared since everything’s a set.)

There are two syntactic operators, import and include , which bring parts of other modules into the current module. Figure 12 shows a module Example with an import -ed module foo and an include -ed module bar. The imported module declarations and definitions are as if simply copied into the current module. The imported module definitions only are copied in; furthermore the definition names (i.e., the word to the left of a ‘ =^{Δ} ’) are prefixed with ‘BB.’, (as stated by the as clause), and the variables (flexible and rigid) are substituted for as specified in the with clause. The result is as if the module ExampleII in Figure 13 had been written instead of module

module AbstractBuffer Example I

…

actions push(a) =^{Δ} ∧ a ∈ Data ∧ Len(buffer) < N ∧ buffer′ = buffer ∘ ⟨a ⟩

badly-written actions

push(a) =^{Δ}

a ∈ Data ∧ Len(buffer) < N ∧ buffer′ = buffer ∘ ⟨a ⟩

Figure 10: An Action, Well- and Badly-Written

module AbstractBuffer Example II

…

actions push(a|b) =^{Δ} ∧ a ∈ Data ∧ Len(buffer) < N ∧ ∨ buffer′ = buffer ∘ ⟨a ⟩ ∨ buffer′ = buffer ∘ ⟨b ⟩

badly-written actions

push(a|b) =^{Δ}

a ∈ Data ∧ Len(buffer) < N ∧

(buffer′ = buffer ∘ ⟨a ⟩ ∨ buffer′ = buffer ∘ ⟨b ⟩)

Figure 11: An Action, Well-Written and Really-Badly-Written Example – with one caveat.

The caveat is that that neither import nor include are transitive. Importing or including Example is thus not the same thing as importing or including ExampleII ! If one import -s Example in a further module, only what is explicitly written as parameters and definitions is copied, not items appearing in Example under import or include . Similarly if Example is include -ed: only definitions are included, with the specified definition-name alteration and the substitution of variables. So if you want the other things too, you had better explicitly import or include the modules that Example imports and includes.

I introduce some keywords that actually aren’t in the TLA+ definition (besides import and include ). TLA+ makes no formal distinction between definitions of predicates, actions, and temporal formulas defining the state machine specification. They are all just definitions. I find it helpful to distinguish certain kinds of definitions, such as the definitions of state predicates, the definitions of actions, and the definition of the temporal formula that is the specification of the machine; the formula whose behaviors are supposed to be exactly the possible behaviors of the machine. These keywords in the modules I write are thus to be regarded as aliases for the TLA+ keyword definitions. See [Lama] for a syntax definition of TLA+.

5.2 Building the Specification Modules

In order to discuss the construction of the modules, I write pieces of module in the LaTeX module-definition language. This creates pseudo-modules, in that they are not complete or syntactically correct TLA+ modules. I indicate such pseudo-modules by using

• funny names

• elision signs, namely “ …”.

This should cause little confusion – I think the reader will easily be able to distinguish pseudo-modules from genuine modules.

There is precisely one flexible variable in each buffer-machine. The abstract one is called buffer and the concrete one Buffer. These variables must be declared in the TLA+ modules.

First, we define the starting states of the variables, as in Figure 14. The push actions are compared in Figure 15, the pop actions in Figure 16, and

module Example

parameters x : variable a : constant

imports foo

includes bar as BB with z ← x

module foo

parameters y : variable b : constant

definitions foodef =^{Δ} (y′ ≠ y)

module Example

parameters z : variable c : constant

definitions bardef =^{Δ} (z′ = z)

Figure 12: Example Modules

module ExampleII

parameters x : variable a : constant y : variable b : constant

definitions foodef =^{Δ} (y′ ≠ y) BB.bardef =^{Δ} (x′ = x)

Figure 13: The Equivalent Example Module

the move action, which is intended to correspond to a no-op in the abstract buffer, in Figure 17.

We have defined the initial conditions and the actions. But we don’t yet have a specification. A specification defines the initial conditions and the actions, and also the sentence that says

• the system starts in the initial condition

• if variables change values, it must be because of a defined action (safety)

• it’s always true that some desired action eventually happens if it can (liveness)

The entire Abstract Buffer Specification is shown in Figure 18, and the entire Concrete Buffer specification in Figure 19. The temporal logic formula that defines the state machine is called Spec in both modules, captioned by the keyword temporal. To understand the Specs, it is helpful to recall the notation:

[A]_{x} means A ∨ (x′ = x)
[A]_{⟨x,y ⟩} means A ∨ (x′ = x ∧ y′ = y)

Intuitively, [A]_{x} means Either A, or x doesn’t change value.

Now we need to state the theorem that is to be proved in the verification: namely, that the implementation machine simulates the specification

module Bits of AbstractBuffer

…

predicates

Init =^{Δ} buffer = ⟨ ⟩ …

module Bits of ConcreteBuffer

…

predicates

Init =^{Δ} ∀ n ∈ 1..N : Buffer[n] = ⊥ …

Figure 14: The Start State Definitions, written in Pseudo-Modules

machine. The two sentences defining these machines are compared in the pseudo-module in Figure 20.

To verify the buffer implementation, we may think we want to show that

Conc-Buffer-Spec ⇒ Abs-Buffer-Spec

But this formula is not provable! Conc-Buffer-Spec has a variable Buffer which doesn’t occur in Abs-Buffer-Spec, and Abs-Buffer-Spec has a variable buffer which doesn’t occur in Conc-Buffer-Spec. We have forgotten the refinement mapping: the state function of the concrete Buffer that tells us how the abstract buffer is obtained from the concrete one. The refinement mapping shows a uniform way to convert any value of the concrete Buffer into a value of the abstract buffer. In order properly to state the theorem which we need to prove to prove the machine simulation, we need to state it itself in a TLA+ module, as we do in Figure 21.

The module in Figure 21 includes include statements. Recalling what include means, the definitions (predicates, actions, and temporals) from ConcreteBuffer are valid definitions in this buffer, using the same names but with a prefix given by the as clause. The variables used in the include-ed

module Bits of Buffer Actions

…

abstract actions push(a) =^{Δ} ∧ a ∈ Data ∧ Len(buffer) < N ∧ buffer′ = buffer ∘ ⟨a ⟩

concrete actions push(a) =^{Δ} ∧ a ∈ Data ∧ Buffer[N ] = ⊥ ∧ Buffer′[N ] = a ∧ ∀ i ∈ 1..(N − 1) : unchanged Buffer[i]

…

Figure 15: The Push Definitions Compared

module Bits of Buffer Actions

…

abstract actions pop =^{Δ} ∧ Len(buffer) > 0 ∧ buffer′ = tail(buffer)

concrete actions pop =^{Δ} ∧ Buffer[1] ≠ ⊥ ∧ Buffer′[1] = ⊥ ∧ ∀ i ∈ 2..N : unchanged Buffer[i]

…

Figure 16: The Pop Definitions Compared

module Bits of Buffer Actions

… abstract actions

no − op =^{Δ} ???

concrete actions move(k) =^{Δ} ∧ k ∈ 2..N ∧ Buffer[k] ≠ ⊥ ∧ Buffer[k − 1] = ⊥ ∧ Buffer′[k] = ⊥ ∧ Buffer′[k − 1] = Buffer[k] ∧ ∀ i ∈ 1..(k − 2) : unchanged Buffer[i] ∧ ∀ i ∈ (k + 1)..N : unchanged Buffer[i]

…

Figure 17: The Move Definitions Compared

module AbstractBuffer

imports Sequences, Naturals

parameters buffer : variable Data, N : constant

predicates Init =^{Δ} buffer = ⟨ ⟩

actions push(a) =^{Δ} ∧ a ∈ Data ∧ Len(buffer) < N ∧ buffer′ = buffer ∘ ⟨a ⟩

pop =^{Δ} ∧ Len(buffer) > 0 ∧ buffer′ = tail(buffer)

temporal Spec =^{Δ} ∧ Init ∧ □[pop ∨ ∃ b : push(b)]_{buffer} ∧ WF_{buffer}(pop)

Figure 18: The Abstract Buffer Specification

module ConcreteBuffer

imports Naturals

parameters Buffer : variable Data, N : constant

assertions ⊥ ∉ Data

predicates Init =^{Δ} ∧ ∀ n ∈ 1..N : Buffer[n] = ⊥

actions push(a) =^{Δ} ∧ a ∈ Data ∧ Buffer[N ] = ⊥ ∧ Buffer′[N ] = a ∧ ∀ i ∈ 1..(N − 1) : unchanged Buffer[i]

pop =^{Δ} ∧ Buffer[1] ≠ ⊥ ∧ Buffer′[1] = ⊥ ∧ ∀ i ∈ 2..N : unchanged Buffer[i]

move(k) =^{Δ} ∧ k ∈ 2..N ∧ Buffer[k] ≠ ⊥ ∧ Buffer[k − 1] = ⊥ ∧ Buffer′[k] = ⊥ ∧ Buffer′[k − 1] = Buffer[k] ∧ ∀ i ∈ 1..(k − 2) : unchanged Buffer[i] ∧ ∀ i ∈ (k + 1)..N : unchanged Buffer[i]

temporal Spec =^{Δ} ∧ Init ∧ □[\begin{matrix}∨ pop \\ ∨ ∃ b : push(b) \\ ∨ ∃ k : move(k)]_{Buffer} \end{matrix} ∧ WF_{Buffer}(pop) ∧ WF_{Buffer}(∃ k : move(k))

Figure 19: The Concrete Buffer Specification 36

module Specification Comparison

… temporal

Conc − Buffer − Spec =^{Δ} ∧ Init ∧ □[\begin{matrix}∨ pop \\ ∨ ∃ b : push(b) \\ ∨ ∃ k : move(k)]_{Buffer} \end{matrix}

∧ WF_{Buffer}(pop) ∧ WF_{Buffer}(∃ k : move(k))

Abs − Buffer − Spec =^{Δ} ∧ Init ∧ □[\begin{matrix}∨ pop \\ ∨ ∃ b : push(b)]_{buffer} \end{matrix}

∧ WF_{buffer}(pop)

Figure 20: Comparison of the Two Spec Statements

module Theorems

parameters Buffer, buffer : variable Data, N : constant

includes ConcreteBuffer asCB(N ) AbstractBuffer asAB(N )

theorems CB(N ).Spec ⇒ AB(N ).Spec[buffer/f (Buffer)]

f (Buffer) is the Refinement Mapping that we must define.

Figure 21: The Verification Theorem

definitions must be explicitly defined (or import-ed) in the current (includeing) module, before they are used, that is, before the include statements. Hence the flexible variables Buffer and buffer must be declared, as must the rigid variables Data and N. The action definition names become, e.g. CB.move and AB.pop, and of course the specification formulae AB.Spec and CB.Spec.

By convention, we let

\overline{buffer} =^{Δ} f (Buffer)

(this is the Refinement Mapping) and by extension \overline{AB(N ).Spec} be AB(N ).Spec, with every occurrence of buffer replaced by \overline{buffer}.

6 The Proof

We must prove ⟨1⟩1. CB(N ).Spec ⇒ \overline{AB(N ).Spec}

6.1 Partitioning the Proof

We are proceeding top-down, using the hierarchical method and the L^{A}TEX support style files tla.sty and pf.sty, which includes the proof-step numbering scheme. Each specification is a conjunction of three clauses

Init ∧ □[action ∨ action ∨ ...]_{variables} ∧ Liveness

so it seems most reasonable to prove each conjunct separately:

⟨2⟩1. CB(N ).Spec ⇒ \overline{AB(N ).Init} ⟨2⟩2. CB(N ).Spec ⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix}

⟨2⟩3. CB(N ).Spec ⇒ \overline{WF_{buffer}(AB(N ).pop)} ⟨2⟩4. Q.E.D.

Proof: Follows by propositional logic from the conjunction of steps ⟨2⟩1, ⟨2⟩2 and ⟨2⟩3.

The first step of this, along with its substep, is

⟨2⟩1. CB(N ).Spec ⇒ \overline{AB(N ).Init} ⟨3⟩1. CB(N ).Init ⇒ \overline{AB(N ).Init} ⟨3⟩2. Q.E.D.

Proof: Follows directly by propositional logic.

The second step, along with its substep, is

⟨2⟩2. CB(N ).Spec ⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix}

Proof:

⟨3⟩1. ∧ CB(N ).Init

∧ □[\begin{matrix}∨ CB(N ).pop \\ ∨ ∃ a : CB(N ).push(a) \\ ∨ ∃ k : CB(N ).move(k)]_{Buffer} \end{matrix}

⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix}

⟨3⟩2. Q.E.D.

Proof: Follows directly by propositional logic.

The third step,

⟨2⟩3. CB(N ).Spec ⇒ \overline{WF_{buffer}(AB(N ).pop)}

had better wait until we understand a little more about liveness.

6.2 The Initial Condition

The initial condition substep is trivial to prove:

⟨3⟩1. CB(N ).Init ⇒ \overline{AB(N ).Init} Proof: .... is simply trivial math.

Putting it together, we have for this step

⟨2⟩1. CB(N ).Spec ⇒ \overline{AB(N ).Init}

⟨3⟩1. CB(N ).Init ⇒ \overline{AB(N ).Init}

Proof: .... is simply trivial math. ⟨3⟩2. Q.E.D.

Proof: Follows directly by propositional logic.

This is thus what the first part of the hierarchical proof looks like. The safety condition is more complicated.

6.3 The Safety Condition

First, a trivial logical manipulation. We can get rid of one unused conjunct of the antecedent:

⟨3⟩1. ∧ CB(N ).Init

∧ □[\begin{matrix}∨ CB(N ).pop \\ ∨ ∃ a : CB(N ).push(a) \\ ∨ ∃ k : CB(N ).move(k)]_{Buffer} \end{matrix}

⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix}

Proof:

⟨4⟩1. □[∨ CB(N ).pop ∨ ∃ a : CB(N ).push(a) ∨ ∃ k : CB(N ).move(k)]_{Buffer} ⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix} ⟨4⟩2. Q.E.D.

Proof:Trivial propositional logic.

and this substep may in turn be proved using an application of STL4:

⟨4⟩1. □[∨ CB(N ).pop ∨ ∃ a : CB(N ).push(a) ∨ ∃ k : CB(N ).move(k)]_{Buffer} ⇒ □[\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)}]\overline{buffer} \end{matrix}

Proof: ⟨5⟩1. (∨ CB(N ).pop ∨ ∃ a : CB(N ).push(a) ∨ ∃ k : CB(N ).move(k) ∨ Buffer′ = Buffer) ⇒ (\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)} \\ ∨ \overline{buffer}′ = \overline{buffer}) \end{matrix}

⟨5⟩2. Q.E.D.

Proof: directly by STL4:

\frac{F ⇒ G}{□F ⇒ □G}

Notice that we have now got rid of all the temporal operators, and we’re left with a substep that is ‘pure math’ - that is, first-order set theory at most. Bye, bye temporal logic. So let’s do it......

⟨5⟩1. (∨ CB(N ).pop ∨ ∃ a : CB(N ).push(a) ∨ ∃ k : CB(N ).move(k) ∨ Buffer′ = Buffer) ⇒ (\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)} \\ ∨ \overline{buffer}′ = \overline{buffer}) \end{matrix}

Proof: ⟨6⟩1. CB(N ).pop ⇒ \overline{AB(N ).pop} ⟨6⟩2. ∃ a : CB(N ).push(a) ⇒ ∃ a : \overline{AB(N ).push(a)} ⟨6⟩3. ∃ k : CB(N ).move(k) ⇒ \overline{buffer}′ = \overline{buffer} ⟨6⟩4. Buffer′ = Buffer ⇒ \overline{buffer}′ = \overline{buffer} ⟨6⟩5. Q.E.D.

Proof: Follows by propositional logic from ⟨6⟩1, ⟨6⟩2, ⟨6⟩3, and ⟨6⟩4: (A ⇒ X ) ∧ (B ⇒ Y ) ∧ (C ⇒ Z ) ∧ (D ⇒ Z )

⇒ (A ∨ B ∨ C ∨ D ⇒ X ∨ Y ∨ Z )

We’ll now select one of these substeps to prove as an example, namely ⟨6⟩2: a concrete push is an abstract push. The substep for this assertion consists in getting rid of the rigid-variable quantifiers. It’s the same value a that is going to be push’d on either buffer, so it’s rather easy to get rid of the quantifiers:

⟨6⟩2. ∃ a : CB.push(a)

⇒

∃ a : AB.push(a) Proof: Let: a : constant ⟨7⟩1. CB.push(a)

⇒

\overline{AB.push}(a)

⟨7⟩2. Q.E.D.

Follows from ⟨7⟩1 by predicate logic.

Proving this statement is a matter of unravelling the definitions of the actions. AB.push is a conjunct, so again the substeps consist in proving each of the conjuncts separately from the antecedent. The first is trivial:

⟨7⟩1. CB.push(a)

⇒

\overline{AB.push}(a)

Proof: ⟨8⟩1. CB.push(a) ⇒ a ∈ Data

Proof: Immediate from the definition

of CB.push. ⟨8⟩2. CB.push(a) ⇒ Len(\overline{buffer}) < N ⟨8⟩3. CB.push(a) ⇒ \overline{buffer}′ = \overline{buffer} ∘ ⟨a ⟩ ⟨8⟩4. Q.E.D.

Proof: Follows immediately from ⟨8⟩1, ⟨8⟩2 and ⟨8⟩3 using propositional logic.

Now, at this point, it would be good to know what the refinement mapping actually is! The correspondence has been given intuitively in the pictures and explanation of how the variables correspond, but now it’s time for a syntactical definition. Basically, we want to take the non-⊥ elements of the array and arrange them in a sequence in the order in which they occur in the array. It turns out in TLA that an array of length N is a finite function with domain 1..N =^{Δ} {n ∈ Nat : 1 ≤ n ≤ N }, where Nat is the set of natural numbers. A sequence is also defined as a finite function with domain 1..k for some k ∈ Nat. The point here is that Buffer and buffer don’t always have the same length – buffer just contains the non-⊥ elements of Buffer. So we define a function SelectSeq(⟨sequence⟩, ⟨Predicate⟩), which makes a subsequence of ⟨sequence⟩ of exactly those elements that satisfy the ⟨Predicate⟩. We shall need some auxiliary math definitions from the module Sequences, defined in Figure 22. Definition of this module is by Lamport. Don’t let the let...in syntax in SelectSeq worry you - it’s just a way of formally defining an auxiliary notation (with the let) and then inserting some actual parameters (in the in). SelectSeq is defined iteratively as one goes through the sequence: the head of the sequence is test-ed. If it satisfies the predicate test, it’s left in and we proceed to the tail of the sequence. If not, it’s omitted. So in the refinement mapping, we proceed through the sequence Buffer, testing each element to see whether it’s non-⊥, and including it in \overline{buffer} just in case it is.

The refinement mapping may now be defined as

NonVoid(k) =^{Δ} k ≠ ⊥
\overline{buffer} =^{Δ} SelectSeq(Buffer, NonVoid)

6.4 Digging Down into the Details of Safety

The two remaining clauses of the push simulation require some detailed reasoning. Here’s the first one, with its substeps. I cheat (remember the definition of cheating!) on the second substep.

⟨8⟩2. CB.push(a) ⇒ Len(\overline{buffer}) < N Proof: ⟨9⟩1. CB.push(a) ⇒ Buffer[N ] = ⊥

module Sequences

import Naturals

m..n  =^{Δ} {i ∈ Nat : (m ≤ i) ∧ (i ≤ n)}

Len(s) =^{Δ}

choose n : (n ∈ Nat) ∧ ((Domain s) = (1..n))

Head(s) =^{Δ} s[1]

Tail(s) =^{Δ} [i ∈ 1..(Len(s) − 1) ↦ s[i + 1]]

s ∘ t =^{Δ}

[i ∈ 1..(Len(s) + Len(t)) ↦

if i ≤ Len(s) then s[i]
else t[i − Len(s)]]

Seq(S) =^{Δ} union {[(1..n) → S] : n ∈ Nat} SubSeq(s, m, n) =^{Δ}

[i ∈ (1..(1 + n − m)) ↦ s[i + m − 1]] SelectSeq(s, test()) =^{Δ} let F [ t : Seq({s[i] : i ∈ (1..Len(s))}) ] =^{Δ}

if t = ⟨ ⟩ then ⟨ ⟩

else if test(Head(t))

then ⟨Head(t)⟩∘ ; F [Tail(t)]

else F [Tail(t)]
in F [s]

Figure 22: The Module Sequences Proof: Immediate from the definition of CB.push(a).

⟨9⟩2. Buffer[N ] = ⊥ ⇒

Len(SelectSeq(Buffer, NonVoid)) < N Proof: Follows from the definition of SelectSeq and Len, along with a certain amount of data structure manipulation, which is omitted.

⟨9⟩3. Q.E.D.

Proof: Follows from ⟨9⟩1, ⟨9⟩2 and the definition of \overline{buffer} by propositional logic.

Now for the third substep of the push simulation assertion. This breaks down into three substeps, which are all math.

⟨8⟩3. CB.push(a) ⇒ \overline{buffer}′ = \overline{buffer} ∘ ⟨a ⟩ Proof: ⟨9⟩1. CB.push(a) ⇒

SelectSeq(Buffer, NonVoid) =

SelectSeq(

[i ∈ 1..(N − 1) ↦ Buffer[i]],

NonVoid)

⟨9⟩2. CB.push(a)

⇒

Buffer′ =
[i ∈ 1..(N − 1) ↦ Buffer[i]] ∘ ⟨a ⟩

⟨9⟩3. SelectSeq([i ∈ 1..(N − 1) ↦ Buffer[i]]

∘ ⟨a ⟩, NonVoid) =

SelectSeq([i ∈ 1..(N − 1) ↦ Buffer[i]], NonVoid)

∘ ⟨a ⟩

⟨9⟩4. Q.E.D.

Proof: Follows immediately from ⟨9⟩1, ⟨9⟩2, ⟨9⟩3 by propositional logic, substitution, and the definition of \overline{buffer}.

We take these one at a time, in order. First, the first:

⟨9⟩1. CB.push(a) ⇒

SelectSeq(Buffer, NonVoid) =

SelectSeq(

[i ∈ 1..(N − 1) ↦ Buffer[i]], NonVoid)

Proof:

⟨10⟩1. CB.push(a) ⇒ Buffer[N ] = ⊥ Proof: Immediate from the definition of CB.push(a).

⟨10⟩2. Buffer[N ] = ⊥ ⇒ SelectSeq(Buffer, NonVoid) = SelectSeq([i ∈ 1..(N − 1)

↦ Buffer[i]], NonVoid)

Proof: Follows immediately from the definition of SelectSeq using manipulations of the data structure.

⟨10⟩3. Q.E.D.

Proof: Follows immediately from ⟨10⟩1 and ⟨10⟩2 by propositional logic.

Next, the second and third are really straightforward, if one cheats (definition!) a little:

⟨9⟩2. CB.push(a)

⇒

Buffer′ =
[i ∈ 1..(N − 1) ↦ Buffer[i]] ∘ ⟨a ⟩

Proof: Follows from the definition of CB.push and the sequence operations.

⟨9⟩3. SelectSeq([i ∈ 1..(N − 1) ↦ Buffer[i]]

∘ ⟨a ⟩, NonVoid) =

SelectSeq([i ∈ 1..(N − 1) ↦ Buffer[i]], NonVoid)

∘ ⟨a ⟩

Proof: Follows from the definition of SelectSeq and NonVoid.

So where are we? To recap: we’re done with the ⟨9⟩’s, which proved ⟨8⟩3; we’d done ⟨8⟩2 all in one, and ⟨8⟩1 was trivial; thus we’ve proved

⟨6⟩1. CB.push(a)

⇒

\overline{AB.push}(a)

which in turn proved ⟨5⟩2. ∃ a : CB.push(a)

⇒

∃ a : AB.push(a)

....which is the second clause in the simulation theorem:

⟨4⟩1. (∨ CB(N ).pop ∨ ∃ a : CB(N ).push(a) ∨ ∃ k : CB(N ).move(k) ∨ Buffer′ = Buffer) ⇒ (\begin{matrix}∨ \overline{AB(N ).pop} \\ ∨ \overline{∃ a : AB(N )push(a)} \\ ∨ n\overline{buffer}′ = \overline{buffer}) \end{matrix}

We leave the other two clauses (for pop and move) to the reader, and hereby deem this proof completed! All the substeps have been proved. The buffer implementation is safe. The full proof, without cheating, in full hierarchical style, may be seen in [Lad96a].

6.5 The Proof Scheme

The reader will have observed that there is a scheme to this. Now we’ll take a look at the general proof scheme for TLA verifications. In the proof scheme, some of the steps are pro forma applications of inference rules, and some require different proofs for each verification. The steps that are new for each verification are indicated by ******Novel.****** in the proof scheme. The proof scheme helps to control the proof.

The proof scheme is a little more complicated than the buffer example we have just seen. In a general safety proof, we shall need to find an invariant. An invariant is an assertion that remains always true throughout every behavior of the machine; that is, an assertion of the form □A that can be proved from the safety property and initial condition. This invariant will then be used to establish the safety property of the specification machine. This proof method is known as the invariant assertion method, and was first adapted for use in concurrent programs by Ashcroft [Ash75] and extended by Owicki and Gries [OG76] and Lamport, amongst others. Choosing a good invariant is often the hardest part of a safety proof. There is no formula for picking an invariant – it requires practice and experience.

⟨1⟩1. (Init_{1} ∧ □[N_{1}]_{v} ∧ L_{1})

⇒ (\overline{Init_{2}} ∧ □\overline{[N_{2}]_{w}} ∧ \overline{L_{2}}) Proof: ⟨2⟩1. Init_{1} ⇒ \overline{Init_{2}}

Proof: ******Novel.******. ⟨2⟩2. Init_{1} ∧ □[N_{1}]_{v} ⇒ □\overline{[N_{2}]_{w}}

Proof: See next slides. ⟨2⟩3. Init_{1} ∧ □[N_{1}]_{v} ∧ L_{1} ⇒ \overline{L_{2}}

Proof: ******Novel.******. ⟨2⟩4. Q.E.D.

Proof: By propositional logic from ⟨2⟩1, ⟨2⟩2, ⟨2⟩3.

Note that the novel step ⟨2⟩1 is pure math. Let’s look at ⟨2⟩2: safety:

⟨2⟩2. Init_{1} ∧ □[N_{1}]_{v} ⇒ □\overline{[N_{2}]_{w}} Proof: First we must find an invariant Inv: ⟨3⟩1. Init_{1} ∧ □[N_{1}]_{v} ⇒ □Inv ⟨3⟩2. □Inv ∧ □[N_{1}]_{v} ⇒ □[\overline{N_{2}}]\overline{w} ⟨3⟩3. Q.E.D.

Proof: Immediate from ⟨3⟩1 and ⟨3⟩2 by propositional logic.

The substeps to prove the invariant from the safety property of the implementation machine are:

⟨3⟩1. Init_{1} ∧ □[N_{1}]_{v} ⇒ □Inv

⟨4⟩1. Init_{1} ⇒ Inv

Proof: ******Novel.****** ⟨4⟩2. Inv ∧ □[N_{1}]_{v} ⇒ □Inv

Proof:

⟨5⟩1. Inv ∧ [N_{1}]_{v} ⇒ Inv′

Proof: ******Novel.****** ⟨5⟩2. Q.E.D.

Proof: Immediate from ⟨5⟩1 by Rule INV1. ⟨4⟩3. Q.E.D.

Proof: Immediate from ⟨4⟩1 and ⟨4⟩2 by propositional logic.

Now the rest: using the invariant to prove the safety property of the specification machine:

⟨3⟩2. □Inv ∧ □[N_{1}]_{v} ⇒ □[\overline{N_{2}}]\overline{w} Proof: ⟨4⟩1. □Inv ∧ □[N_{1}]_{v} ⇒ □[N_{1} ∧ Inv ∧ Inv′]_{v} Proof: Immediate from INV2 using STL5.

⟨4⟩2. □[N_{1} ∧ Inv ∧ Inv′]_{v} ⇒ □[\overline{N_{2}}]\overline{w} Proof: ⟨5⟩1. [N_{1} ∧ Inv ∧ Inv′]_{v} ⇒ [\overline{N_{2}}]\overline{w} Proof: ******Novel.******.

⟨5⟩2. Q.E.D.

Proof: Immediate from ⟨5⟩1 by STL4. ⟨4⟩3. Q.E.D.

Proof: Immediate from ⟨4⟩1 and ⟨4⟩2 by propositional logic.

6.5.1 The Proof Obligations

We can see from this scheme that there are novel parts and pro forma parts. The novel parts, which must be proved anew for each example, are called proof obligations. Thus the proof obligations are:

⟨2⟩2. Init_{1} ⇒ \overline{Init_{2}} ⟨2⟩3. (Safety)

⟨3⟩1. (Inv−holds)

⟨4⟩1. Init_{1} ⇒ Inv

⟨4⟩2. (Inv is persistent)

⟨5⟩1. Inv ∧ [N_{1}]_{v} ⇒ Inv′ ⟨3⟩2. (Inv−used)

⟨4⟩1. (technical step)

⟨4⟩2. (persisent simulation)

⟨5⟩1. [N_{1} ∧ Inv ∧ Inv′]_{v} ⇒ [\overline{N_{2}}]\overline{w}

Notice that all of these proof obligations are statements of TLA without temporal operators. Therefore, they are ZF set theory only. No temporallogical inference needed.

The step ⟨5⟩1. Inv ∧ [N_{1}]_{v} ⇒ Inv′ may (should) also be broken down into individual actions:

N_{1} =^{Δ} A_{1} ∨ A_{2} ∨ … ∨ A_{k}

⟨5⟩1. Inv ∧ [N_{1}]_{v} ⇒ Inv′ Proof: ⟨6⟩1. Inv ∧ A_{1} ⇒ Inv′ ⟨6⟩2. Inv ∧ A_{2} ⇒ Inv′ … ⟨6⟩3. Inv ∧ A_{k} ⇒ Inv′ ⟨6⟩4. Inv ∧ v′ = v ⇒ Inv′

Since Inv only includes variables in v, the last step is trivial.

Similarly for the other obligation: ⟨5⟩1. [N_{1} ∧ Inv ∧ Inv′]_{v} ⇒ [\overline{N_{2}}]\overline{w}

But don’t forget that maybe the hardest part of all is picking the invariant Inv!

7 Liveness

What about the Liveness properties? We need to prove

WF_{buffer}(pop)

given

WF_{Buffer}(pop) ∧ WF_{Buffer}(∃ k : move(k))

and the initial condition and safety property of ConcreteBuffer. Hard to do, since we don’t yet know what it means :-) Intuitively, \overline{WF_{buffer}(pop)} means

• When pop can happen, it must – sometime

• i.e., when pop is continually enabled (can occur from the current state)

• it must happen sometime (in the present or future)

Let’s look at this intuitively with an example.

ConcreteBuffer  AbstractBuffer ⊥ ⊥ b ⊥ d ⊥ ⊥  \overline{}< b,d >

Figure 23: Are the pop’s Enabled?

7.1 The Predicate Enabled

Consider Figure 23. Is \overline{AB.pop} enabled in this Figure? Is CB.pop enabled?

Intuitively, AB.pop is enabled when buffer is nonempty. So Buffer is also nonempty, but there is nothing in Buffer[1], so CB.pop is not enabled. So we have the situation that AB.pop may be enabled, even though CB.pop is not. The formal definition of WF_{v}(A) is

□◇¬Enabled⟨A⟩_{v} ∨ □◇⟨A⟩_{v}

that is, using the definitions of [A]_{v}, and ⟨A⟩_{v} =^{Δ} (A ∧ v′ ≠ v),

□◇¬Enabled(A ∧ v′ ≠ v) ∨ □◇(A ∧ v′ ≠ v)

What’s the formal definition of Enabled? Suppose I have an action such as

pop =^{Δ} ∧ Len(buffer) > 0 ∧ buffer′ = tail(buffer)

It has variable buffer, and next value buffer′. If I say there is a next value, that means I can do pop, in principle. That’s exactly what Enabled says:

Enabled(pop) =^{Δ} ∃ newvalue : ∧ Len(buffer) > 0 ∧ newvalue = tail(buffer)

In general, let action A have flexible variables y, z.

Enabled(A) =^{Δ} ∃ newy, newz : A(y′/newy, z′/newz)

where

• newy and newz are new symbols not otherwise occurring in A; • A(y′/newy, z′/newz) is A, but with newy

substituted for every occurrence of y′, similarly for newz

So,

Enabled(pop) =^{Δ} ∃ newvalue : ∧ Len(buffer) > 0 ∧ newvalue = tail(buffer)

⇔

∧ Len(buffer) > 0

∧ ∃ newvalue : newvalue = tail(buffer)

But ∃ newvalue : newvalue = tail(buffer) is a theorem of predicate logic with equality. Why? Let ϕ(x) =^{Δ} (x = tail(buffer)). Then ϕ(tail(buffer)) is an instance of an equality axiom. We may now invoke the rule (crudely written) \frac{ϕ(t)}{∃x : ϕ(x)} to infer ∃ newvalue : ϕ(newvalue). So it follows that

∧ Len(buffer) > 0

∧ ∃ newvalue : newvalue = tail(buffer)

⇔

∧ Len(buffer) > 0 ∧ true

⇔

Len(buffer) > 0

Similarly, Enabled(CB.pop ∧ Buffer′ ≠ Buffer)

⇔

∃ Newbuf : ∧ Buffer[1] ≠ ⊥ ∧ Newbuf [1] = ⊥ ∧ ∀ i ∈ 2..N : Newbuf [i] = Buffer[i]

⇔

∧ Buffer[1] ≠ ⊥

∧ ∃ Newbuf : ∧ Newbuf [1] = ⊥ ∧ ∀ i ∈ 2..N : Newbuf [i] = Buffer[i] The second conjunct is, however, a theorem of ZF:

∃ Newbuf : ∧ Newbuf [1] = ⊥ ∧ ∀ i ∈ 2..N : Newbuf [i] = Buffer[i]

To show it, define

TheRealNewBuf =^{Δ} [Buffer except ![1] = ⊥]

Then ∧ TheRealNewBuf [1] = ⊥ ∧ ∀ i ∈ 2..N : TheRealNewBuf [i] = Buffer[i]

follows immediately from the definition. So it follows finally that

Enabled⟨CB.pop ⟩_{Buffer} ⇔ (Buffer[1] ≠ ⊥)

As an exercise, try it with ∃ k : CB.move(k). It’s very similar to CB.pop.

7.2 Proof Rules for Liveness

We need to introduce some more rules: the LATTICE rule and the Weak Fairness rules. (We shall not concern ourselves here with the Strong Fairness rules.)

7.2.1 LATTICE

LATTICE embodies structural induction (SI). SI is a data-structure induction rule. It is used in the theory of programming for showing that recursive programs terminate. It is to be distinguished from the rule TLA1, which describes semantical induction over the structure of a behavior. In contrast, LATTICE is mathematical rather than temporal-logical. LATTICE is the embodiment of structural induction.

LATTICE. \frac{Given ≻ a well−founded partial order on a set S and c a variable not free in F or G F ∧ (c ∈ S) ⇒ (H_{c} ⇝ (G ∨ ∃d ∈ S : (c ≻ d) ∧ H_{d}))}{F ⇒ ((∃c ∈ S : H_{c}) ⇝ G)} Here, H_{c} indicates a formula H which contains the variable c free, and H_{d} denotes H [c/d]. Assume that no occurrence of c occurs within the scope of a d-quantifier in H ! The notation A ⇝ B is defined as □(A ⇒ ◇B). The intuitive meaning of ⇝= leads to is that whenever A happens in the future, at some later time B must happen.

So what’s a well-founded partial order? First, an order is a bianry relation with certain properties: the numbers are ordered by <, but this order is total: namely, all numbers are comparable. Sets are ordered by ⊂, and this order is partial: namely, not every pair of sets is comparable. Sequences are ordered by prefix, and it’s also partial.

An order is well-founded if it satisfies the following condition: if you follow a ‘descending chain’ of elements: x_{1} > x_{2} > x_{3} > ....., it always stops. For example, try it with natural numbers, say x_{1} = 510. It always stops: the order on natural numbers is well-founded. So is the order on sequences. Try it: say x_{1} = ⟨5, 7, 24, 610, 7, 5, 4, 2, 3, 2⟩.

Lattice is used for fairness proofs as a way of ensuring that recursion stops (always provided you’ve written the recursion properly). Think of G as the ground clause and H_{c} as the recursive call with parameter c

7.2.2 Weak Fairness

The Weak Fairness Rules are:

WF1.

\frac{P ∧ [N]_{f} ⇒ (P′ ∨ Q′) P ∧ ⟨N ∧ A⟩_{f} ⇒ Q′ P ⇒ Enabled ⟨A⟩_{f}}{□[N]_{f} ∧ WF_{f} (A) ⇒ (P ⇝ Q)}

WF2.

\frac{⟨N ∧ B⟩_{f} ⇒ ⟨\overline{M}⟩\overline{g} P ∧ P′ ∧ ⟨N ∧ A⟩_{f} ∧ \overline{Enabled ⟨M⟩_{g}} ⇒ B P ∧ \overline{Enabled ⟨M⟩_{g}} ⇒ Enabled ⟨A⟩_{f} □[N ∧ ¬B]_{f} ∧ WF_{f} (A) ∧ □F ∧ ◇□\overline{Enabled ⟨M⟩_{g}} ⇒ ◇□P}{□[N]_{f} ∧ WF_{f} (A) ∧ □F ⇒ \overline{WF_{g}(M)}}

where F , G, H_{c} are TLA formulas, P, Q, I are predicates, A, B, N, M are actions, f , g are state functions.

While WF1 is fairly straightforward, WF2 embodies a form of proof by contradiction. Let’s try to clarify this intuitively, premise by premise. The first premise,

⟨N ∧ B⟩_{f} ⇒ ⟨\overline{M}⟩\overline{g}

says that when an N action occurs that’s also a B action, an M action is simulated. The second premise

P ∧ P′ ∧ ⟨N ∧ A⟩_{f} ∧ \overline{Enabled ⟨M⟩_{g}} ⇒ B

says that, supposing an N -and-A action occurs when M is enabled and P is invariant, then that’s also a B action (so by the first premise, that’ll also be an M action). The third premise,

P ∧ \overline{Enabled ⟨M⟩_{g}} ⇒ Enabled ⟨A⟩_{f}

ensures that the antecedent of Premise 2 is not obviously self-contradictory: if P holds and M is enabled, A is also enabled. Finally the fourth premise:

□[N ∧ ¬B]_{f} ∧ WF_{f} (A) ∧ □F ∧ ◇□\overline{Enabled ⟨M⟩_{g}}
⇒ ◇□P

says that if a B action never occurs, and A satisfies a weak fairness condition, and eventually M becomes enabled and stays enabled, then eventually P becomes true and stays true. Recall that by Premise 3, in such a state A is also enabled; therefore also eventually continuously enabled in this situation.

That condition makes in the future most of the conjuncts in Premise 2 true. So if an A action ever occurs in this situation, it will be an M action also. M will then have occurred after being continuously enabled, which is what \overline{WF_{g}(M)} requires. Which is what we want to prove.

But how do we know an A action will ever occur in this circumstance? By assuming it is weakly fair - since it is continuously enabled, we required it to happen sometime. And its happening will also be an M-simulation that we want to show happens. We are done. We have argued intuitively that

□[N]_{f} ∧ WF_{f} (A) ∧ □F ⇒ \overline{WF_{g}(M)}

is true providing the four premises are temporal-logical truths.

Of course, one should take care to distinguish informal argument – informal justification of this proof rule – from temporal-logical validity. There are some lines of ‘intuitive’ reasoning that just aren’t valid (for one thing, the Deduction Theorem doesn’t hold). Notwithstanding, the reasoning above is a reasonable guide to the validity of WF2.

Why is it a proof by contradiction? Because there’s a conjunct in the antecedent of the fourth premise that asserts that B never happens, but nevertheless we can show an A must happen under P and \overline{Enabled ⟨M⟩_{g}}, and in these circumstances, by Premise 2 this must be a B action. So we derive the occurrence of a B action by assuming it never can happen. If this presumption is wrong (which it must be - since it’s contradictory given the other premises) then every behavior contains an unending chain of B actions and Premise 1 tells us we have an unending chain of M actions, which is one disjunct of \overline{WF_{g}(M)}.

According to the fourth premise, if B never occurs, and M becomes and remains enabled, then P becomes and remains true. The second premise (it must be a temporal-logical truth as it occurs as an assertion in a TLA proof) must be true in all states, and therefore in none of these future states in which M remains enabled and P remains true can any N action that is A ever occur, since else B occurs, contrary to supposition. And A is, in this situation, also possible, since it is enabled by Premise 3.

7.3 The Liveness Proof

Let’s look now at the buffers. First, I introduce some auxiliary definitions (as well as a reminder of two that we’ve seen before):

Let: NonVoid(k) =^{Δ} k ≠ ⊥ \overline{buffer} =^{Δ} SelectSeq(Buffer, NonVoid) FirstFull =^{Δ} Buffer[1] ≠ ⊥ NotEmpty =^{Δ} ∃ i ∈ 1..N : Buffer[i] ≠ ⊥ NotStuffed =^{Δ} ∃ i ∈ 1..(N − 1) : Buffer[i] = ⊥ Descending(Buffer) =^{Δ} ∑ _{i=1}^{N}(Buffer[i] = ⊥).2^{N −i} MaxDescending =^{Δ} ∑ _{i=1}^{N}2^{N −i}

We want to prove (I’ll cheat a bit on the numbering by performing a logical simplification without changing the proof step number):

⟨2⟩3.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ CB.pop \\ ∨ ∃ k : CB.move(k)]_{Buffer}] \end{matrix}

∧ WF_{Buffer}(CB.pop)

∧ WF_{Buffer}(∃ k : CB.move(k))

⇒
WF_{buffer}(AB.pop)

so we take

N =^{Δ} (\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ CB.pop \\ ∨ ∃ k : CB.move(k)) \end{matrix}

f =^{Δ} Buffer

A =^{Δ} B =^{Δ} CB.pop

M =^{Δ} AB.pop

g =^{Δ} buffer P =^{Δ} FirstFull □F =^{Δ} WF_{Buffer}(∃ k : CB.move(k))

and instantiate the conclusion of WF2. The substeps are the premises:

⟨2⟩3.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ CB.pop \\ ∨ ∃ k : CB.move(k)]_{Buffer}] \end{matrix}

∧ WF_{Buffer}(CB.pop)

∧ WF_{Buffer}(∃ k : CB.move(k))

⇒
WF_{buffer}(AB.pop)

Proof:

⟨3⟩1. ⟨\begin{matrix}∧ ∨ ∃ a : CB.push(a) ∨ CB.pop ∨ ∃ k : CB.move(k)) \\ ∧ CB.pop ⟩_{Buffer} \end{matrix}

⇒
⟨\overline{AB.pop} ⟩\overline{buffer}

⟨3⟩2.

∧ FirstFull ∧ FirstFull′ ∧ ⟨CB.pop ⟩_{Buffer} ∧ \overline{Enabled⟨AB.pop ⟩_{buffer}}

⇒

CB.pop ⟨3⟩3.

∧ FirstFull ∧ \overline{Enabled⟨AB.pop ⟩_{buffer}}

⇒

Enabled⟨CB.pop ⟩_{Buffer}

⟨3⟩4.

∧ □[\begin{matrix}∧ ∨ ∃ a : CB.push(a) ∨ CB.pop ∨ ∃ k : CB.move(k) \\ ∧ ¬CB.pop]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ∧ ◇□\overline{Enabled⟨AB.pop ⟩_{buffer}}

⇒

◇□FirstFull

⟨3⟩5. Q.E.D.

Proof: Follows from ⟨3⟩1, ⟨3⟩2, ⟨3⟩3 and ⟨3⟩4 using TLA Proof Rule WF2 with the substitutions given above.

We take these one by one.

⟨3⟩1. ⟨\begin{matrix}∧ ∨ ∃ a : CB.push(a) ∨ CB.pop ∨ ∃ k : CB.move(k) \\ ∧ CB.pop ⟩_{Buffer} \end{matrix}

⇒ ⟨\overline{AB.pop} ⟩\overline{buffer} Proof:

⟨4⟩1.

∧ ∨ ∃ a : CB.push(a) ∨ CB.pop ∨ ∃ k : CB.move(k))

∧ CB.pop ≡

CB.pop

⟨4⟩2. ⟨CB.pop ⟩_{Buffer} ⇒

⟨\overline{AB.pop} ⟩\overline{buffer}

Both of these substeps are pretty trivial. We won’t go further into them. Now for the next step.

⟨3⟩2.

∧ FirstFull ∧ FirstFull′ ∧ ⟨CB.pop ⟩_{Buffer} ∧ \overline{Enabled⟨AB.pop ⟩_{buffer}}

⇒ CB.pop

This is also logically trivial - just look at it!

The third step, with its substeps is:

⟨3⟩3.

∧ FirstFull ∧ \overline{Enabled⟨AB.pop ⟩_{buffer}}

⇒ Enabled⟨CB.pop ⟩_{Buffer}

⟨4⟩1. FirstFull ⇒

Enabled⟨CB.pop ⟩_{Buffer}

This substep is also trivial. Finally, the rather horrendous fourth premise, with a substep that is a logically-simplified version, substituting the predicate NotEmpty for the Enabled condition in the antecedent:

⟨3⟩4.

∧ □[\begin{matrix}∧ ∨ ∃ a : CB.push(a) ∨ CB.pop ∨ ∃ k : CB.move(k) \\ ∧ ¬CB.pop]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ∧ ◇□\overline{Enabled⟨AB.pop ⟩_{buffer}} ⇒ ◇□FirstFull Proof: ⟨4⟩1.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ∧ ◇□NotEmpty ⇒ ◇□FirstFull

Actually, rather than show this substep, we show this further substep (we’ll Bridge the gap below):

⟨5⟩2.

∧ □[∃ a : CB.push(a).....]_{Buffer} ∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ⇒ □(NotEmpty ⇒ ◇FirstFull)

which because of

⟨6⟩1. (NotEmpty ∧ NotStuffed ⇒ ◇FirstFull) ≡ (NotEmpty ⇒ ◇FirstFull)

is equivalent to ⟨6⟩2.

∧ □[∃ a : CB.push(a)......]_{Buffer} ∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ⇒ □(∧ NotEmpty ; ∧ NotStuffed ⇒ ◇FirstFull)

which is shown using the Lattice Rule We have somehow to get ⟨6⟩2 into a form in which we can apply LATTICE. First, we fiddle with the conclusion:

⟨7⟩1. ∧ □[∃ a : CB.push(a).........

∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k))

⇒

□(∧ ∃ i ∈ 0..MaxDescending : Descending(Buffer) = i ∧ NotEmpty ∧ NotStuffed ⇒ ◇FirstFull)

Then we use the Lattice Rule to derive this. If this is to be the conclusion of LATTICE, the substep must be the premise:

⟨8⟩1.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop)

∧ WF_{Buffer}(∃ k : CB.move(k))

∧ i ∈ 0..MaxDescending ⇒ □(∧ NotEmpty ∧ NotStuffed ∧ Descending(Buffer) = i) ⇒ ◇(\begin{matrix}∨ FirstFull \\ ∨ ∃ j ∈ 0..MaxDescending : ∧ j < i ∧ NotEmpty ∧ NotStuffed ∧ Descending(Buffer) = j ) \end{matrix}

This step itself is justified by an application of WF1 (and simple propositional logic). It is taken as the conclusion of an application of WF1 with the instantiations N =^{Δ} ∃ a : CB.push(a) ∨ ∃ k : CB.move(k), A =^{Δ} ∃ k : CB.move(k), P =^{Δ} ∧ NotEmpty ∧ NotStuffed ∧ Descending(Buffer) = i

and Q =^{Δ} ∨ FirstFull ∨ ∃ j ∈ 0..MaxDescending : ∧ j < i ∧ NotEmpty ∧ NotStuffed ∧ Descending(Buffer) = j ).

The substeps consist of the premises of WF1. I leave this to the reader (See [Lad96a] to cheat!... I mean, to steal!)

7.4 Using The Bridge Rule

The question remaining is to Bridge the gap we left earlier. How do we obtain

⟨4⟩4.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop)

∧ WF_{Buffer}(∃ k : CB.move(k)) ∧ ◇□NotEmpty ⇒ ◇□FirstFull

from

⟨5⟩2.

∧ □[∃ a : CB.push(a).....]_{Buffer} ∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ⇒ □(NotEmpty ⇒ ◇FirstFull)

?????

Answer: by the Bridge Rule [Lad96a] (you were presumably waiting for that!).

Let X , Y , Z be propositional variables (that is, symbols standing for any sentences). Then the Bridge Rule is the derived rule

\frac{\begin{matrix}□X ∧ Y ⇒ □Y \\ □X ⇒ □(Z ⇒ ◇Y ) \end{matrix}}{□X ∧ ◇□Z ⇒ ◇□Y}

To apply the Bridge Rule, we’ll use the substitutions □X =^{Δ} ∧□[N ∧ ¬CB.pop]_{Buffer}

∧WF_{Buffer}(CB.pop)

∧WF_{Buffer}(∃ k : CB.move(k))

Y =^{Δ} FirstFull Z =^{Δ} NonEmpty

(I really mean: the conjunction is by STL5 equivalent to a formula of the form □X ).

What does the Bridge Rule mean? Suppose X is an action, which is a disjunction of actions. Then □X is a safety property. Let Y be some state predicate. Consider the first premise

□X ∧ Y ⇒ □Y

If we can prove the first premise, we are showing that Y would be an invariant if B were never to happen. Well, in our present buffer example, that’s right! So it looks as though we can prove this first premise. The second premise of the Bridge Rule is

□X ⇒ □(Z ⇒ ◇Y )

which is the same as (is defined to be)

□X ⇒ (Z ⇝ Y )

which is the conclusion of the Lattice Rule. The conclusion of the Bridge Rule is

□X ∧ ◇□Z ⇒ ◇□Y

which is the form of the fourth premise of WF2. Thus the Bridge Rule bridges the logical gap between the Lattice Rule and the fourth WF2 premise.

To summarise: we prove the first premise

□X ∧ Y ⇒ □Y

using INV1:

\frac{□X ∧ Y ⇒ Y′}{□X ∧ Y ⇒ □Y}

We prove the second premise

□X ⇒ □(Z ⇒ ◇Y )

using the Lattice Rule. And we draw the conclusion

□X ∧ ◇□Z ⇒ ◇□Y

which is the temporal fourth premise of WF2.

So to complete our proof, we need to prove the first Bridge Rule premise:

⟨5⟩1.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ WF_{Buffer}(CB.pop) ∧ WF_{Buffer}(∃ k : CB.move(k)) ∧ FirstFull

⇒

□FirstFull Proof: ⟨6⟩1.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ FirstFull ⇒ □FirstFull

⟨6⟩2. Q.E.D. Proof: Immediate from ⟨6⟩1 by adding a hypothesis.

⟨6⟩1.

∧ □[\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ FirstFull ⇒ □FirstFull

Proof: ⟨7⟩1.

∧ [\begin{matrix}∨ ∃ a : CB.push(a) \\ ∨ ∃ k : CB.move(k)]_{Buffer} \end{matrix}

∧ FirstFull ⇒ FirstFull′

Proof: Follows from the definitions of push and move. ⟨7⟩2. Q.E.D.

Proof: Immediate by TLA Proof Rule INV1 from ⟨7⟩1, with the antecedent conjuncts in the other order.

That shows the first BR premise and so we’re Done, not only with the BR premise but with the entire proof.

8 Conclusions

I have tried to motivate some of the features of TLA first of all by considering change, and a vocabulary needed to talk reasonably in a logical language about change. I then considered what kind of proof rules we would need in this language: the ‘standard’ natural deduction rules as given, for example, by Prawitz, also for modal logic; an extra rule (axiom) for Simple Temporal Logic; two rules for dealing with the prime operator; and induction rules (called Invariant rules) which arise from the structure of behaviors (they are ω-sequences of states).

I showed how these rules suffice to follow the invariant assertion method for safety proofs. I worked through an example, a buffer implementation, in detail, and discussed a proof scheme and the resulting proof obligations. No proof obligations have temporal operators: ergo pure math (that is, first order set theory) suffices.

We came then to liveness. Liveness is important for TLA, because TLA specifications are stuttering invariant (stuttering invariance is one of many important concepts that I deliberately overlooked in this tutorial). Because a safety assertion is of the form □[N]_{v}, a safety assertion conjoined with an initial condition cannot imply that anything ever changes - a possible behavior is that variable v never changes its value from the initial condition, ever. Thus, to ensure that deep down in the forest something stirs, we need to add liveness assertions to every machine specification.

Lamport has shown that the TLA rules as presented here (with the exception of Strong Liveness) suffice to prove any verification in which both machines are specified by TLA specifications. A TLA specification has the form

Init ∧ □[N]_{v} ∧ Liveness

in which Init is a state predicate, N is an action formula containing only variables occurring in v (v might be a term, rather than a simple variable – in fact, usually is), and Liveness is a Boolean combination of weak and strong fairness conditions.

The point of this tutorial was to do all and only that which is necessary to follow a particular TLA verification – that of the buffer implementation. This has entailed that many fundamental TLA concepts just have not arisen. The style has been discursive - the notes have been constructed from the slides for the tutorial itself. For a full hierarchical proof of the buffer implementation, see [Lad96a]. Another example, with a little more cheating on the lowlevel details, is [Lad96b]. And for lots and lots of information about TLA itself, including the definition of TLA+; real-time additions to TLA; use of TLA in specifying and verifying hybrid (part physical) systems; theorems on refinement mappings; difficult verifications; and links to other work around the world, see [Lama].

References

[AL91] M. Abadi and L. Lamport. The existence of refinement mappings. Theoretical Computer Science, 82(2):253–284, May 1991.

[Ash75] Edward A. Ashcroft. Proving assertions about parallel programs. Journal of Computer and System Sciences, 10:110–135, February 1975.

[BF93] Ricky W. Butler and George B. Finelli. The infeasibility of quantifying the reliability of life-critical real-time software. IEEE Transactions on Software Engineering, 19(1):3–12, January 1993.

[Bra97] F. H. Bradley. Appearance and Reality. Macmillan, New York, 2nd edition, 1897.

[Eng95] Urban Engberg. TLP.

         http://www.daimi.aau.dk/~urban/tlp/tlp.html, 1995.

[Gol92] Robert Goldblatt. Logics of Time and Computation, volume 7 of Lecture Notes. CSLI Press, Stanford, CA, USA, 2nd revised and expanded edition, 1992.

[Kal] Sara Kalvala. A formulation of TLA in Isabelle. Available through [Lama].

[Lad96a] Peter B. Ladkin. Formal but lively buffers in TLA+. Technical Report RVS-RR-96-07, RVS Group, Universität Bielefeld, Technische Fakultät, January 1996. Available through

http://www.rvs.uni-bielefeld.de.

[Lad96b] Peter B. Ladkin. Lazy Cache implements Complete Cache. Technical Report RVS-RR-96-06, RVS Group, Universität Bielefeld, Technische Fakultät, January 1996. Available through

http://www.rvs.uni-bielefeld.de.

[Lama] Leslie Lamport. The Temporal Logic of Actions page.

http://www.research.digital.com/SRC/tla.

[Lamb] Leslie Lamport. Various TLA+ documents. In [Lama].

[Lam93] Leslie Lamport. How to write a proof, January 1993. In

http://www.research.digital.com/SRC/tla/.

[Lam94a] Leslie Lamport. How to write a long formula. Formal Aspects of Computing, 6:580–584, 1994.

[Lam94b] Leslie Lamport. The temporal logic of actions. ACM Transactions on Programming Languages and Systems, 16(3):872–923, May 1994.

[Lam95] Leslie Lamport. How to write a proof. American Mathematical Monthly, 102(7):600–608, August-September 1995.

[Lam96] Leslie Lamport. personal communication. 1996.

[LS84] S. S. Lam and A. U. Shankar. Protocol verification via projections. IEEE Transactions on Software Engineering, 10(4):325–342, July 1984.

[LS93] Bev Littlewood and Lorenzo Strigini. Validation of ultrahigh dependability for software-based systems. Communications of the ACM, 36(11):69–80, November 1993.

[McT27] J. M. E. McTaggart. The Nature of Existence, chapter 33. Cambridge University Press, Cambridge, 1927. Also Chapter 1 of [PM93].

[Mer97] Stephan Merz. Isabelle/TLA.

http://www4.informatik.tu-muenchen.de/~merz/isabelle/,

1997.

[MP92] Zohar Manna and Amir Pnueli. The Temporal Logic of Reactive and Concurrent Systems, volume 1: Specification. Springer-Verlag, 1992.

[Neu] Peter G. Neumann. Past Security Projects page.

http://www.csl.sri.com/sri-csl-security/past-projects.html.

[OG76] Susan Owicki and David Gries. An axiomatic proof technique for parallel programs I. Acta Informatica, 6(4):319–340, 1976.

[PM93] Robin Le Poidevin and Murray MacBeath, editors. The Philosophy of Time. Oxford Readings in Philosophy. Oxford University Press, Oxford, 1993.

[Pra65] Dag Prawitz. Natural Deduction: A Proof-Theoretical Study. Almqvist and Wiksell, Uppsala, 1965.

[Req92] Requirements and Technical Concepts for Aviation (RTCA). Software considerations in airborne systems and equipment certification. RTCA Paper No. DO-178-B, 7th Draft, July 1992.

[Res95] Nicholas Rescher. Being and becoming. In Jaegwon Kim and Ernest Sosa, editors, A Companion To Metaphysics, Blackwell Companions to Philosophy, pages 46–47. Basil Blackwell, Oxford, 1995.

[Rus] John Rushby. Formal Methods and Dependable Systems page.

http://www.csl.sri.com/sri-csl-fm.html.

[RvHO91] John Rushby, Friedrich von Henke, and Sam Owre. An introduction to formal specification and verification uding EHDM. Technical Report SRI-CSL-91-02, SRI Computer Science Lab, Menlo Park, CA, USA, February 1991. Abstract in [SRIb].

[SRIa]  SRI Computer Science Lab.  Fault Tolerance at SRI.

http://www.csl.sri.com/ft-history.html.

[SRIb] SRI Computer Science Lab. Technical Report Abstracts.

http://www.csl.sri.com/tr-abstracts.html.

[Vaa] Frits Vaandrager. Publications.

http://www.cs.kun.nl/~fvaan/publications.html.

[VR92] Kim J. Vicente and Jens Rasmussen. Ecological interface design: Theoretical foundations. IEEE Transactions on Systems, Man and Cybernetics, 22(4):589–606, July/August 1992.