In this chapter, we take a more serious look at how to use Lean as a
tool to study other things. Our case study is a simple imperative
programming language called Imp, embodying a tiny core fragment of
conventional mainstream languages such as C and Java.
Here is a familiar mathematical function written in Imp.
Z := X;
Y := 1;
while (Z ≠ 0) {
Y := Y * Z;
Z := Z - 1
}
We concentrate here on defining the syntax and semantics of Imp;
later in this volume we develop a theory of program equivalence and introduce
Hoare Logic, a popular logic for reasoning about imperative programs.
We build Imp in three layers. The first — a core language of arithmetic and
boolean expressions — is developed in its own chapter, Slang;
read that one first. There you meet the abstract syntax of arithmetic
expressions (Aexp) and boolean expressions (Bexp), their evaluation both as
a recursive function and as an inductive relation (proved equivalent), and a
small optimize0plus program transformation together with its correctness
proof. Those expressions are variable-free.
This chapter picks up from there. First we extend the expressions with
variables; then we add a language of commands — assignment, conditionals,
sequencing, and loops.
Let's return to defining Imp. The next thing we need to do is to
enrich our arithmetic and boolean expressions with variables. To keep
things simple, we'll assume that all variables are global and that they
only hold numbers.
Since we'll want to look variables up to find out their current values,
we'll use total maps from the Typeclasses chapter of Logical Foundations. A machine state (or
just state) represents the current values of all variables at some
point in the execution of a program.
For simplicity, we assume that the state is defined for all variables,
even though any given program is only able to mention a finite number of
them. Because each variable stores a natural number, we represent the
state as a total map from strings (variable names) to Nat, and will use
0 as the default value in the store.
We give the type of variable identifiers a name, Ident. For now it is just
String; naming it makes the intent clearer.
We can add variables to the arithmetic expressions we had before simply
by including one more constructor. (This is a fresh Aexp, replacing
the variable-free one from the Slang chapter.)
To make Imp programs easier to read and write, we introduce some notations.
You do not need to understand exactly what these declarations do. Briefly, though,
here is how the two blocks below fit together:
The declare_syntax_cat directive adds a new non-terminal to Lean's grammar, called
imp_aexp. We'll add additional non-terminals further below.
Each syntax directive defines a grammar production. Seven of them build the
imp_aexp category itself: the first two make a numeric literal and an
identifier into an imp_aexp, the next three build larger expressions (with
annotations that fix precedence and associativity), and the last two are
parentheses for grouping and ~, the escape back to Lean. The eighth,
aexp { … }, is a production of Lean's own term category — it is what lets
an Imp expression appear in ordinary Lean code.
~e splices an already-elaborated Lean term e into Imp syntax. We will rarely need
to use this in this book, however. By convention, our notation always treats
identifiers starting with capital Latin letters as being literal names in Imp.
Thus X, Y, and Z are Imp variables. Meanwhile, names beginning with
lowercase Latin letters like (a or c) are treated as Lean variables. This will
be useful later when we need to write theorems about Imp programs. We only need to use
the ~ when we want to insert a larger Lean expression into an Imp term. We'll
point out examples of this when they occur.
Finally, macro_rules is used to translate each production of the imp_aexp non-terminal
into a Lean expression.
Boolean expressions and, later, commands follow this same pattern exactly, so
their declarations are collapsed where they appear: open one if you want to see
the pattern repeated, and skip it otherwise.
Notation encoding: arithmetic expressions/-- Arithmetic expressions of Imp -/declare_syntax_catimp_aexp/-- Numeric literal -/syntax:maxnum:imp_aexp/-- `Ident` or Lean identifier -/syntax:maxident:imp_aexp/-- Addition -/syntax:65imp_aexp:65" + "imp_aexp:66:imp_aexp/-- Subtraction -/syntax:65imp_aexp:65" - "imp_aexp:66:imp_aexp/-- Multiplication -/syntax:70imp_aexp:70" * "imp_aexp:71:imp_aexp/-- Parentheses for grouping -/syntax:max"("imp_aexp")":imp_aexp/-- Escape to Lean -/syntax:max"~"term:max:imp_aexp/-- Embed an Imp arithmetic expression into a Lean term -/syntax:80"aexp ""{"imp_aexp"}":termnamespaceImp.ElabopenLeanElabTermMetadefwithSourceInfoOf{kind:Name}(ref:Syntax)(stx:TSyntaxkind)(canonical:=true):TSyntaxkind:=letinfo:=SourceInfo.fromRefref(canonical:=canonical)⟨stx.raw.setInfoinfo⟩defisGreek(c:Char):Bool:=letn:=c.val.toNatdecide((0x0370≤n∧n≤0x03ff)∨(0x1f00≤n∧n≤0x1fff))inductiveIdentKindwhere|object|metavarderivingBEqdefclassifyIdent?(id:Lean.Ident):Option(IdentKind×String):=doletName.str.anonymouss:=id.getId.eraseMacroScopes|failureifs.isEmpty||s.contains'.'thenfailureletc:=s.frontifc.isUpperthenreturn(.object,s)elseifc.isLower||isGreekcthenreturn(.metavar,s)elsefailuredefmkObjectIdentFrom(ref:Syntax)(name:String):Lean.Ident:=mkIdentFromref(Name.mkSimplename)defelabMetavarOnlyIdent(what:String)(expectedType:Term)(x:Lean.Ident):MacroMTerm:=domatchclassifyIdent?xwith|some(.metavar,_)=>`(($x:$expectedType))|some(.object,name)=>Macro.throwErrorAtxs!"no Imp {what} named `{name}`; capitalized bare names are always \
read as Imp identifiers — use a lowercase name or escape with `~` to refer to Lean name"|none=>Macro.throwErrorAtx"invalid bare identifier"macro_rules|`(aexp{$exp:imp_aexp})=>doletstx←matchexpwith|`(imp_aexp|$n:num)=>``(Aexp.num$n)|`(imp_aexp|~$e:term)=>``(($e:Aexp))|`(imp_aexp|$x:ident)=>matchclassifyIdent?xwith|some(.object,name)=>letnameLit:Term:=⟨Syntax.mkStrLitname⟩``(Aexp.id$nameLit)|some(.metavar,_)=>``(($x:Aexp))|none=>Macro.throwErrorAtx"invalid bare identifier"|`(imp_aexp|$a+$b)=>``(Aexp.plus(aexp{$a})(aexp{$b}))|`(imp_aexp|$a-$b)=>``(Aexp.minus(aexp{$a})(aexp{$b}))|`(imp_aexp|$a*$b)=>``(Aexp.mult(aexp{$a})(aexp{$b}))|`(imp_aexp|($a))=>``(aexp{$a})|_=>Lean.Macro.throwUnsupportedreturnwithSourceInfoOfexpstxendImp.ElabNotation encoding: boolean expressions/-- Boolean expressions of Imp -/declare_syntax_catimp_bexp/-- Boolean literal (`true` or `false`) and Lean identifier -/syntax:maxident:imp_bexp/-- Equality of arithmetic expressions -/syntax:50imp_aexp:51" = "imp_aexp:51:imp_bexp/-- Disequality of arithmetic expressions -/syntax:50imp_aexp:51" ≠ "imp_aexp:51:imp_bexp/-- Less than or equal -/syntax:50imp_aexp:51" ≤ "imp_aexp:51:imp_bexp/-- Greater than -/syntax:50imp_aexp:51" > "imp_aexp:51:imp_bexp/-- Boolean negation -/syntax:70"¬ "imp_bexp:70:imp_bexp/-- Boolean conjunction (right associative) -/syntax:35imp_bexp:36" ∧ "imp_bexp:35:imp_bexp/-- Parentheses for grouping -/syntax:max"("imp_bexp")":imp_bexp/-- Escape to Lean -/syntax:max"~"term:max:imp_bexp/-- Embed an Imp boolean expression into a Lean term -/syntax:80"bexp ""{"imp_bexp"}":termNotation encoding: boolean expressions, macro rulesnamespaceImp.ElabopenLeanmacro_rules|`(bexp{$exp:imp_bexp})=>doletstx←matchexpwith|`(imp_bexp|true)=>``(Bexp.booltrue)|`(imp_bexp|false)=>``(Bexp.boolfalse)|`(imp_bexp|$x:ident)=>doelabMetavarOnlyIdent"boolean"(←``(Bexp))x|`(imp_bexp|~$e:term)=>``(($e:Bexp))|`(imp_bexp|$a:imp_aexp=$b:imp_aexp)=>``(Bexp.eq(aexp{$a})(aexp{$b}))|`(imp_bexp|$a:imp_aexp≠$b:imp_aexp)=>``(Bexp.neq(aexp{$a})(aexp{$b}))|`(imp_bexp|$a:imp_aexp≤$b:imp_aexp)=>``(Bexp.le(aexp{$a})(aexp{$b}))|`(imp_bexp|$a:imp_aexp>$b:imp_aexp)=>``(Bexp.gt(aexp{$a})(aexp{$b}))|`(imp_bexp|¬$b:imp_bexp)=>``(Bexp.not(bexp{$b}))|`(imp_bexp|$b₁:imp_bexp∧$b₂:imp_bexp)=>``(Bexp.and(bexp{$b₁})(bexp{$b₂}))|`(imp_bexp|($b:imp_bexp))=>``(bexp{$b})|_=>Macro.throwUnsupportedreturnwithSourceInfoOfexpstxendImp.Elab(Aexp.num3).plus((Aexp.id"X").mult(Aexp.num2)) : Aexp#checkaexp{3+(X*2)}(Bexp.booltrue).and(Bexp.le(Aexp.id"X")(Aexp.num4)).not : Bexp#checkbexp{true∧¬(X≤4)}
Next, we write a suite of delaborators for Aexp and Bexp.
Delaborators are like the opposite of macro_rules -- they are used to pretty print elaborated terms back to the user.
Notation encoding: printing expressions backnamespaceImp.DelabopenLeanPrettyPrinterDelaboratorSubExprParenthesizerImp.Elab@[category_parenthesizerimp_aexp]defimp_aexp.parenthesizer:CategoryParenthesizer:=funprec=>domaybeParenthesize`imp_aexptruewrapParensprec<|parenthesizeCategoryCore`imp_aexpprecwherewrapParens(stx:Syntax):Syntax:=Unhygienic.rundoletstxInfo:=SourceInfo.fromRefstxletstx:=stx.setInfo.noneletpstx←`(imp_aexp|($(⟨stx⟩)))returnpstx.raw.setInfostxInfo@[category_parenthesizerimp_bexp]defimp_bexp.parenthesizer:CategoryParenthesizer:=funprec=>doParenthesizer.maybeParenthesize`imp_bexptruewrapParensprec<|Parenthesizer.parenthesizeCategoryCore`imp_bexpprecwherewrapParens(stx:Syntax):Syntax:=Unhygienic.rundoletstxInfo:=SourceInfo.fromRefstxletstx:=stx.setInfo.noneletpstx←`(imp_bexp|($(⟨stx⟩)))returnpstx.raw.setInfostxInfoNotation encoding: registering the delaborators/--
Recognizes a term as being an `aexp { ... }` expression.
-/defgetAexp(stx:Term):TSyntax`imp_aexp:=withSourceInfoOf(canonical:=false)stx<|Unhygienic.rundomatchstxwith|`(aexp{$e:imp_aexp})=>returne|`($id:ident)=>matchclassifyIdent?idwith|some(.metavar,_)=>`(imp_aexp|$id:ident)|_=>`(imp_aexp|~$stx)|_=>`(imp_aexp|~$stx)@[app_unexpanderAexp.num]privatedefAexp.unexpandNum:Unexpander|`($_$n:num)=>`(aexp{$n:num})|_=>throw()@[app_unexpanderAexp.id]privatedefAexp.unexpandId:Unexpander|`($_$s:str)=>doletid:=mkObjectIdentFroms.raws.getStringmatchclassifyIdent?idwith|some(.object,_)=>`(aexp{$id:ident})|_=>throw()|_=>throw()@[app_unexpanderAexp.plus]privatedefAexp.unexpandPlus:Unexpander|`($_$a$b)=>`(aexp{$(getAexpa)+$(getAexpb)})|_=>throw()@[app_unexpanderAexp.minus]privatedefAexp.unexpandMinus:Unexpander|`($_$a$b)=>`(aexp{$(getAexpa)-$(getAexpb)})|_=>throw()@[app_unexpanderAexp.mult]privatedefAexp.unexpandMult:Unexpander|`($_$a$b)=>`(aexp{$(getAexpa)*$(getAexpb)})|_=>throw()/--
Recognizes a term as being an `bexp { ... }` expression.
-/defgetBexp(stx:Term):TSyntax`imp_bexp:=withSourceInfoOf(canonical:=false)stx<|Unhygienic.rundomatchstxwith|`(bexp{$e:imp_bexp})=>returne|`($id:ident)=>matchclassifyIdent?idwith|some(.metavar,_)=>`(imp_bexp|$id:ident)|_=>`(imp_bexp|~$stx)|_=>`(imp_bexp|~$stx)/--
Delaborator for `Bexp.bool`. This is needed since we want to be sure we are
matching on the actual `true`/`false` expressions, rather than matching on the
delaborated identifiers `true`/`false` (which might not be accurate).
-/@[app_delabBexp.bool]privatedefBExp.delabBool:Delab:=whenPPOptiongetPPNotationdolete←getExprguard<|e.isAppOfArity``Bexp.bool1match_expre.appArg!with|true=>`(bexp{$(mkIdent`true):ident})|false=>`(bexp{$(mkIdent`false):ident})|_=>failure@[app_unexpanderBexp.eq]privatedefBexp.unexpandEq:Unexpander|`($_$a$b)=>`(bexp{$(getAexpa):imp_aexp=$(getAexpb):imp_aexp})|_=>throw()@[app_unexpanderBexp.neq]privatedefBexp.unexpandNeq:Unexpander|`($_$a$b)=>`(bexp{$(getAexpa):imp_aexp≠$(getAexpb):imp_aexp})|_=>throw()@[app_unexpanderBexp.le]privatedefBexp.unexpandLe:Unexpander|`($_$a$b)=>`(bexp{$(getAexpa):imp_aexp≤$(getAexpb):imp_aexp})|_=>throw()@[app_unexpanderBexp.gt]privatedefBexp.unexpandGt:Unexpander|`($_$a$b)=>`(bexp{$(getAexpa):imp_aexp>$(getAexpb):imp_aexp})|_=>throw()@[app_unexpanderBexp.not]privatedefBexp.unexpandNot:Unexpander|`($_$a)=>`(bexp{¬$(getBexpa):imp_bexp})|_=>throw()@[app_unexpanderBexp.and]privatedefBexp.unexpandAnd:Unexpander|`($_$a$b)=>`(bexp{$(getBexpa):imp_bexp∧$(getBexpb):imp_bexp})|_=>throw()endImp.Delab
With these delaborators in place, Lean pretty-prints Imp expressions with the higher-level
notations rather than their raw constructors.
The pretty-printed version of an expression might not exactly
match its original form.
For example, the parentheses around X * 2 in aexp { 3 + (X * 2) } are not printed because
they are redundant, which the parenthesizer knows.
The arithmetic and boolean evaluators must now be extended to handle
variables, taking a state st as an extra argument. A variable is
looked up in the state with the map-indexing notation st[x] from the
Typeclasses chapter in the Logical Foundations book.
For the notation to work, we used open scoped MyGetElem earlier,
which opens only the scoped items like notation from the module.
Now we are ready to define the syntax and behavior of Imp commands
(or statements). Informally, commands c are described by the
following BNF grammar:
c
::=
skip
|
x:=a
|
c;c
|
if(b){c}else{c}
|
while(b){c}
Here is the formal definition of the abstract syntax of commands.
inductiveComwhere|skip|asgn(x:Ident)(a:Aexp)|seq(c₁c₂:Com)|cond(b:Bexp)(c₁c₂:Com)|whileDo(b:Bexp)(c:Com)Notation encoding: commands, macro rules/-- Imp commands -/declare_syntax_catimp_com/-- The command that does nothing (`skip`) -/syntax:maxident:imp_com/-- Sequencing: one command after another (right associative. min + 1 = 11) -/syntax:80imp_com:11Lean.Parser.semicolonOrLinebreakppHardSpaceimp_com:min:imp_com/-- Assignment -/syntax:maxidentppHardSpace":="ppHardSpaceimp_aexp:imp_com/-- Conditional -/syntax:max"if ""("imp_bexp")"ppHardSpace"{"imp_com"}"ppHardSpace"else"ppHardSpace"{"imp_com"}":imp_com/-- Loop -/syntax:max"while ""("imp_bexp")"ppHardSpace"{"imp_com"}":imp_com/-- Escape to Lean -/syntax:max"~"term:max:imp_com/-- Include an Imp command in Lean code -/syntax:80"imp"ppHardSpace"{"imp_com"}":termnamespaceComopenLeanImp.Elabscopedmacro_rules|`(imp{$s})=>doletstx←matchswith|`(imp_com|skip)=>``(Com.skip)|`(imp_com|$x:ident)=>doelabMetavarOnlyIdent"command"(←``(Com))x|`(imp_com|$c₁;$c₂)=>``(Com.seq(imp{$c₁})(imp{$c₂}))|`(imp_com|$x:ident:=$a)=>matchclassifyIdent?xwith|some(.object,name)=>letnameLit:Term:=⟨Syntax.mkStrLitname⟩``(Com.asgn$nameLit(aexp{$a}))|some(.metavar,_)=>``(Com.asgn$x(aexp{$a}))|none=>Macro.throwErrorAtx"invalid bare identifier"|`(imp_com|if($b){$c₁}else{$c₂})=>``(Com.cond(bexp{$b})(imp{$c₁})(imp{$c₂}))|`(imp_com|while($b){$c})=>``(Com.whileDo(bexp{$b})(imp{$c}))|`(imp_com|~$c)=>`(($c:Com))|_=>Macro.throwUnsupportedreturnwithSourceInfoOfsstxendComopenscopedComNotation encoding: printing commands backnamespaceImp.DelabopenLeanPrettyPrinterDelaboratorSubExprImp.Elab/--
Recognizes a term as being an `imp { ... }` expression.
-/defgetImp(stx:Term):TSyntax`imp_com:=withSourceInfoOf(canonical:=false)stx<|Unhygienic.rundomatchstxwith|`(imp{$e:imp_com})=>returne|`($id:ident)=>matchclassifyIdent?idwith|some(.metavar,_)=>`(imp_com|$id:ident)|_=>`(imp_com|~$stx)|_=>`(imp_com|~$stx)@[app_unexpanderCom.skip]defunexpandComSkip:Unexpander|_=>`(imp{$(mkIdent`skip):ident})@[app_unexpanderCom.asgn]defunexpandComAsgn:Unexpander|`($_$x:ident$a)=>`(imp{$x:ident:=$(getAexpa)})|`($_$s:str$a)=>doletid:=mkObjectIdentFroms.raws.getStringmatchclassifyIdent?idwith|some(.object,_)=>`(imp{$id:ident:=$(getAexpa)})|_=>throw()|_=>throw()@[app_unexpanderCom.seq]defunexpandComSeq:Unexpander|`($_$a$b)=>matchawith|`(imp{$_;$_})=>-- seq syntax is right associative, so need to quote `a``(imp{~$a;$(getImpb):imp_com})|_=>`(imp{$(getImpa):imp_com;$(getImpb):imp_com})|_=>throw()@[app_unexpanderCom.cond]defunexpandComCond:Unexpander|`($_$b$c₁$c₂)=>`(imp{if($(getBexpb)){$(getImpc₁)}else{$(getImpc₂)}})|_=>throw()@[app_unexpanderCom.whileDo]defunexpandComWhileDo:Unexpander|`($_$b$c)=>`(imp{while($(getBexpb)){$(getImpc)}})|_=>throw()endImp.Delab
As an example, here is the factorial function again, written as a formal
definition. When this command terminates, the variable Y will
contain the factorial of the initial value of X. (Compare this to
the concrete Imp program at the very start of the chapter.)
Because we registered a delaborator, we can inspect a defined program with
#print, which pretty-prints (i.e., delaborates) the stored definition using the same syntax:
Even though the notations are useful for getting the high-level picture,
it's sometimes helpful to turn off the notation to see the parsed structure as a plain term.
This can be done with set_option pp.notation false
(which we briefly mentioned in the Typeclasses chapter) as follows:
Next we need to define what it means to evaluate an Imp command. The
fact that while loops don't necessarily terminate makes defining an
evaluation function tricky.
In a more conventional functional language like OCaml or Haskell, we could define
the evaluation function as follows:
deffail to show termination forCom.evalwith errorsfailed to infer structural recursion:Cannot use parameter st:the type TotalMapIdentNat does not have a `.brecOn` recursorCannot use parameter c:failed to eliminate recursive applicationevalst(imp{c;while(b){c}})failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goalst:Stateb:Bexpc:Comh✝:Bexp.evalstb=true⊢ 1+sizeOfc+(1+sizeOfb+sizeOfc)<1+sizeOfb+sizeOfcCom.eval(st:State)(c:Com):State:=matchcwith|imp{skip}=>st|imp{x:=a}=>(x→ₜa.evalst;st)|imp{c₁;c₂}=>letst':=evalstc₁evalst'c₂|imp{if(b){c₁}else{c₂}}=>ifb.evalstthenevalstc₁elseevalstc₂|imp{while(b){c}}=>ifb.evalstthenevalst(imp{c;while(b){c}})-- ^-- recursive call without a decreasing argumentelsest
fail to show termination forCom.evalwith errorsfailed to infer structural recursion:Cannot use parameter st:the type TotalMapIdentNat does not have a `.brecOn` recursorCannot use parameter c:failed to eliminate recursive applicationevalst(imp{c;while(b){c}})failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goalst:Stateb:Bexpc:Comh✝:Bexp.evalstb=true⊢ 1+sizeOfc+(1+sizeOfb+sizeOfc)<1+sizeOfb+sizeOfc
Lean doesn't accept such a definition
because the function we want to define is not guaranteed to terminate.
Indeed, it doesn't always terminate: the full Com.eval applied to
the loop program above would run forever. Since Lean aims to be not
just a programming language but also a consistent logic, any
potentially non-terminating function must be rejected.
Here is what would go wrong if Lean allowed non-terminating recursive functions:
theoremfail to show termination forloop_falsewith errorsfailed to infer structural recursion:Not considering parameter n of loop_false:it is unchanged in the recursive callsno parameters suitable for structural recursionwell-founded recursion cannot be used, `loop_false` does not take any (non-fixed) argumentsloop_false(n:Nat):False:=loop_falsen
That is, propositions like False would become provable (loop_false 0
would be a proof of False), a disaster for logical consistency.
Thus, because it doesn't terminate on all inputs, the full Com.eval
cannot be written in Lean -- at least not without additional tricks and
workarounds.
Here's a better way: define Com.eval as a relation rather than a
function -- i.e., make its result a Prop rather than a State,
similar to what we did for Aexp.EvalR in the Slang chapter.
This is an important change. Besides freeing us from awkward workarounds,
it gives us more flexibility in the definition. For example, if we
add nondeterministic features like any to the language, we want the
definition of evaluation to be nondeterministic -- i.e., not only will it
not be total, it will not even be a function!
We'll use the notation st =[ c ]=> st' for the Com.EvalR relation:
st =[ c ]=> st' means that executing program c in a starting state
st results in an ending state st'. This can be pronounced "c takes
state st to st'".
Here is an informal definition of evaluation, presented as inference rules
for readability:
----------------- (skip)
st =[ skip ]=> st
a.eval st = n
-------------------------------- (asgn)
st =[ x := a ]=> (x →ₜ n ; st)
st =[ c₁ ]=> st'
st' =[ c₂ ]=> st''
--------------------- (seq)
st =[ c₁;c₂ ]=> st''
b.eval st = true
st =[ c₁ ]=> st'
--------------------------------------- (ifTrue)
st =[ if (b) { c₁ } else { c₂ } ]=> st'
b.eval st = false
st =[ c₂ ]=> st'
--------------------------------------- (ifFalse)
st =[ if (b) { c₁ } else { c₂ } ]=> st'
b.eval st = false
---------------------------- (whileFalse)
st =[ while (b) { c } ]=> st
b.eval st = true
st =[ c ]=> st'
st' =[ while (b) { c } ]=> st''
------------------------------- (whileTrue)
st =[ while (b) { c } ]=> st''
Here is the formal definition. Make sure you understand how it
corresponds to the inference rules.
The cost of defining evaluation as a relation instead of a function is
that we now need to construct a proof that some program evaluates to
some result state, rather than letting Lean's computation mechanism do
it for us.
Changing from a computational to a relational definition of evaluation
is a good move because it frees us from the artificial requirement that
evaluation be a total function. But it raises a question: is the
relational definition really a partial function? Could the same
command, from the same state, evaluate to two different final states?
In fact, this cannot happen: Com.EvalRis a partial function.
Write an Imp program that sums the numbers from 1 to X (inclusive)
in the variable Y. Your program should update the state as shown in
pup_to_2_ceval, which you can reverse-engineer to discover the program
you should write. The proof of that theorem will be somewhat lengthy.
We'll get into more systematic and powerful techniques for reasoning
about Imp programs in the next chapter, but we can
already do a few things (albeit in a somewhat low-level way) just by
working with the bare definitions. This section explores some examples.
theoremplus2_spec{st:State}{n:Nat}{st':State}(hx:st[X]=n)(heval:st=[plus2]=>st'):st'[X]=n+2:=byst:Staten:Natst':Statehx:st[X]=nheval:st=[plus2]=>st'⊢ st'[X]=n+2-- Inverting `heval` forces one step of the evaluation relation: since-- `plus2` is an assignment, `st'` must be `st` extended at `X`.rw[plus2st:Staten:Natst':Statehx:st[X]=nheval:st=[X:=X+2]=>st'⊢ st'[X]=n+2]athevalst:Staten:Natst':Statehx:st[X]=nheval:st=[X:=X+2]=>st'⊢ st'[X]=n+2inversionhevalwith|asgnmh=>simp[hx]ath⊢asgnst:Staten:Nathx:st[X]=nm:Nath:n+2=m⊢ m=n+2liaAll goals completed! 🐙
-- FILL IN HERE-- FILL IN HEREunexpected end of input
Exercise★★★(loop_never_stops)
Hint: proceed by induction on the assumed derivation showing that loop
terminates. Most of the cases are immediately contradictory and so can be
solved in one step (by simp/contradiction on the impossible command
equation).
The following function yields true just on programs with no while
loops. Using inductive, write a property Com.NoWhilesR that holds
exactly when c is while-free, then prove it equivalent to Com.no_whiles.
defCom.no_whiles(c:Com):Bool:=matchcwith|imp{skip}=>true|imp{Variable name `x` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_xNote: This linter can be disabled with `set_option linter.unusedVariables false`x:=Variable name `a` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_aNote: This linter can be disabled with `set_option linter.unusedVariables false`a}=>true|imp{c₁;c₂}=>no_whilesc₁&&no_whilesc₂|imp{if(Variable name `b` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_bNote: This linter can be disabled with `set_option linter.unusedVariables false`b){ct}else{cf}}=>no_whilesct&&no_whilescf|imp{while(Variable name `b` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_bNote: This linter can be disabled with `set_option linter.unusedVariables false`b){Variable name `c` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_cNote: This linter can be disabled with `set_option linter.unusedVariables false`c}}=>falseinductiveCom.NoWhilesR:Com→Propwhere-- FILL IN HEREtheoremdeclaration uses `sorry`no_whiles_eqv(c:Com):c.no_whiles=true↔Com.NoWhilesRc:=byc:Com⊢ c.no_whiles=true↔c.NoWhilesRsorryAll goals completed! 🐙
Exercise★★★★(no_whiles_terminating)
Imp programs that don't involve while loops always terminate. State and
prove a theorem no_whiles_terminating that says this. Use either
Com.no_whiles or Com.NoWhilesR, as you prefer.
We would like to show that they agree -- if we start factCom in
a state where variable X contains some number n, then it will
terminate in a state where variable Y contains the factorial of
n.
To show this, we rely on the critical idea of a loop
invariant.
One might wonder whether all this work with poking at states and
unfolding definitions could be ameliorated with some more powerful
lemmas and/or more uniform reasoning principles... Indeed, this is
exactly the point of the Hoare chapters!
Exercise★★★★(subtract_slowly_spec) (Optional)
Prove a specification for subtract_slowly, using the above
specification of factCom and the invariant below as
guides.
defSsInvariant(nz:Nat)(st:State):Prop:=st[Z]-st[X]=z-n-- FILL IN HERE
Old HP calculators, programming languages like Forth and Postscript,
and abstract machines like the Java Virtual Machine all evaluate
arithmetic expressions using a stack. For instance, the expression
(2*3)+(3*(4-2))
would be written as
2 3 * 3 4 2 - * +
and evaluated like this (where we show the program being evaluated
on the right and the contents of the stack on the left):
Write a function to evaluate programs in the stack language. It
should take as input a state, a stack represented as a list of
numbers (top stack item is the head of the list), and a program
represented as a list of instructions, and it should return the
stack after executing the program. Test your function on the
examples below.
Note that it is unspecified what to do when encountering an
sPlus, sMinus, or sMult instruction if the stack contains
fewer than two elements. In a sense, it is immaterial what we do,
since a correct compiler will never emit such a malformed program.
But for the sake of later exercises, it would be best to skip the
offending instruction and continue with the next one.
defdeclaration uses `sorry`sExecute(st:State)(stack:ListNat)(prog:ListSinstr):ListNat:=sorry-- FILL IN HEREtheoremdeclaration uses `sorry`sExecute1:sExecute∅[][sPush5,sPush3,sPush1,sMinus]=[2,5]:=by⊢ sExecute∅[][sPush5,sPush3,sPush1,sMinus]=[2,5]sorryAll goals completed! 🐙theoremdeclaration uses `sorry`sExecute2:sExecute{X↦3}[3,4][sPush4,sLoadX,sMult,sPlus]=[15,4]:=by⊢ sExecute{X↦3}[3,4][sPush4,sLoadX,sMult,sPlus]=[15,4]sorryAll goals completed! 🐙
Next, write a function that compiles an Aexp into a stack
machine program. The effect of running the program should be the
same as pushing the value of the expression on the stack.
defdeclaration uses `sorry`sCompile(a:Aexp):ListSinstr:=sorry-- FILL IN HERE
After you've defined sCompile, prove the following to test that it works.
Execution can be decomposed in the following sense: executing
stack program p₁ ++ p₂ is the same as executing p₁, taking
the resulting stack, and executing p₂ from that stack. Prove
that fact.
Now we'll prove the correctness of the compiler implemented in the
previous exercise. Begin by proving the following lemma. If it
becomes difficult, consider whether your implementation of
sExecute or sCompile could be simplified.
Most modern programming languages use a "short-circuit" evaluation
rule for boolean and: to evaluate Bexp.and b₁ b₂, first evaluate
b₁. If it evaluates to false, then the entire and
expression evaluates to false immediately, without evaluating
b₂. Otherwise, b₂ is evaluated to determine the result of the
and expression.
Write an alternate version of Bexp.eval that performs short-circuit
evaluation of Bexp.and in this manner, and prove that it is
equivalent to Bexp.eval. (N.b. This is only true because expression
evaluation in Imp is rather simple. In a bigger language where
evaluating an expression might diverge, the short-circuiting and
would not be equivalent to the original, since it would make more
programs terminate.)
defdeclaration uses `sorry`Bexp.evalSC(st:State)(b:Bexp):Bool:=sorry-- FILL IN HEREtheoremdeclaration uses `sorry`Bexp.eval_eq_evalSC(st:State)(b:Bexp):b.evalst=b.evalSCst:=byst:Stateb:Bexp⊢ evalstb=evalSCstbsorryAll goals completed! 🐙
Exercise★★★(break_imp) (Optional)
Imperative languages like C and Java often include a break or
similar statement for interrupting the execution of loops. In this
exercise we consider how to add break to Imp. First, we need to
enrich the language of commands with an additional case. Because break
is a reserved keyword in Lean, we will abbreviate it as brk.
namespaceImp.BreakinductiveComwhere|skip|brk-- <--- NEW|asgn(x:Ident)(a:Aexp)|seq(c₁c₂:Com)|cond(b:Bexp)(c₁c₂:Com)|whileDo(b:Bexp)(c:Com)Notation encoding: commands, macro rulesnamespaceComopenLeanscopedmacro_rules|`(imp{$s})=>doletstx←matchswith|`(imp_com|skip)=>``(Com.skip)|`(imp_com|brk)=>``(Com.brk)|`(imp_com|$x:ident)=>doImp.Elab.elabMetavarOnlyIdent"command"(←``(Com))x|`(imp_com|$c₁;$c₂)=>``(Com.seq(imp{$c₁})(imp{$c₂}))|`(imp_com|$x:ident:=$a)=>matchImp.Elab.classifyIdent?xwith|some(.object,name)=>letnameLit:Term:=⟨Syntax.mkStrLitname⟩``(Com.asgn$nameLit(aexp{$a}))|some(.metavar,_)=>``(Com.asgn$x(aexp{$a}))|none=>Macro.throwErrorAtx"invalid bare identifier"|`(imp_com|if($b){$c₁}else{$c₂})=>``(Com.cond(bexp{$b})(imp{$c₁})(imp{$c₂}))|`(imp_com|while($b){$c})=>``(Com.whileDo(bexp{$b})(imp{$c}))|`(imp_com|~$c)=>`(($c:Com))|_=>Macro.throwUnsupportedreturnImp.Elab.withSourceInfoOfsstxendComopenscopedComnamespaceDelabopenLeanPrettyPrinterImp.Delab@[app_unexpanderCom.brk]privatedefunexpandComBrk:Unexpander|_=>`(imp{$(mkIdent`brk):ident})attribute[app_unexpanderCom.skip]unexpandComSkipattribute[app_unexpanderCom.asgn]unexpandComAsgnattribute[app_unexpanderCom.seq]unexpandComSeqattribute[app_unexpanderCom.cond]unexpandComCondattribute[app_unexpanderCom.whileDo]unexpandComWhileDoendDelab/-- info: imp {brk} : Com -/#guard_msgsin#checkimp{brk}
Next, we need to define the behavior of brk. Informally,
whenever brk is executed in a sequence of commands, it stops
the execution of that sequence and signals that the innermost
enclosing loop should terminate. (If there aren't any
enclosing loops, then the whole program simply terminates.) The
final state should be the same as the one in which the brk
statement was executed.
One important point is what to do when there are multiple loops
enclosing a given brk. In those cases, brk should only
terminate the innermost loop. Thus, after executing the
following...
X := 0;
Y := 1;
while (0 ≠ Y) {
while (true) {
brk
};
X := 1;
Y := Y - 1
}
... the value of X should be 1, and not 0.
One way of expressing this behavior is to add another parameter to
the evaluation relation that specifies whether evaluation of a
command executes a brk statement:
We will use the syntax st =[ c ]=> st' // s to mean that, if c is started in
state st, then it terminates in state st' and either signals
that the innermost surrounding loop (or the whole program) should
exit immediately (s = sBreak) or that execution should continue
normally (s = sContinue).
The definition of the st =[ c ]=> st' // s relation is very
similar to the one we gave above for the regular evaluation
relation (st =[ c ]=> st') -- we just need to handle the
termination signals appropriately:
If the command is skip, then the state doesn't change and
execution of any enclosing loop can continue normally.
If the command is brk, the state stays unchanged but we
signal a sBreak.
If the command is an assignment, then we update the binding for
that variable in the state accordingly and signal that execution
can continue normally.
If the command is of the form if (b) {c₁} else {c₂}, then
the state is updated as in the original semantics of Imp, except
that we also propagate the signal from the execution of
whichever branch was taken.
If the command is a sequence c₁ ; c₂, we first execute
c₁. If this yields a sBreak, we skip the execution of c₂
and propagate the sBreak signal to the surrounding context;
the resulting state is the same as the one obtained by
executing c₁ alone. Otherwise, we execute c₂ on the state
obtained after executing c₁, and propagate the signal
generated there.
Finally, for a loop of the form while (b) {c}, the
semantics is almost the same as before. The only difference is
that, when b evaluates to true, we execute c and check the
signal that it raises. If that signal is sContinue, then the
execution proceeds as in the original semantics. Otherwise, we
stop the execution of the loop, and the resulting state is the
same as the one resulting from the execution of the current
iteration. In either case, since brk only terminates the
innermost loop, while signals sContinue.
Based on the above description, complete the definition of the
Com.EvalR relation:
inductiveCom.EvalR:Com→State→State→Result→Propwhere|skip{st:State}:EvalR(imp{skip})ststsContinue-- FILL IN HEREscopednotation:40st0:41" =[ "c" ]=> "st1:41" // "s:41=>Com.EvalRcst0st1s
Now prove the following properties of your definition:
Add C-style for loops to the language of commands, update the
Com.EvalR definition to define the semantics of for loops, and add
cases for for loops as needed so that all the proofs in this
file are accepted by Lean.
A for loop should be parameterized by (a) a statement executed
initially, (b) a test that is run on each iteration of the loop to
determine whether the loop should continue, (c) a statement
executed at the end of each loop iteration, and (d) a statement
that makes up the body of the loop. (You don't need to worry
about making up a concrete Notation for for loops, but feel free
to play with this too if you like.)
Source revision: e85fe77, committed 2026-10-06 21:16 UTC