Understanding Programming Languages
Understanding Programming Languages
B. Jones
Understanding
Programming
Languages
Understanding Programming Languages
Cliff B. Jones
Understanding Programming
Languages
Cliff B. Jones
School of Computing
Newcastle University
Newcastle upon Tyne, UK
This Springer imprint is published by the registered company Springer Nature Switzerland AG
The registered company address is: Gewerbestrasse 11, 6330 Cham, Switzerland
Preface
The principal objective of this book is to teach a skill; to equip the reader with a way
to understand programming languages at a deep level.
There exist far more programming languages than it makes sense even to attempt
to enumerate. Very few of these languages can be considered to be free from issues
that complicate –rather than ease– communication of ideas.
Designing a language is a non-trivial task and building tools to process the lan-
guage requires a significant investment of time and resources. The formalism de-
scribed in this book makes it possible to experiment with features of a programming
language far more cheaply than by building a compiler. This makes it possible to
think through combinations of language features and avoid unwanted interactions
that can confuse users of the language. In general, engineers work long and hard on
designs before they commit to create a physical artefact; software engineers need to
embrace formal methods in order to avoid wasted effort.
The principal communication mode that humans use to make computers perform
useful functions is to write programs — normally in “high-level” programming lan-
guages. The actual instruction sets of computers are low-level and constructing pro-
grams at that level is tedious and unintuitive (I say this from personal experience
having even punched such instructions directly into binary cards). Furthermore these
instruction sets vary widely so another bonus from programming in a language like
Java is that the effort can migrate smoothly to computer architectures that did not
even exist when the program was written.
General-purpose programming languages such as Java are referred to simply as
“High-Level Languages” (HLLs). Languages for specific purposes are called “Do-
main Specific” (DSLs). HLLs facilitate expression of a programmer’s intentions by
abstracting away from details of particular machine architectures: iteration can be
expressed in an HLL by an intuitive construct — entry and return from common
code can be achieved by procedure calls or method invocation. Compilers for HLLs
also free a programmer from worrying about when to use fast registers versus slower
store accesses.
Designing an HLL is a challenging engineering task: the bigger the gap between
its abstraction level and the target hardware architecture, the harder the task for the
v
vi Preface
compiler designers. A large gap can also result in programmers complaining that
they cannot get the same efficiency writing in the HLL as if they were to descend to
the machine level.
An amazing number of HLLs have been devised. There are many concepts that
recur in different languages but often deep similarities are disguised by arbitrary
syntactic differences. Sadly, combinations of known concepts with novel ideas often
interact badly and create hidden traps for users of the languages (both writers and
readers).
Fortunately, there is a less expensive way of sorting out the meaning of a pro-
gramming language than writing a compiler. This book is about describing the
meaning (semantics) of programming languages. A major objective is to teach the
skill of writing semantic descriptions because this provides a way to think out and
make choices about the semantic features of a programming language in a cost-
effective way. In one sense a compiler (or an interpreter) offers a complete formal
description of the semantics of its source language. But it is not something that
can be used as a basis for reasoning about the source language; nor can it serve
as a definition of a programming language itself since this must allow a range of
implementations. Writing a formal semantics of a language can yield a far shorter
description and one about which it is possible to reason. To think that it is a sensible
engineering process to go from a collection of sample programs directly to coding a
compiler would be naive in the extreme. What a formal semantic description offers
is a way to think out, record and analyse design choices in a language; such a de-
scription can also be the basis of a systematic development process for subsequent
compilers. To record a description of the semantics of a language requires a notation
— a “meta-language”. The meta-language used in this book is simple and is covered
in easy steps throughout the early chapters.
The practical approach adopted throughout this book is to consider a list of issues
that arise in extant programming languages. Although there are over 60 such issues
mentioned in this book, there is no claim that the list is exhaustive; the issues are
chosen to throw up the challenges that their description represents. This identifies a
far smaller list of techniques that must be mastered in order to write formal semantic
descriptions. It is these techniques that are the main takeaway of the current book.
Largely in industry (mainly in IBM), I have worked on formal semantic descrip-
tions since the 1960s1 and have taught the subject in two UK universities. The payoff
of being able to write formal abstract descriptions of programming languages is that
this skill has a far longer half-life than programming languages that come and go:
one can write a description of any language that one wants to understand; a lan-
guage designer can experiment with combinations of ideas and eliminate “feature
interactions” at far less cost and time than would be the case with writing a compiler.
The skill that this book aims to communicate will equip the reader with a way
to understand programming languages at a deep level. If the reader then wants to
1 This included working with the early operational semantic descriptions of PL/I and writing the
later denotational description of that language. PL/I is a huge language and, not surprisingly, con-
tains many examples of what might be regarded as poor design decisions. These are often taken as
cautionary tales in the book but other languages such as Ada or CHILL are not significantly better.
Preface vii
design a programming language (DSL or HLL), the skill can be put to use in creating
a language with little risk of having hidden feature interactions that will complicate
writing a compiler and/or confuse subsequent users of the language.
In fact, having mastered the skill of writing a formal semantic description, the
reader should be able to sketch the state and environment of a formal model for
most languages in a few pages. Communicating this practical skill is the main aim
of this book; it seeks neither to explore theoretical details nor to teach readers how
to build compilers.
The reader is assumed to know at least one (imperative) HLL and to be aware of
discrete maths notations such as those for logic and set theory — [MS13], for ex-
ample, covers significantly more than is expected of the reader. On the whole, the
current book is intended to be self-contained with respect to notation.
The material in this book has been used in final-year undergraduate teaching for
over a decade; it has evolved and the current text is an almost complete rewrite.
Apart from a course environment, it is hoped that the book will influence design-
ers of programming languages. As indicated in Chapter 1, current languages offer
many unfortunate feature interactions which make their use in building major com-
puter systems both troublesome and unreliable. Programming languages offer the
essential means of expression for programmers — as such they should be as clean
and free from hidden traps as possible. The repeated message throughout this book
is that it is far cheaper and more efficient to think out issues of language design be-
fore beginning to construct compilers or interpreters that might lock in incompletely
thought-out design ideas.
Most chapters in the book offer projects, which vary widely in their challenge.
They are not to be thought of as offering simple finger exercises — some of them
ask for complete descriptions of languages — the projects are there to suggest what
a reader might want to think about at that stage of study.
Some sections are starred as not being essential to the main argument; most chap-
ters include a section of “further material”. Both can be omitted on first reading.
Writing style
“The current author” normally eschews the first person (singular or plural) in tech-
nical writing; clearly, I have not followed this constraint in this preface. Some of the
sections that close each chapter and occasional footnotes also use the first person
singular when a particular observation warrants such employment.
viii Preface
Acknowledgements
I have had the pleasure of working with many colleagues and friends on the subject
of programming language semantics. Rather than list them here, their names will
crop up throughout the book. I have gained inspiration from students who have fol-
lowed my courses at both Newcastle University and the University of Manchester.
I’m extremely grateful to Jamie Charsley for his insertion of indexing commands.
I owe a debt to Troy Astarte, Andrzej Blikle, Tom Helyer, Adrian Johnson and Jim
Woodcock, who kindly offered comments on various drafts of this book. (All re-
maining errors are of course my responsibility.) My collaboration with Springer
–especially with Ronan Nugent– has been a pleasure. I have received many grants
from EPSRC over the years — specifically, the “Strata” Platform Grant helped sup-
port recent work on this book.
Contents
2 Delimiting a language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.1 Concrete syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Abstract syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.3 Further material . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3 Operational semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1 Operational semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.2 Structural Operational Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.3 Further material . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4 Constraining types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.1 Static vs. dynamic error detection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.2 Context conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.3 Semantic objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.4 Further material . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5 Block structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.1 Blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.2 Abstract locations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.3 Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.4 Parameter passing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.5 Further material . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
ix
x Contents
11 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
11.1 Review of challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
11.2 Capabilities of formal description methods . . . . . . . . . . . . . . . . . . . . . 160
11.3 Envoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Contents xi
D COOL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
D.1 Auxiliary objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
D.2 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
D.3 Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
D.4 Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
D.5 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
D.6 Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Chapter 1
Programming languages and their description
This chapter sets the scene for the rest of the book. Sections 1.1–1.3 outline the
problems presented by programming languages and their related tools; Section 1.4
points out that there is material from the study of natural languages that is relevant
to the problems of describing artificial languages such as those used to program
computers; an overview of a range of techniques for recording the meaning of pro-
gramming languages is given in Section 1.5 and Section 1.6 introduces the specific
notation used throughout this book. In common with most of the following chap-
ters, this one closes with a section (1.7) that contains further material — in particular
such sections point to related reading.
Picking up the point about the productivity of programmers from the preceding sec-
tion, there was a panel discussion2 on Programming Languages and their Semantics
at a conference in Pittsburgh in May 2004 to which Vaughan Pratt put the intriguing
question of how much money the panelists thought that high-level programming lan-
guages had saved the world. Pratt was aware of the difficulty of the question because
he added a subsidiary query as to whether an answer to the main question would
qualify for a Nobel Prize in economics. Without hoping –even with time to reflect–
to provide a number, considering Pratt’s question is illuminating. There are almost
certainly millions of people in the world for whom programming forms a significant
part of their job. Programmers tend to be well paid. A good programmer today can
create –using a high-level programming language such as Java– systems that were
unthinkable when programs could only be communicated in machine code. A good
programming language can, moreover, ensure that many mistakes made by even an
average-level programmer are easily detected and corrected. To these powerful sav-
ings, ease of program migration can be added: avoiding the need to write versions of
essentially the same program for different machine instruction sets must itself have
saved huge wastage of time and money.
It is also important to appreciate the distribution of costs around programs: even
back in the days of mainframes, the majority of programs cost more to develop than
their lifetime machine usage costs. Since that period, decades of tracking Moore’s
Law [Moo65] have dramatically reduced the cost of executing programs. With mod-
ern interactive applications, and factoring in the human cost of user time, the actual
machine time costs are close to irrelevant. The productivity of programmers and
their ability to create systems that are useful to the end user are the paramount con-
cerns.
2 The panelists were John McCarthy, John Reynolds, Dana Scott and the current author.
1.2 The importance of HLLs 3
The mere fact that there are thousands3 of programming languages is an indica-
tion that their design is a subject of interest. The additional observation that there is
no one clear “best buy” suggests that designing a high-level programming language
is non-trivial. One tension in design is between offering access to the power of the
underlying machine facilities so that programs can be made to be efficient versus
providing abstractions that make it easier to create programs. The problems of writ-
ing programs are also likely to be exceeded by the costs of their maintenance, where
the intentions of the original programmer must be understood if the person changing
the program is to do so safely.
A good programming language offers several aids to the programmers who use
it to express their ideas:
• data structuring
• common forms of control can be built into a language (with only the compiler
having to fiddle with the specific machine-level instruction sequences that realise
the higher-level expressions)
• protecting programmers from mistakes
It is worth expanding on the issue of how programming languages provide ab-
stractions. Most computers have a small number of registers, in which all basic
computations are performed and instructions can only access one additional storage
cell. A calculation involving several values must –at the machine level– involve a
series of instructions and might require the storage of intermediate results. A first
level of abstraction allows programmers to write arbitrary expressions that have to
be translated into strings of machine operations.
Clear layers of abstraction can be seen with regard to data representation. The
storage of a computer can most easily be viewed as a sequence of small contain-
ers (bits, bytes or words).4 From its inception, the FORTRAN language supported
declaring arrays of multiple dimensions whose elements could be addressed in a
style familiar to mathematicians (e.g. A[I, J ∗ 3]). Such operands have to be trans-
lated into sequences of machine instructions that compute the machine address
in the sequence of addressable storage cells of the machine. The APL language
pushed arrays to extremes and even PL/I provides ways of manipulating slices of n-
dimensional arrays — such sub-arrays can then be manipulated as if they had been
declared to be arrays of lesser dimensions.
Array elements are all of one type. Both the COBOL and Pascal languages of-
fer ways of defining inhomogeneous records5 that facilitate grouping data elements
whose types differ from each other. Furthermore the whole inhomogeneous object
can be used (e.g. as parameters or in input/output) as a single data item or its com-
ponents can be addressed separately.
3 As early as 1969, Jean Sammet’s book [Sam69] recognised 500 programming languages; a web
site that claimed to be listing all known languages got to 8,512 in 2010 then gave up.
4 Of course, computer architectures normally include registers and might themselves provide ab-
straction such as “virtual memory” (supported by “paging”). Some of these features are discussed
when issues relating to code generation are considered in subsequent chapters.
5 Some languages, including PL/I, use the term “structures” rather than records.
4 1 Programming languages and their description
List processing facilitates the creation of arbitrary graphs of data by allowing the
manipulation of something like machine addresses as data. Early languages such as
IPL-V, Lisp and Scheme had to develop a lot of techniques for garbage collection
before list processing could be adopted into more mainstream languages.
The concept of objects is a major contribution to the abstractions, as offered in
object-oriented languages such as Simula, Smalltalk and Java. Object orientation is
discussed in Section 6.2 and its role in taming concurrent computation is the main
topic of Chapter 9.
A similar story of programming languages developing abstraction mechanisms
above the raw conditional jumps of machine level programming could be developed:
conditional if constructs, compound statement lists, for and while looping constructs
and –above all– recursion make it possible to present complicated programs as a
structured and readable text. Tony Hoare reported [Hoa81, p.76] that he could not
express his innovative Quicksort [Hoa61] algorithm until he learned the ALGOL 60
programming language.6 This almost certainly contributed to his judgement on AL-
GOL 60 in [Hoa74b]:
Here is a language so far ahead of its time, that it was not only an improvement on its
predecessors, but also on nearly all its successors. Of particular interest are its introduction
of all the main program structuring concepts, the simplicity and clarity of its description,
rarely equalled and never surpassed.
A final, but crucial, area where programming language designers have sought
to offer abstractions is that of concurrency.7 Most computers offer rather low-level
primitives such as a compare-and-swap instruction; programming even at the level
of Dijkstra’s semaphores [Dij62, Dij68a] is extremely error-prone. The whole sub-
ject of concurrency in programming languages is still in evolution and its modelling
occupies several chapters later in this book. References to histories of the evolution
of programming languages are given in Section 1.7.
Here, the concern is with the problem of knowing how to record the meaning –or
semantics– of useful abstractions such as those sketched above. Semantic descrip-
tion is a non-trivial problem and occupies Chapters 3–10 of this book. The payoff
for mastering these techniques is large and can have effects far beyond the language
design team. It is just not realistic to expect anyone to be able to design a program-
ming language that will overcome the challenges listed above by sketching sample
programs and then proceeding to compiler writing. In fact, such a procedure is a
denial of everything known about engineering. The ability to record the meaning of
a language at an abstract level means that designers can think out, document and
refine their ideas far less expensively than by coding processors. Furthermore, the
6 Although initially designed as a publication language (and the vast majority of algorithms pub-
lished in the Algorithms section of Communications of the ACM were written in ALGOL 60) the
language contributed so many fundamental concepts to programming language design that it has
had enormous influence (see [AJ18, §1.4]).
7 In contrast, so-called “weak” (or “relaxed”) memory is a hardware feature which might in-
flict considerable damage on software development because it is hard to find apposite abstrac-
tions [ŠVZN+ 13, LV16].
1.3 Translators, etc. 5
far bigger bonus is that users of better thought-through languages will become more
productive and stumble into far fewer unexpected “feature interactions”.
Before techniques for describing semantics and the case for formalism are dis-
cussed in Section 1.5, it is worth considering the software tools that process pro-
gramming languages.
In the early years of programming languages, the most frequent phrase that we heard was
that the only way to program a computer was in octal.
9 The origin of this word is explained in [Bey09] as deriving from the first attempts to automate
program construction by collecting (compiling) a group of subroutines. It is surprising that this is
the term that continues to be more commonly used for what is clearly a process of translation.
10 In a detailed study [vdH19] of the ALGOL 60 implementation by Dijkstra and his colleagues,
Gauthier van den Hove makes clear that, even early in the history of language processing, the
question of compiling and interpreting was seen less as a dichotomy and more as a spectrum.
11 Functional languages such as Miranda [Tur85] and Haskell [Hut16] make it easier to reason
about programs as though they were mathematical functions. Early implementations of functional
languages tended to perform considerably more slowly than imperative languages but this gap has
reduced and some serious applications are now written in functional languages. Logic languages
such as Prolog [SS86] make a further step both in expressiveness and in their challenge to offer
performance. (In fact, Prolog still has imperative features.) The techniques presented in this book
6 1 Programming languages and their description
imperative languages might move the arm of a robot, project an image or update a
database.) Most of the statement types actually only orchestrate the order in which
updates to variables are made by assignments.
As outlined above, straightforward expression evaluation has to be implemented
by loads, stores and single-address operations of the machine. But a compiler will
often try to optimise expression evaluation. For example “common sub-expressions”
might be evaluated only once. Even in early FORTRAN compilers, expressions
that occurred inside FOR loops but did not depend on variables whose values were
changed in the loop could be evaluated once before the loop. More subtly, expres-
sions such as those which compute array indexes in the loop could be subject to
strength reduction so that the effect of multiplication could be achieved by addi-
tion each time round a loop. Many of these optimisations are known as “source-
to-source” in the sense that there is an equivalent source program that represents
the optimised code. There are other optimisations such as those concerned with
maximising efficiency by minimising the number of loads and saves for registers
(especially index registers for address calculation) that cannot be expressed in the
source language. In either case, it is clearly essential that the “optimisations” do
not result in a program doing something different from the programmer’s legitimate
expectations. In other words, any optimisations must respect the semantics of the
given high-level language.
Similar points could be made about compiling control constructs. Most machines
provide a primitive (conditional) jump instruction. High-level languages offer far
more structured control constructs. The task of a compiler is to translate the latter
into the former in a way that results in efficient code. But, again, that low-level code
must respect the semantics of the programming language.
Three final points can be made about tools for executing high-level languages:
The languages that are spoken by human beings were not designed by committees13
— they just evolved and they continue to change. The evolution process is all too
obvious from the irregularities in most natural languages. The task of describing
such natural languages is therefore very challenging but, because they have been
around longer, it is precisely on natural languages that the first systematic studies
were undertaken. Charles Sanders Peirce (1839-1914) used the term Semiotics for
the study of languages. Peirce14 divided the study of languages into:
Some general points about semantics can be made concrete by looking at a few
specific programs. The following program (in a syntax close to that of ALGOL 60):
13 Of course, there are a small number of exceptions such as Volapük and Esperanto.
14 Pronounced “Purse”.
8 1 Programming languages and their description
non-determinism: most HLLs actually permit a range of results (e.g. because of concurrency or
to leave implementors some flexibility): even if a user is interested in the result of a program on
1.6 A meta-language 11
As indicated in Section 1.2, a premature leap from example programs for a new
language to beginning to write a compiler for the language does not constitute sound
engineering. A process of experimenting with language choices within a formal
model can iron out many potential consistencies more quickly and far less expen-
sively. The formal model can also serve as the starting point for a systematic design
process for compilers once the language has been thought out.
Chapter 11 lists a number of formal descriptions of extant programming lan-
guages. One possibility opened up by making these descriptions formal is to pro-
vide tools that use them. There is quite good support for reasoning about program
correctness from various forms of property-oriented semantics, although this nor-
mally applies to restricted subsets of major languages such as SPARK-Ada. There
are far more tools based on formal ideas that check specific properties of programs
in languages (e.g. possible dereferencing of null pointers, deadlock detection).
Having listed the technical criteria of being able to reason about programs written
in L and acting as a base for compilers for L , there remains a bigger objective. The
main aim of this book is to ensure that formal models are more widely used in the
design of future programming languages. It is to be regretted that most of the current
main-line programming languages have semantic traps that surprise programmers
and/or complicate the task of providing compilers for the language. Such anomalies
can be detected early by writing formal semantic descriptions before tackling the
far more costly job of programming a compiler.
What then is the impediment to writing, for example, an operational semantics
of a language? Section 1.6 introduces a meta-language that should not prove dif-
ficult for any programmer to understand. With that one meta-language, he or she
can describe any (imperative) programming language. Chapter 3 covers the basic
method of writing an operational semantics and subsequent chapters consider new
challenges and eight known techniques for coping with them. Experience of teach-
ing these methods over the years suggests that the real hurdle is learning to employ
the right degree of abstraction in tackling language descriptions. That can proba-
bly only be learned by looking at examples, and Chapters 3–9 provide many such
examples.
1.6 A meta-language
The term object language can be used to refer to the language whose syntactic and
semantic description is to be undertaken17 and the script letter L is used when
making a general point rather than discussing a specific object language such as
FORTRAN or Java. In contrast, languages that are used in the description of an
object language are referred to as meta-languages.
a single input item, knowing that the result is as required in one implementation of L does not
guarantee that the program will give the same result on a different correct implementation of L .
17 The qualification “object” indicates that it is the object of study; this is not to be confused with
“object code”, which is what a translator generates when it compiles a source program.
12 1 Programming languages and their description
Notation for VDM sequences is introduced in Section 2.2 (see Figure 2.2) and
maps in Section 3.1 (Figure 3.1) when they are needed.
18 Many useful textbooks exist on the notations of discrete mathematics including [Gro09].
1.6 A meta-language 13
B {true, false}
¬E negation (not)
E 1 ∧ E2 conjunction (and)
E1 , E2 are conjuncts
E1 ∨ E2 disjunction (or)
E1 , E2 are disjuncts
E 1 ⇒ E2 implication
E1 antecedent, E2 consequent
E1 ⇔ E2 equivalence
∀x ∈ S · E universal quantification
∃x ∈ S · E existential quantification
The symbols used for logic (technically, first-order predicate calculus) vary be-
tween textbooks and Figure 1.2 indicates the symbols used in this book.
It is common to set out proof rules that define valid deductions about the logical
operators. Examples that are assumed below include a definition of implication:
¬ E1 ∨ E2
⇒ -I
E1 ⇒ E2
equivalence as bi-implication:
E1 ⇒ E2
E ⇒ E1
⇔ 2
E1 ⇔ E2
What can be thought of as a definition of disjunction in terms of conjunction is
characterised by the bi-directional rule:
¬ (¬ E1 ∧ ¬ E2 )
de-Morgan-1
E1 ∨ E2
is one of de Morgan’s laws; another is:
¬ (E1 ∨ E2 )
de-Morgan-2
¬ E1 ∧ ¬ E2
14 1 Programming languages and their description
This book tackles the semantics of imperative languages such as ALGOL and Java.
Descriptions of functional and logic programming languages (e.g. Scheme [ASS85],
Prolog [SS86]) would use the same ideas but it is worth saying a few words about
the differences. Rather than design algorithmic solutions to solve problems, it would
be attractive to write logical assertions and have a program find solutions. Even
in the restricted world of mathematics or logic19 this is impossible in general, but
Prolog-style logic programming moves in this direction. Unfortunately, in order to
circumvent the problems of massive search spaces, imperative features have been
included in Prolog itself [SS86].
An intermediate approach between fully imperative and logic languages is the
class of functional programming languages. In the extreme, such languages avoid
all imperative constructs such as assignment. This makes it possible to reason
about functional programs as though they are mathematical functions. Among other
things, this avoids the need for a Hoare-style logic. Most functional languages actu-
ally offer some limited ability to “change the world”.
The reason that the reader should have no difficulty with these extended meanings
is that key properties such as the symmetry of conjunction and disjunction hold:
E1 ∨ E2
∨ -sym
E2 ∨ E1
E1 ∧ E2
∧-sym
E2 ∧ E1
In fact, the key difference with conventional propositional logic is that the so-called
“law of the excluded middle” (P ∨ ¬ P) only holds in LPF where it is established
that P denotes a truth value.
a b ¬a a∧b a∨b a ⇒ b a ⇔ b
true true false true true true true
∗ true ∗ ∗ true true ∗
false true true false true true false
true ∗ ∗ true ∗ ∗
∗ ∗ ∗ ∗ ∗ ∗
false ∗ false ∗ true ∗
true false false true false false
∗ false false ∗ ∗ ∗
false false false false true true
Quantifiers are in no way mysterious. Over finite sets, they are just convenient
abbreviations:
(∃i ∈ {1, · · · , 3} · p(i)) ⇔ (p(1) ∨ p(2) ∨ p(3))
(∀i ∈ {1, · · · , 3} · p(i)) ⇔ (p(1) ∧ p(2) ∧ p(3))
Even the infinite cases should present no difficulty:
∀i ∈ N · ∃j ∈ N · i < j
With all of the quantifiers, the scope is assumed to extend as far as possible to
the right; parentheses are not required for this case but they can be used to define
different grouping.
This leaves only the end cases with the empty range for the bound variable to
note:
∃i ∈ { } · p(i) ⇔ false
∀i ∈ { } · p(i) ⇔ true
which are obviously related from the quantifier versions of de Morgan’s laws:
¬ (∃x · p(x))
de-Morgan-3
∀x · ¬ p(x)
1.7 Further material 17
¬ (∀x · p(x))
de-Morgan-4
∃x · ¬ p(x)
There are other logics that attempt to handle terms that fail to denote a value
and a comparison is given in [CJ91]. Details of the specific LPF used in VDM
are addressed in [BCJ84, JM94]. Kleene (in [Kle52]) attributes the propositional
operator definitions in Figure 1.3 to [Łuk20]. Other papers that address the issue of
undefinedness include [Kol76, Bla86, KTB88, Bli88].
Chapter 2
Delimiting a language
The body of this book addresses the task of describing –or designing– the semantics
of programming languages. This chapter prepares the way for that task by introduc-
ing the division between syntax and semantics. A tiny language is introduced which,
because it has few surprises, can be used to explain the description method. As more
complicated language features are considered in later chapters of this book, they are
treated independently as far as is possible (e.g. input/output is modelled in Sec-
tion 4.3.1 and a similar extension could be made to the concurrent object-oriented
language in Chapter 9).
For an extant language,1 it is necessary to delimit the set of allowed programs
before their meaning can be discussed: Section 2.1 outlines “concrete syntax” no-
tations for fixing the textual strings of an object language (such strings include var-
ious marks that make it possible to parse the strings); Section 2.2 addresses a way
of defining the “abstract syntax” of programs without the symbols needed to facili-
tate parsing. This chapter also covers most of the VDM notation used in the current
book. The topic of semantics is first tackled in Chapter 3 and runs throughout the
remainder of this book.
Normal Form2 (also known as Backus-Naur Form).3 BNF was used in [BBG+ 60]
to define the concrete syntax of ALGOL 60. A slight elaboration of BNF is Niklaus
Wirth’s Extended BNF — EBNF is described in [Wir77].
Despite claiming that devising syntactic meta-languages is relatively simple, the
first “challenge” is:
Syntax is about content and “concrete syntax” concerns the linear sequence of
characters that are allowed in the object language. A well-designed concrete syntax
can also suggest a structure that points to the semantics of the object language. Cru-
cially, a concrete syntax must be devised that makes it possible to “parse” strings4
but it is easier to consider the generation of strings first.
Starting with a simple example from natural language, a grammar can be written
as a set of BNF rules:
hSimpleSentencei : : = hPronounihVerbihSpreadi.
hPronouni : : = I | You
hVerbi : : = like | hate
hSpreadi : : = Marmite | Peanut Butter
This defines a set of eight sentences:
I like Marmite., I like Peanut Butter.,
I hate Marmite., I hate Peanut Butter.,
You like Marmite., You like Peanut Butter.,
You hate Marmite., You hate Peanut Butter.
Looking more carefully at the BNF rules, each rule starts with a “non-terminal”,
which is marked by enclosure in h· · ·i; the non-terminal that is being defined is
separated from its definition by “: : =”; this is followed by the definition, which is
intended to fix the set of possible strings; all but the first rule above list options
separated by a vertical bar (|). Such a definition can be made of a sequence of items
that can be either “terminal” strings or non-terminal symbols that should be defined
in other rules. Terminal (in the sense that no further production is needed) symbols
just stand for themselves; non-terminal symbols can be replaced by any string that
is valid from their production rule.
The rules above are not unique in generating the strings — all of the above and
more are generated by:
hSimpleSenti : : = hWordihWordihWordi.
hWordi : : = I | You | like | hate | Marmite | Peanut Butter
2 Marking the insight of John Warner Backus (1924–2007) who proposed the notation.
3 Acknowledging Peter Naur’s (1928–2016) contribution to the development and use of BNF.
4 It is noted below that designing languages that are easy to parse is itself a technical challenge but
is not within the scope of the current book — references to this material are given in Section 2.3.
2.1 Concrete syntax 21
One way to move from a finite language like hSimpleSenti to languages with an
unbounded number of possible strings5 is to use recursion — for example:
hParagraphi : : = hSimpleSenti | hParagraphihSimpleSenti
A potential problem can be seen if a pronoun (“He”) that requires a different
form of the verb (“likes”) is added to the language. Writing:
hPronouni : : = I | You | He
hVerbi : : = like | likes | · · ·
can generate the ungrammatical string “He like Marmite.” This could be resolved
by splitting hSimpleSentencei but the more general issue of “context dependancy” is
addressed below in Section 4.2.
Moving to describing the concrete syntax of an example programming language,
it has to be recognised that there is a huge variety of syntax styles across the many
known programming languages and debates about such stylistic differences often
generate more heat than light. Interestingly for the main purpose of this book, most
such syntactic argument has no impact at all on semantics. It is for this reason that,
from Section 2.2 onwards, semantic discussions are based on abstract syntaxes.
When however example programs are presented, concrete syntax is used. The
stylistic choice here is that a vaguely ALGOL/Pascal flavour of concrete syntax is
used for sequential programs and a move towards Java syntax is made for concurrent
(object-oriented) languages.
For the initial simple language, a complete hProgrami might be bracketed by key-
words and contain (yet to be defined) lists of allowed variable names and statements:
hProgrami : : = program vars hIdsi: hStmtsi end
The design choice recorded in this rule is that the names of variables used in
hStmtsi of a hProgrami must be declared in the list of identifiers given after the key-
word vars. For now, variables can only contain natural numbers (N) — multiple
types are addressed in Chapter 4. Lists of identifiers are separated by commas:
hIdsi : : = hIdi [, hIdsi]
Here, the square brackets of EBNF are used to show that the bracketed portion can
be omitted. This could equally be written as:
hIdsi : : = hIdi | hIdi, hIdsi
No grammar is given here for hIdi — were this done, it would probably require
that the first character was a letter followed by a string of digits or letters.6
5 EBNF provides the alternative of showing that a group of things can be repeated:
hParagraphi : : = hSimpleSenti∗
But recursion is stronger than iteration because the former can describe arbitrary nesting such as
bracketing in (()())
6 Some languages limit the length of identifiers.
22 2 Delimiting a language
Statements are separated by semicolons — notice that this syntax eliminates writ-
ing a semicolon after the last statement in a list:7
hStmtsi : : = [hStmti [; hStmtsi]]
Statements can be one of three types (for now):
hStmti : : = hAssigni | hIfi | hWhilei
The idea that the left- and right-hand sides of hAssigni should be separated by an
equality sign is anathema to mathematicians who point out that:
x = x+1
is a nonsense. ALGOL 60 uses : = between the left-hand side reference and the
expression which is to be evaluated and assigned to that reference:8
hAssigni : : = hIdi : = hArithExpri
There is an interesting parsing issue associated with hIfi — but this is discussed
below — for now a closing bracket (fi) is given to complete the conditional state-
ment:
hIfi : : = if hRelExpri then hStmtsi [else hStmtsi] fi
This is far from the only way to make a grammar unambiguous but some such device
is necessary to ensure that parsing is unique.9 Similarly:
hWhilei : : = while hRelExpri do hStmtsi od
Moving on to the definition of the two forms of expression:
hArithExpri : : = hBinArithExpri | hNaturalNumberi | hIdi | (hArithExpri)
notice that the fourth option for hArithExpri allows the insertion of parentheses to
distinguish the priority of operators between x ∗ y + z and x ∗ (y + z).
hBinArithExpri : : = hArithExprihBinArithOperatorihArithExpri
hBinArithOperatori : : = + | ∗
hRelExpri : : = hArithExprihCompareOperatorihArithExpri
hCompareOperatori : : = =|≤
hNaturalNumberi : : = hDigiti | hDigitihNaturalNumberi
hDigiti : : = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
7 Writing the semicolon between hStmti is in contrast to terminating every statement with the
punctuation symbol (as is done in most (European) languages, where a full stop marks the end
of a sentence). Many years ago, Jim Horning reported an experiment in which he compared the
number of mistakes made by programmers under each rule: in his experiment, the terminating
semicolon was actually the cause of fewer slips than its placing as a separator.
8 Some languages use “←” between the left- and right-hand sides of assignments.
9 Appendix A.1.2 shows how Java disambiguates conditional (and looping) statements. Tony Hoare
proposed the more radical idea of writing the test between the two statement sequences:
hIfi : : = hStmtsi / hRelExpri . hStmtsi
This proves convenient for showing algebraic properties of conditionals.
2.1 Concrete syntax 23
Notice that hRelExpri offers only a restricted way of obtaining a Boolean value;
it would be easy to add a more general form of logical expression to the syntax.
The intention is that any given hProgrami is finite (i.e. instead of taking recursive
options all of the time, the option to use terminal symbols is eventually chosen) but
the set of possible programs is infinite.
As with the natural language example above, there are other ways of defining
hArithExpri. One option would be to have a single set of strings for hExpri that
allowed all four operators. This would define a larger class of strings.
Grammars like those above can be used to generate the strings of a language; in
fact, a single generator program can easily be written that takes a grammar as input
and can generate (random) strings of the given syntax. Christopher Strachey showed
that a simple grammar could be used to generate English paragraphs that could pass
as love letters and IBM had a project in the 1960s that created random PL/I test
cases: “APEX” generated random strings from a grammar of PL/I.10
Using grammars as generators of languages finesses the issue of “ambiguity”.
Even writing the deliberately ambiguous grammar:
hAmbiguousi : : = a | ahAmbiguousi | hAmbiguousia
allows many different ways of generating strings of the letter “a” but their unique
generation can be recorded. If however the task is to analyse a string to determine
how the rules could be used to generate such a string, the ambiguity issue is serious.
Such analysis programs are called parsers and writing general-purpose parsers is a
significant challenge.11
and:
if a = b then
if c = d then x: = 1
else x: = 2
and these have different meanings.12
This is why some form of bracketing is added to the syntax of conditionals —
here it is the closing keyword fi but it can equally be (as in Java) some explicit
bracketing around the two sequences of statements contained in each conditional
statement — see Appendix A.1.2.
The front end of a compiler or interpreter for a language has to:
• decompose the sequence of characters into distinct “tokens” (keywords, identi-
fiers and constants);
• create a parse tree that shows how the sequence of tokens can be generated from
the grammar of the language (and produce, hopefully useful, diagnostics if the
input string is not valid with respect to the grammar of the language).
The grammar of a language must not be ambiguous in such a way that different
generating sequences have different meanings.
12This is sometimes referred to as the “dangling else” problem. With his typical pragmatism,
Niklaus Wirth simply ordained that, in Pascal, the else clause related to the closest if.
2.2 Abstract syntax 25
Section 2.2 shows how the use of an “abstract syntax” avoids these complica-
tions, so they are left aside here. There are however many concrete syntax issues for
language designers to resolve.
The concrete syntax notations covered in Section 2.1 give both a way of producing
the texts of programs and a way of parsing such texts. But, even for simple lan-
guages, such a concrete syntax can be “fussy” in that it is concerned with details
which make it possible to parse strings (e.g. the commas, semicolons, keywords
and those marks that serve to bracket strings that occur in recursive definitions).
Peter Landin used the lovely term “syntactic sugar” (which can be sprinkled on the
essential content).
26 2 Delimiting a language
ArithExpr
NaturalNumber
Id
( ArithExpr )
For a programming language such as C or Java, there are many different ways
of writing semantically indistinguishable programs. In PL/I, variable declarations
can be placed anywhere in a block but their position has no meaning; most lan-
guages allow comments that have no influence on semantics. It should thus be clear
that the concrete syntax is not a convenient basis for semantic descriptions. The
concrete syntax does have to be defined but, since the syntactic variants have iden-
tical semantics, basing a semantic description on a concrete syntax clouds it in an
unnecessary way. Here, the first big dose of abstraction is deployed and all of the
subsequent work is based on an “abstract syntax”.
It is again useful to relate what is done in a language description to the tools that
process programs. The parsing phase of a compiler or interpreter generates (a repre-
sentation of) a tree form of the program to be compiled. An abstract syntax defines
a class of objects that retain as little superfluous information as possible. In most
cases, such objects are tree-like in that they are (nested) VDM composite objects.
To achieve abstraction, sets, sequences and maps are used whenever appropriate.
This section reviews more VDM notation. The concepts will almost certainly be
familiar to any reader. The VDM notation for sets is listed in the previous chap-
2.2 Abstract syntax 27
ter (Figure 1.1). Sequences (sometimes referred to as lists) provide another useful
abstraction whose operators are listed in Figure 2.2.
Just as with X-set, instances of X ∗ are always finite. Notice that the head of a
sequence is the first element, thus hd [a, b] = a, whereas the tail of a sequence is a
sequence without its first element: tl [a, b] = [b]. Either of these operators applied to
the empty sequence is undefined and this serves as a reminder of VDM’s use of a
Logic of Partial Functions — see Section 1.6.
The indexes of a list are the set of natural numbers that can be used as indexes:
inds s = {1, · · · , len s}
The 4 is an equality but is used for a definition rather than a simple assertion such
as 1 + 1 = 2. An equivalent definition of uniquel would be
uniquel : X ∗ → B
uniquel(s) 4 len s = card elems s
28 2 Delimiting a language
The definition can even be written recursively to illustrate that VDM function
notation allows both case constructs and conditionals:
uniquel : X ∗ → B
uniquel(s) 4 cases s of
[] → true
[hd] y rest → if hd ∈ elems rest
then false
else uniquel(rest)
fi
end
Functions such as uniquel that yield Boolean results are referred to as predicates.
The concept of records is present in many programming languages (PL/I called
them structures); its use in meta-languages dates back to John McCarthy’s [McC66].
In VDM these sources are pulled together to make several facets automatic. An
example record class is defined as follows:
Example :: field-1 : X
field-2 : Y
Such a record definition automatically defines a function mk-R for record type R —
for building objects of Example from values of type X and Y:
mk-Example: X × Y → Example
where nil is a unique elementary object. Notice that, in contrast to record values
where the constructor makes the set unique, equality rules are genuine set equalities.
So with:
X=A|B
Y =B|C
it is not true that X and Y are disjoint.
Assign :: lhs : Id
rhs : ArithExpr
If :: test : RelExpr
then : Stmt∗
else : Stmt∗
While :: test : RelExpr
body : Stmt∗
ArithExpr = BinArithExpr | N | Id
Writing out such objects is clearly longwinded for actual programs so example
programs will normally be given in (some arbitrary) concrete syntax. The case for
using abstract syntax as a basis for semantic descriptions becomes ever clearer as
the languages covered in later chapters become more realistic.
Looking at Figure 2.3, notice that:
• the fact that the vars component of SimpleProgram is defined as a set (of Id)
makes it immediately clear that their order has no semantic significance;13
• Stmt is defined recursively (via If and While);
• the concrete syntax markings in hIfi statements are no longer required because
the record nesting resolves ambiguity;
• ArithExpr is also defined recursively;
• the nesting of records fixes the tree structure of expressions, obviating the need
for parentheses;
• the use of the mathematical set N as an option for ArithExpr is reasonable be-
cause there is a simple finite representation for any natural number (more care
would be needed if an abstract syntax used real numbers (R));
• P LUS etc. are constants that stand for unit sets containing a string.
Comparison of Figure 2.3 with the concrete syntax developed in Section 2.1 is
useful but, on such a simple language, shows only limited progress; the difference
between the abstract and concrete syntaxes of a language like Java or PL/I is much
more marked because they offer many alternative ways of writing semantically iden-
tical programs. Another advantage of basing semantic descriptions on abstract syn-
taxes is that it is possible to envisage programs being printed in differing concrete
forms (e.g. the original FORTRAN syntax for DO statements was built around cod-
ing pads and statement numbers but it could be generated from an abstract syntax
that covers the for of ALGOL 60). The description is, in a sense, getting closer to
the underlying “language concept”. Christopher Strachey’s observation was: “one
should work out what one wants to say before fixing on how to say it”.
Notice that the problem identified with concrete syntaxes that there is no con-
straint that the statements in a SimpleProgram should only use identifiers that are
declared in its vars part also pertains to the abstract syntax. A reader who objects
that the declaration of variable names in the vars part of SimpleProgram is not be-
ing used for checking is asked to remain patient until he or she reaches Section 4.2.
There is however an important language issue:
13 The ECMA/ANSI PL/I standard [ANS76] eschewed the use of sets — its authors arguing that
sequences would be more familiar to programmers. Unfortunately this leaves the reader with no
choice but to read hundreds of pages to determine whether the order of a sequence actually has any
influence on the semantics. If not, this fact could have been made completely obvious by employing
a set.
2.3 Further material 31
Projects
Even at this stage where only syntax description is covered, there are many projects
that the reader could pursue:
A concrete syntax can be found in [BBG+ 60] or [BBG+ 63]; write an appropriate
abstract syntax.
2. Add a class of Boolean expressions to SimpleProgram. The basic elements of
Boolean expressions could be elements of RelExpr; a few simple propositional
operators would be for (unary) negation and (binary) disjunction.
3. SimpleProgram has conditional statements; it is easy to envisage conditional ex-
pressions such as:
x := 5 + if a = b then 2 else 3 fi
It is even possible to have conditional references as in:
if a = b then x else y fi := 5
32 2 Delimiting a language
Further reading
Although the next chapter turns to the main subject of this book (i.e. semantics),
there are many interesting publications on syntax. A good starting point might
be [MP99]; the subject of efficient general-purpose parsing is covered in [SJv19].
Historical notes
14More exotic forms might tax the understanding of someone who has to maintain a program
containing statements such as x := + +x ∗ x + +.
Chapter 3
Operational semantics
The abstract syntax of ArithExpr in Figure 2.3 is recursive because both operands
of BinArithExpr are themselves elements of ArithExpr. Any instance of ArithExpr
1 There is no single best solution to the question of order. The appendices of this book illustrate
different orders of presentation of language descriptions. Clearly some interactive tool could escape
the linear constraint of printed pages.
is finite because its leaves are either natural numbers or members of Id. A recursive
function that evaluated arithmetic expressions whose leaves were all constants (N)
would require only the expression as an argument. The fact that variable names oc-
cur in expressions (Id ⊆ ArithExpr) indicates that the meaning of an expression de-
pends on the current values of any such names: the fact that such values are changed
by assignment statements is the essence of imperative programming languages.
Associations of values with some sort of key are a useful tool in most abstract
m
models and their types are recorded in VDM as Key −→ Value. As with VDM’s
X-set, maps are always finite. The basic operators on maps are given in Figure 3.1.
A map value is really just a finite set of pairs:2 the domain of a map (dom m) is the
set of values that are the first element of any pair in m; similarly, rng m is the set of
values contained as the second element of any pair in m.
m
D −→ R finite maps from D to R
dom m domain of a map
rng m range of a map
m(d) map application
{d1 7→ r1 , d2 7→ r2 , . . . , dn 7→ rn } map enumeration
{7→} empty map
{d 7→ f (d) ∈ D × R | p(d)} map defined by comprehension
m1 † m2 map overwrite
Applying a map to a value (m(d)) yields the second element of a pair whose first
element is equal to d. Because maps are like functions, there can be at most one such
pair. Map application m(d) is undefined if there is no pair in m whose first element
is d.3
Further intuition about maps comes from noting that sequences are a special case
m
of maps: T ∗ can be thought of as N −→ T. The sequence operator inds s gives the
domain of the map m and elems s yields its range; selecting an element of a sequence
(s(i)) is the same as applying the appropriate map to i.
It is somewhat of a tradition in semantic descriptions to use the Greek letter
Sigma (Σ) for the set of all states and σ ∈ Σ for specific values; this convention is
followed here — thus the state required for SimpleProgram is:
m
Σ = Id −→ N
Maps are many:one associations in that different identifiers can map to the same
value but any identifier can, in any particular σ ∈ Σ, only map to one value in N.
Given this, the signature of the function to evaluate expressions is:
eval: ArithExpr × Σ → N
2 General functions (e.g. square) can be used where infinite associations are required; these can be
understood to define infinite sets of pairs.
3 Limiting the propagation of such undefined terms is one of the benefits of the “Logic of Partial
Functions” (LPF) mentioned in Section 1.5 and described in more detail in Section 1.7.3.
3.1 Operational semantics 35
eval : ArithExpr × Σ → N
eval(e, σ ) 4
if e ∈ BinArithExpr
then if e.operator = P LUS
then eval(e.operand1, σ ) + eval(e.operand2, σ )
else eval(e.operand1, σ ) ∗ eval(e.operand2, σ )
fi
else if e ∈ Id
then σ (e)
else e
fi
fi
Map application is written in the same way as function application; thus the value
of a given Id (say a) can be accessed in σ by writing σ (a).
Thus:
eval(mk-BinArithExpr(1, P LUS, i), {i 7→ 2, j 7→ 4}) = 3
The meaning of VDM’s conditional expressions should be clear but the next
convention removes the need for them in this context.
Even for such a small language, the definition of eval above looks slightly heavy
and this style becomes inconvenient for larger descriptions. A pattern matching style
was used in early VDM language descriptions and it is much more convenient to
split the definition of a function like eval by cases (and use the pattern to define
local names for the values of fields) so that the definition of eval is given in cases
such as:
The six cases are given in full in Figure 3.2. This style becomes very natural
for larger language description. Formally, the constructors (mk-) and any constants
(e.g. P LUS) are defining when a match occurs and any other identifiers become
bound to the values present in the specific argument passed. Thus:
∃op1, op2 ∈ ArithExpr ·
e = mk-BinArithExpr(op1, P LUS, op2) ⇒
eval(e, σ ) = eval(op1, σ ) + eval(op2, σ )
Repeating the example above:
eval(mk-BinArithExpr(1, P LUS, i), {i 7→ 2, j 7→ 4}) =
eval(1, {i 7→ 2, j 7→ 4}) + eval(i, {i 7→ 2, j 7→ 4})
36 3 Operational semantics
e ∈ N ⇒ eval(e, σ ) = e
e ∈ Id ⇒ eval(e, σ ) = σ (e)
the order in which the assignments are executed. Given that statements are to change
the state, it is reasonable that a semantic function exec should have the signature:
exec: Stmt × Σ → Σ
The map operator that can be used to update a map is the overwrite (†) and it is
used as follows:
Thus:
exec(mk-Assign(j, mk-BinArithExpr(1, P LUS, i)), {i 7→ 2, j 7→ 4}) =
{i 7→ 2, j 7→ 3}
It is important to appreciate that where an identifier is placed affects how it is
interpreted: on the right of an assignment, the identifier denotes its value in the
current store; on the left, the identifier is the name of the variable to be updated.
Christopher Strachey used the terms “right-hand value” and “left-hand value” to
distinguish these uses. Having precise terms is useful because there are other places
where the distinction is important (e.g. different parameter mechanisms use either
the left- or right-hand value — see Section 5.4). This discussion is returned to in
Section 5.2.
The abstract syntax of Stmt in Figure 2.3 is recursive (via Stmt∗ ) so –just as with
eval– the exec semantic function is recursive: it is straightforward to define:
exec(mk-While(b, body), σ ) 4
if eval(b, σ )
then exec(mk-While(b, body), exec-list(body, σ ))
else σ
fi
exec-list([ ], σ ) 4 σ
The function exec-list fixes the semantic order of execution of statements as left
to right. The way this is done fits exactly the description that the semantics is given
by an abstract interpreter. An operational description mimics the operation of an
abstract machine. As the object languages being described acquire more features in
later chapters, extra constraints on the preferred operational style come into play.
The preceding section provides an intuition for operational semantics using func-
tions. As pointed out in Section 3.3, this fits the historical development of ideas.
The approach is also convenient with respect to providing tool support for a se-
mantic description: such an operational semantic description could be implemented
using a functional programming language such as Haskell [Hut16] or Scala [OS16]
or supported by the VDM Toolset.6 This section addresses the challenge of non-
determinism where a language description has to define the range of acceptable
answers for acceptable implementations of a language.
The first category is messy and discussion of this language feature is postponed
until Section 6.5. Category two can be introduced at will and an example different
from “guarded commands” is given below. It is really the third of these categories
that is both most interesting and is the strongest reason for switching to a way of pre-
senting semantics that copes naturally with any form of non-determinism. The ma-
terial on concurrency (including a concurrent object-based language) is addressed
in Chapters 8 and 9; here an alternative way is chosen to introduce non-determinism
into the simple language.
3.2.1 Relations
A mathematical function defines an infinite set of pairs. For example, the function:
square : Z → Z
square(i) 4 i ∗ i
There are many ways in which such a relation could be defined — as with the
permutation example (is-perm), it could be characterised by a predicate:
valid-transition: Stmt-set × Σ × Σ → B
and defined as follows:
s ∈ stmts ∧
exec(s, σ ) = σ 0 ∧
valid-transition((stmts − {s}, σ 0 ), σ 00 ) ⇒
valid-transition((stmts, σ ), σ 00 )
A more readable notation (than writing the predicate valid-transition in func-
tional style) is to define the relation as an infix operator marked by an arrow that
indicates the type of the first argument — for statements:
st
(stmts, σ ) −→ σ 0
this can be thought of as stating that configuration (stmts, σ ) “can transition to” state
σ 0 . For:
3.2 Structural Operational Semantics 41
st
−→: P((Stmt × Σ) × Σ)
it is possible that both:
st
({x := 1, x := 2}, {x 7→ 3, y 7→ 4}) −→ {x 7→ 1, y 7→ 4}
and:
st
({x := 1, x := 2}, {x 7→ 3, y 7→ 4}) −→ {x 7→ 2, y 7→ 4}
hold.
“Natural Deduction” presentations of logic [Pra65] use inference rules such as:
E1 ∧ E2
∧-E
Ei
E
∨ -I
E ∨ E0
Even in logic, some of the inference rules require multiple hypotheses (Ei ` E
means that E can be deduced from Ei ):
E1 ∨ E2
E1 ` E
E `E
∨ -E 2
E
Similar rules can be used to define semantic relations conveniently — for exam-
ple:
s ∈ stmts
st
(s, σ ) −→ σ 0
st
(stmts − {s}, σ 0 ) −→ σ 00
st
(stmts, σ ) −→ σ 00
Just as with Natural Deduction, the rules are “schema” into which matching val-
ues can be substituted. Figure 3.3 shows two valid deductions from the rule above
and establishes formally that the simple program {x := 1, x := 2} can give rise to
different final states.
The essence of the rule notation is that the relation under the line in a rule holds if
all of the hypotheses above the line hold. More is said about this reading as inference
rules below but for now the rule above can be read as:
Given that stmts is a set of Assign statements and an s ∈ stmts can be found (thus stmts is a
non-empty set), if (s, σ ) can transition to σ 0 and stmts without s can transition from σ 0 to
σ 00 , then it is valid to conclude that (stmts, σ ) can transition to σ 00 .
In fact, the rule style becomes so natural that most people use it even for simple
sequential (deterministic) languages and it is first applied to the language as given
in Figure 2.3. The same pattern-matching idea is used. With:
42 3 Operational semantics
(x := 1) ∈ {x := 1, x := 2}
st
(x := 1, {x 7→ 3}) −→ {x 7→ 1}
st
({x := 2}, {x 7→ 1}) −→ {x 7→ 2}
st
({x := 1, x := 2}, {x 7→ 3}) −→ {x 7→ 2}
(x := 2) ∈ {x := 1, x := 2}
st
(x := 2, {x 7→ 3}) −→ {x 7→ 2}
st
({x := 1}, {x 7→ 2}) −→ {x 7→ 1}
st
({x := 1, x := 2}, {x 7→ 3}) −→ {x 7→ 1}
Fig. 3.3 Two deductions from a non-deterministic rule
m
Σ = Id −→ N
st
−→: P((Stmt × Σ) × Σ)
stl
−→: P((Stmt∗ × Σ) × Σ)
st
(s, σ ) −→ σ 0
stl
(rl, σ 0 ) −→ σ 00
([s] y rl, σ ) −→ σ 00
stl
Unlike the description with functions in Section 3.2.1, the description here is
given top-down.
The style of semantic description used in Figure 3.4 was dubbed “Structural Op-
erational Semantics” by its originator Gordon Plotkin.7 (The figure only gives the
description of the statements of the language; Appendix A includes the SOS rules
for expressions.) The adjective “structural” indicates that the semantic rules should
follow closely the structure of the (abstract) syntax of the language. This objective
offers greater benefits when the semantic objects of the language need to be more
complicated, such as with environments in Chapter 5.
st stl ex
The idea that SOS rules define inference relations (−→ / −→ / −→) is very
important and Figure 3.5 illustrates two possible chains of inference for:
fn := 1;
while n =6 0 do
fn := fn ∗ n;
n := n − 1
od
ex
The SOS rules for expressions (−→) are given in Appendix A. Apart from the
argument that the use of inference rules offers uniformity of presentation, there is
ex
a technical reason for defining −→ as a relation: the fact that it can be undefined
(in the case where a reference is made to an undeclared identifier) means that it is
7 See Section 3.3 for references and a sketch of the history.
3.2 Structural Operational Semantics 43
stl
([ ], σ ) −→ σ
st
(s, σ ) −→ σ 0
stl
(rest, σ 0 ) −→ σ 00
([s] y rest, σ ) −→ σ 00
stl
ex
(test, σ ) −→ true
stl
(th, σ ) −→ σ 0
st
(mk-If (test, th, el), σ ) −→ σ 0
ex
(test, σ ) −→ false
stl
(el, σ ) −→ σ 0
st
(mk-If (test, th, el), σ ) −→ σ 0
ex
(test, σ ) −→ false
st
(mk-While(test, body), σ ) −→ σ
ex
(test, σ ) −→ true
stl
(body, σ ) −→ σ 0
st
(mk-While(test, body), σ 0 ) −→ σ 00
st
(mk-While(test, body), σ ) −→ σ 00
ex
eval(rhs, σ ) −→ v
st
(mk-Assign(lhs, rhs), σ ) −→ σ † {lhs 7→ v}
Fig. 3.4 SOS of the statements in SimpleProgram
ex
(e, {fn 7→ 1, n 7→ 0}) −→ false
st
(mk-While(e, b), {fn 7→ 1, n 7→ 0}) −→ {fn 7→ 1, n 7→ 0}
ex
(e, {fn 7→ 1, n 7→ 1}) −→ true
st
(b, {fn 7→ 1, n 7→ 1}) −→ {fn 7→ 1, n 7→ 0}
st
(mk-While(e, b), {fn 7→ 1, n 7→ 0}) −→ {fn 7→ 1, n 7→ 0}
st
(mk-While(e, b), {fn 7→ 1, n 7→ 1}) −→ {fn 7→ 1, n 7→ 0}
where:
e = mk-RelExpr(n, N OT E QUALS, 0)
b = [mk-Assign(fn, mk-BinArithExpr(fn, T IMES, n)),
mk-Assign(n, mk-BinArithExpr(n, M INUS, 1))]
Fig. 3.5 Two traces of inferences from the SOS in Figure 3.4
44 3 Operational semantics
actually wrong to define eval as a function. With the inference rules, there is simply
no rule whose hypotheses are all dischargeable. In this case, the computation is
undefined.
It is also worth repeating the point that, so far, expression evaluation cannot
change the state and this is made clear by the form of the semantic relation:
ex
−→: P((Expr × Σ) × N)
This situation changes with the inclusion of functions in a language8 — see Sec-
tion 6.5.
set = { }
st
(mk-Repeat(c, set, body), σ ) −→ σ
v ∈ set
stl
(body, σ † {c 7→ v}) −→ σ 0
st
(mk-Repeat(c, set − {v}, body), σ 0 ) −→ σ 00
st
(mk-Repeat(c, set, body), σ ) −→ σ 00
Projects
Semantics can now be tackled for all of the enumerated syntactic projects listed in
Section 2.3.10 The final unnumbered challenge of looking at a whole language is
likely to present the challenges discussed in subsequent chapters and thus should
be postponed until description techniques for handling these challenges have been
understood.
9 This way of recording the semantics requires that Repeat is an acceptable first element of the
st iter
pairs in the −→ relation. An alternative would be to define a separate relation −→.
10 There is an interesting interaction between for loops and declarations in that some languages
treat the control variable as being “bound” within the loop — this aspect is picked up in Section 5.5.
An alternative adopted in ALGOL 60 is to say that the value of the control variable is undefined
on termination of the loop.
46 3 Operational semantics
Alternatives
The semantics given so far are often referred to as “big-step” (or “natural” seman-
tics). The applicability of this term is seen in the rule for statement sequences:
st
(s, σ ) −→ σ 0
stl
(rest, σ 0 ) −→ σ 00
([s] y rest, σ ) −→ σ 00
stl
where the conclusion of the SOS rule gives the relation for the whole sequence. (It is
shown in Chapter 8 that describing the merging of threads in concurrency requires a
“small-step” semantics. In a small-step semantics, a “configuration” keeps track of
the activity remaining in each thread and a step makes an atomic transition of one
thread at a time; SOS rules both show the state change and update the text remaining
to be executed. Such a semantics is necessarily non-deterministic.)
An important issue arises with big-step semantics and termination. Consider the
rule given for while statements:
ex
(test, σ ) −→ true
stl
(body, σ ) −→ σ 0
st
(mk-While(test, body), σ 0 ) −→ σ 00
st
(mk-While(test, body), σ ) −→ σ 00
For a non-terminating loop (in the extreme, while true do x := x + 1 od) it will never
be possible to discharge the third hypothesis of the rule. Thus non-termination in
the program is modelled by non-termination in the semantics.11
This fits with the notion that an operational semantics is providing an abstract
interpreter. There is, of course, no way in general that the semantics could solve the
“halting problem”.12
The situation is even more complicated with non-deterministic programs where
–from the same state– a program might either diverge or converge. A yet further
issue involves “fairness” — see [Fra86, vGH15].
There are many ways of recording the relations between (program + initial state)
and final state. An obvious candidate is to write the program as though it is a re-
lation symbol (as could be done for permutations; [A, A, B, B] permutes [A, B, B, A]);
thus σ [[S]]σ 0 — this does not however extend cleanly to “small-step” semantics (see
Chapter 8) because the text of the program has to be updated as well as the state.
Other options for recording the semantic relation include writing the program text
on the relation arrow — see also Peter Mosses’ “Modular SOS” [Mos04, Mos09]
11 A more mathematically pleasing treatment is possible with denotational semantics and this is
discussed in Section 7.1.
12 Although the “Halting Problem” is often associated with Alan Turing’s name, the undecidabil-
ity result in [Tur36] is different because it concerns infinite number representations and Turing’s
programs were not in general intended to terminate. The impossibility of constructing a program
that will determine whether an arbitrary machine will halt is given in [Dav65a]. Useful historical
notes are [Pet08, pp.328–329] and further discussion by Post and Davis is in [Dav65b].
3.3 Further material 47
and [HC12] from Rob Colin and Ian Hayes. What is common to these presentations
is the necessity of using relations and the convenience of defining such relations by
inference rules.
It is worth noting that the advantages of mathematical abstractions like objects,
functions and relations do not guarantee ease of implementation. This point becomes
clearer when more advanced object language features like “heap storage” and the
consequent need for “garbage collection” are considered in Chapter 6. One point
that can be made here is that mathematical abstractions such as natural numbers
cannot be implemented — for example, the non-terminating program given above
is bound to overflow on any actual computer.
Further reading
There are a number of books that treat operational semantics including [RNN92,
Hen90]. They could be said to go deeper into the theory whereas the current book
aims to show how to apply the ideas of operational semantics to realistic program-
ming languages. (References to books on other semantic approaches are given in
Chapter 6.)
Historical notes
process of capturing this valuable material.13 The relevance of this is that the IBM
Vienna Laboratory went on to produce operational semantic descriptions of the PL/I
language. This was a huge undertaking: PL/I had most of the features found in any
of FORTRAN, COBOL and ALGOL (sadly without the elegance and the taste of
the last of those three). Not only did the Vienna group have to find ways of mod-
elling all of the features of ALGOL omitted from McCarthy’s Micro-ALGOL, they
also had to model features such as the concurrency that came with “tasking” in PL/I,
“exceptions” and under-determined storage mapping.
The PL/I formal descriptions were labelled “ULD-III” which stood for “Uniform
Language Description” — the Roman numbering three gave absolute precedence
to IBM’s official natural-language document that claimed to describe the language
and the existence of a semi-formal ULD-II written by the IBM UK Lab in Hursley.
There were three complete versions of ULD-III.14 Probably the most useful first-
hand account is [LW69]. J.A.N. Lee coined the name “Vienna Definition Language”
(VDL is not to be confused with VDM, which came later — see [Jon01]) for the
description method.
As is so often the case, hindsight provides rather unfair judgements but it is true
that the VDL description method had unfortunate properties that complicated its
use. The Vienna group acknowledged their main influences as:
• John McCarthy — including [McC66],
• Cal Elgot — especially [ER64] and
• Peter Landin — [Lan66b], which was also presented at the Baden bei Wien IFIP
Working Conference.
Perhaps the most troublesome features of VDL descriptions can be grouped under
McCarthy’s term of “grand-state” descriptions:15
1. The full text of a PL/I program is included in the ULD-III state because of ab-
normal sequencing (whether from goto statements or exceptions). This decision
is a magnified version of the program counter in McCarthy’s [McC66] — but the
magnification makes a monster of the original.
2. The state of VDL descriptions included a stack of “environments” (see Sec-
tion 5.2). This fits with Landin’s SECD machine idea but –as explained in Sec-
tion 5.5– is a definite impediment to proofs about VDL descriptions.
McCarthy’s phrase pinpoints the fact that there are serious disadvantages in
putting things in the state that are not changed by executing (normal) statements
of the program.
In passing, it is worth noting how ALGOL 60 became a testbed for semantic
description techniques — in [JA16, AJ18] four more-or-less complete descriptions
13 More is said on this meeting in [AJ18] and at greater length in [Ast19].
14 These have all been scanned and, along with key working documents, are available from my
web site: http://homepages.cs.ncl.ac.uk/cliff.jones/semantics-library/
15 McCarthy used this term in several discussions — an early reference is in an exchange at an
IFIP Working Group discussion [Wal69, p.33] — but it is also clear that the concern is behind
Strachey’s comment on McCarthy’s talk at the 1964 Working Conference (see [McC66, p.11]).
3.3 Further material 49
of that language are described along with references to other attempts. The earliest
of these ([Lau68]) was undertaken by Peter Lauer because Heinz Zemanek wanted
evidence to show that the criticism levelled at the ULD descriptions of PL/I had far
more to do with the chosen object language than being a valid objection to the VDL
meta-language. (The problem referred to as having a grand state did however also
dog Lauer’s ALGOL description.)
The comment in Section 1.5 about a language description being used as a cri-
terion for compiler correctness can now be expressed formally. If a compiler comp
maps source programs to the language of some machine whose semantics is de-
mc st
scribed by −→, that compiler is correct with respect to the language semantics −→
providing: 16
∀s ∈ Stmt ·
mc st
∀σ , σ 0 · (comp(s), σ ) −→ σ 0 ⇒ (s, σ ) −→ σ 0
Notice that this is not an equivalence (i.e. the reverse implication is not required)
because the language semantics can be non-deterministic and describe a range of
permissible results.
Two of the earliest publications on using a formal description in compiler correct-
ness proofs are [MP66, Pai67]. Details of the IBM Vienna Lab work on developing
compilers from formal language descriptions (particularly that on the “block con-
cept” such as [Luc68, JL70]) are given in Section 5.5; a useful overview is contained
in [Jon82a] and extensive work on Ada in [BO80b, BO80a]. The work on the Texas
“stack” is published in [Moo19].
Unhappiness with gratuitous difficulties in basing reasoning about compiler de-
signs on such (grand-state operational) definitions led the Vienna group to move to
denotational semantic descriptions (see Sections 7.1–7.2). Again, ALGOL was used
as a demonstration [HJ78, HJ82] and among other object languages, Pascal [AH82]
and Modula-II [AGLP88] have been described using VDM (see Chapter 11).
Using the denotational approach certainly prompts the use of “small-state se-
mantics”. Gordon Plotkin had made significant contributions to the domain the-
ory that underlies the denotational approach when he took a sabbatical (from Ed-
inburgh) to Aarhus University. There he taught an operational approach and his
course notes [Plo81] provide the foundation of Structural Operational Semantics.
These notes were widely circulated and eventually republished as [Plo04b]; the ac-
companying [Plo04a] provides a fascinating commentary on this period.
16 This is a slight simplification in that –in reality– the states of the machine will differ from those
of the language description. Expanding this is not difficult but does show the interesting need to
relate the run-time state to the abstraction (see [Jon76] for further details).
Chapter 4
Constraining types
In early versions of FORTRAN, the type of a variable was determined by the first
letter of its identifier. This chapter shows how to describe an object language that
expands on the idea of listing the permissible names of variables and looks at the
advantages of declaring a specific type for each variable. This makes it possible
to detect –before a program is executed– some mistakes that a programmer might
make. More generally, all forms of context dependancy offer ways of avoiding an
attempt to give semantics to meaningless programs. There is nothing in formal se-
mantic approaches that requires that object languages be strongly typed. Methods
for describing languages at various points on the strength-of-typing spectrum are
presented in Section 4.1.
There are many things that can be wrong with a program. In general, they cannot
all be detected by any algorithm. There are, however, some aspects of languages for
which it can be said that certain programs make no sense.
In fact, the language descriptions in Chapters 2 and 3 have not so far excluded
SimplePrograms that use identifiers in statements that are not listed in the vars list.
This can be checked statically for a SimpleProgram: all uses of Id in expressions
or on the left-hand side of assignments must have been declared in the vars list.
This is a simplified case of the more general context dependancies considered in
Section 4.2.
There is a related issue that requires a dynamic –or run-time– check and that is
the danger that a program attempts to access a variable before any assignment has
been made to it. Of course, one way of avoiding this problem is for the language
designer to arrange that all declared variables are given initial values at the start of
execution. This is however a design choice. If a language does not decree that such
initialisation occurs, there needs to be a check on variable access such as the second
hypothesis in the following SOS rule:
e ∈ Id
e ∈ dom σ
ex
(e, σ ) −→ σ (e)
The description in Section 3.2.2 of reading SOS descriptions as inference rules
indicates that, if no rule can be found whose hypotheses are fulfilled, the execution
is undefined and thus the program is erroneous. It might, of course, be desirable in a
full language to include some form of exception-raising and -handling mechanism
but this topic is deferred to Chapter 10.
A prime example of context dependancy concerns the use of variables in ways which
do not correspond to their intended types. Language designers can choose to make
compile-time detection of such errors possible by recording rules that are statically
checkable. For example, a language might rule out adding a character string to a
number. If variables are untyped and can be assigned any values, the consequences
of such violations can only be detected at run-time. In contrast, if variables are
typed, it is possible for a compiler to flag any program which applies an arithmetic
operator to a character string operand.
There are, of course, cases where operators are intended to be polymorphic in
the sense that the same operator can be applied to operands of different types. For
example, the plus symbol might be overloaded and –as well as being applied to
numbers– used in a programming language to mean concatenation of strings. A lan-
guage can also be designed to be permissive in the sense that some type differences
in operands to a binary operator are resolved by coercing the type of one operand
to that of the other. Thus an arithmetic operator with operands that are integers or
reals could convert an integer operand to a floating-point number before applying
the operator.
Language issue 9: Type information
The designer of a language must decide the extent to which type information
is required in programs. Type information provides redundancy and makes it
possible to detect some program errors from the static text of a program. The
availability of type information can also facilitate generation of more efficient
code than is possible if no information is available about the intended roles of
different variables.
In order to introduce the alternative description techniques, a rather coarse dis-
tinction is made in this chapter between static and dynamic checking. This degree of
difference can be demonstrated by allowing variables in BasePrograms to take val-
ues that are either integers (Z) or Booleans (B). The full description of BaseProgram
is contained in Appendix B; the text of this chapter picks out the main decisions;
after understanding these points, the reader should have no difficulty in reading the
description in the appendix.
Note that some distinctions from the abstract syntax of SimpleProgram of Chap-
ter 2 have been dropped in that the test parts of both If and While here appear to
allow any Expr — the checking on types is now done in the context conditions.
Given that there are two types of variables, meaningless programs can arise in a
number of ways — for example:
• the test expression in an If or While statement might be an arithmetic expression;
• an arithmetic operator might be applied to expressions (RelExpr) that yield a
Boolean result.
If there were no type indications in programs, it would be possible to say that the
type of a variable was determined by the first assignment to its name. In this case,
54 4 Constraining types
it would not be possible –in general– to detect type errors statically. It is true that
egregious cases of these errors such as:
if 42 then n := 1 else n := 2 fi
n := true + 7
could be detected statically but, since assignments to variables can depend on the
flow of control, types cannot –in general– be determined statically. The checking
would then have to be made in the SOS rules as in Section 4.1 above.
Because both BNF and the basic description of objects can essentially only cope
with context-free languages, the challenge faced by those describing –or designing–
a programming language is:
wf-BaseProgram
wf-Program BaseProgram
Program
Fig. 4.1 Context conditions define a subset of objects given by an abstract syntax
The first of these is trivial, just requiring that all elements of the list of statements
are well formed (with respect to the same type map):
The test for well-formed If and While in Appendix B should be obvious and the
full definition of c-type is easy to read.
Finally, it has to be shown how the type information in a BaseProgram provides
the TypeMap:1
wf -BaseProgram : BaseProgram → B
wf -BaseProgram(mk-BaseProgram(types, body)) 4
wf -StmtList(body, types)
1 In languages with nested blocks/procedures (see Chapter 5) or classes and methods (see Chap-
ter 9), the TypeMap has to be updated with declaration information that is contained in nested
texts.
56 4 Constraining types
Because semantics are only given for well-formed texts (see Figure 4.1), the
appropriate wf predicate can be thought of as an extra hypothesis for any SOS rule.
The elision of such lines fits with the position in VDM that data type invariants
restrict types.
It is useful to think of the context conditions in relation to the type checking done
in a compiler. Just as with the diagnostics in a compiler, it is not always obvious how
far such checks should extend. For example, a while statement whose test is true
will definitely loop forever — but so would one that looked for a counterexample
to Fermat’s last theorem. An indication of what is reasonable to check in context
conditions is whether the test depends only on symbols (such as operators) rather
than their meaning.
A reader who has followed the semantics in Section 3.2 will have no difficulty in
reading the semantics of BaseProgram in Appendix B. In that description, the class
of Id has not been divided; a completely equivalent description of the language
could be written separating, say, IntId/BoolId. This point becomes more pressing
when identifiers for procedures (or classes and methods) are present in a language.
The decision on sub-dividing the class of identifiers is really a matter of taste. The
position adopted here is that, if the written forms of the identifiers are not distin-
guished, the work of defining their use should be done in the context conditions and
a single class of Id used in the abstract syntax.
It is an important property that –for elements of BaseProgram that satisfy
wf -BaseProgram– there will be no run-time type errors. This type safety property
is useful because it shows that type information is not needed in the run-time state.2
2 There were a number of technical reports written around the VDM description of PL/I [BBH+ 74]
that addressed the use of the formal description of a language as a basis for compiler design. One
that relates to the current topic is [Izb75], in which Herbert Izbicki proves that –because of checks
4.3 Semantic objects 57
In the introductory languages of Chapter 3 and Appendix B, the states (Σ) are simple
mappings that associate values with identifier names. Even there the notion of the
state as the objects underlying the semantic description is informative but the role
of semantic objects is even clearer with richer languages.
In fact, the notion of the abstract state space used in a description of a language
tells a skilled reader an enormous amount about the language. This is extremely
valuable because the description of the semantic objects –even for a large language–
is likely to be rather short (e.g. the VDM description of PL/I [BBH+ 74] comprises
over 100 pages of formulae but the definition of the semantic objects is less than two
pages long). More importantly, returning to the Leitmotiv of this book, states are a
crucial starting point for the designer of a programming language.
This section outlines the way in which semantic objects are enriched for several
modest extensions to the language of Appendix B; more ambitious languages are
modelled in Chapters 5–10.
4.3.1 Input/output
The key characteristic of imperative languages is that their core statements change
something. That something could be a database, a projection of some virtual real-
ity or the position of a robot arm. In the languages considered above, assignment
statements can change the values of variables. In order to illustrate how other sorts
of state can be manipulated, BasePrograms can be extended to include input/output
(I/O) by adding Read and Write statements. This can be illustrated by rather simple
forms of I/O but extensions to multiple named files etc. are considered below.
The syntax and semantics of While and If statements are unchanged — they
continue to play the role of orchestrating which core (state-changing) statements
are executed and in what order.
The simple message here is that, if the core statements of a language can change
something, the state (Σ) must contain fields in which the current values are stored.
Adding a simple Write as an option to the abstract syntax for Stmt is trivial:
that have been made in the context conditions– no run-time type mismatch errors can occur. Such
“type soundness” arguments are also discussed in [Sym99].
58 4 Constraining types
Stmt = · · · | Write
Notice that executing a Write statement does not change the store.
The SOS rules for accessing values from identifiers and changing them by as-
signments need straightforward revision:3
ex
(rhs, store) −→ v
st
(mk-Assign(lhs, rhs), mk- Σ(store, out)) −→ mk- Σ(store † {lhs 7→ v}, out)
Again, what does not change is important: executing an assignment does not change
the output file.
Adding an input statement poses only one extra question — the syntax is straight-
forward:
Stmt = · · · | Read
Read :: lhs : Id
Of course, the Read identifies a place where the next input value should be placed
and thus the lhs field is an identifier rather than an Expr as in Write.
The extension of the semantic objects is also obvious:
m
Σ :: store : Id −→ ScalarValue
output : N∗
input : N∗
The additional consideration is that, presumably, a Read from an empty input should
fail — see the discussion on run-time errors in Section 4.1. So the SOS rule might
be:
in 6= [ ]
st
(mk-Read(lhs), mk- Σ(store, out, in)) −→
mk- Σ(store † {lhs 7→ hd in}, out, tl in)
3 Peter Mosses’ M-SOS is discussed in Section 4.4: his approach attempts to record semantic rules
in a way which enhances their re-usability in different contexts.
4.3 Semantic objects 59
To return to the point about the extent to which semantic objects can contribute
disproportionally to understanding a language, consider the semantic object:
m
Σ :: store : Id −→ ScalarValue
m
files : FileId −→ N∗
This would immediately prompt a reader to think about a language in which I/O
statements can create and access any number of named files. Furthermore, an exten-
sion to:
m
Σ :: store : Id −→ ScalarValue
m
files : FileId −→ File
File :: contents : ScalarValue∗
index : N
would support a language with statements that operate at indexed points within files.
Further extensions might include ownership and (read/write) permissions as in Unix.
4.3.2 Arrays
The simple languages considered so far have manipulated only ScalarValues. The
message about semantic objects being helpful in grasping what can –and cannot–
be done in a programming language is reinforced when composite values such as
arrays and records are considered.
The ability to manipulate some form of array value is present in most program-
ming languages.
Arrays can then be defined as vectors of things that could either be scalar or array:
Array = (ScalarValue ∪ Array)∗
But this has the disadvantage that it would appear to allow a form of “ragged ar-
ray” in which different indexes at the outermost level select sub-objects of varying
dimensionality.4 A better basic model might be:
m
Array = N∗ −→ ScalarValue
For simplicity, this assumes that the indexing of any dimension starts at one but it is
easy to change this so that, for example, defining array dimensions A(5: 15, −10: 20)
is permitted.
Normal arrays might have a denseness requirement that all valid index lists are
in the domain of the array model but there are also applications for “sparse arrays”
that allow gaps.
4.3.3 Records
Viewed abstractly, there are two differences between records (known as “structures”
in PL/I) and arrays. Firstly, array elements are homogenous in the sense that all
elements are of the same type, whereas the fields of a structure can be of different
types. Secondly, array elements are accessed by numerical indexing whilst the fields
of a structure are identified by identifiers. In a sense,
4 APL [Ive62] did actually allow such raggedness.
4.3 Semantic objects 61
A: array(3) of N
and
S: struct
one: N
two: N
three: N
could serve the same purpose.
Language issue 17: Supporting records
Many programming languages offer the ability to define records (or structures).
Furthermore, the fields of such records –as well as being scalar values– can be
arrays and the elements of arrays can be records.
There are issues around declaring record types that are addressed in Section 6.3.
It should come as no surprise that a model of the store can be built around:5
m
Store = Id −→ Value
Projects
Further reading
Despite the emphasis put here on the distinction between problems that can be de-
tected statically (compile-time) and dynamically (run-time), many formal semantic
descriptions (e.g. [Lau68, Mos74]) handle them together. This has the unfortunate
result that the semantic description is further complicated by errors that are manifest
in the source text alone.
Furthermore, what is here referred to as “context conditions” is sometimes called
“static semantics” (and what is here just “semantics” is termed “dynamic seman-
tics”). These terms are avoided in the current book. The term “context conditions”
probably made its first appearance in [vWMPK69]. Aad van Wijngaarden’s own ap-
proach involved “two-level grammars” (see [Sin67, vWMPK69]) but any compari-
son is complicated by the fact that, although such grammars could clearly describe
context conditions, Van Wijngaarden chose, in the description of ALGOL 68 to min-
imise the distinction between syntax and semantics [vWSM+ 76, LvdM80, Lin93].
6 The major issues would concern concurrency control — this point is picked up in Section 8.6.
4.4 Further material 63
Another approach is the “dynamic syntax” idea of [HJ73], in which the process of
parsing declarations dynamically creates appropriate syntax rules for parsing state-
ments.
It is also worth noting that well-formedness could be expressed using inference
rules such as:
lhs ∈ Id
rhs ∈ Expr
c-type(rhs, tpm) = tpm(lhs)
mk-Assign(lhs, rhs) ∈ Assign
The subject of types has its own wide literature — see for example [Pie02].
A valuable approach that is not covered here is “type inference”, in which as
much type information as can be deduced from use of identifiers etc. is used to
determine a sensible typing for the whole text — see [Mil78b].
Chapter 5
Block structure
Section 4.3 emphasises the important role that semantic objects can play in under-
standing or, indeed, designing a programming language. This point becomes more
obvious as the language challenges increase. This chapter examines ideas used to
model the way in which blocks can be used to define different scopes for names of
variables and a variety of parameter passing modes for procedures.
It is again the case that the meta-languages introduced for simple languages cope
with describing the new language features.
5.1 Blocks
program
begin
bool a; int i int j;
a := true; i := 1; j := 2;
begin
int a; int b;
a := 1; j := 3; b := 7
end;
if i = j then a := false fi
end
end
Thus any sequence of statements can contain blocks and blocks can be nested to any
depth the programmer chooses.
In an initial abstract syntax for Block, only the local variable declarations are
considered (in Section 5.3 the ellipses are replaced by procedure declarations):1
m
Block :: var-types : Id −→ ScalarType
···
body : Stmt∗
As in Section 4.2, a TypeMap is needed to define the context conditions — this
is also extended in Section 5.3 to cover procedures — but, as far as the scalars are
concerned, it suffices to have:
m
TypeMap = Id −→ (ScalarType | · · ·)
The interesting context condition shows how the local type information over-
writes that of the context in which the block is located:2
wf -BlocksProgram : BlocksProgram → B
wf -BlocksProgram(mk-BlocksProgram(b)) 4 wf -Stmt(b, {7→})
1 Again, there are alternative ways of defining the same language content. It might for example be
sensible to insist that the Stmt in the body of BlocksProgram is always a Block. In contrast to earlier
chapters, a Compound statement is introduced in Appendix C — as becomes clear, this is the same
as a Block with no local declarations. Such choices have only minor influence on the semantics.
2 Remember that the VDM map overwrite operator gives precedence to pairs from its second
operand.
5.1 Blocks 67
program
begin
bool a; int i int j;
a := true; i := 1; j := 2;
σ = {a 7→ true, i 7→ 1, j 7→ 2}
begin
int a; int b;
a := 1; j := 3; b := 7
σi0 = {a 7→ 1, i 7→ 1, j 7→ 3, b 7→ 7}
end;
σ 0 = {a 7→ true, i 7→ 1, j 7→ 3}
if i = j then a := false fi
end
end
A way of simplifying this description is given in Section 5.2 but, before that is
done, the useful VDM map operators C/− C are explored.
The state resulting from a block is defined in the SOS rule above to be the union
m
of two mappings (of type Id −→ ScalarValue). Looking firstly at the left operand of
the union, assume for the moment that:
dom σi0 = dom σi
68 5 Block structure
The definition of C
− expands to give:
(dom vm) C
− σi0 = {id 7→ σi0 (id) | id ∈ dom σi0 ∧ id ∈
/ dom vm}
Then it follows that:
dom σi0 = dom σi = (dom σ ∪ dom vm) ⇒
(dom vm) C
− σi0 = {id 7→ σi0 (id) | id ∈ dom σ ∧ id ∈
/ dom vm}
Turning to the second operand of the union:
(dom vm) C σ = {id 7→ σ (id) | id ∈ dom σ ∧ id ∈ dom vm}
Combining the results shows that the initial assumption:
dom σ 0 = dom σ
holds.
Moreover, if all variables from the outer context are redeclared in the inner block:
dom σ ⊆ vm
st
then (writing σ 0 for the right-hand side of −→ in the SOS rule):3
σ0 = σ
The preceding section addresses the language issues around the same name refer-
ring to different variables (in different scopes); this section moves towards a more
delicate feature of many programming languages. Section 5.4.1 shows how to model
3 Of course, this throws doubt on the perceivable effect of such a block. In fact, if the body of
a BlocksProgram is a Block, the conclusion has to be that executing the program has no effect.
It would, however, be straightforward to add some form of I/O to this language as described in
Section 4.3.1.
5.2 Abstract locations 69
one way in which different identifiers can refer to the same variable. (Different pa-
rameter passing modes for the example program in Figure 5.6 are discussed along
with Issue 25. Other ways of explicitly manipulating addresses include “heap vari-
ables” — see Section 6.4.)
Two important modelling points can be extracted before “by reference” parame-
ter passing is tackled:
• locations serve as an abstraction of machine addresses; and
• the fact that the relationship between identifiers and locations can be held in an
environment that changes less often than the store (Σ).
Both of these points can also be used to provide a clearer model of the semantics
of Block than is given in the preceding section. The set ScalarLoc is an infinite set of
objects about which nothing is known other than the ability to test them for equality.
The modelling decision is to split the mapping from identifiers to their values into
two mappings:
m
Env = Id −→ ScalarLoc
m
Σ = ScalarLoc −→ ScalarValue
st
The semantic relation −→ now has the type:
st
−→: P((Stmt × Env × Σ) × Σ)
This makes clear that the environment (Env) cannot be changed by executing a state-
ment (Stmt): the i + 1th statement in a list is executed in the same environment as the
ith statement even if the ith statement is a Block.4 (Moreover, sharing can be mod-
elled as in Figure 5.3, where two different identifiers can be mapped (in an Env) to
the same location.)
The abstract syntax of Assign is unchanged from that in Appendix B:
Assign :: lhs : Id
rhs : Expr
but its semantics now has to obtain the location corresponding to the identifier (lhs);
the state (σ ) is updated at the appropriate location:
ex
(rhs, env, σ ) −→ v
st
(mk-Assign(lhs, rhs), env, σ ) −→ σ † {env(lhs) 7→ v}
4 As discussed in Section 5.5, not making this clear was a serious disadvantage of the form of
operational semantics used in the early Vienna Lab formalisation of PL/I.
70 5 Block structure
Similarly, the option that a simple form of expression can be just an identifier is
unchanged in the abstract syntax:
Expr = · · · | Id
ex
but the type of the −→ relation becomes:
ex
−→: P((Expr × Env × Σ) × ScalarValue)
and the SOS rule has to obtain the location (from env) before it can use the location
to access the value (from σ ):
e ∈ Id
ex
(e, env, σ ) −→ σ (env(e))
Christopher Strachey –who made many wise observations about programming
languages– referred to env(id) as the left-hand value and σ (env(id)) as the right-
hand value of id. These terms obviously derive from assignment statements but are
useful in discussing differing evaluation modes in other contexts including parame-
ter passing (see Section 5.4).
Id ScalarLoc ScalarVal
l v
Env !
The following SOS rule shows that newlocs is a one:one mapping5 whose domain
is exactly the set of identifiers for local variables of the Block and whose range is
disjoint from any locations in use in the current σ . After executing the body of
a block, the state that results from executing the whole Block is found simply by
restricting σ 0 to the set of locations that existed before the Block was executed.
There is less to do than in the model above because identifiers from the context of
the block that were redeclared within the block had different locations under which
their values were stored:
m
newlocs ∈ (Id ←→ ScalarLoc)
dom newlocs = dom vm
rng newlocs ∩ dom σ = { }
env0 = env † newlocs † · · ·
σi = σ † ({env(id) 7→ 0 | id ∈ dom vm ∧ vm(id) = I NT T P} ∪
{env(id) 7→ true | id ∈ dom vm ∧ vm(id) = B OOLT P})
stl
(body, env0 , σi ) −→ σi0
st
(mk-Block(vm, pm, body), env, σ ) −→ dom σ C σi0
The definition of env0 is completed below when procedures are added to the lan-
guage.
Figure 5.4 provides an annotated version of Figure 5.1 showing the environments
and states at key points. As predicted in Section 5.1, deriving (from σi0 ) the state that
results from the whole block is easier in the presence of env because the locations
form the appropriate bridge (notice that, in Figure 5.4, σi0 retains the value for la but
that this location is not in env0 ).
It is important that ScalarLoc is a set of unanalysed tokens. Were it, for exam-
ple, to be made equal to some form of number (N) it would be unclear whether a
program could perform address arithmetic. Whilst there are programming languages
that allow such manipulation, this is not the intention here and the constraint is again
made completely clear by the choice of semantic objects.
The choice of new locations in the SOS rule for blocks is non-deterministic but,
given the preceding point about the set ScalarLoc being just tokens, there is no pro-
gram that can be influenced by the choice. Furthermore, there is no difference in the
resulting state after the execution of the block terminates, whatever choice is made
5 m
A one:one mapping Id ←→ ScalarLoc is employed in preference to a normal many:one mapping
m
Id −→ ScalarLoc, which would require a data type invariant:
m
one-one: (X −→ Y) → B
or:
program
begin
bool a; int i int j;
a := true; i := 1; j := 2;
env = {a 7→ la, i 7→ li, j 7→ lj}
σ = {la 7→ true, li 7→ 1, lj 7→ 2}
begin
int a; int b;
a := 1; j := 3; b := 7
env0 = {a 7→ ln, i 7→ li, j 7→ lj, b 7→ lb}
σi0 = {la 7→ true, ln 7→ 1, li 7→ 1, lj 7→ 3, lb 7→ 7}
end;
env = {a 7→ la, i 7→ li, j 7→ lj}
σ 0 = {la 7→ true, li 7→ 1, lj 7→ 3}
if i = j then a := false fi
end
end
Fig. 5.4 The example of Figure 5.1 annotated with environments and states
for rng newlocs. Interestingly, there is a strong technical reason for showing the non-
determinism. The obvious way to compile blocks is to reflect the stack structure of
blocks6 and to allocate the next n machine addresses for the local variables of any
block; on exit from the block, the stack pointer is simply set back to the machine
address on block entry and a sibling block would reuse the same addresses for its lo-
cal variables. But this is not the only possibility: a compiler could compute different
addresses for all blocks contained in one scope (this would create space for all con-
tained blocks regardless of the fact that sibling blocks cannot be active at the same
time). It is straightforward to show that this is an allowable implementation of the
non-deterministic choice of locations; a much messier equivalence proof would be
required if natural numbers were used for locations and the description essentially
“bolted in” the stack implementation.
As observed, careful choice of abstractions such as tokens for ScalarLoc makes
properties of a language description manifest without needing to draw out conse-
quences from detailed sequences of state transitions.
6 Stack variables (in blocks and procedures) can be contrasted to “heap” variables, which are
discussed in Section 6.4.
5.3 Procedures 73
5.3 Procedures
Procedures are considered first — functions are discussed in Section 6.5. Proce-
dures are named and their definitions (ProcDef ) are local to a block:
m
Block :: var-types : Id −→ ScalarType
m
proc-defs : Id −→ ProcDef
body : Stmt∗
In the same way as variable names are local to the block in which they are declared,
any procedures declared in a block are only known within their declaring block. As
in Section 4.2, no attempt is made to subdivide the class of Id; since their written
forms are taken to be the same, checking for disjointness of variable and procedure
names is left to the context conditions.
A possible abstract syntax for procedure definitions lists the names of parameters
and separates their types.7 The body of a procedure is shown as a Stmt:
ProcDef :: params : Id∗
paramtypes : ScalarType∗
body : Stmt
The TypeMap needed for the context conditions can now be completed (from that
in Section 5.1):
m
TypeMap = Id −→ (ScalarType | ProcType)
binding of non-local identifiers often use a “copy rule” that replaces the procedure call with a copy
of its definition. This poses a serious danger of getting the wrong binding and the danger is only
avoided by a rather complicated (and oft-times imprecise) renaming of clashing variable names.
5.3 Procedures 75
begin
int i;
proc p() · · · ; i := 1; · · · end
..
.
begin
int i;
..
.
call p()
..
.
end
..
.
end
The semantic rules for different parameter passing modes are discussed in the
remainder of this chapter and the two predominant modes are described in Ap-
pendix C. What is common to the two rules for a call to a procedure, say p, is
that its denotation is retrieved from the environment in which the call is written; that
procedure denotation includes the environment of the context where the procedure
was declared; the denotation also contains the list of parameter names and the body
of the procedure. A local environment is generated (differently in the two param-
eter passing modes) and the state extended in the case of call-by-value parameter
passing. The body of the block is then executed using this environment and state.
Notice that type information is not required in the procedure denotations because
the body of the procedure has been type checked against the information about the
types of the parameters.
10 Restriction to identifiers means that the same syntax can be used for call by reference (see
Section 5.4.1) and call by value (see Section 5.4.2); the latter case can use general expressions as
arguments.
76 5 Block structure
Although not the main topic of the current book, there are points at which it is
worth drawing attention to the challenges of implementing features in high-level
programming languages. As pointed out in [vdH19], finding a way of keeping track
of the accessible stack variables in a language (ALGOL 60) with nested blocks and
recursive procedures required the invention of Dijkstra’s “display” mechanism. This
idea became a key test bed for the justification of implementation ideas with respect
to formal language descriptions — see [JL71, HJ70, HJ71].
There are many modes in which arguments can be passed to procedures.11 The
two most widely used of these are to pass the address of the argument or to pass its
value; these are modelled in Sections 5.4.1 and 5.4.2 respectively. Further alternative
parameter passing modes are discussed in Section 5.5.
The outline program in Figure 5.6 provides a basis for comparing parameter pass-
ing modes.
program
begin
int i, j;
proc p(int x, int y)
i := i + 1; x := x + 1; y := y + 1
end
..
.
i := 1; j := 2;
call p(i, j);
write(i, j);
call p(i, i);
write(i, j)
end
end
11 The terminology used here is: “arguments” are what occur in the call statement (or function ref-
erence); “parameters” are the names used within the header of the procedure or function definition.
ALGOL 60 uses the terms “actual parameter” and “formal parameter”.
5.4 Parameter passing 77
One option for parameter passing is to pass the address of the argument — at least
for scalar values, this is a very simple idea. Different languages employ the terms
“by reference” or “by location” for this mode of parameter passing.12
There are several reasons why languages allow some form of “by reference”
parameter passing:
• it avoids copying values (this point is more important when considering arrays
etc. — see below);
• it makes it possible, for example, to switch the values of arguments;13
• it provides a way of returning more than one result (effect) of a procedure invo-
cation.
program
begin
int i, j;
cenv = {i 7→ li, j 7→ lj}
proc p(int x, int y)
lenv = {i 7→ li, x 7→ li, j 7→ lj, y 7→ lj}
σ = {li 7→ 1, lj 7→ 2}
i := i + 1; x := x + 1; y := y + 1
σ 0 = {li 7→ 3, lj 7→ 3}
end
..
.
i := 1; j := 2;
cenv = {i 7→ li, j 7→ lj}
σ = {li 7→ 1, lj 7→ 2}
call p(i, j)
σ 0 = {li 7→ 3, lj 7→ 3}
end
end
The program in Figure 5.7 is decorated with the environment (env) and state (σ )
at key points in the execution. The invocation of p(i, j) passes the location of the
outer variables i and j to x and y respectively; within this invocation of p, references
to x are effectively the same as references to i (and the same is true of y/j). Therefore
after the execution of p(i, j) the values of i and j are both 3.
A subsequent invocation of p(i, i) would essentially equate all of the addresses
of i, x and y and the resulting value of i would be 6.
The SOS rule for Call in the case of “by location” parameter passing is actually
simpler than that in Section 5.4.2 because the left-hand value of an identifier can be
obtained with env(args(i)). Thus:
mk-ProcDen(parms, body, cenv) = env(p)
lenv = cenv † {parms(i) 7→ env(args(i)) | i ∈ inds parms}
st
(body, lenv, σ ) −→ σ 0
st
(mk-Call(p, args), env, σ ) −→ σ 0
It should be noted that there is a serious complication with this mode of param-
eter passing in that a reader of a program cannot assume that different identifiers
denote distinct variables; someone reading a program (even its original author after
some elapsed time) might miss quite subtle errors deriving from an assumption of
separation.
It is useful now to see how clean ideas can be combined. If the model of arrays
discussed in Section 4.3.2 is changed so that:
m
Env = Id −→ Den
The preceding language issue indicates another place where it is easy to define
things on a mathematical abstraction that are non-trivial to implement. To take the
two-dimensional case, an n × m array must be mapped onto the linear addresses of
the hardware either in row-dimensional or column-dimensional order. Whichever
mapping order is chosen, a slice on one dimension will be in contiguous store but
5.4 Parameter passing 79
Parameter passing “by value” does just what its name suggests. To use Strachey’s
terminology (see Section 5.2) it is the right-hand value that is passed to the called
procedure. The semantics of Call essentially creates a local block introducing new
locations for the arguments; the values of the arguments from the call are then in-
stalled as the initial values of these new locations. In contrast to call by reference,
assignment to a named parameter has no effect outside the block. (However, assign-
ments to non-local variables can cause side effects and this point becomes important
when considering functions — see Section 6.5.) Just as in a block written by the pro-
grammer, the local variables disappear on exit from the called procedure.
The semantics for this form of call are included in Appendix C, and Figure 5.8
indicates the values of env/σ for the specific program under call-by-name parameter
passing. In the semantics of Call, the value of a single identifier can be obtained
by σ (env(args(i))) for each argument. Figure 5.6 has used simple identifiers as
arguments so that the contrast with passing “by location” from Section 5.4.1 can be
made, but there is, in fact, no reason with “by value” why the arguments should not
be general expressions. In this case, the semantics would have to construct a list of
ex
values using (args(i), env, σ ) −→ vl(i).
program
begin
int i, j;
cenv = {i 7→ li, j 7→ lj}
proc p(int x, int y)
lenv = {i 7→ li, j 7→ lj, x 7→ lx, y 7→ ly}
σ = {li 7→ 1, lj 7→ 2, lx 7→ 1, ly 7→ 2}
i := i + 1; x := x + 1; y := y + 1
σ = {li 7→ 2, lj 7→ 2, lx 7→ 2, ly 7→ 3}
end
..
.
i := 1; j := 2;
cenv = {i 7→ li, j 7→ lj}
σ = {li 7→ 1, lj 7→ 2}
call p(i, j)
σ = {li 7→ 2, lj 7→ 2}
end
end
Projects
Because the languages being considered are themselves getting interesting, there are
many projects that the reader could now enjoy. For example:
1. The syntax of iterative for loops is a suggested project in Section 2.3; the seman-
tics of the option to bind the control variable as local to the statement could now
be fully explored.
2. It is relatively straightforward to write out the semantics of parameter passing
“by value/return” because it works like a combination of “by value” parameter
passing and the creation of a new block (but remember that a value must be
passed back at the end of the procedure execution). There is a need for a context
condition to avoid two different values being passed back to the same variable.
3. ALGOL 60’s full by name parameter passing is slightly trickier and requires that
there is a check that the argument is only a single identifier if the parameter is
used in a “left-hand” context.
4. Modify the description in Appendix C to allow arrays to be passed as by location
arguments. A slightly more ambitious version of this project would include the
ability to pass “slices” of arrays (the reader might also want to think about the
attendant need for functions that make it possible to determine the lower and
higher bounds of a dimension lbound/hbound).
5. Thinking about “separate compilation” is interesting because it focuses on what
information is needed in descriptions of the interface.
5.5 Further material 81
6. Pascal offers an intriguing with statement that unfolds the names of the fields in
a record.
Further reading
It was indicated in Section 3.3 that there are various ways of recording a semantic
relation (between pairs of Program/ Σ and Σ); now that environments (Env) are
involved it would also be possible to emphasise their relative constancy by writing
them separately from the main relation — e.g.
ex
(rhs, σ ) −→ v
env ` st
(mk-Assign(lhs, rhs), σ ) −→ σ † {lhs 7→ v}
The clear separation of the environment (Env) from the state (Σ) is important and
has an interesting history. The early operational semantics work in the IBM Vienna
Lab focussed on the PL/I language. This was a huge undertaking; an outline of the
effort is given in [AJ18, §3] and a first-hand account in [LW69]; further connec-
tions are also traced in Chapter 11. Jan Lee introduced the term Vienna Definition
Language (VDL) [Lee72] (which is not to be confused with VDM).
The state of the VDL operational descriptions of PL/I was huge and among other
things included a stack of environments for all contexts that had been entered but
not completed. This had unforeseen consequences when it came to basing proofs on
VDL semantics: the property alluded to above that the environment is the same after
any statement as it was before that statement required a messy argument because of
the presence of the stack of Envs. Peter Lucas wrote the first such “twin machine”
proof [Luc68] but even the more developed [JL71] spent more space on this lemma
than on the real core of the design.
These difficulties were identified as shortcomings of the operational approach
and contributed to the move to denotational semantics (see Section 7.1), where the
separation of the environment from the state was almost mandatory. Subsequent to
the Vienna group moving to the denotational approach [BBH+ 74] (see [Jon01] for
more details), Gordon Plotkin proposed Structural Operational Semantics (SOS),
where the separation of environment from store is also clear [Plo81].
Chapter 6
Further issues in sequential languages
As made clear at the beginning of this book, it is not the aim to cover all possible lan-
guage challenges. This chapter mentions some interesting extensions to languages
and either sketches an approach to their models or provides references to where
such models are developed. The material here concerns only sequential features of
languages — mainly in the ALGOL or Pascal families; material on concurrency in
the spirit of Eiffel [Mey88], Go [DK15] or even Java [GJSB00] (i.e. concurrency in
an object-oriented context) is deferred to Chapter 9.
Here again, an important message is that the meta-language introduced to cope
with the semantics of languages as simple as that in Chapter 3 suffices to describe
programming languages that have been –or are still– used to build significant com-
puter applications.
There is an obvious issue of making the effect of executing a program visible beyond
its execution.
Language issue 28: Effects of a program
There are many ways in which programs can have an influence beyond their
execution — for example:
• Input and output are discussed in Section 4.3.1;
• Updating databases is touched on in Section 4.4 and further discussion is in
Section 9.7;
• Object-oriented programs can be linked to object stores (see Chapter 9).
Within a program, there is a related issue of how to retain the values of block-
local variables beyond an execution of a Block. Note that, in Chapter 5, even the
locations of local variables are discarded at block exit.
This forgetting of variables from blocks or procedures is made clear by the fact
that the statement after a block or a call uses the environment that sets the context
prior to that action.
Objects were first implemented in the Simula language [DMN68]. As its name
hints, the application was writing simulation programs. The specific challenge was
writing Monte Carlo simulations of ships docking at piers. Faced with the chal-
lenge of writing programs that represented arbitrary numbers of physical-world ob-
jects, the designers of SIMULA (Ole-Johan Dahl and Kristen Nygaard) realised that
blocks could have multiple instantiations to stand for such things as the boats in their
simulations.
Thus blocks can be thought of as defining classes, whose instances are objects.
Furthermore, procedures associated with a block provide a guide to what become
the methods of the class. But there are three essential differences to normal blocks:
• Firstly, for the intended purpose, objects must retain their values between uses.
(It is perhaps worth comparing this with the idea of own variables in ALGOL 60
which are discussed in Section 6.1.) This means that the semantic objects for a
language like Simula need at least a mapping from object References to a VDM
record that has as one component a mapping from (local) variable names to their
values.
• Secondly, since objects do not have the same ephemeral existence as Blocks,
there will need to be a way of garbage collecting objects that can no longer be
used.
• Thirdly and, perhaps most interestingly, the scope of method names is deliber-
ately external to the objects (in contrast to procedures in ALGOL-like languages,
which methods otherwise resemble).
Pascal is a well-designed and clean language from one of the world’s greatest de-
signers of programming languages: Niklaus Wirth and Tony Hoare provided key
insights for ALGOL W [WH66, BBG+ 68] and Wirth was the major designer of
86 6 Further issues in sequential languages
both the Modula series [CDG+ 89] and the Oberon series [Fra00] of languages.2
The Pascal feature of variant records has however some messy consequences and it
is interesting to look at how a formal description pinpoints the problems.
and re-reading pages 180–186 of that reference brings out the pain that Derek An-
drews and Wolfgang Henhapl experienced in having to model the interaction of such
a collection of features.
grams dynamically.
88 6 Further issues in sequential languages
6.5 Functions
In most respects, the issues around modelling functions are the same as those ad-
dressed in Chapter 5 for procedures but there are some additional points that are
of interest for models of languages. One important distinction is that, whereas pro-
cedures are invoked in a statement context, functions are activated as expressions.
As such, functions should obviously be given a return type in a language which is
strongly typed.
A related question is how the portion of the program that defines the function (its
body) should identify the value to be returned:
• A language can require that an explicit return statement identifies the value to be
returned; the programmer would write something like:
function f (· · ·)
···
return(e);
···
end
As well as causing evaluation of e to yield the value to be returned to the calling
context, executing the return terminates execution of the body.
• In some languages (including ALGOL 60) the return value is indicated by an
assignment to the name of the function as in:
90 6 Further issues in sequential languages
function f (· · ·)
···
f := · · ·
···
end
The value returned is that of the last assignment executed before the body com-
pletes (i.e. the assignment does not cause execution of the body to terminate).
• In languages where statements have values, the value of the function can be the
value of its defining body.
In languages that have an explicit return statement, that statement can be placed
inside other phrases such as loops, blocks etc. Modelling abnormal termination is
itself a problem whose discussion is postponed to Chapter 10.
Both procedures and functions can, in most procedural languages, give rise to side
effects in that statements to be executed in their body part can assign to variables
that are non-local to that body or even perform input/output.
Procedure calls in a sequential language are executed in a clearly defined order.
Function calls are initiated in expression contexts such as:
x := f (x) + g(y)
This can open up a form of non-determinacy that must be faced in a semantic de-
scription. A –possibly unexpected– feature interaction is with the fact that language
designers do not typically constrain the order in which sub-expressions are evalu-
ated. There are good reasons for this:
• compiler writers are normally faced with a limited set of fast registers and will
want to optimise their use — this can result in evaluating sub-expressions in
non-obvious orders;
• an even more extreme optimisation is that a compiler might be written to evaluate
“common sub-expressions” only once.
Thus the fact that function calls occur within expressions gives rise to some messy
questions about non-determinacy (because of side effects).
Pascal is a rather clean language but has a most unpleasant surprise for the writer of
its formal description. The compiler writer is given permission to re-order how sub-
expressions are evaluated because the Pascal documentation says that any program
6.5 Functions 91
6.5.3 Recursion
Most of the topics in this sub-section could be discussed in the context of procedures
but they are issues which really beg resolution for functions. In particular, recursive
procedures can be useful but recursion is almost ubiquitous for functions.
Amending the context conditions for BlocksProgram in Appendix C to allow
recursion is straightforward: it is only necessary to add the local proc-tpm to the
updated environment used in:
∀p ∈ dom pm · wf -ProcDef (pm(p), tpm0 † proc-tpm)
Changing the semantics for Block is more difficult because of the need to store
the environment of the procedures (or functions) within their denotations. One way
of solving this is to have a separate labelled collection of environments and to store
the label of the environment rather than the object itself. A more elegant solution
is to accept the recursive definition of environments and to define the value as the
relevant fixed point. This idea is explained in Section 7.1.
A useful way of achieving generic programs is to write functions that accept argu-
ments that are functions. For example, a function that returns a sequence that results
from applying its functional argument to every element of an argument sequence
might be:
apply : (X → Y) × X ∗ → Y ∗
apply(f , s) 4 if s = [ ]
then [ ]
else [f (hd s)]
y apply(f , tl s)
fi
The type of apply shows that its first argument is itself a function. The function
apply could be used for many different purposes (e.g. doubling every number in a
list or reversing every string in a list of sequences of characters).
With care on the part of programmers, higher-order functions can also be used in
imperative programming languages but indisciplined use of imperative features such
as side effects can subvert any advantages that might otherwise be gained by this
style of programming. Models of full-blown passing of functions and procedures
are given in the ALGOL 60 descriptions cited in [AJ18].
Procedure variables/results
In geometry, orthogonal lines are at right angles; more generally, orthogonality has
to do with independence. The term is sometimes used in programming language
design to argue that values of various types must be subject to the same rules.
Earlier projects have indicated that there is virtue in saying that a concept like
conditional statements invites consideration of conditional expressions and even
conditional references (see Section 2.3). ALGOL 60 extended this argument to la-
bels: since there were constant labels, there should be switch variables to which
labels could be assigned and such switch variables could be used in goto statements.
The designers of ALGOL 68 went further and argued that there should be variables
that could take procedures as values.
It is worth examining this plausible-sounding argument in terms of formal mod-
els. As Hans Bekič showed in [Bek73], such variables can result in violations of
the normal scoping rules. While it is true that passing procedures or functions as
arguments to other procedures or functions can only result in them being called
whist their context is still active, procedure variables can be assigned values that ex-
ist longer than their context. The same problem can arise with returning procedure
values from functions.
6.6 Further material 93
Projects
The main focus in this book is on the operational approach to documenting the
semantics of programming languages. There are however other approaches and un-
derstanding them is both instructive in itself and also throws light on operational
semantics by clarifying their relationship thereto.
A broad distinction between semantic methods can be made:
• “Model-oriented” methods are built around an explicit notion of an (abstract)
state of a machine underlying the semantics.
• “Property-oriented” approaches attempt to define the semantics in terms of prop-
erties of texts in the language.
Operational semantics is clearly model oriented in that meaning is given to texts
in a language L by defining how those texts transform an underlying abstract state.
Denotational semantics makes an important step of abstraction by fixing the se-
mantics of L by mapping its constructs into functions from states to states. It turns
out that the states in operational and denotational approaches can be identical for
simple languages and this supports viewing both of these approaches as model ori-
ented.
Early in attempts to capture the semantics of programming languages, researchers
investigated fixing key aspects of semantics by characterising equivalencies between
texts (e.g. [Bek64]). This is certainly one way to define semantics by properties and
relates to recent research on “algebraic semantics” (see Section 7.5). More promi-
nent in the property-oriented semantics world is the research on “axiomatic seman-
tics”, in which logics are provided for deducing properties of programs written in a
language L . Denotational semantics is outlined in Sections 7.1 and 7.2; Sections 7.3
and 7.4 discuss axiomatic semantics. A full study of these approaches would require
far more than this short chapter and fortunately good texts exist already — a selec-
tion of these are cited in Sections 7.2 and 7.4.
The step from operational to denotational semantics can be compared to that from
interpreters to translators. An operational semantics provides an abstract interpreter
that takes a program and a starting state and –for a deterministic language– computes
the final state. (The extension to use relations to final states for non-deterministic
languages is introduced in Section 3.2.) Denotational semantics maps a determinis-
tic program to a function from states to states. Just as operational approaches provide
abstract interpreters that avoid the details required in a machine-code interpreter, the
functions that serve as denotations of programs are more abstract than a machine-
code program generated by a translator for the language.
As is shown below, the abstract states used in denotational semantics are, in sim-
ple cases, the same as would be used in an operational description. The denotational
description is more abstract than an operational description because the former ab-
stracts away from the initial state required by the operational description. There is
however a cost associated with this abstraction: denotational semantics needs some
more sophisticated mathematical concepts than underlie operational descriptions.1
This section only outlines the main objectives of the denotational approach and
mentions the mathematical challenges.
Starting with the observation made in earlier chapters that the effective state-
ments in an imperative programming language are those that change the state, a
way of creating a function from states to states is required — for example a mean-
ing function M applied to Assign statements should yield a function:2
M [[mk-Assign(lhs, rhs)]] 4 · · ·
For a simple language such as that in Chapter 3, the type of this meaning function
M is:
M [[ ]]: Stmt → (Σ → Σ)
The (M ) semantics for assignment statements could be written with the state (σ )
made explicit:
M [[mk-Assign(lhs, rhs)]](σ ) 4 σ † {lhs 7→ eval(rhs, σ )}
The discussion that follows about the need to have a uniform way of defining M
as having the type Stmt → (Σ → Σ) for any statement argues for having a direct way
of defining M [[mk-Assign(lhs, rhs)]] without applying it to the state argument. For-
tunately Alonzo Church’s Lambda notation (see for example [Han04]) provides a
way of writing unnamed functions. For example, the identity function can be defined
in the Lambda notation as:
Id = λ σ · σ
1 During the development by Strachey, Scott and colleagues at the University of Oxford, the term
“mathematical semantics” was used; use of the adjective “denotational” came later — see [Sto77].
2 The use of “Strachey brackets” ([[ ]]) is a convention that has no deep meaning.
7.1 Denotational semantics 97
A Lambda expression follows the Greek letter λ with a list (in this case one) of
parameter names with the definition of the function after a dot.
The Lambda calculus is more than a notation — its semantics is fixed by a theory
of equality given by a small collection of equality rules between Lambda expres-
sions. Lambda functions can have type decorations — a specific identity function
could be defined:
Id = λ σ : Σ · σ
and this point becomes important below. Although it was clear that the typed
Lambda calculus had models, until Dana Scott’s ground-breaking research in Ox-
ford, no one had succeeded in showing that there were underlying models of the
untyped Lambda calculus. Unfortunately, features of programming languages like
ALGOL 60 relied on the untyped calculus.
Using Lambda notation:
λ σ · σ † {lhs 7→ eval(rhs, σ )}
is a function from states to states and can be used to avoid writing the σ on the left
of the defining 4 above:
M [[mk-Assign(lhs, rhs)]] 4 λ σ · σ † {lhs 7→ eval(rhs, σ )}
A key goal of denotational semantics is to express the meaning (denotation) of
compound statements in terms of the meaning of the components of the compound
object. Technically, this notion is that M is a homomorphic mapping from syntactic
objects to their denotations.
For simple compounds, this works nicely with mathematical composition of two
functions defined as:3
f1 ◦ f2 4 λ x · f2 (f1 (x))
Fixing the meaning of compound statements simply composes the meanings of the
two statements:
M [[S1; S2]] 4 M [[S1]] ◦ M [[S2]]
The mathematical challenge begins to increase with the denotation of while state-
ments. With the identity function (Id) as above and using an obvious notation for
conditionals,4 the denotation of While can be written:
M [[mk-While(test, body)]] 4
M [[test]] → M [[mk-While(test, body)]] ◦ M (body)
Id
The fact that the left-hand side of the definition (i.e. M [[mk-While(test, body)]])
also appears on the right of the definition symbol requires some clarification. Given
3 Mathematicians are divided about which order defines composition — the choice makes no es-
sential difference to the rest of the discussion.
4 The conditional can be encoded as a Lambda function but this detail is not germane to what
follows.
98 7 Other semantic approaches
certain conditions, such definitions can be considered to define fixed points. Fixed
points of recursive definitions can be built up — consider:
WH = while i 6= 0 do i := i − 1 od
Where the test is false, the recursive branch of the definition given above is not
needed and the whole function is defined to be the identity function. Therefore the
pair (0, 0) ∈ M [[WH]]. But once that base element is in M [[WH]], so must be the
pair (1, 0) ∈ M [[WH]]. Iterating this process requires that at least:
{(i, 0) | i ∈ N} ⊆ M [[WH]]
The set {(i, 0) | i ∈ N} is a fixed point of M [[WH]] because the recursive definition
does not force the addition of any further pairs. It is in fact the least fixed point
because arbitrarily adding, say, (−7, 0) results in further values. But the least fixed
point is the denotation that makes sense for While.
Since the least-fixed-point construction creates infinite objects in general, it does
not actually offer a useful tool for calculation. But it is the underlying semantics
and in terms of that semantics the useful proof rule of fixed-point induction can be
justified.
It is useful to return to the comparison between operational semantics as offering
an (abstract) interpreter and denotational semantics as defining a translation. The
mapping provided in the latter case is really an expression of the Lambda notation.
This is what Peter Landin envisioned in his important papers [Lan65a, Lan65b]. It
must be understood, however, that obtaining an expression for say the application
of the meaning function M to a program for factorial does not immediately yield
the mathematical function:
{(i, i!) | i ∈ N}
To prove this requires properties of the factorial operator. But given such properties,
there is a mathematical rule for such proofs.
In contrast, using an operational semantics needs not only the properties of the
factorial function but any proof has to be an induction over the steps of the com-
putation. This means that there are proofs that are more elegant when based on de-
notational semantic descriptions than if they were based on operational semantics.
Typical cases where denotational semantics shine are to show that composition is
associative or that unwrapping a while loop with a conditional preserves the original
meaning.
The advantages of abstracting denotations become clearer for languages that are
modelled with environments:
M [[ ]]: Stmt → (Env → (Σ → Σ))
Firstly, there is an expression for
M [[mk-Assign(· · ·)]]: Env → (Σ → Σ)
The environment has been bundled into the definition. Furthermore, the formula:
M [[S1; S2]] 4 M [[S1]] ◦ M [[S2]]
7.2 Further material 99
Chris Hankin’s [Han04] is more than adequate to provide the necessary background
on the Lambda notation.6
5 Just as Cantor had shown that there are more reals than rational numbers by an enumeration argu-
ment, it appeared that a cardinality contradiction existed for functions that could take themselves
as arguments.
6 Church’s [Chu41] is the original source and includes the wonderfully clear description:
100 7 Other semantic approaches
7 http://podcasts.ox.ac.uk/series/strachey-100-oxford-computing-pioneer
7.3 The axiomatic approach 101
is in [Jon76]. Key aspects of the approach include viewing the run-time state of
execution as a representation of the abstract states (see Section 7.3.5) and relating
programs that satisfy the concrete syntax to the abstract syntax of the language
description.
As described in more detail in [AJ18, JA18], the work was terminated when
IBM cancelled the machine for which the compiler was being constructed. First
as an LNCS [BJ78] –and later as [BJ82]– the aspects of VDM relating to lan-
guage description eventually received wider publication. Chapter 11 mentions other
uses of VDM as a basis for compiler development including the Danish Ada com-
piler [BO80a].
It is both useful and historically relevant to begin with the idea of recording asser-
tions about the state of a computation on a flowchart. A key reference that had a
significant influence on subsequent research is Bob Floyd’s “Assigning meanings to
programs”: in [Flo67] a program is presented by its “flowchart” but, as well as the
instructions and tests being written in rectangles and ovals, logical assertions are
associated with the arcs between boxes.
8The case is made below that post conditions should be relations between initial and final states;
Sections 7.3.1 and 7.3.2 follow the historical development where even post conditions were origi-
nally taken to be predicates of a single state.
102 7 Other semantic approaches
Figure 7.1 contains a version of Floyd’s annotated flowchart for an algorithm that
computes integer division by successive subtraction. The algorithm is straightfor-
ward: x is to be divided by y computing the quotient in q and leaving any remainder
in r.9 Looking at the decorating assertions, the overall required effect is associated
with the exit from the program (just before the oval marked HALT) as:
0≤r<y
x≥0
x = r+q∗y
The initial conditions are marked on the arc after the oval marked START as:
x≥0
y>0
Some of the assertions can be shown to be mechanically derivable from others but
the assertion within the loop is crucial to establishing correctness:
r≥y>0
x≥0
q≥0
x = r+q∗y
START
x ≥ 0, y > 0
q := 0
x ≥ 0, y > 0, q = 0
r := x
x ≥ 0, y > 0, q = 0, r = x
No
r ≥ y > 0, x ≥ 0, q ≥ 0, x = r + q*y
r := r - y
r ≥ 0, y > 0, x ≥ 0, q ≥ 0, x = r + (q+1)*y
q := q + 1
r ≥0, x ≥ 0, y > 0, q > 0, x = r + q*y
The final ellipses contain an expression in terms of two to the power of the word
size of the machine!
They can also be thought of as local “data type invariants” that are like context conditions.
104 7 Other semantic approaches
[Floyd67]
44
Fig. 7.2 Floyd’s original version of Figure 7.1
tain two logical assertions (predicates) surrounding a program text — they are now
written:12
{P} S {Q}
and are to be read as asserting that, if program S is started in a state that satisfies
predicate P, any final state will satisfy predicate Q. The predicate P is referred to as
the pre condition and Q as the post condition of S. One of Hoare’s claims was that
it was not necessary to pin down more details of the domain of these predicates. It
facilitates the comparison with operational semantics to assume that they are pred-
icates on the state of the computation, and this is certainly the way in which Hoare
triples are most commonly used.
An inference system can be defined for deducing valid judgements that are
recorded as Hoare triples. “Axioms” (or rules of inference) for the simple language
of Chapter 3 are given in Figure 7.3. The use of inference rules is of course famil-
12 In fact, Hoare originally (in [Hoa69]) chose to present the triples bracketed as P {S} Q.
7.3 The axiomatic approach 105
{P} S1 {Q}
{Q} S2 {R}
;
{P} S1 ; S2 {R}
{P ∧ b} S1 {Q}
{P ∧ ¬ b} S2 {Q}
if
{P} if b then S1 else S2 fi {Q}
{P ∧ b} S {P}
while
{P} while b do S od {P ∧ ¬ b}
:=
{P[e/x]} x := e {P}
P0 ⇒ P
Q ⇒ Q0
{P} S {Q}
consequence
{P0 } S {Q0 }
iar from SOS (see discussion in Section 3.2.2) and –as with SOS rules– those in
Figure 7.3 are generic in the sense that any valid substitution is taken to be allowed.
The first rule in Figure 7.3 is for the composition of two statements and identifies
the predicate that characterises the post state of S1 with the pre condition for S2 . An
example inference that corresponds to the body of the loop in Figure 7.1 would be:
{x = r + q ∗ y} r := r − y {x = r + (q + 1) ∗ y}
{x = r + (q + 1) ∗ y} q := q + 1 {x = r + q ∗ y}
{x = r + q ∗ y} r := r − y; q := q + 1 {x = r + q ∗ y}
The two hypotheses of that example can both be justified using the fourth rule in
Figure 7.3, which is for assignment statements. That assignment rule uses a notion
of substitution of an expression for an identifier: P[e/x] is the predicate expression
P with all occurrences of x replaced by e. The axiom (with no hypotheses) says that
P[e/x] is a valid pre condition for the assignment x := e to achieve a post state that
satisfies P. Thus the first hypothesis of the argument above about the body of the
loop follows from:
{x = (r − y) + (q + 1) ∗ y} r := r − y {x = r + (q + 1) ∗ y}
The most interesting of the rules in Figure 7.3 is the one (while) that addresses
loops because it brings in the important notion of a loop invariant. Ignoring the
occurrences of b for the moment, the rule states that, if P is a predicate whose truth
is preserved by S, then it follows that any number (including zero) of iterations of
S will preserve P. The actual rule makes discharging the hypothesis easier to do by
noting that S will only be executed in situations where b holds. Furthermore, the
conclusion can be strengthened by noting that, when the loop terminates, b cannot
hold.
106 7 Other semantic approaches
A key property of the loop in Figure 7.1 could be justified by the following in-
stance of this rule (which uses the result established above for the body of the loop):
x = r + q ∗ y r := r − y;
{x = r + q ∗ y}
r≥y q := q + 1
while r ≥ y do
r := r − y; x = r+q∗y
{x = r + q ∗ y}
q := q + 1 r<y
od
The overall pre/post for Figure 7.1 would be:
{x ≥ y ∧ y > 0} DIV {0 ≤ r < y ∧ x ≥ 0 ∧ x = r + q ∗ y}
which follows from:
• simple instances of the assignment and composition axioms to verify the initial-
isation; and
• a composition of that initialisation with the result about the loop.
The rule for conditional statements (the second in Figure 7.3) should be obvious.
The consequence rule notes that, given {P} S {Q} has been established, a triple
with a stronger pre condition and/or a weaker post condition must also hold.
Note that the rules as given in Figure 7.3 do not offer a way of establishing
termination — this and other comments on the method itself are given in Section 7.4.
The conditional result is that a program will satisfy its specification if it terminates;
this is sometimes referred to as “partial correctness” but the term is not used further
in this book.
It is interesting to observe that checking programs which contain assertions does
not fit the strict distinction between static context conditions and run-time errors.
The idea of program verification is certainly that it should be conducted prior to
execution on the static text of a program but checking assertions is not –in general–
a decidable process because it requires theorem proving.
Of more interest for now is that there are two senses in which axiomatic seman-
tics can be viewed as complementary to model-oriented approaches such as SOS:
• It should be possible to reason in a natural way about the correctness of programs
written in a language L . An axiomatic semantics for L has formal rules for such
reasoning and it is possible to mechanise the checking of such rules in a theo-
rem proving system such as Isabelle [NPW02]. Beyond the question of whether
formal proofs will be written for programs in L , it should be realised that dif-
ficulties in constructing an axiomatic semantics is a warning that even informal
reasoning might be error prone. One obvious example is that the proof rule given
for assignments in Figure 7.3 does not hold for a language that permits parameter
passing by location because an assignment to one identifier could affect the value
of what appears to be a distinct variable.
• There are well-known dangers in writing “axioms”. In particular, it is difficult
with extended sets of rules to be certain that they are “consistent” in the sense that
7.3 The axiomatic approach 107
There is also the question of the completeness of a set of axioms. For a system
such as Hoare’s, this asks whether all true statements about programs can be de-
duced from the axioms. This question becomes rather technical because of concerns
about the expressiveness of predicates and the inevitable undecidability of the pred-
icate calculus over arithmetic. An insightful description of the completeness issue
is given in [AO19].
Assertions on states as in the style of Floyd are certainly useful in proving that a
given program satisfies a stated specification. With some care, such assertions can
also be used in program development. But Hoare-style axioms make it much easier
to see how a development process can be based on formalism. The idea is to start
with a formal specification13 of the program and to use the inference rules to decom-
pose the task. Thus an overall specification can be realised by a decomposition that
introduces putative components that are –at that point– only given as specifications.
Such decomposition steps are repeated until all of the specifications have been de-
veloped to code. The final executable program is the collection of these expansions.
This idea has prompted various authors (e.g. Andrzej Blikle [Bli81], Carroll Mor-
gan [Mor88, Mor90] and Ralph Back [BvW98]) to include a “specification state-
ment” in a programming language and for contracts to be included in the Eiffel
language [Mey88]. Morgan uses:
frame: [P, R]
where frame lists the names of variables that can be changed, P is a predicate of
one state as the pre condition and R is a relation over two states that is the post
condition.14
It is interesting to see how easy it is to extend the language description in Chap-
ter 3 to allow specification statements embedded in a program; such a specification
will contain a pre condition (a predicate of a single state) and a post condition (a
relation over two states). The differences between Morgan-style specification state-
ments, Eiffel-style contracts or some two-dimensional layout (with keywords dis-
tinguishing the predicates) are just concrete syntax details.
Thus, extending Stmt in SimpleProgram of Chapter 3:
13 This does not, of course, answer the question of how a formal specification of a complex system
is obtained. Research in this area is contained for example in [Jac00] and given a more formal basis
in [JHJ07, BHJ20].
14 The move to relational post conditions is discussed in Section 7.4.
108 7 Other semantic approaches
Stmt = · · · | Spec
Spec :: frame : Id-set
pre : LogExpr
post : LogExpr
The semantics of Spec is both partial and non-deterministic so the hypotheses of
the SOS rule for Spec require that the pre condition P is true and that the relational
post condition holds for the pair of states σ , σ 0 :15
P(σ )
− σ 0 = frame C
frame C −σ
Q(σ , σ 0 )
st
(mk-Spec(frame, P, Q), σ ) −→ σ 0
So, for example:
st
mk-Spec({y}, true, x ≤ y0 ≤ (x + 2), σ1 ) −→ σ2
non-deterministically allows:
{x 7→ 1, y 7→ 0}
σ1 =
{x 7→ 1, y 7→ 1}
σ2 ∈ {x 7→ 1, y 7→ 2}
{x 7→ 1, y 7→ 3}
There is a danger with specifications that they ask for something infeasible such
as finding the largest prime number; the immediately preceding specification can be
made unrealisable by changing its frame:
{ }: [true, x ≤ y0 ≤ (x + 2)]
which would only be achievable if the initial value of y already satisfied the post
condition whereas the pre condition specifies that an implementation should work
for any state.
After Hoare’s 1969 paper, it was realised that there was an even more important use
for the axioms than reasoning about finished programs: the stepwise development
of programs from their specifications could be formalised using the same rules of
inference. Hoare published a stepwise development of his famous Quicksort algo-
rithm [Hoa61] in [Hoa71b, FH71] and a variety of formal development approaches
followed. These include the program development aspects of VDM from the early
1970s that were eventually published as a book [Jon80].
The reason that having a formal basis for design decisions is important in that
their validity can be checked as they are made — long before all of the code is de-
veloped: under the assumption that subsequent steps will find valid implementations
15 Detailed syntax and semantics for LogExpr are omitted here.
7.3 The axiomatic approach 109
operator. Here again, there is an echo of the role of programs as providing the route to extending
the expressive power of a language.
110 7 Other semantic approaches
The specification statement on the right can be developed (with no need to modify
j) to:
{r, i}: [0 ≤ i, r0 = r + i ∗ j] satby
while i 6= 0 do {r, i}: [0 < i, r0 + i0 ∗ j0 = r + i ∗ j ∧ 0 ≤ i0 < i] od
And the specification of the body of the loop:
{r, i}: [0 < i, r0 + i0 ∗ j0 = r + i ∗ j ∧ 0 ≤ i0 < i] satby
r := r + j; i := i − 1
This gives an algorithm that takes time linear in the initial value of j but it is possible
to get a logarithmic performance by taking advantage of the ability to change j:
{r, i, j}: [0 < i, r0 + i0 ∗ j0 = r + i ∗ j ∧ 0 ≤ i0 < i] satby
{r, i, j}: [0 < i, r0 + i0 ∗ j0 = r + i ∗ j ∧ 0 ≤ i0 ≤ i ∧ ¬ is-even(i)];
r := r + j; i := i − 1
and use shifts to multiply/divide by two:
{r, i, j}: [0 < i, r0 + i0 ∗ j0 = r + i ∗ j ∧ 0 ≤ i0 ≤ i] satby
while is-even(i) do i := i/2; j := j ∗ 2 od
There is a crucial property of the satby ordering. The technical expression is that
the constructs of the programming language are monotonic in this order. Simply
put this says that if a program fragment C has been shown to satisfy a specification
S — and C contains a component that is given by a specification scomp — then a
development of C where scomp is replaced by anything that satisfies the specification
scomp will also satisfy S.
So, for example:
[P, Q] satby while b do [Pc , Qc ] od ∧
[Pc , Qc ] satby C ⇒
[P, Q] satby while b do C od
This justifies collecting the steps above to justify that the program:
r := 0;
while i 6= 0 do
while is-even(i) do
i := i/2;
j := j ∗ 2
od
r := r + j;
i := i − 1
od
is non-deterministic in that it does not say by how much the value of i should be
reduced. This flexibility is used to develop both the linear algorithm in which the
reduction is by one per execution of the loop body and the faster algorithm in which
i is halved as long as its value remains even.
The rules for VDM differ from those for the refinement calculus only in the way
that termination is proved. There is also a difference in concrete syntax because
VDM specifications have tended to be used on applications where long pre and post
conditions do not fit conveniently into a single-line specification statement. VDM
specifications are usually displayed vertically with keywords marking the pre/post
conditions (see [Jon90]):
Mult
ext wr r, i : Z
rd j : Z
pre 0 ≤ i
post r0 = i ∗ j
pre 0 ≤ i
r := 0;
pre 0 ≤ i
while i 6= 0 do
inv 0 ≤ i
rel r0 + i0 ∗ j = r + i ∗ j ∧ i0 < i
while is-even(i) do
inv 0 ≤ i
rel r0 + i0 ∗ j = r + i ∗ j ∧ i0 < i
i := i/2;
j := j ∗ 2
od;
r := r + j;
i := i − 1
od
post r0 + i0 ∗ j = r + i ∗ j ∧ i0 = 0
post r0 = i ∗ j
Fig. 7.4 Annotated version of the multiplication program.
von Neumann to Herman Goldstine dated March 1947. It must be said that the
description in [GvN47] is far from clear.
• Alan Turing’s “Checking a large routine” [Tur49] does have a clear programme
of annotating a flowchart with assertions. This is a remarkable paper: in just three
pages Turing gives an inspired motivation for assertions, a proof of a doubly
nested program and an argument for its termination.
Sadly, neither of these papers had any significant effect on verification re-
search: [GvN47] introduced the idea which became known as the von Neumann
(computer) architecture and was studied mainly by people who were designing
early digital computers; [Tur49]20 was not known to Floyd or Hoare until after their
key papers were published. As noted in [Jon03], van Wijngaarden was at the 1949
conference where Turing gave his talk but he failed (or refused) to link it to his
own [vW66a].
• Bob Floyd’s paper [Flo67] (discussed above) certainly set the stage for many
subsequent steps on program verification.21 As published, it used a complicated
“forward assignment rule” that requires an existential quantifier. The paper does
include termination proofs of the two algorithms considered and gives proper-
ties that are required of sensible proof rules for programming constructs. (Di-
jkstra [Dij76] would later formalise such rules as healthiness conditions for his
predicate transformers.)
• Jim King’s Ph.D. [Kin69] was supervised at CMU by Floyd — King built the
Effigy system [Kin71] that both attempted to check Floyd-style annotations to
(PL/I) programs and deploy symbolic execution as an additional tool.22
• As well as acknowledging the influence of Floyd’s paper, Hoare’s [Hoa69] cites
Aad van Wijngaarden’s [vW66a], which tackles axioms for finite computer arith-
metic, and Peter Naur’s [Nau66], which uses general snapshots to record asser-
tions but expressed more as comments than in a formal logical notation.
• Hoare (possibly prompted by Floyd’s form of annotating assertions), Dijkstra and
others used post conditions that were predicates of a single state. From early pub-
lications, VDM used relational post conditions. The consequent inference rules
are bound to be somewhat more complicated but unfortunately those in [Jon80]
were (to use Peter Aczel’s understatement) “unmemorable”. Aczel showed in an
unpublished note [Acz82] that rules for post conditions of two states (a) were bet-
ter and (b) could be presented clearly. These rules were then employed in [Jon86]
and subsequent publications on VDM. Other specification languages such as
Z [Hay87], B [Abr96] and Event-B [Abr10] also use relational post conditions.
20 Not only are these proceedings somewhat inaccessible, Turing’s short paper was printed with
many typographical errors that impaired understanding — it was “exhumed” and republished
in [MJ84].
21 Floyd’s paper was first available as a mimeographed copy in 1966 and can be seen at:
http://homepages.cs.ncl.ac.uk/cliff.jones/publications/MSs/Floyd67.pdf
22 King moved to IBM Research and the current author used Effigy and showed (around 1976)
that it could be used to formally develop programs by using Prove/Assume commands to record
specifications of undeveloped sub-components.
7.4 Further material 115
• The SIEVE example mentioned above in connection with data reification pro-
vides a more compelling example of the desirability of “active decomposition”.
The post condition of the whole program specifies that the final state should
contain only primes (up to some given n). A natural decomposition in the devel-
opment of the Eratosthenes program is to have an initialisation phase that puts all
natural numbers from 2..n into the state and a second phase that removes com-
posites. Following a weakest pre condition method computes the pre condition
of the sieving phase to be exactly the post condition of initialisation. But the
sieving process functions perfectly well on any initial state: it will remove com-
posites if there are any. It is for this reason that the ; -I rule of VDM shown above
emphasises finding an interface predicate (interface) that the designer can use to
separate the sub-components properly.
• Although static proofs about programs provide much more assurance than test-
ing, even without such proofs, run-time evaluation of assertions provides a way
of detecting errors much closer to their source than trying to trace back from
a program crash resulting from corrupt data. This idea was proposed in Ed Sat-
terthwaite’s thesis [Sat75], is used in Eiffel [Mey88] and GCC23 and is employed
informally by many industrial groups.
• The topic of termination arguments has an interesting history. Turing and Floyd
both used formal arguments about reducing quantities; Dijkstra [Dij76] confined
himself to predicates of a single state so formalised the idea of reducing quantities
with rules about variant functions; VDM uses the fact that termination follows
directly if the relation for the loop is well founded.
• In addition to the problem of proving that loops do not run forever, there is a
danger that they abort in some way such as division by zero or computer rep-
resentations of numbers overflowing. (This was why van Wijngaarden looked at
the axiomatisation of finite computer arithmetic in [vW66a].) Dick Sites in his
beautiful thesis [Sit74] describes needing to prove clean termination — the same
problem is tackled in [CH79].
• Since King’s early Effigy system referred to above, huge strides have been
made in providing software that supports the task of creating (machine-checked)
proofs. General theorem provers include HOL-light [Har09], Isabelle [NPW02].
and Coq.24 ACL-2 (the most recent development of the Texas work that began
with [BM81]), KIV25 and Dafny26 are examples of tools more closely geared to
software development.
Returning to the history of the ideas on axiomatic semantics, there is an interest-
ing connection with the famous (1964) Baden bei Wien Working Conference. Hoare
did not present a paper but expressed strongly the idea that a language description
should be able to leave some things undefined (more in the tone of the current book,
one might say “under-defined”). Hoare went on to produce at least two significant
23 https://en.wikipedia.org/wiki/GNU Compiler Collection
24 https://coq.inria.fr
25 https://www.uni-augsburg.de/en/fakultaet/fai/isse/software/kiv/
26 https://en.wikipedia.org/wiki/Dafny
116 7 Other semantic approaches
drafts of an approach that attempted to be more axiomatic than say McCarthy’s op-
erational semantics. Floyd’s paper was sent to Hoare by Peter Lucas because the
Vienna group had been studying it; Hoare saw that Floyd’s assertions provided a
key idea that resolved issues with his earlier attempts and quickly wrote the defini-
tive [Hoa69]. Hoare has reflected on this experience in [HJ89] and talked about how
he might have done things differently in an ACM recorded interview.27
Hoare and colleagues went on to tackle various other programming constructs
including [Hoa71a, CH72] but the attempt to provide an axiomatisation of Pas-
cal [HW73] is incomplete. The only full language description in the axiomatic style
appears to be the Turing language [HMRC87]. A more promising avenue that is
pursued in SPARK-Ada [Bar06] and “featherweight Java” [IPW01] is to identify
subsets of complicated languages that can be axiomatised.
Given the range of semantic approaches, it is worth indicating where this author
considers their respective contributions are most likely to be effective.
Authors of early operational semantic descriptions tended to put too many things
into a monolithic “grand” state. This had the effect of making it hard to establish
properties of such definitions. Plotkin’s “Structural Operational Semantics” essen-
tially resolved this issue and, for example, the split between environments and states
made in denotational descriptions can be mirrored in operational descriptions. The
consistent argument throughout this book is that SOS descriptions provide a very
productive tool for both language understanding and design. With little need for so-
phisticated mathematical concepts, features of modern programming languages can
be written and read. The argument is often made that compiler designers should
base their developments on denotational descriptions but this author would also use
an operational description as a basis for compiler design.
An unassailable point is that programming languages have a sequential aspect
that can be difficult to express in approaches that might look more mathematically
elegant. Model-oriented approaches use an explicit notion of the state of a com-
putation; this affords a way of coping with the fact that so-called variables change
their values during a computation. Operational descriptions make this explicit; de-
notational descriptions do have a neat mathematical model of composing functions
from states to states. But concurrent threads updating a shared state (as discussed in
Chapter 8) are much harder to cope with denotationally precisely because this sort
of interference is inherently operational. A further challenge comes with exceptional
ordering (as discussed in Chapter 10).
The above praise of operational semantics is in no way intended to deny the ad-
vantages of denotational semantics for looking at deeper properties of programming
languages. One important example is the way that denotational semantics provides
27 https://www.acm.org/turing-award-50/turing-laureate-interviews
7.5 Roles for semantic approaches 117
This chapter moves beyond issues present in sequential languages typified by AL-
GOL descendants. The topic of concurrency is important and challenging in many
ways.
There are several reasons why programs need to exploit parallelism.
• applications such as those that support many simultaneous users are inherently
parallel;
• using fast processor cycles when some threads of execution are held up waiting
for slower external devices;
• as circuits approach atomic limits, hardware speed increase and miniaturisation
are unlikely to continue to follow Moore’s law and provide the speedup on which
society has relied for decades — fortunately, it is now practical to put many cores
on a wafer — but this potential parallelism has to be exploitable via software.
8.1 Interference
In some cases, programs can achieve rapid execution using parallel threads with
disjoint data.1 However, as soon as there is a need for threads to access and change
shared data, the resulting concurrent threads become extremely difficult to design:
• The number of paths through a sequential program is exponential with respect
to the number of branch points; with concurrency, the number of effective paths
explodes because of interference from state changes made by concurrent threads.
• It is notoriously difficult to debug concurrent programs since executions starting
in identical states can progress differently because of interference from concur-
rent processes.
• One particularly unpleasant consequence of the preceding point is that a pro-
grammer who is trying to locate the source of erroneous behaviour can add trac-
1 Often referred to as “Single Instruction Multiple Data” (SIMD) parallelism.
ing statements that change the timing behaviour in a way that hides the error (this
gives rise to the term “Heisenbugs”).
Language issue 39: Concurrency
There are actually many issues in concurrency: they include interference and its
control for mutual exclusion, synchronisation, the transfer of information and
deadlock detection/avoidance.
Typically, hardware provides low-level concurrency primitives (e.g. a “compare
and swap” instruction) for synchronisation. Programming language designers have
devised a range of ideas in an attempt to make the design and justification of con-
current programs somewhat tractable. Dijkstra’s semaphore idea (using p/v) is one
of the earliest.2 More structured language extensions followed including:
• Conditional critical sections [Hoa72]
• Monitors [BH73, Hoa74a]
• software transactional memory
• designers of process algebras [Hoa85, Mil89, Bae90] attempted to eschew the
notion of shared state but communication-based concurrency does not, in fact,
slay the dragon of interference.3
Modelling such constructs is an interesting challenge which is addressed in this
chapter. The specific target in Chapter 9 is to show how to use object-oriented ideas
as a way of structuring concurrency but the modelling ideas are generic over most
concurrency constructs.
Describing concurrent programming languages poses some of the same chal-
lenges as face the programmer using such languages: the interaction between
threads makes it difficult to describe aspects of a language in a structured way. The
good news is that there is no need to extend the meta-language developed in earlier
chapters. The challenge is to express interference in a reasonably structured way.
Challenge VII: Modelling concurrency
How can shared-variable concurrency –and the inherent interference that man-
ifests itself by state changes that give rise to massive non-determinacy– be de-
scribed using SOS?
Section 8.2 explains the essential development of the operational semantic de-
scriptions that is required to model concurrent shared-variable threads; a small (and
clearly artificial) language is used to explain the core idea with a minimum of dis-
tractions. Section 8.3 extends the discussion on granularity; Sections 8.4 and 8.5
pick up the topic of reasoning about programs written in the object languages.
The topic of object-oriented languages (initiated in Section 6.2) is resumed in
Chapter 9 because such languages can provide an extremely useful way of control-
ling concurrency and thus provide tractable languages for programmers.
2 It is interesting that Gary Peterson [Pet81] found a way of programming the p and v operations
without hardware support.
3 The focus in this book is on shared-variable concurrency — some discussion of process algebras
is in Section 8.6.
8.2 Small-step semantics 121
The first issue to get clear is the way in which (shared-variable) concurrency gives
rise to non-determinacy. With two threads (S1 , S2 ), (S3 , S4 ) running in parallel,
(S1 ; S2 ) || (S3 , S4 )
there are six possible orders in which the statements can be executed even if state-
ments are considered to execute atomically4 — the set of sequences is:
[S1 ; S2 ; S3 ; S4 ]
[S1 ; S3 ; S2 ; S4 ]
[S1 ; S3 ; S4 ; S2 ]
[S3 ; S4 ; S1 ; S2 ]
[S3 ; S1 ; S4 ; S2 ]
[S3 ; S1 ; S2 ; S4 ]
To see how this affects the results, consider the following instances of the Si :
(x := 1; x := x + 3) || (x := 2; x := x ∗ 2)
Again, for the moment, assuming that assignment statements execute atomically, the
final value of x is in the set {4, 5, 7, 8, 10}. It is the task of the language description
to say that all of these outcomes are allowed — and to make clear that no others are
considered to be correct.
The point is made in Section 3.2 that SOS rules provide a natural way of describ-
ing non-determinacy and they are therefore ideal for concurrency. What has to be
recognised, in an operational framework, is that there needs to be a way to record
the statements that are still to be executed in each thread. In previous chapters, the
SOS rules are written so that they discard executed statements. The most obvious
case is the left-to-right evaluation of a list of statements, where the head of the list
is executed and the rest of the computation is only affected by the tail of the list.
With concurrent threads, there is essentially a tree of putative next steps. In the
early Vienna Lab (VDL) operational descriptions, this control tree was completely
explicit as a state component. An advantage of SOS is that the selection of next
steps is implicit in the selection of SOS rules. The same choices as were explicit in
the VDL control tree have to be indicated but SOS succeeds in factoring the non-
determinacy out of the state and into rule selection.
The key response to the challenge of describing concurrency is to define the
semantic relation over configurations that pair the remaining text to be executed
with the state.
To illustrate this, a part of an artificial programming language with two parallel
threads is considered: threads contain only assignment statements (issues such as
blocks and procedures are postponed):
Par :: thrd1 : Assign∗
thrd2 : Assign∗
4 This unrealistic assumption is reconsidered in Section 8.3.
122 8 Shared-variable concurrency
8.3 Granularity
to prohibit any sharing of variables between parallel threads.5 This certainly makes
it easy to reason independently about the threads but the constraint is too extreme
for many applications. There is a spectrum ranging from low-level system code that
often results from intimate access to shared variables through to applications that
revolve around large shared databases. Although the detailed language resolutions
differ, the general need for ways to control access from separate threads to shared
variables is something that must be modelled.
The serious challenge for semantic description is to move in the direction of finer
granularity.
The comment is made in Section 8.2 that the assumption that assignment state-
ments can be executed atomically is unrealistic. This is because a compiler will
typically expand a statement such as x := x ∗ 2 into steps that place the (right-hand)
value of the variable x into a register, then perform the multiplication before writ-
ing the computed result back into the location (left-hand value) for x. If another
thread accesses and changes x between these steps, that update can be overwritten.6
Thus the example threads at the beginning of Section 8.2 could –under the realistic
assumption that only variable read and write are atomic– also give rise to the ad-
ditional outcome that execution of the two threads would result in a final outcome
with x0 = 2. To see how this can come about, the following sequence of steps makes
explicit the use of a temporary variable t — the two threads might interleave their
steps as follows:
x := 2; x := 1; t := x; x := x + 3; x := t ∗ 2
This is by no means an arcane detail: leaving aside for the moment that many cru-
cial low-level programs have to be written in terms of such sequences of accesses,
it is difficult to avoid similar problems at the level of transferring money between
back accounts. This is a clear case against a language with such ill-constrained in-
terference.
Although such low-level interference is undesirable, it is worth sketching how
it can be modelled. The key to modelling finer-level thread merging is to modify
configurations at the appropriate level. Thus a single SOS rule might be needed to
show accessing one scalar value and replacing the identifier with the accessed value.
5 Such a standpoint is adopted by many process algebras –see Section 8.6– as explained there,
unfortunately this does not get around the problem of interference.
6 One proposal to avoid this problem is known as “Reynolds’ rule” (although John Reynolds told
the current author that he had nothing to do with it!), which requires that only one shared variable
occurs in any assignment. Unfortunately this fix does not resolve the real problem.
124 8 Shared-variable concurrency
This optional section picks up –from Section 7.3– the theme of providing ways of
formally reasoning about –or formally developing– programs in an object language.
Here, of course, the interest is in how to provide inference rules that support the
introduction of concurrent threads whereas Chapter 7 addressed the story for se-
quential programming languages.
The rule for decomposing a specified task into two components that are to be
executed sequentially shows that the second statement is initiated in the state that
results from executing the first.7 In contrast, parallel threads are initiated in identical
states. Assuming that the two threads are specified as:
{P1 } S1 {Q1 }
{P2 } S2 {Q2 }
then, under rather strong assumptions, it would be true that their specifications can
be combined as follows:
{P1 ∧ P2 } S1 || S2 {Q1 ∧ Q2 }
The key assumption is that there is no interference between the threads. This is a
useful observation (and looks forward to the ideas in Section 8.5). Unfortunately,
many interesting uses of concurrency have to cope with interference and the SOS
rules covered in the earlier parts of this chapter are aimed at exactly characterising
such interference.
This leaves the challenge of how a proof-oriented approach can deal with inter-
ference. This section outlines one approach that tackles interference head on and
Section 8.5 outlines a line of attack that is predicated on avoiding interference.
The Rely/Guarantee (R/G) approach extends specification by pre/post conditions
both to face interference and to provide ways of reasoning about it in program de-
velopment. The fact that few programs can achieve their post relation in arbitrary
starting states is recognised by recording pre conditions as part of a specification.
Almost no useful post condition could be achieved by a program that experienced
unconstrained interference on its variables so an R/G specification uses a rely con-
dition that describes the interference that executions of the program must tolerate.
Rely conditions are relations over two states; this fits naturally with VDM’s rela-
tional post conditions and admits the view of a rely condition as the post condition
of a potential interference step.
As emphasised by the colouring in Figure 8.1, both the pre and rely conditions
are assumptions that the designer can make; ensuring that they are satisfied is a
requirement on the context; in other words, the decomposition that introduces the
specified components must show that the conditions pertain.
To this end, it is also necessary to document –for each component– its guaran-
tee condition that expresses the maximum interference that it can inflict on sibling
7This holds in either the original Hoare rules as in Figure 7.3 or the VDM style that uses relational
post conditions (see Section 7.4).
8.4 Rely/Guarantee reasoning [*] 125
processes. Like post conditions, guarantee conditions are obligations on the running
code.
Figure 8.1 indicates how the various predicates apply to the execution of the
ongoing process and any other processes that can interfere with its variables. The
contention is that rely and guarantee conditions offer a useful abstraction of inter-
ference. This claim is supported by evidence from a corpus of examples.
pre rely
z}|{ z }| {
σ0 ··· σi σi+1 ··· σj σj+1 ··· σf
| {z }
guar
| {z }
post
and post conditions can be written as a quintuple wrapped around the program text
that is to be executed: {P, R} S {G, Q}.8 To indicate how the rely/guarantee rules
relate to the non-interfering version of the parallel rule as at the beginning of this
section, a slight simplification of the actual rule is:9
{P, R ∨ G2 } S1 {G1 , Q1 }
{P, R ∨ G1 } S2 {G2 , Q2 }
|| -RG
{P, R} S1 || S2 {G1 ∨ G2 , Q1 ∧ Q2 ∧ · · ·}
This rule shows that the pre and post conditions of the two parallel components can
be combined providing the rely and guarantee conditions of the components agree.
The development of the parallel sieve in [HJ18] makes a subsequent data reifica-
tion of the set into arrays of bits.
The “Sieve” example involves a collection of threads that (apart from their pa-
rameter) have identical specifications. Applications where the processes differ such
as senders and receivers in “Asynchronous Communication Mechanisms” are more
interesting (see [JH16]) and can be handled with the same proof rules.
R/G specifications can be written as five-tuples (pre, rely, program, guarantee,
post) and proof rules given for justifying the introduction of concurrent processes
(such a rule is given in [Jon00]).
Recent research has embraced the idea of specification statements and records
rely and guarantee conditions as clauses to be wrapped around any specifica-
tion. This way of presenting R/G thinking makes it possible to emphasise al-
gebraic properties such as the distribution of rely conditions over decomposi-
tion [JHC15, HCM+ 16].
Ian Hayes presented a tutorial in Chengdu (China) during 2018 and the proceed-
ings [HJ18] include two worked examples (the tutorial itself additionally covered the
Treiber stack). Further examples in the literature include: parallel “cleanup” opera-
tions for the Fisher/Galler algorithm [CJ00]; Simpson’s “four-slot” implementation
of Asynchronous Communication Mechanisms (ACMs) [JP11, JH16]; concurrent
garbage collection [JVY17]; and Mergesort [JY15].
The origins of the R/G approach (in particular its relationship to the Owicki-
Gries approach [Owi75, OG76]) are explored in [dRdBH+ 01]. Examples of R/G
developments clearly indicate a top-down design approach; finding a compositional
approach to developing concurrent programs was a major objective of the research
(again see [dRdBH+ 01]).
The key reference for Concurrent Separation Logic (CSL) is [O’H07]. In that pa-
per Peter O’Hearn emphasises that CSL supports reasoning about (data) “race free-
8 This quintuple version of rely-guarantee obviously follows Hoare triples (see Section 7.3.2).
9 The simplification is that a stronger post condition can use information from the guarantee con-
ditions.
8.5 Concurrent Separation Logic [*] 127
dom” and contrasts this with the rely/guarantee approach, which tackles “racy pro-
grams”. It is useful to again look at the idealised rule at the beginning of Section 8.4:
what this indicates is that a parallel combination can combine the pre and post
conditions of its sub-components providing there is no interference. Tony Hoare
in [Hoa72] could establish non-interference by looking at the alphabets of the two
parallel processes because only normal (i.e. stack) variables were being considered.
John Reynolds’ “Separation Logic” [Rey02] tackles reasoning about heap variables
(i.e. dynamically allocated variables). This was in itself a bold and important step.
Concurrency adds to this the challenge that the ownership of dynamic addresses can
be exchanged between concurrent threads. The success of CSL is that it makes it
possible to reason about programs that achieve disjointness –and thus avoid data
races– even in the presence of such ownership exchanges.
The key CSL rule for reasoning about concurrent threads is:
{P1} S1 {Q1}
{P2} S2 {Q2}
|| -SL
{P1 ∗ P2} S1 || S2 {Q1 ∗ Q2}
This differs from the ideal rule at the head of Section 8.4 only in that logical con-
junction has been replaced by “separating conjunction” (written as “*”). This oper-
ator requires that the addresses in the two operands do not overlap. It is important
to remember that both Reynolds’ original Separation Logic and CSL address heap
variables.10
CSL owes its origins to detailed analysis of intricate pieces of code and tends to
be used in a bottom-up analysis of such programs rather than in top-down design.
That having been said and despite their different attitudes to data races, there are
many connections between CSL and R/G methods:
• RGSep [VP07, Vaf07] and SAGL [FFS07] offer explicit combinations of the
approaches;
• Local R/G [Fen09] brings local reasoning and information hiding to concurrency
verification;
• Deny/Guarantee [DFPV09] tackles fork/join concurrency, which is not obviously
handled by the original phrase-structured R/G rules;
• research on “Views” [DYBG+ 13] provides a common framework for justifying
proof obligations.
Projects
The technique of small-step SOS is exploited in the next chapter and any number
of projects can be attempted there. The reader might like at this point to experiment
with changing the semantics of the non-deterministic for loop from Section 3.2.3 so
that all instances are executed concurrently.
Further reading
There are many interesting and useful books on the general topic of concurrency
including [Sch97, MK99, BA06].
Even for operational semantics, there are further issues around concurrency that
are left aside here. One that deserves at least a mention is fairness — consider:
x := 0 || while x 6= 0 do i := 1 + 1 od
There is clearly no a priori limit on the value of i but the question of whether the
right-hand loop terminates depends on whether the scheduler is fair in the sense that
it ensures the left-hand assignment does eventually execute. The standard reference
on fairness is [Fra86]; Ian Hayes and Larissa Meinicke have also explored [HM18]
the notion of “justness”.
Because of its essentially operational nature, concurrency poses strong chal-
lenges for denotational semantics: Plotkin [Plo76] showed how to use power do-
mains to handle concurrency; resumptions are described in [BA90]; other ap-
proaches include game semantics [Abr13].
A more radical approach to concurrency is to attempt to move away entirely from
shared variables. Tony Hoare [Hoa78, Hoa85] and Robin Milner [Mil78a, Mil80]
each developed process algebras in which communication was the main focus.
Hoare’s CSP is given a semantics in [BHR84] in terms of traces and refusals.
It is, however, worth emphasising that process algebras do not avoid the problem
of interference as can be seen by the ease with which analogues of shared variables
can be programmed in these notations. The question can then be asked whether
traces and refusals offer a more convenient way of reasoning about interference
than, say, R/G.
Further afield, many researchers prefer to reason about concurrent programs us-
ing Temporal Logics. Classic texts in this area include [MP95, Mos85] and a re-
cent book is [Fis11]. An interesting combination of interval temporal logic and R/G
is [STER11].
So-called true concurrency (as opposed to an interleaving model) has been stud-
ied via Petri nets — see [Rei12, Rei13].
8.6 Further material 129
A language that shows that object-oriented ideas can make concurrency tractable is
Pierre America’s POOL [Ame89]; the language COOL introduced in this section
and fully described in Appendix D is inspired by POOL.
As mentioned in Section 6.2, a central idea in OOLs is that each object has its
own copy of the instance variables of a class — this offers separation providing only
the methods of the class are allowed to access the (instance) variables. At first sight,
this might appear to go too far and make the activity in objects entirely disjoint. Such
extreme isolation is overcome by allowing objects to communicate via method calls
or invocations.
As explained in Section 6.2, to make this communication possible, it is necessary
to ensure appropriate visibility of method names. In say ALGOL 60, procedure
names declared within a block are visible only inside that block (see Section 5.3);
in OOLs method names are visible to other classes. In fact, method invocation is
precisely the means by which objects interact.
As also pointed out in Section 6.2, instance variables in OOLs preserve their
values between method invocations.
A number of key questions remain about how to embed concurrency in a man-
ageable OOL and options for COOL are investigated throughout this chapter.
In order to introduce COOL, consider the task of creating a “sorting ladder” that
keeps a series of objects in ascending order of values of their v field as in Figure 9.1.
Instances of the Sort class are linked via their l field (the final element in the ladder
has a nil value in l). Thus the class description might declare variables:1
1Many years ago (in a Heuriger in Vienna) T.C. Chen outlined a potential use of “bubble memory”
which could sort numbers in time (constant) one! The idea is to use parallel logic at each memory
9.1 Objects for concurrency 133
Sort class
vars v: N; l: ref(Sort); · · ·
..
.
By insisting that variables that contain references are declared to be specific to
one class, it is possible to check statically that only known methods are invoked.2
v 2 v 4 v 6 v 8
l l l l nil
cell so that inserted values trickle down to the appropriate place in the ladder; the smallest value
can always be obtained from the first element of the ladder (followed by shuffling values up). This
is effectively a concurrent algorithm — see Section 9.5.
2 An alternative would be dynamic checking and, hopefully, some form of exception for unknown
pieces of code (as though they were translated into abstract syntax form).
134 9 Concurrent OOLs
Suppose a client object –that has a variable l pointing to the first Sorter object in
a ladder– executes:
activate(l.insert(7)); activate(l.insert(3)); activate(l.insert(9))
v 2 v 4 v 6 v 8
l l l l nil
The descriptions of the languages in earlier chapters have been followed by an indi-
cation of how informative their semantic objects can be. For COOL, the description
here starts with the semantic objects in order to emphasise how much insight they
convey about a language even before any detailed SOS rules are written.
Classes define the shape of objects including their instance variables; each object
created for a class has local values for each of the instance variables. If each object
is uniquely keyed by a Reference, a prime aspect of the state must be:
m
ObjMap = Reference −→ ObjInfo
ObjInfo :: · · ·
σ : VarStore
···
m
VarStore = Id −→ Val
9.1 Objects for concurrency 135
The values allowed include integers and Booleans; in addition values of type
Reference can be stored (and nil used to mark an uninitialised variable of type
Reference).
Val = Z | B | Reference
In the basic version of COOL, any object is created in a quiescent state being
R EADY for a method call (this decision is reconsidered in Section 9.7). Objects can
also be in an Active state and, as in the language sketch in Section 8.2, the remaining
code of the method being executed is the other essential information for Active to
function as a “configuration” (compare Section 8.2).
Furthermore:
• an active method records in client the identity of the object that gave rise to its
activity;4 and
• method activation requires that the body of a method can be located, so the class
field of ObjInfo contains the name of the class.
Thus the key semantic objects are:5
ObjInfo :: class : Id
σ : VarStore
mode : R EADY | Active
Active :: rem : Stmt ∗
client : Reference
The fact that the rem field has exactly one sequence of statements indicates the
important decision in COOL that at most one method can be active in any object
at any one time. An example of ObjMap that corresponds to Figure 9.1 is shown in
Figure 9.3(b).
r1 r2 r3 r4
v 2 v 4 v 6 v 8
l l l l nil
(b) The ObjMap corresponding to the picture above with all objects quiescent.
r1 r2 r3 r4
v 2 v 4 v 6 v 8
l l l l nil
(d) A possible ObjMap during the three method activations in the picture above.
Fig. 9.3 Examples of ObjMap
of that object;6 as observed above, this is what achieves encapsulation of data rep-
resentations. Providing only one method can be active at any one time, the danger
of data races on instance variables is eliminated. This cautious position is taken in
the initial form of COOL. (An associated risk that comes from sharing references is
discussed in Section 9.7.)
There remain two key further questions about adding concurrency to the language
discussed in Section 6.2:
6Some languages (including Java) offer ways of exposing internal details of representations — an
extension of this sort is considered in Section 9.7.
9.2 Expressions 137
Appendix D contains a full description of COOL including its abstract syntax and
context conditions. It is however important to note how much of the capability of
the language has been brought out by looking at the semantic objects before writing
any SOS rules:
• ObjInfo clarifies what an object (instance of a Class) needs to contain.
• A newly created object is in the R EADY state.
• Knowledge of the variables (and later methods) of a class has to be obtained from
the text of the Classes.
• The values of (instance) variables are local to each object and can only be ac-
cessed and changed by methods of that object.
• These values are preserved between method calls.
• An ObjInfo also records the reference of the client on whose behalf it is execut-
ing.
• The remaining code to be executed in a method is stored in the ObjInfo.
• (Crucially) the granularity of interleaving between threads is set at the level of
single statements.
9.2 Expressions
The syntax of COOL expressions given in Appendix D has only one extension from
the languages considered in earlier chapters and that is the addition of a unary test
as to whether a variable contains a nil reference:
Expr = · · · | TestNil
TestNil :: object : Id
138 9 Concurrent OOLs
The relevant change to the context conditions is to make c-type identify this form
of expression as delivering a Boolean result.
To emphasise that the expression evaluation is deterministic, the semantics of
COOL expressions is given as a function:
eval: Expr × VarStore → Val
and the relevant case is:
with:
σ = {v 7→ 6, l 7→ r4 , x 7→ 5}
Executing the assignment should change the variable map of Oi (r) to give:
σ 0 = {v 7→ 5, l 7→ r4 , x 7→ 5}
Furthermore, the completed statement should be removed, leaving only rl in the
rem field — the class and client fields of Oi (r3 ) are unchanged. Thus the resulting
ObjMap would be:
Oi+1 (r3 ) = mk-ObjInfo(Sort, σ 0 , mk-Active(rl, k))
The SOS rule for Assign is:
robj = O(r)
mk-ObjInfo(cl, σ , mk-Active([mk-Assign(lhs, rhs)] y rl, k)) = robj
robj0 = mk-ObjInfo(cl, σ † {lhs 7→ eval(rhs, σ )}, mk-Active(rl, k))
st
(· · · , O) −→ O † {r 7→ robj0 }
The context conditions for Assign should be obvious from earlier language descrip-
tions and are spelled out in Appendix D.
It is worth repeating that methods cannot be invoked from expressions in COOL
— Call (and Delegate) are statements and their semantics is covered in Sec-
tions 9.5.2 and 9.5.3 respectively.
Conditional statements should also offer few surprises given the languages cov-
ered in Chapters 3 and 4. The abstract syntax is:
If :: test : Expr
then : Stmt∗
else : Stmt∗
The context condition for If is given in Appendix D and should anyway be obvi-
ous: c-type of test must yield B OOLT P and both th and el must be well formed.
Turning to the semantics of conditionals in COOL, the ObjMap in Figure 9.3(d)
contains two ObjInfos indexed by r1 , r2 that are ready to execute an If (i.e. iif ).
Because the concurrency in COOL requires a small-step semantics, iif is unrolled
and, if r1 is selected for progress, this would give rise to:
Oi+1 (r1 ) = mk-ObjInfo(Sort, {v 7→ 2, l 7→ r2 , x 7→ 9}, mk-Active(p-i, r0 ))
where:
p-i: activate(l.insert(x))
The SOS rule for the then case of If is:
robj = O(r)
mk-ObjInfo(cl, σ , mk-Active([mk-If (test, th, el)] y rl, k)) = robj
eval(test, σ ) = true
robj0 = mk-ObjInfo(cl, σ , mk-Active(th y rl, k))
st
(· · · , O) −→ O † {r 7→ robj0 }
140 9 Concurrent OOLs
The creation of new instances of classes is more interesting than the foregoing sim-
ple statements. Objects are created as instances of classes using:
Stmt = · · · | New | · · ·
New :: target : Id
No type information is required in the statement itself because the class can be de-
termined from the type of the variable to which the new reference is to be assigned.
The ObjInfo in Figure 9.3(d) that is indexed by r4 will evolve to:
mk-ObjInfo(Sort,
{v 7→ 8, l 7→ nil, x 7→ 7},
mk-Active([mk-New(l), mk-Assign(v, x)], r3 ))
The effect of executing the New should change this to:
mk-ObjInfo(Sort,
{v 7→ 8, l 7→ rn , x 7→ 7},
mk-Active([mk-Assign(v, x)], r3 ))
and create a new quiescent thread rn with default initial values:
mk-ObjInfo(Sort, {v 7→ 0, l 7→ nil}, R EADY)
In order for the semantics of New (and Call below) to locate information about
st
classes, the type of the semantic relation −→ needs to be:
st
−→: P((ClMap × ObjMap) × ObjMap)
with:
m
ClMap = Id −→ Class
m
Class :: vars : Id −→ Type
m
methods : Id −→ Meth
The full SOS rule is given in Appendix D — here the routine description of
state initialisation is omitted in order to focus on the more interesting aspects of the
semantics of New:
9.5 Method activation and synchronisation 141
robj = O(r)
mk-ObjInfo(clr , σr , mk-Active([mk-New(targ)] y rl, k)) = robj
n ∈ (Reference − dom O)
robj0 = mk-ObjInfo(clr , σr † {targ 7→ n}, mk-Active(rl, k))
cln = (C(clr ).vars)(targ)
σn = initial values
nobj = mk-ObjInfo(cln , σn , R EADY)
st
(C, O) −→ O † {r 7→ robj0 , n 7→ nobj}
As can be seen, the new thread (indexed by n) is created in an inactive state. An
alternative would be to have an initial method that executes on object creation —
this idea is outlined in Section 9.7.
A programmer might wish to delete the value of a reference. One way of doing
this would be to add nil as an option in the abstract syntax of Expr but this would
cause a problem with defining c-type in this case because nil could be a value of
any optional reference type. Rather than take this route, Appendix D.5.2 defines a
Discard statement that sets the appropriate reference variable to nil.
Notice that Discard does not destroy the referenced ObjInfo because there could
be other reference variables in the current object or in other objects that contain
the same reference and might need to invoke methods in the referenced object. The
subject of “garbage collection” of objects is also sketched in Section 9.7.
Objects (as instances of classes) can be in a quiescent state, which is marked by the
mode field in their ObjInfo containing R EADY. As explained in Section 9.4 objects
are created in this R EADY state — a server also returns to this state on completion
of activity on behalf of a client.
Activity in a quiescent object can be started by a request from another (client)
object:
• One possibility is that –after activation– there is no need for further communica-
tion between the client and the server. This scenario is described in Section 9.5.1.
• Another possibility is that a result is required by the client and the server must
return such a value before the client can progress. There are actually sub-options
here depending on whether the client has useful work that it can perform in par-
allel with the server before the result is available. Section 9.5.2 gives the descrip-
tions of the relevant parts of COOL.
142 9 Concurrent OOLs
• Another way of enhancing concurrency is for the object that acted as initial server
to delegate computing a result to another object and thus free itself for work on
behalf of some new client. This delegation concept is described in Section 9.5.3.
Methods are activated using the statement whose abstract syntax is:
Activate :: object : Id
method : Id
args : Expr∗
Notice that the object field contains the name of a variable whose value is the refer-
ence of the server object in which the method is to be activated (i.e. a programmer
cannot write a Reference in a statement because they are machine generated). The
context conditions are routine and are contained in Appendix D.4.1.
Executing the first call:
activate(l.insert(7))
in the state depicted in Figure 9.3(a) should make thread r1 become active in order
to execute its insert method; the previous state of the ObjInfo should be updated
with argument values passed (in this case the identifier x gets the value 7).
The picture in Figure 9.4 serves to introduce the formal semantics — it indi-
cates the fact that both the activated server object (rs ) and the client (rc ) continue to
execute.8
Active
rc
Activate
READY Active
rs
The SOS rule to achieve this step has to update two ObjInfos:
cobj = O(c)
mk-ObjInfo(clc , σc ,
mk-Active([mk-Activate(obj, meth, args)] y rlc , k)) = cobj
cobj0 = mk-ObjInfo(clc , σc , mk-Active(rlc , k))
sobj = O(σc (obj))
mk-ObjInfo(cls , σs , R EADY) = sobj
mk-Class(vars, meths) = C(cls )
mk-Meth(rtp, params, paramtps, body) = meths(meth)
σs0 = σs † {params(i) 7→ eval(args(i), σc ) | i ∈ inds args}
sobj0 = mk-ObjInfo(cls , σs0 , mk-Active(body, c))
st
(C, O) −→ O † {c 7→ cobj0 , σc (obj) 7→ sobj0 }
The SOS rule above makes clear that both the object whose reference is cobj0 can
continue actively executing rlc and sobj0 begins executing body.
Notice that this rule can only be used if the server object (sobj indexed by
σc (obj)) is in the R EADY mode. (Remember that all hypotheses of an SOS rule
have to be satisfied for a rule to confirm the relation in its conclusion.) Any attempt
to activate a method in an active object will have to wait. This of course brings
danger of “deadlock”, where the server never makes progress (see Section 9.7).
In the terminology of Section 5.4, the parameter passing mode in COOL is “by
value”: evaluated arguments are installed in local objects of the server. It is however
true that passing a Reference confers considerable power to the receiving method:
possession of a reference makes it possible to invoke any of its methods.
An obvious way to write a method for the class Sort that tests whether a value is
present anywhere in the ladder is with a Call statement, whose execution has to wait
until its server object returns a value:
test(x: N) method : B
if is-nil(l) ∨ x < v then return (false)
elif x = v then return (true)
else call(b, l.test(x)); return (b)
fi
Such a Call statement has an abstract syntax that is similar to that for Activate
but has, in addition, an lhs field to which the server’s return value will be assigned:
Call :: lhs : Id
object : Id
method : Id
arguments : Expr∗
144 9 Concurrent OOLs
Here again, the context conditions are straightforward and are spelled out in Ap-
pendix D.4.2.
Active Active
rc
Call
Return
READY Active READY
rs
Figure 9.5 indicates the flow of control with the gap in the upper line indicating
that rc has to suspend activity until the server rs returns a result. (The fact that rs can
continue activity after the return is explained below.)
The test method above could evolve into an ObjMap with:
r1 7→ mk-ObjInfo(Sort,
2, l r x 7},
{v →
7 →
7 , →
7
2
mk-Active([mk-Call(b, l, test, x), mk-Return(b)], r0 )),
r2 7→ mk-ObjInfo(Sort, {v 7→ 4, l 7→ r3 }, R EADY),
..
.
The vertical ellipses are to remind the reader that there could be other threads that
are also candidates for execution.
This form of call statement is in fact a special case of a more interesting “future
call” present in ABC/L [Yon90]. This more general synchorisation starts with a
future call whose syntax does not need the lhs field; the semantics allows the client
object to continue execution until it needs the result from the server; at this point, the
client executes an Await statement that indicates where (in the client) the returned
value is to be stored.
Await :: lhs : Id
The flow of control is pictured in Figure 9.6 but, instead of using future call,
Activate serves the same purpose. It can be seen that the client remains active until
the Await statement is executed. Furthermore, the server can return a value and
continue executing until the method code is exhausted.
It is simple to describe Call (which requires an answer before the client can make
progress) in terms of the more general case and use Await in the description of Call.9
Thus the SOS rule is:
9 An alternative –but equivalent– model could add a new mode to the ObjInfo semantic object.
9.5 Method activation and synchronisation 145
Active Active
rc
Activate Await
Return []
READY Active READY
rs
cobj = O(c)
mk-ObjInfo(clc , σc ,
mk-Active([mk-Call(lhs, obj, meth, args)] y rlc , k)) = cobj
cobj = mk-ObjInfo(clc , σc , mk-Active([mk-Await(lhs)] y rlc , k))
0
After computation further down the ladder the ObjMap will arrive at:
r1 7→ mk-ObjInfo(Sort,
{v 7→ 2, l 7→ r2 , x 7→ 7},
mk-Active([mk-Await(b), mk-Return(b)], r
)),
0
r2 7→ mk-ObjInfo(Sort,
{v 7→ 4, l 7→ r3 , x 7→ 7},
mk-Active([mk-Return( r
false )], )),
1
.
.
.
When a method has no more statements to execute (a Return can occur anywhere
in the body of a method) it reverts to the quiescent status:
O(s) = mk-ObjInfo(cl, σ , mk-Active([ ], k))
st
(C, O) −→ O † {s 7→ mk-ObjInfo(cl, σ , R EADY)}
Thus the ObjInfo for r2 changes to:
r2 7→ mk-ObjInfo(Sort,
{v 7→ 4, l 7→ r3 },
R EADY),
..
.
Notice however that there is a problem type checking the lhs in an Await state-
ment and this would in general have to be a dynamic check. Furthermore, executing
multiple future calls before an Await could give rise to dangerous program errors.
9.5.3 Delegation
v 2 v 4 v 6 v 8
l l l l nil
v 2 v 4 v 6 v 8
l l l l nil
Delegate :: object : Id
method : Id
arguments : Id∗
Delegate statements are much like Call statements — the distinction is that a
Delegate passes down its client to be the client of the newly called object whereas
the object making a normal Call becomes the client of the called object.
A use of Delegate is indicated in the test method in Figure 9.9. Suppose that
some client r0 has called the test method passing 3 to r1 : r1 cannot determine the
result required by r0 so the code of test reaches Delegate; providing the status of r2
is R EADY, the situation would be:
148 9 Concurrent OOLs
Active Active
rc Await
Activate
READY Active READY
rs
Delegate
Return
READY Active READY
rt
Notice that r1 is now available to act as a server for another client and that the
return from r2 goes directly to r0 .
9.6 Reviewing COOL 149
Figure 9.9 depicts the Sort class with three methods (insert, min and test) in what
is hopefully a readable concrete syntax. As in the illustrative examples in Sec-
tions 9.5.1–9.5.3:
• The insert method does not return a result and is invoked via an activate statement
which causes the method to run concurrently with the object that caused the
activation. In the case that this is the first insert to this object, the parameter
value is stored and a new object is created; in all other cases, work is passed on
to objects further down the sorting ladder.
• The min method is only given to illustrate that the minimum value in the ladder
is always available in the first object.
• The test method is shown as using delegate to achieve concurrency by passing on
the need to return a value to the client that called test.
Sort class
vars v: N; l: ref(Sort)
insert(x: N) method
begin
if is-nil(l)
then {new (l); v := x; }
else if v ≤ x
then {activate(l.insert(x))}
else {activate(l.insert(v)); v := x}
fi
fi
end
min() method : N
return(v)
test(x: N) method : B
begin
if is-nil(l) ∨ x < v
then return (false)
else if x = v
then return (true)
else delegate (l.test(x))
fi
fi
end
Fig. 9.9 Concurrent Sort
COOL summary
A full formal description of COOL is given in Appendix D. This section offers some
observations on COOL as a language.
COOL is strongly typed with variables that contain object references only being
allowed to store references to objects of the declared class (because of this, there is
no need to have a class type argument to the new statement).
The context conditions for COOL are given for each language feature in Ap-
pendix D. The type information (of methods) required is:
m
ClTypes = Id −→ ClInfo
m
ClInfo = Id −→ MethInfo
paramtypes : Type∗
Type = Id | ScType
ScType = I NT T P | B OOLT P
m
VarEnv = Id −→ Type
Here again (see Section 4.2), no attempt is made to subdivide the class of Id.
Since their written forms are taken to be the same, relating variable, method and
class names is left to the context conditions.
The least conventional aspect of the semantics of COOL is the insistence that
at most one method can be active in any particular object at any point in time.10
This decison certainly increases the possibilities of deadlocks in COOL programs.
On the other hand, it has the advantage that there is no possibility of data races on
instance variables within an object. If a programmer wants to share data, this can be
achieved by placing the data in an object whose references can be shared. This puts
explicit sharing firmly in the hands of programmers and it should be exploited with
great care.
Although related to POOL [AdB91], COOL is not intended to be a full language
— its features have been chosen to illustrate points about semantic description.
10 A similar approach is taken by Bertrand Meyer in his SCOOP proposal.
9.7 Further material 151
There are many ways (e.g. inheritance) in which COOL could be extended and a
number of these are outlined as projects in Section 9.7.
There are many projects that can be developed from the description in Appendix D
including:
1. The semantics for Call in Section 9.5.2 is indirect in that it employs another
statement (Await) to put the client into a waiting state. An alternative would be
to define another mode for ObjInfo.
2. Rather than have newly created objects start life in the quiescent (R EADY) status,
an initialisation method could be added to each class that is activated on creation.
This, of course, provides another source of concurrent execution. Options include
whether or not New passes arguments to this method.
3. There is no attempt in Appendix D to make the access to objects “fair” in the
sense that two attempts to call an active object will not necessarily be served in
order when the called object becomes free. It is not difficult to add some form of
queue to control the order of invocation.
4. A solution that is perhaps more satisfactory than the preceding point might be to
add a form of conditional Call where the client has a list of alternative statements
that are executed in the event that the sought-after server object is not Ready.
5. It would be possible to add a limited form of “self call” that either works like a
local Delegate or is only allowed to call methods that are limited in some way as
to the variables they can access or change.
6. There are several approaches to “garbage collection” of unwanted objects. An ex-
plicit Destroy statement should have some pre conditions; automatic collection of
objects to which no reference remains is not difficult to write formally but could
be expensive to implement; collecting “circular garbage” is more challenging.
7. Java allows access from object ri to internal variables of any object for which
ri holds a reference; such an extension is not difficult to add to COOL but it is
important to note that it reintroduces the danger of data races.
8. The Go language has an activate statement but activated “Go-routines” are closed
when the main routine finishes. Go also offers a form of channel similar to those
in the π-calculus [MPW92] in that channel names can be passed.
9. Various forms of inheritance could be added to COOL.
10. Careful addition of arrays (notably of references) can introduce extra ways of
generating concurrent activity.
There is a rich literature relating to formal models of object-oriented languages.
Starting with items close to COOL and working outwards:
• The debt to POOL2 has been acknowledged. An overview is given in [Ame89];
a layered semantics is given in [AR92, AdBKR89]; proof theory is considered
in [AdB91, dB91].
152 9 Concurrent OOLs
• Bertrand Meyer’s Eiffel language [Mey88] is a fully fledged and interesting lan-
guage; his SCOOP proposal for simple concurrency shares many features with
COOL.
• Transformations that preserve observational equivalence and can be used to in-
troduce more concurrent threads were studied for a language referred to as
πoβ λ whose semantics were given in [Jon93] by mapping to Robin Milner’s
π-calculus [MPW92, SW01]. (David Walker had already published [Wal91].) It
proved non-trivial to justify the equivalences via bi-simulation but Davide San-
giorgi settled the issue in [San99].11 The validity of the equivalences links to
Hogg’s notion [Hog91] of “islands”.
• Simula [DMN68] and Smalltalk [GR83] were early object-oriented languages.
The task of providing semantics was addressed in [Wol88].
• A careful look at the basic ideas behind object orientation is [AC12].
11 This links to the issue raised in Section 7.1 about the tractability of denotations when a semantics
is given by mapping to another language.
Chapter 10
Exceptional ordering [*]
This short optional chapter addresses the topic of modelling statements that cause
execution to occur in orders different from their textual juxtaposition. The contro-
versial goto statement provides the classic example of this modelling challenge but
it is not the only manifestation of the difficulty. The discussion can be undertaken
without worrying about concurrency and is limited below to consideration of se-
quential languages.
This chapter also moves more freely than earlier parts of the book between oper-
ational and denotational description techniques.1 There are two aspects of the chal-
lenge of describing the semantics of the goto statement: on the one hand, it is nec-
essary to show the change of order of execution; on the other hand, there is the
question of the denotation of the label for a statement.
Normal sequential execution of S1; S2 dictates that execution of S1 is followed
by execution of S2. This is clear in a “big-step” SOS rule for statement sequencing:
st
(s, σ ) −→ σ 0
stl
(rl, σ 0 ) −→ σ 00
([s] y rl, σ ) −→ σ 00
stl
1Gordon Plotkin observes in [Plo04a] that ideas from operational semantics have influenced de-
notational semantics.
The reason that this optional chapter considers exceptional ordering is that, even
without goto statements, there remain features in most languages that present a sim-
ilar challenge to providing formal models. A key interaction of language features
is that abnormal execution order can cut across the structure of the syntax of a lan-
guage. This requires that the semantics essentially has to perform clean up actions
even though previously anticipated executions are abandoned. (Chapter 5 shows that
pre-planned entry and exit from procedures and functions can be modelled without
difficulty.) This chapter tackles the modelling of programming language constructs
that cause execution to deviate from neat composition of statements.
Language issue 46: Exceptions
A further example of a language issue that complicates semantic description
is exception handling. Unlike function calls, exception handlers can abandon
previous anticipated execution.
The last of the language challenges is:
Challenge VIII: Modelling exits
How can a neat model be given for the semantics of a language in which fea-
tures permit abnormal exits from structured text?
To return to the point about feature interaction, there are several language fea-
tures that do cut across the phrase structure of a language but whose modelling is
unproblematic.
• Premature termination of a looping construct is easy to model because nothing
has to be reinstated at the end of a loop.
• A return statement from a method in COOL can be embedded within other phrase
structures but OOLs leave the values of instance variables alone at method ter-
mination so all that the semantics of Return has to ensure is that the rest of the
rem code is discarded.
These cases contrast with a goto that abnormally terminates a Block or Procedure.
As is shown in Chapter 5, there are changes to the state (Σ) that must be made at
normal termination of the text of a block or procedure; such resetting steps that clean
up the state by removing locations also have to be executed when a goto statement
causes abnormal termination.
Two distinct approaches to coping with Challenge VIII are described in the fol-
lowing sub-sections; it would however be possible to mix some aspects of the ap-
proaches described in Sections 10.1 and 10.2. A key distinguishing aspect of the
approaches is whether they work forwards or backwards from labels.
In the old VDL style of operational semantics, one component of the (grand) state
of the description contained the text of the program that was being executed. State-
10.1 Abnormal exit model 155
ments such as Goto could then be given a meaning by changing the text component.
Any required cleanup operations could be programmed in terms of this tree naviga-
tion.
Operationally, this does not fit with the objective of being structural and it cer-
tainly fails to fit the homomorphic objective of denotational approaches.
A much more structural account of the way in which goto statements explicitly
appoint their successor statement can be given in terms of relations that are extended
(beyond Σ × Σ) to mark any abnormal sequencing that results from executing a por-
tion of program text. The range of the relation can contain an optional “abnormal”
component which need only be a label in the case of a language allowing a simple
goto statement:
st
−→: P((Stmt × Σ) × (Σ × Id ))
and:
st
(s, σ ) −→ (σ 0 , nil)
stl
(rl, σ 0 ) −→ (σ 00 , abn)
([s] y rl, σ ) −→ (σ 00 , abn)
stl
st
It is, however, simple to adopt a convention that the default for −→ is to return
nil unless otherwise marked, which restores the description to:
ex
(rhs, σ ) −→ v
st
(mk-Assign(lhs, rhs), σ ) −→ σ † {lhs 7→ v}
Such conventions are related to the use of the abnormal exit model in denotational
descriptions, which is discussed further in Section 10.3. Further combinators that
trap abnormal exits are also described there.
2 This becomes cumbersome but was actually carried through in the ALGOL 60 description
in [ACJ72].
156 10 Exceptional ordering [*]
As is explained in the next section, the use of the exit approach is the largest dif-
ference between VDM denotational descriptions3 and those associated with Oxford
(where the “continuation” approach was developed and employed). The connections
between these approaches are reviewed in Section 10.3.
10.2 Continuations
Continuations can describe language constructs for which no obvious exit model
has been found. But power is not necessarily an advantage: as observed in other
contexts, making clear that something cannot be done can simplify reasoning about
a language description. This section relates the exit and continuation approaches to
3 Peter Mosses points out in [Mos11] that the “semicolon combinator” used in [BBH+ 74] can be
related to Moggi’s “monads”, which were published as [Mog89]. The use of a semicolon combi-
nator to mean either simple functional composition or a function-level version of the composition
sketched above goes some way towards the reuse of formal language descriptions. Mosses goes
much further in his work on component-based specifications “funcons” [BSM16],
10.4 Further material 157
Projects
ALGOL 60 introduced switch variables whose values are labels. (This would appear
to result again from another spurious argument of orthogonality: making labels into
first-class objects.) Modelling switch variables would let the reader explore the snags
that this idea brings with it.
4 As a consequence, the environment has to be defined using a fixed point.
158 10 Exceptional ordering [*]
Historical notes
5Reynolds revisited this discussion in a talk given at the BCS Computer Conservation Society in
2004 (a video recording of this talk exists).
Chapter 11
Conclusions
This short concluding chapter begins with a review of the eight challenges discussed
in the body of this book; this is followed by comments on some significant formal
language descriptions or specifications.
Despite the many “language issues” that are discussed and modelled in the body of
this book, only eight significant “challenges” are teased out: they are summarised
here.
(I) Delimiting a language (concrete representation)
How can the set of valid strings of an object language be delimited?
BNF –or a variant such as EBNF– is the most common notation used for de-
scribing concrete syntax but less common notations such as Wirth’s “railroad”
diagrams have equivalent expressive power.
(II) Delimiting the abstract content of a language
How can the abstract syntax of a language be defined?
In Chapter 2 and on all subsequent examples, simple parts of VDM notation for
describing objects are used for describing the abstract syntax of programming
languages.
(III) Recording semantics (deterministic languages)
How can the semantics of a deterministic language be recorded?
An operational semantics can be defined by a recursive function (over the abstract
syntax) and is used in Section 3.1.
(IV) Operational semantics (non-determinism)
How can an operational semantics describe a non-deterministic language in a
way that clearly relates the structure of the semantics to its abstract syntax?
The key step in Section 3.2 is to recognise that the semantics is a relation be-
tween initial states and permissible final states. “Structural Operational Seman-
tics” (SOS) defines the requisite relation by using inference rules.
(V) Context dependancy
How can abstract syntax objects that exhibit type errors be ruled out before se-
mantics are tackled?
Section 4.2 shows how recursive predicates over the abstract syntax can define
the set of type correct programs. These predicates are conventionally named wf -X
for objects of type X.
(VI) Modelling sharing
How can a language description model sharing?
Section 5.2 shows how the introduction of a surrogate such as Loc as an ab-
straction of machine addresses makes it possible to have an environment (Env)
that maps different identifiers to the same location. It is important to indicate
that the environment is separate from –and changes less often than– Σ. Simi-
larly, Section 9.1.2 uses object references to record the sharing of objects in an
object-oriented language.
(VII) Modelling concurrency
How can shared-variable concurrency be described using SOS?
The fact that the inference rules of SOS provide a natural way of defining seman-
tics as a relation means that non-determinacy is handled easily. The key change
from big-step to small-step semantics is described in Section 8.2.
(VIII) Modelling exits
How can a neat model be given for the semantics of a language in which features
permit abnormal exits from structured text?
Chapter 10 outlines both the use of abnormal signals and the continuation model.
Although the author has complained that insufficient use has been made of formal
language description ideas, it is important to record some of the examples that indi-
cate that formal methods can cope with realistic programming languages.
Clearly, the optimal use is in the actual specification of a programming language
(rather than a post hoc description). The Modula-II standard uses VDM notation
(see [AGLP88]). The situation with PL/I is more complicated: the repeated updat-
ings of VDL operational semantic descriptions of PL/I were followed by the VDM
denotational description [BBH+ 74]; the standard [ANS76] builds on this work — it
has a formal description of the state of a semantics but then attempts to describe the
mapping to denotations in words. The standardisation effort also came many years
after IBM had gone through its normal language control processes.
A far more promising example of the use of formal methods by language design-
ers is SML. Two versions are [MTHM97] and [HMT88]; there is also a useful web
site:
http://scholarpedia.org/article/Standard ML language
11.2 Capabilities of formal description methods 161
11.3 Envoi
This appendix separates syntax, context conditions and semantics and is thus in the
order in which the topics are introduced in Chapters 2–4 of the current book.
Abbreviations used in the description:
Assign :: lhs : Id
rhs : ArithExpr
If :: test : RelExpr
then : Stmt∗
else : Stmt∗
While :: test : RelExpr
body : Stmt∗
ArithExpr = BinArithExpr | Id | N
A.3 Semantics
Statements
m
Σ = Id −→ N
The semantic transition relation for statement lists is
st
−→: P((Stmt × Σ) × Σ)
st
(s, σ ) −→ σ 0
stl
(rl, σ 0 ) −→ σ 00
([s] y rl, σ ) −→ σ 00
stl
stl
([ ]s, σ ) −→ σ
The semantic transition relation for single statements is given by cases below.
stl
−→: P((Stmt∗ × Σ) × Σ)
ex
(rhs, σ ) −→ v
st
(mk-Assign(lhs, rhs), σ ) −→ σ † {lhs 7→ v}
ex
(test, σ ) −→ true
stl
(th, σ ) −→ σ 0
st
(mk-If (test, th, el), σ ) −→ σ 0
ex
(test, σ ) −→ false
stl
(el, σ ) −→ σ 0
st
(mk-If (test, th, el), σ ) −→ σ 0
ex
(test, σ ) −→ true
stl
(body, σ ) −→ σ 0
st
(mk-While(test, body), σ 0 ) −→ σ 00
st
(mk-While(test, body), σ ) −→ σ 00
ex
(test, σ ) −→ false
st
(mk-While(test, body), σ ) −→ σ
A.3 Semantics 167
Expressions
e ∈ Id
ex
(e, σ ) −→ σ (e)
e∈N
ex
(e, σ ) −→ e
Appendix B
Typed language
The formulae in this appendix separate abstract syntax, context conditions and se-
mantics. This is not the order used in subsequent appendices but it serves at this
stage to emphasise the distinctions.
Abbreviations used in the description:
m
BaseProgram :: types : Id −→ ScalarType
body : Stmt∗
ScalarType = I NT T P | B OOLT P
Assign :: lhs : Id
rhs : Expr
If :: test : Expr
then : Stmt∗
else : Stmt∗
While :: test : Expr
body : Stmt∗
ScalarValue = Z | B
B.2 Context conditions 171
wf -BaseProgram : BaseProgram → B
wf -BaseProgram(mk-BaseProgram(types, body)) 4
wf -StmtList(body, types)
B.3 Semantics
stl
([ ], σ ) −→ σ
st
(s, σ ) −→ σ 0
stl
(rest, σ 0 ) −→ σ 00
([s] y rest, σ ) −→ σ 00
stl
e ∈ Id
ex
(e, σ ) −→ σ (e)
e ∈ ScalarValue
ex
(e, σ ) −→ e
Appendix C
Blocks language
Unlike the preceding appendices (where the whole of the abstract syntax is given
before all of the context conditions to be followed by the semantics for every con-
struct), this appendix is in the “preferred order”: that is, it is ordered by language
concept. For reference purposes, this order is normally most convenient. There re-
mains the decision whether to present the parts of a language in a top-down (from
BlocksProgram to Expr) order or bottom-up: this decision is fairly arbitrary. What
is really needed is an interactive support system!
Abbreviations
σ ∈ Σ a single “state”
Σ the set of all “states”
Arith Arithmetic
Def Definition
Den Denotation
env a single “environment”
Env the set of all “environments”
Expr Expression
param parameter
Proc Procedure
Rel Relational
Stmt Statement
The objects required for both context conditions and semantic rules are given first.
The following objects are needed in the description of the Context Conditions.
m
TypeMap = Id −→ (ScalarType | ProcType)
Semantic objects
uniquel : (X ∗ ) → B
uniquel(l) 4 len l = card elems l
C.2 Programs
Abstract syntax
Context conditions
wf -BlocksProgram : BlocksProgram → B
wf -BlocksProgram(mk-BlocksProgram(b)) 4 wf -Stmt(b, {7→})
Semantics
pr
−→: P(BlocksProgram × D ONE)
env0 = {7→}
σ0 = {7→}
st
(b, env0 , σ0 ) −→ σ 0
pr
mk-BlocksProgram(b) −→ D ONE
C.3 Statements
Abstract syntax
Context conditions
Semantics
The semantic relation (which is also given by cases below) for statements is:
st
−→: P((Stmt × Env × Σ) × Σ)
Assignment
Abstract syntax
Assign :: lhs : Id
rhs : Expr
.
Context conditions
Semantics
ex
(rhs, env, σ ) −→ v
st
(mk-Assign(lhs, rhs), env, σ ) −→ σ † {env(lhs) 7→ v}
If
Abstract syntax
If :: test : Expr
then : Stmt
else : Stmt
.
C.5 Compound statements 179
Context conditions
Semantics
ex
(test, env, σ ) −→ true
st
(th, env, σ ) −→ σ 0
st
(mk-If (test, th, el), env, σ ) −→ σ 0
ex
(test, env, σ ) −→ false
st
(el, env, σ ) −→ σ 0
st
(mk-If (test, th, el), env, σ ) −→ σ 0
Abstract syntax
Context conditions
Semantics
stl
(stl, env, σ ) −→ σ 0
st
(mk-Compound(stl), env, σ ) −→ σ 0
180 C Blocks language
Statement lists
stl
([ ], env, σ ) −→ σ
st
(s, env, σ ) −→ σ 0
stl
(rl, env, σ 0 ) −→ σ 00
([s] y rl, env, σ ) −→ σ 00
stl
C.6 Blocks
Abstract syntax
m
Block :: var-types : Id −→ ScalarType
m
proc-defs : Id −→ ProcDef
body : Stmt∗
.
Context conditions
Semantics
m
newlocs ∈ (Id ←→ ScalarLoc)
dom newlocs = dom vm
rng newlocs ∩ dom σ = { }
penv = {p 7→ mk-ProcDen(pm(p).params, pm(p).body, env) | p ∈ dom pm}
env0 = env † newlocs † penv
σi = σ † ({env(id) 7→ 0 | id ∈ dom vm ∧ vm(id) = I NT T P} ∪
{env(id) 7→ true | id ∈ dom vm ∧ vm(id) = B OOLT P})
stl
(body, env0 , σi ) −→ σi0
st
(mk-Block(vm, pm, body), env, σ ) −→ dom σ C σi0
Procedure definition
Abstract syntax
Context conditions
Abstract syntax
Call :: proc : Id
args : Id∗
.
182 C Blocks language
Context conditions
C.8 Expressions
The only interesting deviation from earlier languages is the case of identifiers.
Abstract syntax
Expr = · · · | Id
Semantics
ex
−→: P((Expr × Env × Σ) × ScalarValue)
e ∈ Id
ex
(e, env, σ ) −→ σ (env(e))
Appendix D
COOL
This appendix is –like Appendix C– in the “preferred order”: i.e. the abstract syntax,
context conditions and semantics are grouped under each language concept.
Abbreviations
Arith Arithmetic
Cl Class
Expr Expression
Obj Object
opd operand
Meth Method
Rel Relational
rem remaining
Sc Scalar
Stmt Statement
Val (semantic) Value
Var Variable
The following types are needed in the description of the Context Conditions.
m
ClTypes = Id −→ ClInfo
m
ClInfo = Id −→ MethInfo
MethInfo :: restype : Type
paramtypes : Type∗
Type = ScType | Id
ScType = I NT T P | B OOLT P
m
VarEnv = Id −→ Type
The types of the context condition predicates are:
c-type: Expr × ClTypes × VarEnv → Type
wf -Stmt: Stmt × ClTypes × VarEnv × Type → B
wf -CoolProgram: CoolProgram → B
In addition to the abstract syntax of ClMap (see abstract syntax in Section D.6), the
following types are needed in the description of the semantics.
m
ObjMap = Reference −→ ObjInfo
ObjInfo :: class : Id
σ : VarStore
mode : R EADY | Active
m
VarStore = Id −→ Val
Val = Z | B | Reference
client : Reference
D.2 Expressions
Abstract syntax
Value = Z | B
Context conditions
The context conditions here are similar to those for the typed language (Appendix B)
except that an additional parameter (ctps) carries the types of classes so that a check
can be made that the argument to TestNil refers to an (instance) variable of type
Reference; the result of applying c-type to a TestNil will of course be Boolean.
Semantics
The semantics of Expr also broadly follows those for simpler languages because
expression evaluation depends only on the local instance variables (cf. semantics of
Assign) and method calls are disallowed within expressions.
eval: Expr × VarStore → (Z | B)
The semantics of TestNil are:
D.3 Statements
D.3.1 Assignments
Abstract syntax
Assign :: lhs : Id
rhs : Expr
.
Context conditions
Semantics
robj = O(r)
mk-ObjInfo(cl, σ , mk-Active([mk-Assign(lhs, rhs)] y rl, k)) = robj
robj0 = mk-ObjInfo(cl, σ † {lhs 7→ eval(rhs, σ )}, mk-Active(rl, k))
st
(C, O) −→ O † {r 7→ robj0 }
D.3 Statements 189
D.3.2 If statements
Abstract syntax
If :: test : Expr
then : Stmt∗
else : Stmt∗
.
Context conditions
Semantics
robj = O(r)
mk-ObjInfo(cl, σ , mk-Active([mk-If (test, th, el)] y rl, k)) = robj
eval(test, σ ) = true
robj0 = mk-ObjInfo(cl, σ , mk-Active(th y rl, k))
st
(C, O) −→ O † {r 7→ robj0 }
robj = O(r)
mk-ObjInfo(cl, σ , mk-Active([mk-If (test, th, el)] y rl, k)) = robj
eval(test, σ ) = false
robj0 = mk-ObjInfo(cl, σ , mk-Active(el y rl, k))
st
(C, O) −→ O † {r 7→ robj0 }
190 D COOL
D.4 Methods
Abstract syntax
params : Id∗
m
paramtypes : Id −→ Type
body : Stmt∗
.
Context conditions
Abstract syntax
Activate :: object : Id
method : Id
arguments : Expr∗
.
Context conditions
Semantics
Active
rc
Activate
READY Active
rs
cobj = O(c)
mk-ObjInfo(clc , σc ,
mk-Active([mk-Activate(obj, meth, args)] y rlc , k)) = cobj
0
cobj = mk-ObjInfo(clc , σc , mk-Active(rlc , k))
sobj = O(σc (obj))
mk-ObjInfo(cls , σs , R EADY) = sobj
mk-Class(vars, meths) = C(cls )
mk-Meth(rtp, params, paramtps, body) = meths(meth)
σs0 = σs † {params(i) 7→ eval(args(i), σc ) | i ∈ inds args}
sobj0 = mk-ObjInfo(cls , σs0 , mk-Active(body, c))
st
(C, O) −→ O † {c 7→ cobj0 , σc (obj) 7→ sobj0 }
192 D COOL
Abstract syntax
Call :: lhs : Id
object : Id
method : Id
arguments : Expr∗
.
Context conditions
Semantics
Active Active
rc
Call
Return
READY Active READY
rs
cobj = O(c)
mk-ObjInfo(clc , σc ,
mk-Active([mk-Call(lhs, obj, meth, args)] y rlc , k)) = cobj
cobj0 = mk-ObjInfo(clc , σc , mk-Active([mk-Await(lhs)] y rlc , k))
sobj = O(σc (obj))
mk-ObjInfo(cls , σs , R EADY) = sobj
mk-Class(vars, meths) = C(cls )
mk-Meth(rtp, params, paramtps, body) = meths(meth)
σs0 = σs † {params(i) 7→ eval(args(i), σc ) | i ∈ inds args}
sobj0 = mk-ObjInfo(cls , σs0 , mk-Active(body, c))
st
(C, O) −→ O † {c 7→ cobj0 , σc (obj) 7→ sobj0 }
D.4 Methods 193
D.4.3 Rendezvous
Abstract syntax
Context conditions
Semantics
Active Active
rc
Activate Await
Return []
READY Active READY
rs
sobj = O(s)
mk-ObjInfo(cls , σs , mk-Active([mk-Return(e)] y rls , c)) = sobj
sobj0 = mk-ObjInfo(cls , σs , mk-Active(rls , nil))
cobj = O(c)
mk-ObjInfo(clc , σc , mk-Active([mk-Await(lhs)] y rlc , k)) = cobj
cobj0 = mk-ObjInfo(clc , σc † {lhs 7→ eval(e, σs )}, mk-Active(rlc , k))
st
(C, O) −→ O † {s 7→ sobj0 , c 7→ cobj0 }
D.4.5 Delegation
Abstract syntax
Delegate :: object : Id
method : Id
arguments : Id∗
.
Context conditions
Semantics
Active Active
rc Await
Activate
READY Active READY
rs
Delegate
Return
READY Active READY
rt
D.5 Classes 195
sobj = O(s)
mk-ObjInfo(cls , σs ,
mk-Active([mk-Delegate(obj, meth, args)] y rls , k)) = sobj
0
sobj = mk-ObjInfo(cls , σs , mk-Active(rls , nil))
tobj = O(σs (obj))
mk-ObjInfo(clt , σt , R EADY) = tobj
mk-Class(vars, meths) = C(clt )
mk-Meth(rtp, params, paramtps, body) = meths(meth)
σt0 = σt † {params(i) 7→ eval(args(i), σs ) | i ∈ inds args}
tobj0 = mk-ObjInfo(clt , σt0 , mk-Active(body, k))
st
(C, O) −→ O † {s 7→ sobj0 , σs (obj) 7→ tobj0 }
D.5 Classes
Abstract syntax
m
Class :: vars : Id −→ Type
m
methods : Id −→ Meth
.
Context conditions
There are no semantics for classes as such — the semantics of New follows.
196 D COOL
Context conditions
wf -Stmt(mk-New(targ), ctps, v-env, mtp) 4
targ ∈ dom v-env ∧
v-env(targ) ∈ Id
Semantics
robj = O(r)
mk-ObjInfo(clr , σr , mk-Active([mk-New(targ)] y rl, k)) = robj
n ∈ (Reference − dom O)
robj0 = mk-ObjInfo(clr , σr † {targ 7→ n}, mk-Active(rl, k))
cln = (C(clr ).vars)(targ)
mk-Class(vars, meths) = C(cln )
σn =
{v 7→ 0 | v ∈ dom vars ∧ vars(v) = I NT T P}∪
{v 7→ false | v ∈ dom vars ∧ vars(v) = B OOLT P}∪
{v 7→ nil | v ∈ dom vars ∧ vars(v) ∈
/ ScType}
nobj = mk-ObjInfo(cln , σn , R EADY)
st
(C, O) −→ O † {r 7→ robj0 , n 7→ nobj}
D.5 Classes 197
Abstract syntax
Discard :: target : Id
.
Context conditions
Semantics
robj = O(r)
mk-ObjInfo(clr , σr , mk-Active([mk-Discard(targ)] y rl, k)) = robj
robj0 = mk-ObjInfo(clr , σr † {targ 7→ nil}, mk-Active(rl, k))
st
(C, O) −→ O † {r 7→ robj0 }
198 D COOL
D.6 Programs
Abstract syntax
Context conditions
wf -CoolProgram : CoolProgram → B
wf -CoolProgram(mk-CoolProgram(cm, start-cl, start-m)) 4
start-cl ∈ dom cm ∧
start-m ∈ dom (cm(start-cl).methods) ∧
let ctps = {c 7→ c-clinfo(cm(c)) | c ∈ dom cm}
m
apply : X ∗ × (X −→ Y) → Y ∗
apply(l, m) 4
if l = [ ]
then [ ]
else [m(hd l)]
y apply(tl l, m)
fi
D.6 Programs 199
Semantics
a b ¬a a∧b a ∨ b a ⇒ b a ⇔ b
true true false true true true true
∗ true ∗ ∗ true true ∗
false true true false true true false
true ∗ ∗ true ∗ ∗
∗ ∗ ∗ ∗ ∗ ∗
false ∗ false ∗ true ∗
true false false true false false
∗ false false ∗ ∗ ∗
false false false false true true
The symbols used in VDM for the operators of set theory are:
T-set all finite subsets of T
{t1 , t2 , . . . , tn } set enumeration
{} empty set
B {true, false}
N {0, · · ·}
Z {· · · , −1, 0, 1, · · ·}
{x ∈ S | p(x)} set comprehension
{i, · · · , j} subset of integers (from i to j inclusive)
t∈S set membership
t∈
/S ¬ (t ∈ S)
S1 ⊆ S2 set containment (subset of)
S1 ⊂ S2 strict set containment
S1 ∩ S2 set intersection
S1 ∪ S2 set union
S1 − S2 set difference
card S cardinality (size) of a set
P(X) power set
The following pictorial representation of the set operators can be a useful re-
minder of their signatures. The ovals contain the names of types; operators are
marked with incoming arcs indicating the types of their operands and an outgoing
arc indicating the type of the result.
.
∩∪−
.
card
X X-set ℕ
∈∉
. .⊂⊆
!
E.3 List (sequence) notation
The symbols
E.3 List used in VDM
(sequence) for the operators of sequence theory are:
notation 203
◆◆
y
r ⇣ .
⌧ ?⌧ ⌧
r r
dconc len
(X ⇤ )⇤ - X⇤ - N
.
6 len
. ⌃r ⇧
X*
⌧
ℕ
tl r ()
tl
inds . elems . .
hd
r
. (i)
hd
? ⌧
- X
ℕ-set
X-set X
1 7! r1 , 2 7! r2 , . . . , n 7! rn
{7!The emptyof map
} symbols used in VDM for the operators map theory are:
{d 7! f (d)D2 !mD R⇥ R | p(d)} mapfinite
defined by comprehension
maps from D to R
m204
1 † m 2 dom m map overwrite
domain of a map
E VDM notation
rng m range of a map
E.4 Map notation
m(d) map application
{d1 7! r1 , d2 7! r2 , . . . , dn 7! rn } map enumeration
{7!}used in VDM for the operators
The symbols emptyofmapmap theory are:
{d 7! f (d) 2 D ⇥ R | p(d)} map defined by comprehension
m
D
m1−→† m2R mapfinite maps from D to R
overwrite
dom m domain of a map
rng m range of a map
m(d) map application
{d1 7→ r1 , d2 7→ r2 , . . . , dn 7→ rn } map enumeration
{7→} empty map
{d 7→ f (d) ∈ D × R | p(d)} map defined by comprehension
m1 † m2 map overwrite
m1 ∪ m2
The pictorial representation of the map operators map union
is:
sCm domain restriction of a map
sC−m domain deletion of a map
The pictorial representation of the map operators is:
◆◆r ⇣
[ operators is:
The pictorial representation of the†,map
◆◆
.
r ⇣
†, [
⌧ ?⌧ ⌧
⌧ ? ⌧ rng ⌧
r rngr
dom
r r - - R-setR-set
dom m
D-set D-set D ! mR
D !R
. .
dom rng
D-set D6 6R
m
R-set
✓ ✓r ⌘
✓ ✓
. r C,⌘
.()
C
C,C
⌧ D ⌧
D⌧ r - R ⌧
r
()
D - R
R
()
E.6 Function notation 205
:: record
mk-N(. . .) generator
o.s1 selector
Type optional
nil omitted object
µ(o, s1 7→ t) modify a component
Each record definition gives distinct selector and constructor functions. For:
m
Program :: vars : Id −→ Type
body : Stmt∗
the pictorial representation of its types would be:
. .
vars body
mk-Program .
f : D1 × D2 → R signature
f (d) application
if · · · then · · · else · · · conditional
let x = · · · in · · · local definition
f (d: D) r: R
pre · · · d · · ·
post · · · d · · · r · · ·
Appendix F
Notes on influential people
This appendix contains brief notes about the main people who have played a part
in the development of formal semantics and are mentioned in the body of the book.
The notes are not intended to be biographies and are limited to facts related to the
subject of this book. A more historical account of many of the interactions between
these players can be found in [Ast19].
Hans Bekič (1936–1982) Unlike the majority of his colleagues at the IBM Lab-
oratory in Vienna, who were engineers, Bekič was a mathematician. He worked
with Lucas on an early ALGOL 60 compiler and then on the 1960s VDL (opera-
tional) semantics of PL/I. He spent a year in London working with Peter Landin
and this put him in a position where he encouraged the move towards deno-
tational description methods for VDM. As well as his role in developing and
co-authoring the VDM semantics of PL/I [BBH+ 74], Bekič was making semi-
nal contributions to concurrency before his untimely death. A collection of his
papers was published posthumously as [BJ84].
Robert W. Floyd (1936–2001) Bob Floyd made many contributions to the theo-
retical side of computing (see [Knu03] for a fitting tribute). One of his significant
contributions to research on semantics was [Flo67], which provides a clear ac-
count of one way of verifying programs. Floyd’s approach was predicated on
flowcharts but the cited paper had a major influence on Hoare’s [Hoa69], which,
in turn, is the foundation stone of 50 years of productive research on the formal
development of programs.
Charles Antony Richard Hoare (b. 1934) Tony Hoare has made many key con-
tributions to the theory of computing and has also been involved in seeing that
theoretical ideas are transferred to practical applications. The major semantic av-
enue discussed in Section 7.3 derives from his paper [Hoa69] on “Axiomatic se-
mantics”. Hoare has tackled the search for unification of the various approaches
to semantics (see [HH98]). His contributions have been recognised by the ACM
Turing Award in 1983, the Kyoto Prize in 2000, two “Queen’s Awards to Indus-
try” and numerous honorary degrees. There are recorded video interviews on the
ACM web site for many Turing Laureates — that for Hoare is available at:
https://amturing.acm.org/interviews/hoare 4622167.cfm
Two Festschriften are [Ros94, JRW10] and a selection of his papers up to 1989
is contained in [HJ89].
Peter John Landin (1930–2009) Peter Landin noted the link between program-
ming languages and Church’s Lambda Calculus in [Lan65a, Lan65b]; he also
spoke on the subject at the 1964 “Formal Language Description Languages” con-
ference at Baden bei Wien (Landin’s paper is printed as [Lan66b] and a master-
ful overview of the approaches presented is [Ste66, p. 290]). His [Lan66a] is a
classic paper. Together with Rod Burstall, Landin went on to consider algebraic
approaches to reasoning about language descriptions.
Peter Lauer (b. 1934) Peter Lauer was a member of the IBM Laboratory in Vi-
enna. His interests were more philosophical than most members of the group.
Two key points of contact with the material in the current book are his VDL
description of ALGOL 60 [Lau68] (undertaken to show that it was the PL/I
language that gave rise to the huge size of its description — not the VDL
method) and his Ph.D. research supervised by Tony Hoare that showed that the
axioms of a language were consistent with an underlying operational seman-
tics [Lau71a, HL74].
Peter Lucas (1935–2015) was one of the original members of the IBM Labora-
tory in Vienna. He wrote an early ALGOL 60 compiler together with Bekič and
then became a key member of the team that wrote three versions of the VDL
(operational) description of PL/I in the 1960s. See [LW69, Luc71]. Importantly,
he then started to consider how such a description could be used as the basis
from which to design compiling algorithms. He championed the idea of identi-
fying “language concepts” that he hoped could be considered separately. A key
output from this was his “twin machine” argument which was used to justify
implementations of the “block concept” (locating the correct instance of an iden-
tifier with nested blocks, procedure calls etc.). References to this research in-
clude [Luc68, JL70] — Lucas managed one of the IBM Vienna groups working
on the 1970s PL/I compiler and is a co-author of the VDM (denotational) seman-
tics of PL/I [BBH+ 74]. He left the Vienna Lab to move to IBM Research in the
USA.
John McCarthy (1927–2011) is best known as a father of Artificial Intelligence
(AI) research. He did, however, also make significant contributions to formal
description of programming languages: his [McC66] paper was presented at the
1964 Formal Language Description Languages working conference at Baden
bei Wien. This paper presented a clear case for operational semantic descriptions
using “Micro-ALGOL” as an example. His work on AI and formal approaches to
computing coalesced in his research on support for theorem proving.
Arthur John Robin Gorell Milner (1934–2010) Robin Milner made many impor-
tant contributions to computer science including LCF [GMW79] (which influ-
enced nearly all subsequent theorem proving assistants), Type inference and the
ML programming language [HMT87]. His study of process algebras created CCS
and the π-calculus. Plotkin gives credit to Milner for the way that semantics can
F Notes on influential people 209
1 Towards the end of this useful interview, Wirth says: “My idea was that programming languages
allow programming on a higher level of abstraction compared to machine coding. You can abstract
specific properties and facilities of a specific machine. You can abstract to a higher level and create
programs that will then be available and runnable on all computers. That’s called abstraction. And
the term “higher-level languages” comes exactly from that. . . . Look at today’s situation. People
program in C++, the worst disease ever created. Or C# or Java, which are a bit better. But they
all suffer from their mightiness. I’m always expecting they’re going to collapse under their own
weight.”
References
[A+ 97] Samson Abramsky et al. Semantics of interaction: an introduction to game semantics.
Semantics and Logics of Computation, 14(1):1–32, 1997.
[AB87] Bijan Arbab and Daniel M Berry. Operational and denotational semantics of Prolog.
The Journal of Logic Programming, 4(4):309–329, 1987.
[Abr96] J.-R. Abrial. The B-Book: Assigning Programs to Meanings. Cambridge University
Press, 1996.
[Abr10] J.-R. Abrial. The Event-B Book. Cambridge University Press, Cambridge, UK, 2010.
[Abr13] Samson Abramsky. Semantics of interaction. arXiv preprint arXiv:1312.0121, 2013.
[AC12] Martin Abadi and Luca Cardelli. A Theory of Objects. Springer-Verlag, 2012.
[ACJ72] C. D. Allen, D. N. Chapman, and C. B. Jones. A formal definition of ALGOL 60.
Technical Report 12.105, IBM Laboratory Hursley, 8 1972.
[Acz82] P. H. G. Aczel. A note on program verification. (private communication) Manuscript,
Manchester, 1 1982.
[AdB91] Pierre America and Frank S. de Boer. A proof theory for a sequential version of
POOL. CWI, Nationaal Instituut voor Onderzoek op het gebied van Wiskunde en
Informatica, 1991.
[AdBKR89] Pierre America, Jaco de Bakker, Joost N Kok, and Jan Rutten. Denotational seman-
tics of a parallel object-oriented language. Information and Computation, 83(2):152–
205, 1989.
[AGLP88] D.J. Andrews, A. Garg, S.P.A. Lau, and J.R. Pitchers. The formal definition of
Modula-2 and its associated interpreter. In Robin E. Bloomfield, Lynn S. Marshall,
and Roger B. Jones, editors, VDM ’88 VDM — The Way Ahead, volume 328 of Lec-
ture Notes in Computer Science, pages 167–177. Springer-Verlag, 1988.
[AH82] Derek Andrews and Wolfgang Henhapl. Pascal. In Bjørner and Jones [BJ82], chap-
ter 6, pages 175–252.
[AJ18] Troy K. Astarte and Cliff B. Jones. Formal semantics of ALGOL 60: Four descrip-
tions in their historical context. In Liesbeth De Mol and Giuseppe Primiero, editors,
Reflections on Programming Systems - Historical and Philosophical Aspects, pages
71–141. Springer Philosophical Studies Series, 2018.
[Ame89] P. America. Issues in the design of a parallel object-oriented language. Formal
Aspects of Computing, 1(4):366–411, 1989.
[And92] James H. Andrews. Logic Programming: Operational Semantics and Proof Theory.
Distinguished Dissertations in Computer Science. Cambridge, 1992.
[ANS76] ANSI. Programming language PL/I. Technical Report X3.53-1976, American Na-
tional Standard, 1976.
[AO19] Krzysztof R. Apt and Ernst-Rüdiger Olderog. Fifty years of Hoare’s logic. Formal
Aspects of Computing, 31(6):751–807, 2019.
[Apt81] Krzysztof R. Apt. Ten years of Hoare’s logic: a survey—part I. ACM Transactions
on Programming Languages and Systems, 3(4):431–483, 10 1981.
[AR92] Pierre America and Jan Rutten. A layered semantics for a parallel object-oriented
language. Formal Aspects of Computing, 4(4):376–408, 1992.
[ASS85] Harold Abelson, Gerald Jay Sussman, and Julie Sussman. Structure and Interpreta-
tion of Computer Programs. MIT Press, 1985.
[Ast19] Troy K. Astarte. Formalising Meaning: a History of Programming Language Se-
mantics. PhD thesis, Newcastle University, 6 2019.
[BA90] Mordechai Ben-Ari. Principles of Concurrent and Distributed Programming. Pren-
tice Hall International Series in Computer Science. Prentice Hall, 1990.
[BA06] Mordechai Ben-Ari. Principles of Concurrent and Distributed Programming. Pear-
son Education, 2006.
[Bac78] John Backus. Can programming be liberated from the Von Neumann style?: A func-
tional style and its algebra of programs. Communications of the ACM, 21(8):613–
641, August 1978.
[Bae90] J. C. M. Baeten, editor. Applications of Process Algebra. Cambridge University
Press, 1990.
[Bar06] John Barnes. High Integrity Software: The SPARK Approach to Safety and Security.
Addison-Wesley, 2006.
[BBG+ 60] John W. Backus, Friedrich L. Bauer, Julien Green, Charles Katz, John McCarthy,
Peter Naur, Alan J. Perlis, Heinz Rutishauser, Klaus Samelson, Bernard Vauquois,
et al. Report on the algorithmic language ALGOL 60. Numerische Mathematik,
2(1):106–136, 1960.
[BBG+ 63] John W. Backus, Friedrich L. Bauer, Julien Green, Charles Katz, John McCarthy,
Peter Naur, Alan J. Perlis, Heinz Rutishauser, Klaus Samelson, Bernard Vauquois,
Joseph H. Wegstein, Adriaan van Wijngaarden, and Michael Woodger. Revised re-
port on the algorithmic language ALGOL 60. The Computer Journal, 5(4):349–367,
1963.
[BBG+ 68] Henry Bauer, Sheldon Becker, Susan L Graham, Edwin Satterthwaite, and Richard L
Sites. Algol W language description. Technical Report CS89, Computer Science
Dept., Stanford Univ, 1968.
[BBH+ 74] Hans Bekič, Dines Bjørner, Wolfgang Henhapl, Cliff B. Jones, and Peter Lucas. A
formal definition of a PL/I subset. Technical Report 25.139, IBM Laboratory Vienna,
12 1974.
[BBHS63] D. W. Barron, J. N. Buxton, D. F. Hartley, and C. Strachey. The main features of
CPL. Computer Journal, 6(2):134–143, 1963.
[BCJ84] H. Barringer, J.H. Cheng, and C. B. Jones. A logic covering undefinedness in pro-
gram proofs. Acta Informatica, 21(3):251–269, 1984.
[Bek64] Hans Bekič. Defining a language in its own terms. Technical Report 25.3.016, IBM
Laboratory Vienna, 12 1964.
[Bek73] Hans Bekič. An introduction to ALGOL 68. Annual Review in Automatic Program-
ming, 7:143–169, 1973. Hard copy.
[Bey09] Kurt W. Beyer. Grace Hopper and the Invention of the Information Age. The MIT
Press, 2009.
[BG96] Thomas J. Bergin and Richard G. Gibson, editors. History of Programming
Languages—II. ACM Press, New York, NY, USA, 1996.
[BGP00] László Böszörményi, Jürg Gutknecht, and Gustav Pomberger, editors. The School of
Niklaus Wirth: the art of simplicity. dpunkt. verlag, 2000.
[BH73] P. Brinch Hansen. Operating System Principles. Prentice Hall Series in Automatic
Computation. Prentice Hall, 1973.
[BHG87] P. A. Bernstein, V. Hadzilacos, and N. Goodman. Concurrency Control and Recovery
in Database Systems. Addison-Wesley, 1987.
[BHJ20] Alan Burns, Ian J. Hayes, and Cliff B. Jones. Deriving specifications of control
programs for cyber physical systems. The Computer Journal, 63(5):774–790, 2020.
References 213
[BHR84] Stephen Brookes, Charles Anthony Richard Hoare, and Andrew William Roscoe. A
theory of communicating sequential processes. Journal of the ACM, 31(3):560–599,
7 1984.
[BIJW75] H. Bekič, H. Izbicki, C. B. Jones, and F. Weissenböck. Some experiments with
using a formal language definition in compiler development. Laboratory Note LN
25.3.107, IBM Laboratory, Vienna, 12 1975.
[BJ78] D. Bjørner and C. B. Jones, editors. The Vienna Development Method: The Meta-
Language, volume 61 of Lecture Notes in Computer Science. Springer-Verlag, 1978.
[BJ82] Dines Bjørner and Cliff B. Jones, editors. Formal Specification and Software Devel-
opment. Prentice Hall International, 1982.
[BJ84] Hans Bekič and Cliff B. Jones. Programming Languages and Their Definition: Hans
Bekič (1936-1982). Selected papers, volume 177 of Lecture Notes in Computer Sci-
ence. Springer-Verlag, 1984.
[Bla86] Stephen Blamey. Partial-Valued logic. PhD thesis, University of Oxford, 1986.
[Bli81] Andrzej J. Blikle. On the development of correct specified programs. IEEE Trans-
actions on Software Engineering, 7(5):519–527, 1981.
[Bli88] A. Blikle. Three-valued predicates for software specification and validation. In
R. Bloomfield, L. Marshall, and R. Jones, editors, VDM—The Way Ahead, volume
328 of Lecture Notes in Computer Science, pages 243–266. Springer-Verlag, 1988.
[BM81] R. S. Boyer and J. S. Moore. The Correctness Problem in Computer Science. Inter-
national Lecture Series in Computer Science. Academic Press, London, 1981.
[BO80a] D. Bjørner and O. N. Oest, editors. The DDC Ada Compiler Development Project,
chapter 0. Volume 98 of Bjørner and Oest [BO80b], 1980.
[BO80b] D. Bjørner and O. N. Oest, editors. Towards a Formal Description of Ada, volume 98
of Lecture Notes in Computer Science. Springer-Verlag, Berlin, 1980.
[BSM16] L Binsbergen, Neil Sculthorpe, and Peter D Mosses. Tool support for component-
based semantics. In Companion Proceedings of the 15th International Conference
on Modularity, pages 8–11. ACM, 2016.
[BvW98] Ralph-Johan Back and Joakim von Wright. Refinement Calculus: A Systematic In-
troduction. Springer-Verlag, 1998.
[BW71] H. Bekič and K. Walk. Formalization of storage properties. In E. Engeler, editor,
[Eng71], pages 28–61. Springer-Verlag, 1971.
[CDD+ 15] Cristiano Calcagno, Dino Distefano, Jeremy Dubreil, Dominik Gabi, Pieter
Hooimeijer, Martino Luca, Peter O’Hearn, Irene Papakonstantinou, Jim Purbrick,
and Dulma Rodriguez. Moving fast with software verification. In NASA Formal
Methods, volume 9058 of Lecture Notes in Computer Science, pages 3–11. Springer
International Publishing, 2015.
[CDG+ 89] Luca Cardelli, James Donahue, Lucille Glassman, Mick Jordan, Bill Kalsow, and
Greg Nelson. Modula-3 report (revised). Technical report, DEC SRC, 1989.
[CH72] Maurice Clint and C. A. R. Hoare. Program proving: Jumps and functions. Acta
Informatica, 1(3):214–224, 1972.
[CH79] Derek Coleman and Jane W. Hughes. The clean termination of Pascal programs.
Acta Informatica, 11(3):195–210, 1979.
[Chu41] A. Church. The Calculi of Lambda-Conversion. Princeton University Press, 1941.
[CJ91] J. H. Cheng and C. B. Jones. On the usability of logics which handle partial functions.
In C. Morgan and J. C. P. Woodcock, editors, 3rd Refinement Workshop, pages 51–69.
Springer-Verlag, 1991.
[CJ00] Pierre Collette and Cliff B. Jones. Enhancing the tractability of rely/guarantee speci-
fications in the development of interfering operations. In Gordon Plotkin, Colin Stir-
ling, and Mads Tofte, editors, Proof, Language and Interaction, chapter 10, pages
277–307. MIT Press, 2000.
[CK85] Martin Campbell-Kelly. Christopher Strachey, 1916-1975: A biographical note.
IEEE Annals of the History of Computing, 1(7):19–42, 1985.
214 References
[Coo18] Byron Cook. Formal reasoning about the security of Amazon Web Services. In
International Conference on Computer Aided Verification, pages 38–47. Springer-
Verlag, 2018.
[Dat82] C. J. Date. A formal definition of the relational model. ACM Sigmod Record,
13(1):18–29, 1982.
[Dav65a] Martin Davis. Computability and Undecidability. Dover, 1965.
[Dav65b] Martin Davis, editor. The Undecidable. Raven Press, 1965.
[dB91] Frank de Boer. A proof system for the language POOL. In J.W. de Bakker, W.P.
de Roever, and G. Rozenberg, editors, Foundations of Object-Oriented Languages,
volume 489 of Lecture Notes in Computer Science, pages 124–150. Springer-Verlag,
1991.
[dBS69] J. W. de Bakker and D. Scott. A theory of programs. Manuscript notes for IBM
Seminar, Vienna, 8 1969.
[DDH72] O.-J. Dahl, E. W. Dijkstra, and C. A. R. Hoare, editors. Structured Programming.
Academic Press, 1972.
[DFLO19] Dino Distefano, Manuel Fähndrich, Francesco Logozzo, and Peter W. O’Hearn. Scal-
ing static analyses at Facebook. Communications of the ACM, 62(8):62–70, 2019.
[DFPV09] Mike Dodds, Xinyu Feng, Matthew Parkinson, and Viktor Vafeiadis. Deny-guarantee
reasoning. In Giuseppe Castagna, editor, Programming Languages and Systems,
volume 5502 of Lecture Notes in Computer Science, pages 363–377. Springer Berlin
/ Heidelberg, 2009.
[DGKL80] V. Donzeau-Gouge, G. Kahn, and B. Lang. On the formal definition of Ada. In N.D.
Jones, editor, Semantics-Directed Compiler Generation: Proceedings of a Workshop
Aarhus, Denmark, January 1980, volume 94 of Lecture Notes in Computer Science,
pages 475–489. Springer-Verlag, 1980.
[DHMS12] Brijesh Dongol, Ian J. Hayes, Larissa Meinicke, and Kim Solin. Towards an algebra
for real-time programs. In W. Kahl and T.G. Griffin, editors, 13th International
Conference on Relational and Algebraic Methods in Computer Science (RAMiCS),
volume 7560 of Lecture Notes in Computer Science, pages 50–65. Springer-Verlag,
2012.
[Dij62] E. W. Dijkstra. Over de sequetialiteit van procesbeschrivjingen. EWD35, 1962.
[Dij68a] E. W. Dijkstra. Cooperating sequential processes. In F. Genuys, editor, Programming
Languages, pages 43–112. Academic Press, New York, 1968.
[Dij68b] E. W. Dijkstra. Go to statement considered harmful. Communications of the ACM,
11(3):147–148, 1968.
[Dij76] E. W. Dijkstra. A Discipline of Programming. Prentice Hall, Englewood Cliffs, N.J.,
USA, 1976.
[DK15] Alan AA Donovan and Brian W Kernighan. The Go programming language.
Addison-Wesley Professional, 2015.
[DMN68] O.-J. Dahl, B. Myhrhaug, and K. Nygaard. SIMULA 67 common base language.
Technical Report S-2, Norwegian Computing Center, Oslo, 1968.
[Don76] James Edward Donahue. Complementary Definitions of Programming Language
Semantics, volume 42 of Lecture Notes in Computer Science. Springer-Verlag New
York, Inc., 1976.
[Don80] V. Donzeau-Gouge. Formal Definition of the Ada Programming Language. PhD
thesis, INRIA, 1980.
[dRdBH+ 01] Willem-Paul de Roever, Frank de Boer, Ulrich Hanneman, Jozef Hooman, Yassine
Lakhnech, Mannes Poel, and Job Zwiers. Concurrency Verification: Introduction
to Compositional and Noncompositional Methods. Cambridge Tracts in Theoretical
Computer Science. Cambridge University Press, 2001.
[DYBG+ 13] Thomas Dinsdale-Young, Lars Birkedal, Philippa Gardner, Matthew Parkinson, and
Hongseok Yang. Views: compositional reasoning for concurrent programs. In Pro-
ceedings of the 40th Annual ACM SIGPLAN-SIGACT Symposium on Principles of
Programming Languages, pages 287–300. ACM, 2013.
References 215
[JHC15] Cliff B. Jones, Ian J. Hayes, and Robert J. Colvin. Balancing expressiveness in formal
approaches to concurrency. Formal Aspects of Computing, 27(3):465–497, 2015.
[JHJ07] Cliff B. Jones, Ian J. Hayes, and Michael A. Jackson. Deriving specifications for
systems that are connected to the physical world. In Cliff B. Jones, Zhiming Liu, and
Jim Woodcock, editors, Formal Methods and Hybrid Real-Time Systems: Essays in
Honour of Dines Bjørner and Zhou Chaochen on the Occasion of Their 70th Birth-
days, volume 4700 of Lecture Notes in Computer Science, pages 364–390. Springer
Verlag, 2007.
[JL70] C. B. Jones and P. Lucas. Proving correctness of implementation techniques. Tech-
nical Report TR 25.110, IBM Laboratory Vienna, 8 1970.
[JL71] C. B. Jones and P. Lucas. Proving correctness of implementation techniques. In
E. Engeler, editor, A Symposium on Algorithmic Languages, volume 188 of Lecture
Notes in Mathematics, pages 178–211. Springer-Verlag, 1971.
[JL96] Richard Jones and Rafael D Lins. Garbage Collection: Algorithms for Automatic
Dynamic Memory Management. Wiley, 1996.
[JM94] C.B. Jones and C.A. Middelburg. A typed logic of partial functions reconstructed
classically. Acta Informatica, 31(5):399–430, 1994.
[Jon76] C. B. Jones. Formal definition in compiler development. Technical Report 25.145,
IBM Laboratory Vienna, 2 1976.
[Jon80] C. B. Jones. Software Development: A Rigorous Approach. Prentice Hall Interna-
tional, Englewood Cliffs, N.J., USA, 1980.
[Jon82a] Cliff B. Jones. Compiler design. In Bjørner and Jones [BJ82], chapter 8, pages
253–270.
[Jon82b] Cliff B. Jones. More on exception mechanisms. In Dines Bjørner and Cliff B. Jones,
editors, Formal Specification and Software Development, chapter 5, pages 125–140.
Prentice Hall International, 1982.
[Jon86] C. B. Jones. Systematic Software Development Using VDM. Prentice Hall Interna-
tional, 1986.
[Jon90] C. B. Jones. Systematic Software Development Using VDM. Prentice Hall Interna-
tional, second edition, 1990.
[Jon93] C. B. Jones. A pi-calculus semantics for an object-based design notation. In E. Best,
editor, CONCUR’93: 4th International Conference on Concurrency Theory, volume
715 of Lecture Notes in Computer Science, pages 158–172. Springer-Verlag, 1993.
[Jon00] C. B. Jones. Compositionality, interference and concurrency. In Jim Davies, Bill
Roscoe, and Jim Woodcock, editors, Millennial Perspectives in Computer Science,
pages 175–186. Macmillan Press, 2000.
[Jon01] C. B. Jones. The transition from VDL to VDM. Journal of Universal Computer
Science, 7(8):631–640, 2001.
[Jon03] Cliff B. Jones. The early search for tractable ways of reasoning about programs.
IEEE Annals of the History of Computing, 25(2):26–49, 2003.
[JP11] Cliff B. Jones and Ken G. Pierce. Elucidating concurrent algorithms via layers of
abstraction and reification. Formal Aspects of Computing, 23(3):289–306, 2011.
[JRW10] Cliff B Jones, A William Roscoe, and Kenneth R Wood, editors. Reflections on the
Work of C.A.R. Hoare. Springer Science & Business Media, 2010.
[JVY17] Cliff B. Jones, Andrius Velykis, and Nisansala Yatapanage. General lessons from a
rely/guarantee development. In Kim Guldstrand Larsen, Oleg Sokolsky, and Ji Wang,
editors, Dependable Software Engineering: Theories, Tools, and Applications, vol-
ume 10606 of Lecture Notes in Computer Science, pages 3–24. Springer-Verlag,
2017.
[JY15] Cliff B. Jones and Nisansala Yatapanage. Reasoning about separation using abstrac-
tion and reification. In Radu Calinescu and Bernhard Rumpe, editors, Software En-
gineering and Formal Methods, volume 9276 of Lecture Notes in Computer Science,
pages 3–19. Springer-Verlag, 2015.
[Kin69] J. C. King. A Program Verifier. PhD thesis, Department of Computer Science,
Carnegie-Mellon University, 1969.
References 219
[LW69] Peter Lucas and Kurt Walk. On the formal description of PL/I. Annual Review in
Automatic Programming, 6:105–182, 1969.
[McC66] John McCarthy. A formal description of a subset of ALGOL. In Formal Language
Description Languages for Computer Programming, pages 1–12. North-Holland,
1966.
[Mey88] B. Meyer. Object-oriented Software Construction. Prentice Hall, 1988.
[Mil78a] Robin Milner. Synthesis of communicating behaviour. Mathematical Foundations
of Computer Science 1978, 64:71–83, 1978.
[Mil78b] Robin Milner. A theory of type polymorphism in programming. Journal of Computer
and System Sciences, 17(3):348–375, 12 1978.
[Mil80] R. Milner. A Calculus of Communicating Systems, volume 92 of Lecture Notes in
Computer Science. Springer-Verlag, 1980.
[Mil89] Robin Milner. Communication and Concurrency. Prentice Hall, January 1989.
[Mil93] Robin Milner. Elements of interaction. Communications of the ACM, 36(1):78–89,
1993.
[MJ84] F. L. Morris and C. B. Jones. An early program proof by Alan Turing. Annals of the
History of Computing, 6(2):139–143, 1984.
[MK99] Jeff Magee and Jeff Kramer. State Models and Java Programs. Wiley, 1999.
[ML65] John McCarthy and Michael I Levin. LISP 1.5 Programmer’s Manual. MIT Press,
1965.
[Mog89] Eugenio Moggi. An Abstract View of Programming Languages. PhD thesis, Edin-
burgh University Laboratory for the Foundations of Computer Science, 1989.
[Moo65] Gordon E. Moore. Cramming more components onto integrated circuits. Electronics,
38(8), 4 1965.
[Moo19] J Strother Moore. Milestones from the Pure Lisp theorem prover to ACL2. Formal
Aspects of Computing, 31(6):699–732, 2019.
[Mor70] F. L. Morris. The next 700 formal language descriptions. Manuscript, 1970.
[Mor88] Carroll Morgan. The specification statement. ACM Trans. Program. Lang. Syst.,
10(3):403–419, July 1988.
[Mor90] Carroll Morgan. Programming from Specifications. Prentice Hall, 1990.
[Mos74] Peter David Mosses. The mathematical semantics of ALGOL 60. Technical report,
Programming Research Group, 1 1974.
[Mos75] Peter David Mosses. Mathematical semantics and compiler generation. PhD thesis,
University of Oxford, 4 1975.
[Mos85] Ben Moszkowski. Executing temporal logic programs. In Stephen D. Brookes, An-
drew William Roscoe, and Glynn Winskel, editors, Seminar on Concurrency, volume
197 of Lecture Notes in Computer Science, pages 111–130. Springer Berlin Heidel-
berg, 1985.
[Mos92] Peter D. Mosses. Action Semantics. Number 26 in Cambridge Tracts in Theoretical
Computer Science. Cambridge Press, 1992.
[Mos04] Peter D Mosses. Modular structural operational semantics. The Journal of Logic and
Algebraic Programming, 60:195–228, 2004.
[Mos09] Peter D Mosses. Component-based semantics. In Proceedings of the 8th Inter-
national Workshop on Specification and Verification of Component-Based Systems,
pages 3–10. ACM, 2009.
[Mos11] Peter D. Mosses. VDM semantics of programming languages: combinators and mon-
ads. Formal Aspects of Computing, 23(2):221–238, 2011.
[MP66] John McCarthy and James A. Painter. Correctness of a compiler for arithmetic ex-
pressions. Technical Report CS38, Computer Science Department, Stanford Univer-
sity, 4 1966.
[MP95] Z. Manna and A. Pnueli. Temporal Verification of Reactive Systems: Safety. Springer-
Verlag, 1995.
[MP99] Dale Miller and Catuscia Palamidessi. Foundational aspects of syntax. ACM Comput.
Surv., 31(3es):11, 1999.
References 221
[Rad81] George Radin. The early history and characteristics of PL/I. In Richard L. Wexelblat,
editor, History of programming languages, pages 551–589. Academic Press, 1981.
[Rei12] Wolfgang Reisig. Petri Nets: an Introduction, volume 4 of Monographs in Theoreti-
cal Computer Science. Springer Science & Business Media, 2012.
[Rei13] Wolfgang Reisig. Understanding Petri Nets: Modeling Techniques, Analysis Meth-
ods, Case Studies. Springer-Verlag, 2013.
[Rey93] John C Reynolds. The discoveries of continuations. Lisp and Symbolic Computation,
6(3-4):233–247, 1993.
[Rey02] John Reynolds. A logic for shared mutable data structures. In Gordon Plotkin, editor,
Proceedings of the Seventeenth Annual IEEE Symp. on Logic in Computer Science,
LICS 2002, pages 55–74. IEEE Computer Society Press, 7 2002.
[RH07] Barbara G. Ryder and Brent Hailpern, editors. HOPL III: Proceedings of the Third
ACM SIGPLAN Conference on History of Programming Languages, New York, NY,
USA, 2007. ACM.
[RNN92] H. Riis Nielson and F. Nielson. Semantics with Applications: A Formal Introduction.
Wiley, 1992.
[Ros94] Bill Roscoe, editor. A Classical Mind: Essays in Honour of CAR Hoare. Pearson
Education, 1994.
[Sam69] Jean E Sammet. Programming Languages: History and Fundamentals. Prentice
Hall, Inc., 1969.
[San99] Davide Sangiorgi. Typed π-calculus at work: a correctness proof of Jones’s paralleli-
sation transformation on concurrent objects. Theory and Practice of Object Systems,
5(1):25–34, 1999.
[Sat75] Edwin H. Satterthwaite. Source Language Debugging Tools. PhD thesis, Stanford
University, 1975.
[Sch97] Fred B. Schneider. On Concurrent Programming. Texts in Computer Science.
Springer-Verlag, 1997.
[Sco69] D. Scott. A type-theoretical alternative to CUCH, ISWIM, OWHY. Typescript –
Oxford, 10 1969.
[Sco77] Dana S Scott. Logic and programming languages. Communications of the ACM,
20(9):634–641, 1977.
[Sco80] Dana Scott. Lambda calculus: some models, some philosophy. Studies in Logic and
the Foundations of Mathematics, 101:223–265, 1980.
[Sco00] Michael L. Scott. Programming Language Pragmatics. Morgan Kaufmann, 2000.
ISBN 1-55860-578-9.
[Seb16] Robert W. Sebesta. Concepts of Programming Languages. Pearson, eleventh edition,
2016.
[Sin67] Michel Sintzoff. Existence of a van Wijngaarden sytnax for every recursively enu-
merable set. Annales Soc. Sci. Bruxelles, 81(2):115–118, 1967.
[Sit74] R. L. Sites. Proving that Computer Programs Terminate Cleanly. PhD thesis, Com-
puter Science Department, Stanford University, 1974. Printed as STAN-CS-74-418.
[SJv19] Elizabeth Scott, Adrian Johnstone, and J. Thomas van Binsbergen. Derivation repre-
sentation using binary subtree sets. Science of Computer Programming, 2019.
[SN86] Herbert A Simon and Allen Newell. Information Processing Language V on the IBM
650. IEEE Annals of the History of Computing, 8(1):47–49, 1986.
[SS86] L. Sterling and E. Shapiro. The Art of Prolog: Advanced Programming Techniques.
MIT Press, 1986.
[Ste66] T. B. Steel, editor. Formal Language Description Languages for Computer Program-
ming. North-Holland, 1966.
[STER11] G. Schellhorn, B. Tofan, G. Ernst, and W. Reif. Interleaved programs and rely-
guarantee reasoning with ITL. In Temporal Representation and Reasoning (TIME),
2011 Eighteenth International Symposium on, pages 99–106, 2011.
[Sto77] Joseph E. Stoy. Denotational Semantics: The Scott-Strachey Approach to Program-
ming Language Theory. MIT Press, Cambridge, MA, USA, 1977.
References 223