We need to figure out our approach to text width, especially
for proofs. Quite a few proofs here don't render into the
chosen page width, and for terse mode it will be worse.
In Logical Foundations (LF) we went through the basics of how to use Lean to
prove theorems and write functional programs. Now, we begin to shift gears
to using it to reason about properties of programs and programming languages.
We begin by looking at a language we call Slang (for simple
language). Despite its simplicity, Slang lets us introduce key concepts for
specifying the syntax and semantics of programming languages and show how
those concepts are realized in Lean.
(This chapter is shared, word for word, between two volumes: Type Systems (TS) and
Hoare Logic (HL). If you have already worked through it in the other volume, you can
safely skip ahead to the next chapter of this one.)
In this chapter, we'll ignore the translation from the concrete
syntax that a programmer would actually write to these abstract syntax
trees -- the process that, for example, would translate the string
"1 + 2 * 3" to the AST .plus (.num 1) (.mult (.num 2) (.num 3)).
For comparison, here's a conventional BNF (Backus-Naur Form) grammar
defining the same abstract syntax:
a
::=
nat
|
a+a
|
a−a
|
a*a
b
::=
bool
|
a=a
|
a≠a
|
a≤a
|
a>a
|
¬b
|
b∧b
Compared to the Lean version above...
The BNF is more informal -- for example, it gives some suggestions
about the surface syntax of expressions (like the fact that the
addition operation is written with an infix +) while leaving other
aspects of lexical analysis and parsing (like the relative precedence
of +, -, and *, the use of parens to group subexpressions, etc.)
unspecified. Some additional information -- and human intelligence --
would be required to turn this description into a formal definition,
e.g., for implementing a compiler.
The Lean version consistently omits all this information and
concentrates on the abstract syntax only.
Conversely, the BNF version is lighter and easier to read. Its
informality makes it flexible, a big advantage in situations like
discussions at the blackboard, where conveying general ideas is more
important than nailing down every detail precisely.
Indeed, there are dozens of BNF-like notations and people switch
freely among them -- usually without bothering to say which kind of
BNF they're using, because there is no need to: a rough-and-ready
informal understanding is all that's important.
It's good to be comfortable with both sorts of notations: informal ones
for communicating between humans and formal ones for carrying out
implementations and proofs.
It's worth noting that ≤ and > are Prop-valued, i.e. a₁.eval st ≤ a₂.eval st is a proposition,
but Bexp.eval returns a Bool, so Lean implicitly inserts a decide coercion.
You can observe the call to decide by hovering over Bexp.eval_le and Bexp.eval_gt.
We can now start to get some mileage out of these definitions. Suppose we define a
function that takes an arithmetic expression and slightly simplifies it, changing
every occurrence of 0 + e (i.e., .plus (.num 0) e) into just e.
But if we want to be certain the optimization is correct -- that
evaluating an optimized expression always gives the same result as
the original -- we should prove it!
Here is a first, deliberately explicit, proof, by induction on a. The
interesting case is Aexp.plus: because Aexp.optimize0plus treats plus (num 0) e
specially, we case-split on the left operand a₁ -- and, when it is a numeral,
on whether that numeral is 0 -- to line the proof up with the function's own
branches. Once the constructors are exposed, each case is discharged by
essentially the same incantation: unfold Aexp.optimize0plus, rewrite Aexp.eval by its
characterizing lemmas, then finish with the induction hypotheses. Notice how
repetitive that makes the proof.
We can do much better. The case analysis we performed by hand -- peeling
plus apart to reach the plus (num 0) e branch -- is exactly the case
analysis that Aexp.optimize0plus itself performs.
The fun_induction tactic
inducts along a function's own recursion structure: fun_induction
Aexp.optimize0plus a hands us one goal per branch of optimize0plus -- the
special plus (num 0) e branch included -- so the nested cases disappear.
Before applying fun_induction to a function as complex as Aexp.optimize0plus,
let's see how it works on somthing simpler. Recall the definition of Nat.even and Nat.odd:
Normally, if we perform induction on n, we get two cases - 0 and n' + 1 -
one for each of the cases in the inductive definition of natural numbers.
Functional induction on Nat.even, however, gives us three cases - 0, 1, and n' + 2 -
corresponding to each of the cases of its definition.
Now let's try using fun_induction on Aexp.optimize0plus. When we do this,
every goal has the same shape, so we can attack them uniformly
with the <;> combinator and a single tactic, simp_all, which rewrites
Aexp.eval by the @[simp] characterizing lemmas and uses the induction hypotheses
-- which it picks up from the local context automatically -- to close
each goal. The whole proof collapses to two lines.
Since the Aexp.optimize0plus transformation doesn't change the value of an
Aexp, we should be able to apply it to all the Aexps that appear in a
Bexp without changing the Bexp's value. Write a function that
performs this transformation on Bexps and prove it sound. Use the
combinators we've just seen to make the proof as short and elegant as
possible.
The optimization implemented by our Aexp.optimize0plus is only one of
many possible optimizations on arithmetic and boolean expressions. Write a more
sophisticated optimizer and prove it correct. (You will probably find it easiest
to start small -- add just a single, simple optimization and its correctness proof --
and build up incrementally to something more interesting.)
We have presented Aexp.eval and Bexp.eval as functions defined by
recursion. Another way to think about evaluation -- one that is often
more flexible -- is as a relation between expressions and their
values. This perspective leads to inductive definitions like the
following.
The version above makes the rules somewhat easier to read, but gives less control over naming
the hypotheses during proofs involving the relation. For this reason we adopt the named style.
It will be convenient to have an infix notation for Aexp.EvalR. We'll
write e ⇓ n to mean that arithmetic expression e evaluates to
value n. The ⇓ symbol is typed \Downarrow.
The notation is declared right after the inductive.
The scoped keyword allows us to scope the notation to the Aexp namespace so it doesn't
collide with other notations we use for different evaluation relations later.
In informal discussions, it is convenient to write the rules for
Aexp.EvalR and similar relations in the more readable graphical form of
inference rules, where the premises above the line justify the
conclusion below the line. For example, the constructor plus
can be written like this as an inference rule:
Formally, there is nothing deep about inference rules: they are just
an informal notation for implications.
You can read the rule name on the right as the name of the
constructor and read each of the linebreaks between the premises above the
line (as well as the line itself) as →. All the variables mentioned in
the rule (a₁, n₁, etc.) are implicitly bound by universal quantifiers
at the beginning. (Such variables are often called metavariables to
distinguish them from the variables of whatever language we are defining. At
the moment, our arithmetic expressions don't include variables, but we'll
soon be adding them.) The whole collection of rules is understood as being
wrapped, implicitly, in an inductive declaration.
In informal prose, this is sometimes
indicated by saying something like "Let Aexp.EvalR be the smallest relation
closed under the following rules...".
To summarize: a group of inference rules corresponds to a single inductive
definition; each rule's name corresponds to a constructor name; above the
line are the premises, below the line the conclusion; metavariables
like a₁ and n₁ are implicitly universally quantified. The whole
collection of rules defines ⇓ as the smallest relation closed under
them:
--------- (num)
num n ⇓ n
a₁ ⇓ n₁
a₂ ⇓ n₂
------------------ (plus)
plus a₁ a₂ ⇓ n₁ + n₂
a₁ ⇓ n₁
a₂ ⇓ n₂
------------------- (minus)
minus a₁ a₂ ⇓ n₁ - n₂
a₁ ⇓ n₁
a₂ ⇓ n₂
------------------ (mult)
mult a₁ a₂ ⇓ n₁*n₂
Quiz
Which rules are needed to prove the following?
.mult (.plus (.num 3) (.num 1)) (.num 0) ⇓ 0
(A) num and plus
(B) num only
(C) num and mult
(D) mult and plus
(E) num, mult, and plus
Show solution
(E) num, mult, and plus
Note to developers (Michael Hicks @mwhicks1, before next release)
Not sure if we need ⇓b, or whether we can define
⇓ overloaded. Don't understand Lean notation yet!
Note to developers (Chris Henson @chenson₂018, before next release)
About Bexp.eval below: We should discuss a way to recall definitions without
having to write them out manually like this. I think a simple #print may work as an
alternative, assuming there are no namespace issues..
For the definitions of evaluation for arithmetic and boolean
expressions, the choice of whether to use functional or relational
definitions is mainly a matter of taste. However, there are
situations where relational definitions work much better than
functional ones.
namespaceSlang.AevalRDivision
For example, suppose that we wanted to extend the arithmetic operations
with division:
inductiveAexpwhere|num(n:Nat)|plus(a₁a₂:Aexp)|minus(a₁a₂:Aexp)|mult(a₁a₂:Aexp)|div(a₁a₂:Aexp)-- NEW
Extending the definition of Aexp.eval to handle this new operation would
not be straightforward due to division being a partial operation; i.e.,
what should we return as the result of .div (.num 5) (.num 0)?
One option would be to lift the definition of Aexp.eval to return an option:
This definition is a lot wordier than the earlier version. There are tools
to reduce this overhead, namely monads, but we will not discuss these in
Software Foundations in Lean. Curious readers can learn more about them
from Functional Programming in Lean.
By contrast, partiality is no problem for the relational
version of the definition.
As another example, suppose that we want to extend the arithmetic operations by a
nondeterministic number generator any that, when evaluated, may
yield any number. (This is not the same as making a probabilistic
choice among all numbers -- we only say which results are possible.)
Again, extending Aexp.eval would be tricky, since evaluation is now not
a deterministic function from expressions to numbers; but extending the
relation is no problem.
At this point you may be wondering: which of these styles should I use
by default?
Where the thing being defined is not easy to express as a function,
definitions are often simpler. When both
styles are workable, relational definitions can be more elegant and
easier to understand, and Lean generates useful inversion and induction
principles from them. On the other hand, functional definitions are
automatically deterministic and total -- whereas, for a relation,
we must prove these if we need them --
and we can use Lean's computation mechanism to simplify them during proofs.
In large developments it is common to give a definition in both
styles plus a lemma that the two coincide, allowing later proofs to
switch between points of view at will -- exactly what we did above
in Slang.Aexp.evalR_iff_eval and Slang.Bexp.evalR_iff_eval.
Source revision: 9e5dba0, committed 2026-09-25 03:12 UTC