To save content items to your account,
please confirm that you agree to abide by our usage policies.
If this is the first time you use this feature, you will be asked to authorise Cambridge Core to connect with your account.
Find out more about saving content to .
To save content items to your Kindle, first ensure no-reply@cambridge.org
is added to your Approved Personal Document E-mail List under your Personal Document Settings
on the Manage Your Content and Devices page of your Amazon account. Then enter the ‘name’ part
of your Kindle email address below.
Find out more about saving to your Kindle.
Note you can select to save to either the @free.kindle.com or @kindle.com variations.
‘@free.kindle.com’ emails are free but can only be saved to your device when it is connected to wi-fi.
‘@kindle.com’ emails can be delivered even when you are not connected to wi-fi, but note that service fees apply.
For programming complex real-world problems it is no surprise that mistakes are unavoidable: that is the premise on which the formal methods approach is based and –furthermore– why it is so important that an informal presentation of its principal concepts is given, as here. Most, perhaps even all, programmers should be aware of “what’s true here” reasoning, of assertions and loop invariants — not repelled by the apparent (and inaccurate) impression that one must learn formal logic or other more mathematical topics first. Yet that checking is itself error-prone, especially for complex programs: adding up a long restaurant bill on a scrap of paper can be defeated by your forgetting to include a carry during the addition: that is why we use calculators. Thus program checking itself must be checked, and at two levels: one is that the assertions are the right ones, and the other is that some obvious but necessary specification requirement has not been overlooked. Here the analogy is first that an item’s price has been summed correctly, and second that all items listed correspond to what you actually ordered. For the first we need automated reasoning tools.
Can you check your computer program for correctness without using complicated mathematics and logic? With this book, you will see that you can check much more than you might have thought.
Correctness for a computer program means that it meets its specification, that it does exactly what its users expect it to do. If the specification itself is complex, for example “plot a course for a spacecraft to reach Mars”, then inevitably the calculations the program carries out will be complex as well. But translating any specification into a corresponding computer program is a hazardous process for most people. Even a single bug introduced while writing the program, whether it’s simple or complex, can cause catastrophic failure: a crash landing rather than a safe descent.
Formal methods is the body of knowledge used to reduce drastically the bugs introduced as programs are written. It is however often taught with prerequisites that seem to insist that mathematical logic needs to be mastered first. But it need not: that is the “Informally” in our book’s title.
This text shows how to apply reliable principles of program design, using ordinary English to write assertions, the preconditions, postconditions and invariants, not only for conventional programs but also for data structures and for concurrency. However it also provides a conceptual basis, a “launch pad”, for those who would like later to pursue more serious study in formal methods and its uses.
Reasoning about and developing concurrent sequential programs is introduced, showing how its success depends crucially on the methods covered in the first two Parts above, i.e. the focus on “what is true” rather than on the conventional “what happens here”. The latter, though intuitively appealing to beginners, and what is usually taught, is so much less effective. Local- and global correctness are motivated and explained. Simple examples of concurrent systems are used as running examples (automatic teller machines used simultaneously) and motivate the ideas of atomic operations, critical sections and mutual exclusion, and how those, in turn, are implemented by locks that create uninterruptible program sections. The historical origin of those locks, originally presented as “P()” and “V()”, is provided in English translation.
Here are summarised all the reasoning rules given throughout the text, all in one place. A quick search for “What’s the rule for that?” can therefore be done here.
Three “design patterns” induced by concurrency are introduced and explained. The first is the use of primitive test-and-set or compare-and-swap operations, implemented directly in hardware, to implement very specific atomic actions. The design pattern arising there is the “await” statement, generalising both. The second design pattern is the use of await statements themselves to implement locks and unlocks that create critical sections. The third (though not elaborated here) is the importance of the “synchronised methods” that are a principal feature of currency control in some modern high-level programming languages. Deadlock, livelock and starvation –all three new programming risks introduced by concurrency– are explained and motivated. It is pointed out that even while-loops, now universally accepted but once controversial, were once design patterns as well.
As a first example of a transformation trading simplicity for efficiency, it’s shown in detail how to take the Fibonacci-number program from its simple, but slow O(N) formulation to a blinding fast O(log N) version that –on its own– is completely incomprehensible. It is correct, but you can’t see that at all by just browsing its concrete code. Your conviction that it is nevertheless correct is based on your step-by-step careful reasoning in the data-refinement style introduced here.
A key feature of the formally motivated approach is the use of loop invariants, and they are introduced here. Not only do they help to write loops correctly the first time, but they sometimes even guide the programmer towards what the loop’s code should be, guaranteeing its eventual correctness at every step. In this chapter, techniques for finding invariants are explained, and each technique is illustrated by examples of programs they help to construct.
To show the startling effect our approach can have on code quality, a single exercise –slightly tricky but still first-year level– is programmed using three different approaches. The first is how a good student might proceed: it takes nine lines, but uses an extra array and needs diagrams to check it. The second solves the same problem, but applies a teacher-supplied insight to remove the extra array (still needing eight lines of code). The third uses the methods of this text, again on the same problem, but no diagrams or extra array are needed. The resulting program is only four lines long, and is correct by construction.
A well known example of a catastrophically failing program fragment is studied, one that was deployed world-wide. We’ll see how the failure could have been avoided by using the informal yet rigorous methods explained here, i.e. those based on formal methods but applied informally, as explained in the following chapters.
We also give a brief introduction to requirements, specifications, assertions, pre- and postconditions, invariants, variants and infinite loops, all of which are studied in greater detail later on.
Flowcharts and commenting style are contrasted and, in particular, we suggest how to improve program documentation in general, writing comments that state “What’s true here” rather than “What this code did”.
The second part of the analogy proposed above (Chapter 18) is that we need to be sure that all items on the restaurant bill correspond to what we actually ordered: that form of “testing” has little to do with adding things up --- rather it involves going around the table and ticking off (even if only mentally) what people ordered and ate. For programs, that means actually running the program with many different inputs and seeing that its results match what you expect. Formal methods, whether informally or machine checked, therefore form only a small part of the overall path from an original requirements statement to an assurance that the program does what it actually should. This chapter discusses briefly how an overall path, showing how and where (in-)formal methods fit in, is key to the whole enterprise.
The famous “Peterson’s algorithm” for lock-free mutual exclusion is used as a small but intricate case study in the application of the Owicki–Gries method. Although it is only four lines long, the algorithm was discovered a full 15 years after the first solution to that problem (Dekker, eight lines including nested loops). It’s first presented at an abstract level (which nevertheless requires Owicki–Gries to check rigorously even there), and is then made concrete using the data-refinement techniques of Part II to replace a two-element queue by three Booleans (due to Misra). That however leaves a further problem of replacing an atomic multiple assignment by two single assignments, again requiring Owicki–Gries to check its correctness, and finally an issue of an await condition that refers to two shared variables, motivating the concept of Boolean stability.
A case study is presented here of “the Mean Calculator”, an encapsulation that states clearly what it is supposed to do but whose concrete version is radically more efficient (in space) than the abstract version. Some emphasis is laid on the importance of pre- and postconditions of the methods of the class, and how their correct formulation is induced by the data-transformation process.
A program can fail by giving the wrong answer. But sometimes it fails by giving no answer at all: an infinite loop. The conceptual tool for ensuring that loops don’t “go infinite” is the variant, which in its basic form is simply showing that your loop steadily decreases a non-negative integer counter. Yet that is precisely what the program in Chapter 1 did not do: and millions of devices failed as a result. Too simple to bother with? Variants can sometimes however do more interesting things than simply counting, and those are discussed too.
The Owick--Gries method for sequential shared-variable concurrency was an early, perhaps the first, systematic approach to assertion-based reasoning about several programs’ executing simultaneously: it dramatically reduced the size and resulting uncertainly that considering all possible interleavings induced by parallel execution normally entails. Its key concepts of local vs. global correctness are discussed in detail and, fortuitously, local correctness is exactly what was presented and extensively explained in Part I. The novel global correctness also leverages the material of Part I, but in an unexpected way –almost a “eureka” moment– where it is realised that the interference caused by concurrency should be seen as the statements of one thread “interfering” with the assertions of another.
Data structures, and their transformation from “abstract” to “concrete”, are enabled by encapsulation: if certain rules are followed then an abstract, easy to understand but inefficient data structure can systematically and reliably be transformed into one that, in order to improve its efficiency, has necessarily become more complex — yet nevertheless behaves functionally exactly as its original version did. The key tools for that are coupling invariants and, more generally data-type invariants and “ghost” or “auxiliary” variables. Two extended examples are given of how that is done.
This text shows how to apply reliable principles of program design, using ordinary English to write assertions, the preconditions, postconditions and invariants, not only for conventional programs but also for data structures and concurrency. However, it also provides a conceptual basis, a “launch pad”, for those who would like later to pursue more serious study in formal methods and their uses.