A eval(e_i, env); the rule's conclusion becomes what the clause returns. Run the interpreter
and you are, quite literally, constructing the derivation tree the semantics only described.
This is the deepest idea in language implementation, and the phrase itself comes from John C. Reynolds'
1972 paper Definitional Interpreters for Higher-Order Programming Languages — the paper that
launched a thousand compilers. Its punchline is unsettling and is the theme of this whole page: an
interpreter does not fully define a language on its own. It quietly borrows features
from the meta-language it is written in — evaluation order, how functions are represented, how control
flows — and unless you notice, those borrowed features leak into the language you thought you
were defining. Making them explicit, so the interpreter says exactly what it means and nothing more, is
the road that leads to
Take the big-step rule for a binary addition. On paper it is two premises above a line and a conclusion below:
Read it as a recipe and it translates itself. The two premises say "first find the value of the left
operand, then the value of the right operand, both in the same environment
The correspondence is exact and mechanical: premises become recursive calls, the conclusion
becomes the return, the environment let — is a handful of such rules, so a whole definitional interpreter is a
handful of such clauses. That is why it is called definitional: reading it is reading
the definition of the language.
When you evaluate an expression, the recursive calls the interpreter makes trace out exactly the
derivation tree that the big-step semantics would draw for the same judgement. Every
recursive call is a premise; every return supplies the value that the parent needs. Here is the tree
for
The shape of the recursion is the shape of the proof. This is the first sense in which the interpreter is faithful to the semantics — but watch the phrase "first the left, then the right". The semantics never said which premise to establish first: a derivation tree is a static object, with no notion of order. The interpreter, being a program, must pick one. That is the first leak, and it is the subject of the next card.
Below is a complete definitional interpreter for a small higher-order language — literals, variables,
addition, let, first-class functions (lam) and application (app).
Notice that each case is one big-step rule, and that a function value is represented as a
closure: the syntactic lam plus the environment in force where it was
written. That single design choice is what makes the semantics higher-order. Press Run:
Thirty lines, and it runs closures. But notice what the code silently decided that the rules
never stated: the two operands of add are evaluated left-to-right (because
const l = … runs before const r = …); the argument of app is
evaluated before the call (call-by-value, because we evaluate(e.arg, …) eagerly).
Swap those lines and you would define a different language — same rules on paper, different
behaviour in practice. This is Reynolds' warning in miniature.
A meta-circular interpreter is the extreme case: an interpreter for a language written
in that same language — Lisp's eval in Lisp, a Scheme metacircular evaluator in
Scheme. It is gloriously compact, because it reuses the host for everything. But that reuse is exactly
the problem. If the host is lazy, the defined language looks lazy; if the host is eager, it looks
eager. The interpreter has defined nothing about evaluation order — it inherited it. Reynolds
catalogued three such leaks, and the whole discipline of principled interpreters is about plugging them:
apply dispatcher — turning the interpreter into something a first-order machine, or a
compiler, can implement.
call/cc, tail calls) that no host stack can be relied on to provide.
Reynolds' programme is the arc of this whole module: start with the honest but leaky higher-order
interpreter (this page), make functions explicit as
Reynolds' Definitional Interpreters for Higher-Order Programming Languages was presented at the 1972 ACM National Conference and is one of the most reprinted papers in the field (a "retrospective" edition appeared in 1998, with Reynolds gently noting what the 26-year-old paper got right and what it missed). Its lasting gift is a pair of transformations you can apply to any interpreter, in sequence: convert to continuation-passing style to make control first-order, then defunctionalise to make the continuations first-order data — and what falls out the other end is an abstract machine (a stack machine, a SECD-like beast) that you never designed by hand. Danvy and Nielsen later showed this "interpreter → abstract machine" pipeline is completely mechanical, connecting Reynolds' 1972 insight to the CEK, SECD and Krivine machines. One short paper, the whole back end.
apply. Skipping that step is why a pretty
interpreter can be impossible to turn into a compiler.
let/lam must pass an extended one. A closure
must capture the environment at definition time, not look it up at call time — mixing those up is the
static-vs-dynamic-scope bug you meet on the next page.