Here is a one-line program. What does it mean?
You probably answered instantly: a[i++] = i has a
different result in C than your intuition suggests, where "evaluate the operands left to right" was for
decades unspecified and compilers disagreed — informal meaning is a disaster. A
formal semantics is the cure: a precise, mathematical answer to the question "what does
this program mean?", stated so exactly that two people (or two compilers) can never legitimately
disagree.
This is the founding question of programming-language theory. A grammar tells you which strings are well-formed programs — but says nothing about what they do. Semantics supplies the missing half: a mathematical object (a value, a state, a function, a relation) that is the meaning, together with rules that compute it. Get this right and you can prove things: that an optimisation preserves behaviour, that a type system rules out crashes, that two programs are interchangeable. Everything else in this course is built on it.
The first discipline a semanticist learns is to keep two languages rigidly apart. The
object language is the one we are studying — the programming language whose meaning we
want to pin down. The metalanguage is the one we reason in — ordinary
mathematics: sets, functions, relations, inductive definitions, logic. Confusing the two is the classic
beginner's error; it is the sin of thinking the symbol
We even use a distinct arrow style for each world. Object-language operators (+,
*) are typeset as program text; the metalanguage's genuine operations are ordinary maths.
Whenever an object symbol and a meta operation share a glyph, we lean on context — or decorate one — to
keep the border patrolled. This vigilance is not pedantry: nearly every subtle bug in a language
definition is a place where the two languages were quietly conflated.
The cleanest picture of a semantics is a function from syntax to mathematics. On the
left sit the well-formed expressions of the object language; on the right sit mathematical values. The
semantics is the arrow between them — traditionally written with the Scott brackets
This "two-column" mental model survives all three semantic styles below. They differ only in what the right-hand column contains and how the arrow is computed: another program state (operational), a mathematical function (denotational), or a logical assertion about before and after (axiomatic).
Almost every idea in this course can be rehearsed on a language so small it fits in a line — the arithmetic expressions, often called Exp or AExp. Its abstract syntax:
Read the
Here is the meaning, given compositionally — the value of a compound expression is built from the values of its parts, using the corresponding metalanguage operation:
Look hard at the middle equation. The
Let us compute
And here is the very same definition transcribed into TypeScript. The Exp datatype
is the abstract syntax; the meaning function is
+ and *
in the recursive cases are the metalanguage operations. Press Run:
Notice the recursion follows the grammar exactly: one case per production. That is no
accident — it is compositionality, and it is the deep reason a semantics can be both finite (a
handful of equations) and total (defined on infinitely many programs). We will return to this shape
again and again.
There is more than one useful mathematical object to call "the meaning". The three classical schools — which the rest of the course develops in depth — answer the question "what does a program mean?" differently, and each is best at a different job.
| Style | Meaning is… | Answers the question | Best for |
|---|---|---|---|
| Operational | a sequence of computation steps on an abstract machine (a transition relation) | "how does it run?" | implementing interpreters; type-safety proofs |
| Denotational | a mathematical value/function the program denotes, built compositionally | "what is it, abstractly?" | reasoning about equivalence; program logic foundations |
| Axiomatic | logical assertions relating the state before and after (pre/postconditions) | "what is true afterwards?" | verification; proving programs correct |
Crucially, these are not rival theories of the same language so much as complementary
tools: for a well-designed language they agree, and a large part of semantics
is proving adequacy and full abstraction theorems that say so — that the operational
and denotational meanings match. Our little
Constantly — the history of language design is a graveyard of underspecified corners. C's notorious
"sequence points" left expressions like i = i++ + 1 with undefined behaviour: the
standard genuinely did not say what they mean, so different compilers produced different results and the
committee simply declared such programs erroneous. ALGOL 60, by contrast, was defined with unusual care
yet still harboured a famous ambiguity in its call-by-name parameter rule (the "Jensen's device"
surprises). The lesson the field drew is stark: prose specifications, however careful, leak. When
Milner's team built Standard ML in the 1980s they did something almost unheard of —
wrote The Definition of Standard ML, a complete formal operational semantics as inference rules,
the whole language, no prose hand-waving. It remains the gold standard for what "define a language"
should mean, and modern efforts (WebAssembly ships with a formal semantics; CompCert is a
proven-correct C compiler) follow directly in its footsteps.
The commonest confusion at this stage is to blur syntax and meaning —
to think that the expression
A second, subtler slip: assuming every program has a meaning. Introduce division and