David A. Schmidt - Programming Language Semantics
David A. Schmidt - Programming Language Semantics
David A. Schmidt
Department of Computing and Information Sciences
Kansas State University
1 Introduction
A programming language possesses syntax and semantics. Syntax refers to the spelling of the
language’s programs, and semantics refers to the meanings of the programs. A language’s syntax
is formalized by a grammar or syntax chart; such formalizations are found in the back of language
manuals. A language’s semantics should be formalized, too, and this is the topic of this chapter.
Before we begin, we might ask, “What do we gain by formalizing the semantics of a programming
language?” Consider the related question, “What was gained when language syntax was formalized
with BNF?”
• A language’s syntax definition standardizes the official syntax. This is crucial to users, who
require a guide to writing syntactically correct programs, and to implementors, who must
write a correct parser for the language’s compiler.
• The syntax definition permits a formal analysis of its properties, such as whether the definition
is LL(k), LR(k), or ambiguous.
• The syntax definition can be used as input to a compiler front-end generating tool, such as
YACC; it becomes, in effect, the implementation.
• The semantics definition standardizes the official semantics of the language. This is cru-
cial to users, who require a guide to understanding the programs that they write, and to
implementors, who must write a correct code generator for the language’s compiler.
• The semantics definition permits a formal analysis of its properties, such as whether the defini-
tion is strongly typed, block structured, uses single-threaded data structures, is parallelizable,
etc.
• The semantics definition can be used as input to a compiler back-end generating tool [26, 31];
it becomes, in effect, the implementation.
Programming-language syntax was studied intensively in the 1960’s and 1970’s, and program-
ming language semantics is undergoing similar intensive study. Unlike the acceptance of BNF as
the standard for syntax definition, it is unlikely that a single definition method will take hold for
semantics—semantics is harder to formalize than syntax, and it has a wider variety of applications.
Semantics-definition methods fall roughly into three groups:
1
• operational: the meaning of a well-formed program is the trace of computation steps that
results from processing the program’s input. Operational semantics is also called intensional
semantics, because the sequence of internal computation steps (the “intension”) is most im-
portant. For example, two differently coded programs that both compute factorial have
different operational semantics.
We survey the three semantic methods by applying each in turn to the world’s oldest and simplest
programming language, arithmetic. The syntax of our arithmetic language is
E ::= N | E1 + E2
where N stands for the set of numerals {0, 1, 2, ...}. Although this language has no notion of input
and output, it is computational.
The rule scheme states that adding two numerals is a computation step, e.g., 1 + 2 ⇒ 3 is one
computation step. An operational semantics of a program is a sequence of such computation steps.
For example, an operational semantics of (1 + 2) + (4 + 5) goes as follows:
(1 + 2) + (4 + 5) ⇒ 3 + (4 + 5) ⇒ 3 + 9 ⇒ 12
Three computation steps led to the answer, 12. An intermediate expression like 3 + (4 + 5) is a
“state,” so this operational semantics traces the states of the computation.
Another semantics for the example is (1 + 2) + (4 + 5) ⇒ (1 + 2) + 9 ⇒ 3 + 9 ⇒ 12. The
outcome is the same, and a set of rules that has this property is called confluent [25].
A structural operational semantics is a term-rewriting system plus a set of inference rules that
state precisely the context in which a computation step can be undertaken. (A structural opera-
tional semantics is sometimes called a “small-step semantics,” because each computation step is a
small step towards the final answer.) Say that we demand left-to-right computation of arithmetic
expressions. This is encoded as follows:
2
E1 ⇒ E′1 E2 ⇒ E′2
E1 + E2 ⇒ E′1 + E2 N + E2 ⇒ N + E′2
The first rule goes as before; the second rule states, if the left operand of an addition expression
can be rewritten, then do this. The third rule is the crucial one: if the right operand of an addition
expression can be rewritten and the left operand is already a numeral (completely evaluated), then
rewrite the right operand. Working together, the three rules force left-to-right evaluation.
Each computation step must be deduced by the rules. For (1 + 2) + (4 + 5), we deduce this
initial computation step:
1+2 ⇒ 3
(1 + 2) + (4 + 5) ⇒ 3 + (4 + 5)
Thus, the first step is (1+2)+(4+5) ⇒ 3+(4+5); note that we cannot deduce that (1+2)+(4+5) ⇒
(1 + 2) + 9. The next computation step is justified by this deduction:
4+5 ⇒ 9
3 + (4 + 5) ⇒ 3 + 9
The last deduction is simply 3 + 9 ⇒ 12, and we are finished. The example shows why the
semantics is “structural”: each computation step is explicitly embedded into the structure of the
overall program.
Operational semantics is often used to expose implementation concepts, like instruction coun-
ters, storage vectors, and stacks. For example, say our semantics of arithmetic must show how a
stack holds intermediate results. We use a state of form hs, ci, where s is the stack and c is the
arithmetic expression to evaluate. A stack containing n items is written v1 :: v2 :: ... :: vn :: nil,
where v1 is the topmost item and nil marks the bottom of the stack. The c component will be
written as a stack as well. The initial state for an arithmetic expression, p, is written hnil, p :: nili,
and computation proceeds until the state appears as hv :: nil, nili; we say that the result is v.
The semantics uses three rewriting rules:
hs, N :: ci ⇒ hN :: s, ci
hs, E1 + E2 :: ci ⇒ hs, E1 :: E2 :: add :: ci
hN2 :: N1 :: s, add :: ci ⇒ hN′ :: s, ci where N′ is the sum of N1 and N2
The first rule says that a numeral is evaluated by pushing it on the top of the stack. The second
rule decomposes an addition into its operands and operator, which must be computed individually.
The third rule removes the top two items from the stack and adds them. Here is the previous
example, repeated:
hnil, (1 + 2) + (4 + 5) :: nili
⇒ hnil, 1 + 2 :: 4 + 5 :: add :: nili
⇒ hnil, 1 :: 2 :: add :: 4 + 5 :: add :: nili
⇒ h1 :: nil, 2 :: add :: 4 + 5 :: add :: nili
⇒ h2 :: 1 :: nil, add :: 4 + 5 :: add :: nili
⇒ h3 :: nil, 4 + 5 :: add :: nili ⇒ ... ⇒ h12 :: nil, nili
3
def eval(t, stack):
"""eval computes the meaning of t using stack.
parameters: t - an arithmetic term, parsed into a nested list of form:
term ::= N | ["+", term, term], where N is a string of digits
stack - a list of integers
returns: stack with the integer meaning of t pushed on its top.
"""
print "Evaluate", t, "with stack =", stack
if isinstance(t, str) and t.isdigit(): # is t a string of digits?
newstack = [int(t)] + stack # push the int onto the stack
else: # t is a list, ["+", t1, t2]
stack1 = eval(t[1], stack)
stack2 = eval(t[2], stack1)
answer = stack2[1] + stack2[0] # add top two stack values
newstack = [answer] + stack2[2:] # push answer onto popped stack
print "Evaluated", t, " Updated stack =", newstack
return newstack
length of the computation. That is, to prove that a property, P , holds for an operational semantics,
one must show that P holds for all possible computation sequences that can be generated from the
rewriting rules. For an arbitrary computation sequence, it suffices to show that P holds no matter
how long the computation runs. Therefore, one shows (i) P holds after zero computation steps,
that is, at the outset, and (ii) if P holds after n computation steps, it holds after n + 1 steps. See
Nielson and Nielson [34] for examples.
Operational semantics lies close to implementation, and early forms of operational semantics
were definitional interpreters — implementations that generated the state-transition steps [13, 44].
Figure 1 displays a Python-coded definitional interpreter that generates the computation steps
of the stack-based arithmetic semantics, modelling the stack with a list argument and modelling
subexpression evaluation with recursive-function calls. (As an exercise, you should prove the inter-
preter prints a sequence of stacks that matches the one computed by the state-transition rules.) A
definitional interpreter is a powerful, practical tool for language definition and prototyping.
4
The denotational semantics definition of arithmetic is simple and elegant:
E : Expression → N at
E[[N]] = N
E[[E1 + E2 ]] = plus(E[[E1 ]], E[[E2 ]])
The first line states that E is the name of the function that maps arithmetic expressions to
their meanings. Since there are two BNF constructions for expressions, E is completely defined by
the two equational clauses. (This is a Tarksi-style interpretation, as used in symbolic logic to give
meaning to logical propositions [49].) The interesting clause is the one for E1 + E2 ; it says that the
meanings of E1 and E2 are combined compositionally by plus. Here is the denotational semantics
of our example program:
E[[(1 + 2) + (4 + 5)]] = plus(E[[1 + 2]], E[[4 + 5]])
= plus(plus(E[[1]], E[[2]]), plus(E[[4]], E[[5]]))
= plus(plus(1, 2), plus(4, 5)) = plus(3, 9) = 12
Read the above as follows: the meaning of (1 + 2) + (4 + 5) equals the meanings of 1 + 2 and 4 + 5
added together. Since the meaning of 1 + 2 is 3, and the meaning of 4 + 5 is 9, the meaning of
the overall expression is 12. This reading says nothing about order of evaluation or run-time data
structures—it states only mathematical meaning.
Here is an alternative way of understanding the semantics; write a set of simultaneous equations
based on the denotational definition:
Now, solve the equation set to discover that E[[(1 + 2) + (4 + 5)]] is 12.
Since denotational semantics states the meaning of a phrase in terms of the meanings of its
subphrases, its associated proof technique is structural induction. That is, to prove that a property,
P , holds for all programs in the language, one must show that the meaning of each construction in
the language has property P . Therefore, one must show that each equational clause in the semantic
definition produces a meaning with property P . In the case that a clause refers to subphrases (e.g.,
E[[E1 + E2 ]]), one may assume that the meanings of the subphrases have property P . Again, see
Nielson and Nielson [34] for examples.
N ⇒ N
5
E1 ⇒ n 1 E2 ⇒ n 2
where m is the sum of n1 and n2
E 1 + E2 ⇒ m
Read a configuration of the form E ⇒ n as “E evaluates to n.” The rules resemble a denotational
semantics written in inference rule form; this is no accident—natural semantics can be viewed as
a denotational-semantics variant where the internal calculations of meaning are made explicit and
the domains are left implicit. The internal calculations are seen in the natural semantics of our
example expression:
1 ⇒ 1 2 ⇒ 2 4 ⇒ 4 5 ⇒ 5
(1 + 2) ⇒ 3 (4 + 5) ⇒ 9
(1 + 2) + (4 + 5) ⇒ 12
Unlike denotational semantics, natural semantics does not claim that the meaning of a program
is necessarily “mathematical.” And unlike structural operational semantics, where a configuration
e ⇒ e′ says that e transits to an intermediate state, e′ , in natural semantics e ⇒ v asserts that
the final answer for e is v. For this reason, a natural semantics is sometimes called a “big-step
semantics.” An interesting drawback of natural semantics is that semantics derivations can be
drawn only for terminating programs.
The usual proof technique for proving properties of a natural semantics definition is induction
on the height of the derivation trees that are generated from the semantics. Once again, see Nielson
and Nielson [34].
The semantics methods shine when they are applied to programming languages—primary features
of a language are made prominent, and subtle features receive proper mention. Ambiguities and
anomalies stand out like the proverbial sore thumb. In this section, we use a classic block-structured
imperative language to demonstrate. Denotational semantics will be emphasized, but excerpts from
the other semantics formalisms will be provided for comparison.
6
P ∈ Program I ∈ Identifier = alphabetic strings
D ∈ Declaration V ∈ Variable = { X, Y, Z } ⊆ Identifier
C ∈ Command N ∈ Numeral = { 0, 1, 2, ... }
E ∈ Expression
P ::= C.
D ::= proc I = C
C ::= V:=E | C1 ; C2 | begin D in C end | call I | while E do C od
E ::= N | E1 + E2 | E1 not= E2 | V
It is possible to write nonsense programs in the language; an example is: A:=0; call B. Such
programs have no meaning, and we will not attempt to give semantics to them.
7
Store = {hn1 , n2 , n3 i | ni ∈ N at, i ∈ 1..3}
one. (That’s why a “no-op” command is the identity function, id(s) = s, where s ∈ Store.)
But sometimes commands “loop,” and no updated store appears. We use the symbol, ⊥, read
“bottom,” to stand for a looping store, and we use Store⊥ = Store ∪ {⊥} as the possible outputs
from commands. Therefore, the meaning of a command is a function of form Store → Store⊥ .
It is impossible to recover from looping, so if there is a command sequence, C1 ; C2 , and C1 is
looping, then C2 cannot proceed. The check operation is used to watch for this situation.
Finally, here are two commonly used notations. First, functions like id(s) = s are often refor-
matted to read id = λs.s; in general, for f (a) = e, we write f = λa.e, that is, we write the argument
to the function to the right of the equals sign. This is called lambda notation, and stems from the
lambda calculus, an elegant formal system for functions [4]. The notation f = λa.e emphasizes that
(i) the function λa.e is a value in its own right, and (ii) the function’s name is f .
Second, it is common to revise a function that takes multiple arguments, e.g., f (a, b) = e, so
that it takes the arguments one at a time: f = λa.λb.e. So, if the arity of f was A × B → C, its
new arity is A → (B → C). This reformatting trick is named currying, after Haskell Curry, one
of the developers of the lambda calculus.
8
P : P rogram → N at → N at⊥
P[[C.]] = λn.C[[C]]init env (init store n)
D : Declaration → Environment → Environment
D[[proc I = C]] = λe.bind(I, C[[C]]e, e)
C : Command → Environment → Store → Store⊥
C[[V:=E]] = λe.λs.update(find(V, e), E[[E]]e s, s)
C[[C1 ; C2 ]] = λe.λs.check(C[[C2 ]]e, C[[C1 ]]e s)
C[[begin D in C end]] = λe.λs.C[[C]](D[[D]]e)s
C[[call I]] = λe.find(I, e)
C[[while E do C od]] = λe. i≥0 wi
S
w0 = λs.⊥
where
wi+1 = λs.if E[[E]]e s then check(wi , C[[C]]e s) else s
E : Expression → Environment → Store → (N at ∪ Bool)
E[[N]] = λe.λs.N
E[[E1 + E2 ]] = λe.λs.plus(E[[E1 ]]e s, E[[E2 ]]e s)
E[[E1 not= E2 ]] = λe.λs.notequals(E[[E1 ]]e s, E[[E2 ]]e s)
E[[V]] = λe.λs.lookup(find(V, e), s)
we define a valuation function, which produces the meanings of constructions at that level. For
example, at the Expression level, the constructions are mapped to their meanings by E.
What is the meaning of the expression, say, X+5? This would be E[[X+5]], and the meaning
depends on which location is named by X and what number is stored in that location. Therefore,
the meaning is dependent on the current value of the environment and the current value of the
store. So, if the current environment is e0 = (P, λs.s) :: (X, 1) :: (Y, 2) :: (Z, 3) :: nil and the current
store is s0 = h2, 0, 0i, then the meaning of X+5 is 7:
But a crucial point about the meaning of the assignment is that it is a function upon stores. That
is, if we are uncertain of the current value of store, but we know that the environment for the
assignment is e0 , then we can conclude
That is, the assignment with environment e0 is a function that updates a store at location 3.
9
Next, consider this example of a command sequence:
As noted in the earlier section, the check operation verifies that the first command in the se-
quence produces a proper output store; if so, the store is handed to the second command in the
sequence. Also, we see that the meaning of call P is the store updating function bound to P in
the environment.
Procedures are placed in the environment by declarations, as we see in this example: let e1
denote (X, 1) :: (Y, 2) :: (Z, 3) :: nil:
The equality marked by (∗) is significant; we can assert that the function
λs.update(2, lookup(2, s), s) is identical to λs.s by appealing to the extensionality law of
mathematics: if two functions map identical arguments to identical answers, then the functions are
themselves identical. The extensionality law can be used here because in denotational semantics
the meanings of program phrases are mathematical—functions. In contrast, the extensionality law
cannot be used in operational semantics calculations.
Finally, we can combine our series of little examples into the semantics of a complete program:
C[[while0 E do C od]]e = w0
C[[whilei+1 E do C od]]e = wi+1
10
e ⊢ D ⇒ e′ e′ , s ⊢ C ⇒ s′
e ⊢ proc I = C ⇒ bind(I, (e, C), e)
e, s ⊢ begin D in C end ⇒ s′
l = find(V, e) e, s ⊢ E ⇒ n e, s ⊢ C1 ⇒ s′ e, s′ ⊢ C2 ⇒ s′′
e, s ⊢ V:=E ⇒ update(l, n, s) e, s ⊢ C1 ; C2 ⇒ s′′
(e′ , C ′ ) = find(I, e) e′ , s ⊢ C ′ ⇒ s′ e, s ⊢ E ⇒ f alse
e, s ⊢ call I ⇒ s′ e, s ⊢ while E do C od ⇒ s
e, s ⊢ E ⇒ true e, s ⊢ C ⇒ s′ e, s′ ⊢ while E do C od ⇒ s′′
e, s ⊢ while E do C od ⇒ s′′
Since the behavior of a while loop must be the “union” of the behaviors of the whilei -loops, we
conclude that C[[while E do C od]]e = i≥0 wi . The semantic union operation is well defined because
S
each wi is a function from the set Store → Store⊥ , and a function can be represented as a set of
argument-answer pairs. (This is called the graph of the function.) So, i≥0 wi is the union of the
S
C[[while E do C od]]e = w
where w = λs.if E[[E]]e s then check(w, C[[C]]e s) else s
The problem here is that the definition of w is circular, and circular definitions can be malformed.
Fortunately, this definition of w can be claimed to denote the function i≥0 wi because the following
S
equality holds: [ [
wi = λs.if E[[E]]e s then check( wi , C[[C]]e s) else s
i≥0 i≥0
So, i≥0 wi is a solution—a fixed point—of the circular definition, and in fact it is the smallest
S
function that makes the equality hold. Therefore, it is the least fixed point.
Typically, the denotational semantics of the while-loop is presented by the circular definition,
and the claim is then made that the circular definition stands for the least fixed point. This is called
fixed-point semantics. We have omitted many technical details regarding fixed-point semantics;
these are available in several texts [15, 45, 47, 51].
11
let e0 = (X, 1) :: (Y, 2) :: (Z, 3) :: nil
s0 = h2, 0, 0i, s1 = h2, 1, 0i
E0 = Y not=1, C0 = Y:=Y+1
C00 = while E0 do C0 od
bound to I and then calculate the output, n, for E. Finally, l and n are used to update s, producing
the output.
The rules are denotational-like, but differences arise in several key constructions. First, the
semantics of a procedure declaration binds I not to a function but to an environment-command
pair called a closure. When procedure I is called, the closure is disassembled, and its text and
environment are executed. Since a natural semantics does not use function arguments, it is called
a first-order semantics. (Denotational semantics is sometimes called a higher-order semantics.)
Second, the while-loop rules are circular. The second rule states, in order to derive a while-loop
computation that terminates in s′′ , one must derive (i) the test, E is true, (ii) the body, C, outputs
s′ , and (iii) using e and s′ , one can derive a terminating while-loop computation that outputs s′′ .
The rule makes one feel that the while-loop is “running backwards” from its termination to its
starting point, but a complete derivation, like the one shown in Figure 6, shows that the iterations
of the loop can be read from the root to the leaves of the derivation tree.
One important aspect of the natural semantics definition is that derivations can be drawn only
for terminating computations. A nonterminating computation is equated with no computation at
all.
12
e ⊢ hn1 + n2 , si ⇒ n3 where n3 is the sum of n1 and n2
e ⊢ hE, si ⇒ E′
e ⊢ hI:=E, si ⇒ hI:=E′ , si
e ⊢ hI:=n, si ⇒ update(l, n, s) where find(I, e) = l
e ⊢ hC1 , si ⇒ hC′1 , s′ i e ⊢ hC1 , si ⇒ s′
e ⊢ hC1 ; C2 , si ⇒ hC′1 ; C2 , s′ i e ⊢ hC1 ; C2 , si ⇒ hC2 , s′ i
e ⊢ hwhile E do C od, si ⇒ hif E then C ; while E do C od else skip fi, si
e ⊢ hcall I, si ⇒ huse e′ in C′ , si where find(I, e) = (e′ , C′ )
e′ ⊢ hC, si ⇒ hC′ , s′ i e′ ⊢ hC, si ⇒ s′
e ⊢ huse e′ in C, si ⇒ huse e′ in C′ , s′ i e ⊢ huse e′ in C, si ⇒ s′
e ⊢ proc I = C ⇒ bind(I, (e, C), e)
e ⊢ D ⇒ e′
e ⊢ hbegin D in C end, si ⇒ huse e′ in C, si
{[E/I]P } I:=E {P }
13
assert {TOTAL = X+Y}
let P0 be TOTAL = X+Y while X not= 0 do
P1 be TOTAL = X+(Y+1), P2 be TOTAL = (X-1)+(Y+1) invariant {TOTAL = X+Y}
X:= X - 1
E0 = X not= 0, C0 = X:=X-1;Y:=Y+1
assert {TOTAL = X+(Y+1)}
Y:= Y + 1
{P2 } X:=X-1 {P1 } {P1 } Y:=Y+1 {P0 }
(P0 ∧ E0 ) ⊃ P2 P0 ⊃ P0 assert {TOTAL = X+Y}
{P2 } C0 {P0 } od
{P0 ∧ E0 } C0 {P0 } assert {TOTAL = X+Y
{P0 } while E0 do C0 od {P0 ∧ ¬E0 } and not(X not= 0)}
implies {TOTAL = Y}
The completed game is this loop program, while X not= 0 do X:= X-1; Y:= Y+1 od, and the
proof that the loop generates a victory is listed in Figure 9, which shows at the end that TOTAL = Y,
that is, all the pebbles rest in Y’s box. The derivation is difficult to read, but when reformat-
ted vertically, as seen in the right half of the Figure, it shows clearly how the input knowledge,
TOTAL = X+Y, is transformed into the output knowledge, TOTAL = Y.
The key to building the proof is determining a loop invariant. Since the goal was to prove
TOTAL = Y, this made TOTAL = X+Y useful, because X:= X-1 eventually lowers X to zero.2 With
the invariant in hand, it is easy to work backwards, from the end of the loop body, to the top,
to calculate that the loop body is indeed “repeatable code”: The rule for assignment proves both
2
The loop terminates only if X’s value begins as a nonnegative. The laws in Figure 8 do not guarantee termination.
This requires additional logical machinery.
14
{TOTAL = X+(Y+1)} Y:=Y+1 {TOTAL = X+Y}, and also {TOTAL = (X−1)+(Y+1)} X:=X-1 {TOTAL =
X + (Y + 1)}. Finally, TOTAL = (X − 1) + (Y + 1) implies TOTAL = X+Y.
There are three ways of stating the semantics of a command in an axiomatic semantics:
• relational semantics: The meaning of C is the set of P, Q pairs for which {P } C {Q} holds.
Termination is not demanded of C. (This is called “partial correctness.”)
4 Practical Impact
Research on programming-language semantics showed how a language can be defined precisely, its
correctness properties proved, and its implementation realized. Indeed, the area of formal methods
(cf. Chapter 116) is an adaptation of the semantics methods from computer languages to computer
programs — how a program can be specified precisely, its correctness properties proved, and its
specification implemented.
Operational semantics, having the longest life of the semantic approaches, is most entrenched
within software development. From operational semantics came virtual machines to which lan-
guages can be translated. Definitional interpreters are now the norm for prototyping new languages,
especially ”little” or “domain-specific” languages, which are designed for problem solving in a lim-
ited application domain, e.g., file-linking (Make), web-page layout (HTML), spreadsheet layout
(Excel), or linear algebra (Matlab). Scripting languages (see Chapter 110) are also implemented in
definitional-interpreter style.
Axiomatic semantics has undergone a significant revival, not only as the starting point for
specification writing and verification in formal-methods work, but as the language for description
and analysis of secure and safety-critical software systems, where correctness properties, above
all, must be stated so that there can validation or justification, whether by testing, monitoring,
or theorem proving. Indeed, the Object Control Language (OCL) used in the Unified Modelling
Language for software blueprinting is a derivative of the classic axiomatic semantics notation. (See
Volume 2 of this Handbook.) A wide variety of proof assistants and logical frameworks are available
for helping a designer state and validate crucial properties of software, e.g., ACL2 [24], PVS [38],
Isabelle/HOL [35], and Coq [41].
The primary impact of denotational semantics has been to the design of modern programm-
ming languages. The modelling of programming constructions as functions that operate on storage
motivated many designers to add such functions as primitive constructions to the programming
language itself — the results are (i) higher-order constructions, such as the anonymous functions in
ML and Java; (ii) generic and polymorphic constructions, such as class templates in C++ and Scala;
and (iii) reflective constructions, like that found in JavaBeans and 3-Lisp. The importance of poly-
15
morphic constructions to modern-day software assembly has stimulated extensions of denotational
semantics into “higher-order” formats that are expressed within type theory [39, 36].
Within the subarea of programming-languages research, the semantics methods play a central
role. Language designers use semantics definitions to formalize their creations, as was done during
the development of Ada [10] and after development for Scheme [43] and Standard ML [27]. Software
tools are readily available to prototype a syntax-plus-semantics definition into an implementation
[31, 26, 5, 8].
A major success of formal semantics is the analysis and synthesis of data-flow analysis and type-
inference algorithms from semantics definitions. This subject area, called abstract interpretation
[3, 7, 33], uses algebra techniques to map semantic definitions from their “concrete” (execution)
definition to their “abstract” (homomorphic property) definition. The technique can be used to
extract properties from the definitions, generate data flow and type inference, and prove program
correctness or code-improvement transformations. Most automated methods for program validation
use abstract interpretation.
5 Research Issues
The techniques in this chapter have proved successful for defining, improving, and implementing se-
quential programming languages. But new language and software paradigms present new challenges
to the semantics methods.
Challenging issues arise in the object-oriented programming paradigm. Not only can objects
be arguments (“messages”) to other objects’ procedures (“methods”), but coercion laws based on
inheritance allow controlled mismatches between arguments and parameters. For example, an ob-
ject argument that contains methods for addition, subtraction, and multiplication is bound to a
method’s parameter that expects an object with just addition and subtraction methods. Care-
lessly defined coercions lead to unsound programs, so semantics definitions must be extended with
inheritance hierarchies [16, 40]. See Chapter 107 for details.
Yet another challenging topic is defining communication as it arises in distributed programming,
where multiple processes (threads of execution) synchronize through communication. Structural
operational semantics has been adapted to formalize such systems. More importantly, the protocol
used by a system lies at the center of the system’s semantics; semantics approaches based on process
algebra [12, 20, 28] have been developed that express a protocol as a family of simultaneous algebra
equations that must be “solved” to yield a convergent solution. See Chapter 112.
A current challenge is applying semantics methods to define and explain programs that exploit
the architecture of multi-core processors, where subcomputations and their storage requirements
must be allocated to processor cores and their associated cache hierarchy [18]. See Chapter 34 for
background.
Finally, a longstanding crucial topic is the relationship between different forms of semantic
definitions: If one has, say, both a denotational semantics and an axiomatic semantics for the
same programming language, in what sense do the semantics agree? Agreement is crucial, since
a programmer might use the axiomatic semantics to reason about the properties of programs,
whereas a compiler writer might use the denotational semantics to implement the language. This
question generalizes to the problem of refinement of software specifications — see Chapter 116. In
mathematical logic, one uses the concepts of soundness and completeness to relate a logic’s proof
system to its interpretation, and in semantics there are similar notions of soundness and adequacy to
relate one semantics to another [15, 37]. Important as these properties are, they are quite difficult
to realize in practice [1].
16
6 Defining Terms
Loop invariant: In axiomatic semantics, a logical property of a while-loop that holds true no
matter how many iterations the loop executes.
Natural semantics: A hybrid of operational and denotational semantics that shows computation
steps performed in a compositional manner. Also known as a “big-step semantics.”
References
[1] S. Abramsky, R. Jagadeesan, and P. Malacaria. Full abstraction for PCF. In Proc. Theoretical
Aspects of Computer Software, TACS94, Lecture Notes in Computer Science 789, pages 1–15.
Springer, 1994.
[2] K. Apt. Ten years of Hoare’s logic: A survey–part I. ACM Trans. Programming Languages
and Systems, 3:431–484, 1981.
[3] B. Blanchet, et al. A static analyzer for large safety-critical software. In Proc. Programming
Language Design and Implementation, pages 196–207. ACM Press, 2003.
[4] H. Barendregt. The Lambda Calculus: Its Syntax and Semantics. North Holland, Amsterdam,
1984.
[5] D. F. Brown, H. Moura, and D. A. Watt. Actress: an action semantics directed compiler gen-
erator. In CC’92, Proceedings of the 4th International Conference on Compiler Construction,
Paderborn, Lecture Notes in Computer Science 641, pages 95–109. Springer-Verlag, 1992.
[6] M. Clavel, et al. All About Maude - A High-Performance Logical Framework. Springer-Verlag,
2007.
17
[7] P. Cousot and R. Cousot. Abstract interpretation: a unified lattice model for static analysis of
programs. In Proc. 4th ACM Symp. on Principles of Programming Languages, pages 238–252.
ACM Press, 1977.
[8] Th. Despeyroux. Executable specification of static semantics. In G. Kahn, D.B. MacQueen,
and G. Plotkin, editors, Semantics of Data Types, pages 215–234. Lecture Notes in Computer
Science 173, Springer-Verlag, 1984.
[10] V. Donzeau-Gouge. On the formal description of Ada. In N.D. Jones, editor, Semantics-
Directed Compiler Generation. Lecture Notes in Computer Science 94, Springer-Verlag, 1980.
[13] D. Friedman, M. Wand, and C. Haynes. Essentials of Programming Languages, 2d ed. MIT
Press, 2001.
[15] C. Gunter. Semantics of Programming Languages. MIT Press, Cambridge, MA, 1992.
[16] C. Gunter and J. Mitchell, editors. Theoretical Aspects of Object-Oriented Programming. MIT
Press, Cambridge, MA, 1994.
[18] M. Herlihy and N. Shavit. Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
[19] C.A.R. Hoare. An axiomatic basis for computer programming. Commun. ACM, 12:576–580,
1969.
[21] C.A.R. Hoare and N. Wirth. An axiomatic definition of the programming language Pascal.
Acta Informatica, 2:335–355, 1973.
[23] G. Kahn. Natural semantics. In Proc. STACS ’87, pages 22–39. Lecture Notes in Computer
Science 247, Springer, Berlin, 1987.
[24] M. Kaufmann, P. Manolios, and J.S. Moore. Computer-Aided Reasoning: ACL2 Case Studies.
Kluwer, 2000.
[25] J.W. Klop. Term rewriting systems. In T. Maibaum S. Abramsky, D. Gabbay, editor, Handbook
of Logic in Computer Science, volume 2, pages 2–117. Oxford University Press, 1992.
[26] P. Lee. Realistic Compiler Generation. The MIT Press, Cambridge, Massachusetts, 1989.
[27] R. Milner, M. Tofte, and R. Harper. The Definition of Standard ML. The MIT Press, 1990.
18
[28] Robin Milner. Communication and Concurrency. Prentice-Hall, 1989.
[29] J.C. Mitchell. Foundations for Programming Languages. MIT Press, 1996.
[30] C. Morgan. Programming from Specifications, 2d. Ed. Prentice Hall, 1994.
[31] P.D. Mosses. Compiler generation using denotational semantics. In A. Mazurkiewicz, editor,
Mathematical Foundations of Computer Science, Lecture Notes in Computer Science 45, pages
436–441. Springer, Berlin, 1976.
[32] P.D. Mosses. Denotational semantics. In J. van Leeuwen, editor, Handbook of Theoretical
Computer Science, volume B, chapter 11, pages 575–632. Elsevier, 1990.
[33] S. Muchnick and N.D. Jones, editors. Program Flow Analysis: Theory and Applications.
Prentice-Hall, 1981.
[34] H. Riis Nielson and F. Nielson. Semantics with Applications, a formal introduction. Wiley
Professional Computing. John Wiley and Sons, 1992.
[35] T. Nipkow, L. Paulson, and M. Wenzel. Isabelle/HOL: A Proof Assistant for Higher-Order
Logic. Springer-Verlag, 2002.
[36] B. Nordström, K. Petersson, and J. Smith. Programming in Martin-Löf ’s Type Theory. Oxford,
1990.
[37] C.H.-L. Ong. Correspondence between operational and denotational semantics. In S. Abram-
sky, D. Gabbay, and T. Maibaum, editors, Handbook of Computer Science, Vol. 4. Oxford
Univ. Press, 1995.
[38] S. Owre, et al. PVS: Combining specification, proof checking, and model checking. In Proc.
CAV ’87, pages 411–414. Springer-Verlag, 1996.
[39] B. Pierce. Formal models/calculi of programming languages. In Allen Tucker, editor, CRC
Handbook of Computer Science and Engineering. CRC Press, Boca Raton, FL, 1996.
[42] G.D. Plotkin. A structural approach to operational semantics. Technical Report FN-19,
DAIMI, Aarhus, Denmark, September 1981.
[43] J. Rees and W. Clinger. Revised3 report on the algorithmic language Scheme. SIGPLAN
Notices, 21:37–79, 1986.
[45] D.A. Schmidt. Denotational Semantics: A Methodology for Language Development. Allyn and
Bacon, Inc., 1986.
[46] K. Slonneger and B. Kurtz. Formal Syntax and Semantics of Programming Languages: A
Laboratory-Based Approach. Addison-Wesley, Reading, MA, 1995.
19
[47] J.E. Stoy. Denotational Semantics. MIT Press, Cambridge, MA, 1977.
[49] D. van Dalen. Logic and Structure, 3d edition. Springer, Berlin, 1994.
[50] D.A. Watt. Programming Language Syntax and Semantics. Prentice-Hall International, En-
glewood Cliffs, New Jersey, 1991.
[51] G. Winskel. Formal Semantics of Programming Languages. MIT Press, Cambridge, MA, 1993.
7 Further Information
The best starting point for further reading is the comparative semantics text of Nielson and Nielson
[34], which thoroughly develops the topics in this chapter. See also the texts by Slonneger and
Kurtz[46] and Watt[50].
Operational semantics has a long history; modern introductions are Hennessey’s text [17] and
Plotkin’s report on structural operational semantics [42]. The principles of natural semantics are
documented by Kahn [23]. A good example of the interpreter-based approach to semantics is the
text by Friedman, Wand, and Haynes [13].
Mosses’s paper [32] is a useful introduction to denotational semantics; textbook-length treat-
ments include those by Schmidt [45], Stoy [47], Tennent [48], and Winskel [51]. Gunter’s text [15]
uses denotational-semantics-based mathematics to compare several of the semantics approaches.
Pierce [39] and Mitchell [29] provide foundational supporting material.
Of the many textbooks on axiomatic semantics, one might start with books by Dromey [11]
or Gries [14]; both emphasize precondition semantics, which is most effective at deriving correct
code. Apt’s paper [2] is an excellent description of the formal properties of relational semantics,
and Dikstra’s text [9] is the standard reference on precondition semantics. Hoare’s landmark papers
on relational semantics [19, 21] are worth reading as well. Many texts have been written on the
application of axiomatic semantics to systems development; two samples are by Jones [22] and
Morgan [30].
You are also urged to read the other chapters in Section 9 of this Volume.
20