Foundations of Programming Languages · 1/2

Reading programming language papers: the notation, from sets to inference rules

A primer on the maths behind programming language theory: sets, grammars, metavariables, inductive definitions, inference rules, axioms, derivations and evaluation.

On this page

This series follows the course Foundations of Programming Languages in my MSc in Software Engineering at FCUL, taught by Prof. Vasco T. Vasconcelos. It’s built on three books: Benjamin C. Pierce’s Types and Programming Languages (TAPL, MIT Press, 2002), Advanced Topics in Types and Programming Languages (MIT Press, 2005), edited by Pierce, and Session Types (Cambridge University Press, 2025) by Simon J. Gay and Vasco T. Vasconcelos himself. The goals:

  • understand the fundamental abstractions of programming languages, as opposed to how a particular language happens to implement them;
  • reason formally about the properties of a language;
  • get comfortable with advanced ideas: linearity, traces, ownership and borrowing, and session types;
  • see them at work in real languages like Rust and Go.

If that sounds far from day-to-day work, it isn’t. Every time TypeScript refuses to compile, a linter flags code that can never run, or Rust’s borrow checker stops you, you’re watching these ideas at work. This series is about how those tools can know what they know.

Before any of that, the notation. Programming language theory is written in a compact mathematical style that looks intimidating until you know about a dozen conventions. This post collects them in one place, with a tiny example language, so the rest of the series can just use them.

Sets, briefly

Almost everything is a set, and a few symbols carry most of the weight:

NotationRead asExample
x∈Ax \in Axx is an element of AAtrue∈{true,false}\ttrue \in \{\ttrue, \tfalse\}
x∉Ax \notin Axx is not an element of AA0∉{true,false}0 \notin \{\ttrue, \tfalse\}
A⊆BA \subseteq Bevery element of AA is in BB{true}⊆{true,false}\{\ttrue\} \subseteq \{\ttrue, \tfalse\}
A∪BA \cup Belements in AA or BB (or both){true}∪{false}={true,false}\{\ttrue\} \cup \{\tfalse\} = \{\ttrue, \tfalse\}
{ f(x)∣x∈A }\{\, f(x) \mid x \in A \,\}“the set of f(x)f(x) for every xx in AA”{ succ t∣t∈{0} }={succ 0}\{\, \tsucc\ t \mid t \in \{0\} \,\} = \{\tsucc\ 0\}
∅\emptysetthe empty set
⋃iSi\bigcup_i S_ieverything that’s in any SiS_iS0∪S1∪S2∪⋯S_0 \cup S_1 \cup S_2 \cup \cdots

The fancy T\T (a calligraphic T) is the set of terms: every well-formed expression of the language.

Terms, and a tiny language

A term is a piece of syntax: a program, or part of one. To keep things small, take a language with just booleans and a conditional:

tt::=terms|true\mathsf{true}constant true|false\mathsf{false}constant false|if t then t else t\mathsf{if}\ t\ \mathsf{then}\ t\ \mathsf{else}\ tconditional

This is a grammar in BNF style. Read ::= as “can be” and | as “or”: a term is true, or false, or if followed by three terms. So if true then false else true\ite{\ttrue}{\tfalse}{\ttrue} is a term, and so is if (if false then true else false) then true else false\ite{(\ite{\tfalse}{\ttrue}{\tfalse})}{\ttrue}{\tfalse}.

Metavariables

The tt in the grammar is not part of the language. It’s a metavariable: a placeholder that stands for any term, used to talk about the language. When a rule says if true then t2 else t3\ite{\ttrue}{t_2}{t_3}, the t2t_2 and t3t_3 can be replaced by any terms at all. Subscripts and primes (t1t_1, t2t_2, t′t') just give different placeholders different names.

This two-level view is the first thing to get used to: there’s the object language (the language we study, with its true, if…) and the metalanguage (the maths we use to describe it, with its ∈\in, t1t_1, and so on).

Three ways to define a set of terms

A grammar is a convenient shorthand. Underneath, there are three precise definitions of the same set, and each one is handy for a different kind of argument.

1. Inductively: the smallest closed set

Definition 1 (Terms, inductively).

The set of terms T\T is the smallest set such that

  1. {true,false}⊆T\{\ttrue, \tfalse\} \subseteq \T;
  2. if t1∈Tt_1 \in \T, t2∈Tt_2 \in \T and t3∈Tt_3 \in \T, then if t1 then t2 else t3∈T\ite{t_1}{t_2}{t_3} \in \T.

The clauses say what must be in the set. “Smallest” says that nothing else is. Without it, a set that also contained, say, the number 42 would satisfy the clauses too. With it, every term got into T\T because a clause put it there, and that’s what lets us prove things aren’t terms.

2. With inference rules

The same definition, written as inference rules, the notation used for nearly everything in the course:

Terms t∈Tt \in \T
(T-True)
axiom, by T-True:true∈T\ttrue \in \T
(T-False)
axiom, by T-False:false∈T\tfalse \in \T
t1∈Tt_1 \in \T
t2∈Tt_2 \in \T
t3∈Tt_3 \in \T
(T-If)
therefore, by T-If:if t1 then t2 else t3∈T\ite{t_1}{t_2}{t_3} \in \T

An inference rule reads: if everything above the line holds, then what’s below the line holds.

  • The statements above the line are the premises.
  • The statement below it is the conclusion.
  • The label on the side is the rule’s name, so a proof can say which rule it used.

A statement like t∈Tt \in \T, the kind of thing rules prove, is called a judgement.

Axioms and rules

An axiom is a rule with no premises: its conclusion holds unconditionally, so there’s nothing above its bar. T-True and T-False are axioms: true\ttrue is a term, full stop. T-If is not: it only concludes that a conditional is a term once you’ve shown its three parts are.

The distinction matters because every proof has to bottom out somewhere, and it always bottoms out in axioms.

Rules are schemas

A rule with metavariables is really a schema: a template for infinitely many concrete rules, one for each way to fill in t1t_1, t2t_2 and t3t_3. T-If with t1=truet_1 = \ttrue, t2=falset_2 = \tfalse, t3=truet_3 = \ttrue is one instance; with other terms, another. When we use a rule, we always use an instance of it.

Derivations

A derivation is a tree of rule instances: each node is the conclusion of a rule whose premises are the nodes above it, and every leaf is an axiom. A term is in T\T exactly when there’s a finite derivation that ends with it.

(T-False)
axiom, by T-False:false∈T\tfalse \in \T
(T-True)
axiom, by T-True:true∈T\ttrue \in \T
(T-False)
axiom, by T-False:false∈T\tfalse \in \T
(T-If)
therefore, by T-If:if false then true else false∈T\ite{\tfalse}{\ttrue}{\tfalse} \in \T
(T-True)
axiom, by T-True:true∈T\ttrue \in \T
(T-False)
axiom, by T-False:false∈T\tfalse \in \T
(T-If)
therefore, by T-If:if (if false then true else false) then true else false∈T\ite{(\ite{\tfalse}{\ttrue}{\tfalse})}{\ttrue}{\tfalse} \in \T

Read it from the bottom up to see why the term is a term, or from the top down to see how it’s built. Either way, the leaves are all axioms.

3. Concretely: building the set in layers

The third definition mentions neither “smallest” nor rules. It builds the set up in stages, starting from nothing:

S0=∅Si+1={true,false}∪{ if t1 then t2 else t3∣t1,t2,t3∈Si }S=⋃iSi\begin{aligned} S_0 &= \emptyset \\ S_{i+1} &= \{\ttrue, \tfalse\} \cup \{\, \ite{t_1}{t_2}{t_3} \mid t_1, t_2, t_3 \in S_i \,\} \\ S &= \textstyle\bigcup_i S_i \end{aligned}
  • S1={true,false}S_1 = \{\ttrue, \tfalse\}: only the constants, since S0S_0 is empty.
  • S2S_2 adds every conditional built from constants, like if true then false else true\ite{\ttrue}{\tfalse}{\ttrue}: eight of them.
  • S3S_3 adds conditionals whose parts come from S2S_2, and so on.

Each stage contains the one before, every term appears at some finite stage, and SS collects them all. It’s a standard result that this gives exactly the same set: T=S\T = S (TAPL, Proposition 3.2.6, proves it for a slightly bigger language).

This view is the most down-to-earth of the three. It also tells you when a term appears: at the stage equal to its depth. true\ttrue has depth 1 and appears in S1S_1; a conditional of constants has depth 2 and appears in S2S_2.

Which one to use when

DefinitionGood for
Inductive (smallest set)Showing something is not a term: nothing put it there. Proofs by induction.
Inference rulesShowing something is a term: build a derivation. The notation for everything that follows.
Concrete (layers)Intuition, and arguments about how terms are built stage by stage.

Evaluation: rules about a relation

Syntax says which terms exist. Semantics says what they mean. The style used in this course is operational semantics: define how a term runs, one small step at a time.

The judgement is now t⟶t′t \eval t', read ”tt evaluates to t′t' in one step”. It’s a relation between terms: a set of pairs, defined, again, by inference rules.

Evaluation t⟶t′t \eval t'
(E-IfTrue)
axiom, by E-IfTrue:if true then t2 else t3⟶t2\ite{\ttrue}{t_2}{t_3} \eval t_2
(E-IfFalse)
axiom, by E-IfFalse:if false then t2 else t3⟶t3\ite{\tfalse}{t_2}{t_3} \eval t_3
t1⟶t1′t_1 \eval t_1'
(E-If)
therefore, by E-If:if t1 then t2 else t3⟶if t1′ then t2 else t3\ite{t_1}{t_2}{t_3} \eval \ite{t_1'}{t_2}{t_3}

The same vocabulary applies. E-IfTrue and E-IfFalse are axioms: they fire whenever the condition is a literal boolean. E-If is a rule with one premise: to step a conditional whose condition isn’t a value yet, step the condition first.

The rules come in two flavours worth naming:

  • Computation rules do the actual work: E-IfTrue and E-IfFalse replace a conditional by one of its branches.
  • Congruence rules say where to work next: E-If steps inside the condition.

Values, normal forms and stuck terms

  • A value is a finished result: here, true\ttrue and false\tfalse.
  • A normal form is a term that no rule can step.
  • A term is stuck if it’s a normal form but not a value. This tiny language has none, but add numbers and if 0 then true else false\ite{0}{\ttrue}{\tfalse} gets stuck: no rule knows what to do with a condition that’s a number. Stuck terms are the language’s runtime errors, and ruling them out before running anything is what type systems are for.

t⟶∗t′t \evals t' (with a star) is the multi-step relation: zero or more steps. Evaluating a program means following ⟶\eval until you reach a normal form.

Proving things about all terms

The last idea to have in hand is structural induction. Because every term is built from smaller terms by the rules, to prove a property PP holds for every term it’s enough to show:

  • PP holds for the axioms’ terms (here, true\ttrue and false\tfalse), and
  • whenever PP holds for t1t_1, t2t_2 and t3t_3, it holds for if t1 then t2 else t3\ite{t_1}{t_2}{t_3}.

It’s ordinary induction on numbers, generalised to trees, and it works for exactly the reason “smallest” works: there’s no other way for a term to exist. Most proofs in the course have this shape, and the next post has a first one.

Glossary

TermMeaning
Object languageThe language being studied.
MetalanguageThe maths used to describe it.
MetavariableA placeholder (tt, t1t_1, t′t') standing for any term.
GrammarA compact description of the terms, in BNF style (::=, |).
JudgementA statement rules can prove, like t∈Tt \in \T or t⟶t′t \eval t'.
Inference rulePremises above a line, conclusion below: “if these, then that”.
AxiomA rule with no premises.
SchemaA rule with metavariables, standing for all its instances.
DerivationA tree of rule instances whose leaves are axioms.
ValueA finished result of evaluation.
Normal formA term no evaluation rule can step.
Stuck termA normal form that isn’t a value: a runtime error.

Next in the series: the first exercise sheet, on TAPL’s arithmetic expressions, where all of this gets used.