The hiding lean (above in the source file) should not be needed any more and should be removed from all files everywhere it exists.
Note to developers (Michael Hicks @mwhicks1)
This chapter adapts Smallstep to follow Slang, the initial part
of Imp, on just Aexp and Bexp (without variables). This means that parts
of this chapter had to adjust: Concurrent Imp is dropped in favor of Nondeterministic
Aexp, and the stack machine is simplified to just Aexps without variables.
Note to developers (before next release)
In this and later chapters, we are not very consistent about
presenting computation rules first and congruence rules after...
Note to developers
HIDE: Sometime in the early 2010s, we did some mining past exams for
exercises...
Loris: No interesting exercise in Finals of 2007-2009-2010-2011.
Nothing in second midterms except for 2011.
2011 midterm proposes the following exercise: give the small step
relation of FLIP X (alternatively HAVOC, ANYTHING). We could then ask
to extend the proof of equivalence of big step vs small step (personally
don't like it too much).
Maybe we can ask how they would adapt the definition of Hoare triple to
small step (maybe in the exam).
HIDE: BCP: I also have a bunch of slides from earlier offerings of CIS500
that might be good additions to the TERSE notes.
HIDE: Possible major restructuring: This chapter might better be postponed
to later in the course. A big-step presentation of STLC (and maybe even
some of the extensions like subtyping?) could come first. However, this
would invite a much bigger change, where all the variants of STLC (with
refs, with subtyping, ...) are done in big-step style. This requires more
thought...
HIDE: Wonder whether it would be interesting to show them how to make a
correspondence with a "real abstract machine" at a lower level...? There's
a start at an exercise along these lines below.
Here is the same evaluator, written in exactly the same style, but formulated as an
inductively defined relation. We use the notation t ⇓ n for "t evaluates to n."
The notation command below is how that is declared: it introduces ⇓ as
infix syntax for the Eval relation defined with it, with a precedence saying
how tightly it binds.
This is the lightweight way to name a relation; later chapters, where a whole
object language needs a grammar rather than a single operator, reach for
declare_syntax_cat instead.
------- (const)
c n ⇓ n
t₁ ⇓ n₁
t₂ ⇓ n₂
----------------- (plus)
p t₁ t₂ ⇓ n₁ + n₂
Notice: each step reduces the leftmostp node that is ready to go — the first rule tells how
to rewrite it, the second and third tell where to find it — and constants do not step to anything.
Let's pause and check a couple of examples of reasoning with the step relation.
If t₁ steps to t₁', then p t₁ t₂ steps to p t₁' t₂.
The step relation ⟶ is an example of a relation on Tm.
Note to developers (Michael Hicks @mwhicks1, before next release)
Should we be getting this (and Deterministic, Multi, etc.
if appropriate) from the Lean standard library? If not, should we match the
concepts in CSLib, if they exists there?
defRelation(X:Type):=X→X→Prop
One simple property a relation may have is being deterministic: like
Slang's big-step evaluation, each element is related to at most one other.
Theorem: For each t, there is at most one t' such that t steps to
t'. We prove it by induction on the derivation of the first step.
Proof sketch: We show that if x steps to both y₁ and y₂, then y₁
and y₂ are equal, by induction on a derivation of x ⟶ y₁. There are
several cases, depending on the last rule used in this derivation and the
last rule in the given derivation of x ⟶ y₂.
If both are plus, the result is immediate.
The cases when both derivations end with plusLeft or plusRight follow by
the induction hypothesis.
It cannot happen that one is plus and the other is plusLeft/plusRight,
since this would imply that x has the form p t₁ t₂ where both t₁
and t₂ are constants (by plus) and one of t₁ or t₂ has the
form p _.
Similarly, it cannot happen that one is plusLeft and the other is
plusRight, since this would imply that x has the form p t₁ t₂ where
t₁ has both the form p t₁₁ t₁₂ and the form c n.
As a sanity check on this change, let's re-verify determinism. Here's an
informal proof:
Proof sketch: We must show that if x steps to both y₁ and y₂, then
y₁ and y₂ are equal. Consider the final rules used in the derivations
of x ⟶ y₁ and x ⟶ y₂.
If both are plus, the result is immediate.
The cases when both derivations end with plusLeft or plusRight follow by
the induction hypothesis.
It cannot happen that one is plus and the other is plusLeft/plusRight,
since this would imply that x has the form p t₁ t₂ where both t₁
and t₂ are constants (by plus) and one of t₁ or t₂ has the
form p _.
Similarly, it cannot happen that one is plusLeft and the other is
plusRight, since this would imply that x has the form p t₁ t₂ where
t₁ both has the form p t₁₁ t₁₂ and is a value (hence has the form
c n).
Most of this proof is the same as the one above. But to get maximum
benefit from the exercise you should try to write your formal version from
scratch and just use the earlier one if you get stuck. The impossible
cross-cases now also use the fact that a IsValue (a c n) cannot step.
We can use this terminology to generalize the observation we made in the
strong progress theorem: in this language (though not necessarily, in
general), normal forms and values are actually the same thing.
Tactic absurd is first introduced here. Do we want to explain it?
Note to developers (Daniel Sainati @dsainati1)
I think some of these proofs were originally Claude-generated, so we
probably want to redo them from scratch, in which case introducing
absurd is likely not necessary.
Why is this interesting? Because IsValue is a syntactic concept — it is
defined by looking at the way a term is written — while IsNormalForm is a
semantic one — it is defined by looking at how the term steps.
It is not obvious that these concepts should characterize the same set of terms!
Indeed, we could easily have written the definitions (incorrectly) so that
they would not coincide.
Suppose, for example, we define IsValue so that it includes some terms that
are not finished reducing. (Even if you don't work the exercise
value_not_same_as_normal_form1 below and the following ones, make sure you
can think of an example of such a term.)
Or we might (again, wrongly) define Step so that it permits something
designated as a value to reduce further. We again lose the property that
values are the same as normal forms.
namespaceTemp2inductiveIsValue:Tm→Propwhere|const(n:Nat):IsValue(.cn)-- Original definitioninductiveStep:Tm→Tm→Propwhere|funny(n:Nat):Step(.cn)(.p(.cn)(.c0))-- <--- NEW|plus(n₁n₂:Nat):Step(.p(.cn₁)(.cn₂))(.c(n₁+n₂))|plusLeft(t₁t₁'t₂:Tm)(h:Stept₁t₁'):Step(.pt₁t₂)(.pt₁'t₂)|plusRight(v₁t₂t₂':Tm)(hv:IsValuev₁)(h:Stept₂t₂'):Step(.pv₁t₂)(.pv₁t₂')
Quiz
With this definition, to how many different terms does the following term
step (in exactly one step)?
Finally, we might define IsValue and Step so that there is some term that
is not a value but that also cannot take a step. Such terms are said to
be stuck. In this case, this is caused by a mistake in the semantics, but
we will also see situations where, even in a correct language definition, it
makes sense to allow some terms to be stuck. (Note that plusRight is missing
below.)
Having defined the operational semantics of our tiny programming language in
two different ways (big-step and small-step), it makes sense to ask whether
these definitions actually define the same thing!
They do, though it takes
a little work to show it. The details are left as an exercise. We consider
the two implications separately. First, big-step evaluation implies
multi-step reduction to a value.
The key ideas in the proof can be seen in the following picture:
p t₁ t₂ ⟶ (by plusLeft)
p t₁' t₂ ⟶ (by plusLeft)
p t₁'' t₂ ⟶ (by plusLeft)
...
p (c n₁) t₂ ⟶ (by plusRight)
p (c n₁) t₂' ⟶ (by plusRight)
p (c n₁) t₂'' ⟶ (by plusRight)
...
p (c n₁) (c n₂) ⟶ (by plus)
c (n₁ + n₂)
That is, the multi-step reduction of a term of the form p t₁ t₂ proceeds in
three phases:
First, we use plusLeft some number of times to reduce t₁ to a normal
form, which must (by nf_same_as_value) be a term of the form c n₁ for
some n₁.
Next, we use plusRight some number of times to reduce t₂ to a normal
form, which must again be a term of the form c n₂ for some n₂.
Finally, we use plus one time to reduce p (c n₁) (c n₂) to
c (n₁ + n₂).
To formalize this intuition, you'll need the congruence lemmas from above,
plus some basic properties of ⟶* (that it is reflexive, transitive, and
includes ⟶).
Write a detailed informal version of the proof of multistep_of_eval. (A
paper exercise — there is no Lean proof to fill in here.)
For the converse, we need one lemma, which establishes a relation between
single-step reduction and big-step evaluation. A single step preserves the
big-step value.
The fact that small-step reduction implies big-step evaluation is now
straightforward to prove, once we have factored out the observation that
every normal form is a value. The proof proceeds by induction on the
multi-step reduction sequence that is buried in the hypothesis
IsNormalFormOf t t'. (Make sure you understand the statement before you
start to work on the proof.)
Remember that we also defined big-step evaluation of terms as a function
evalF. Prove that it is equivalent to the relational semantics. (Hint: we
just proved that Eval and multistep are equivalent, so logically it
doesn't matter which you choose. One will be easier than the other, though!)
Small-step semantics for the richer Slang arithmetic and boolean
expressions. Notations: ⟶a (arithmetic) and ⟶b (boolean).
We work in the Slang namespace, reusing the arithmetic and boolean expression
syntax (Aexp, Bexp) and the big-step evaluator (Aexp.eval) from the
Slang chapter:
Every arithmetic expression is either a value or can take a step — the same
strong progress property we proved for the toy language, now for the richer
Slang arithmetic expressions.
Which of these properties does this small-step semantics for Slang
expressions satisfy? (Yes or No for each.)
determinism
strong progress (every non-value takes a step)
values and normal forms coincide (i.e., there are no "stuck" terms)
the step relation is normalizing (i.e., evaluation always terminates)
Show solution
Yes to all four. Expression evaluation always terminates, so ⟶a (and ⟶b) are normalizing.
Exercise★★★(astep_deterministic)
The arithmetic step relation is deterministic. (Structurally this is the
value-based determinism proof from the toy language, repeated for +, −,
and ×; the impossible cross-cases close because a value num n cannot step.)
The boolean step relation is deterministic too. The comparison cases (eq,
neq, le, gt) reduce their operands with ⟶a, so they inherit determinism
from astep_deterministic; ¬ and the short-circuiting ∧ contribute only
base cases.
Prove that one nondeterministic step leaves the big-step value unchanged.
Hint: induction on the step derivation; each case is immediate from eval
and, where present, the induction hypothesis.
Now put the pieces together: prove that the deterministic and nondeterministic
semantics always compute the same final result. That is, if a fully
reduces to .num n₁ under ⟶a and to .num n₂ under ⟶n, then n₁ = n₂.
Hint: both .num n₁ and .num n₂ are reachable by ⟶n (use
multi_astep_imp_anstep for the first), and ⟶n preserves eval.
Our last example is a small-step semantics for a stack machine that evaluates
arithmetic expressions. The machine's instructions push a constant or combine the
top two stack entries. The machine's behavior should match the big-step Aexp.eval
function defined earlier.
A program is a list of instructions, and the stack is a list of numbers.
Prove the compiler correct: running the compiled program from the empty stack
reduces, in some number of steps, to a stack holding exactly the value of the
expression.
Hint: this will not go through by a direct induction — the induction
hypothesis is too weak. Prove a more general statement first, about running
compile a followed by any leftover program p, starting from any stack
stk. (Reassociating the ++s with List.append_assoc, and chaining steps
with multi_trans/multi_single, are the moves you need.)
This one script would suffice to prove most concrete reduction sequences
for this simple language. To make it work for others, we would need to supply
constructors for those other languages to solve_by_elim. The languages we
will study in this book can grow to a large number of constructors for their Step
relations, so we'd like a way to supply all of them to solve_by_elim more easily.
Luckily, Lean supports this. We can register a constructor (or lemma) for use with
solve_by_elim with an attribute command:
Attributes
The command below tags all of these constructors with the SimpleArith attribute,
which we can then use to automatically pull all of these constructors in when we use
solve_by_elim. However, due to a limitation of Lean, this attribute needs to be
pre-declared in a different file; we can't create it here and then immediately use it.
For this book, we've predeclared all the attributes we'll use in a file called
AttributeDecls.lean, following the typical pattern from libraries like Mathlib.
We can package all this up into a dedicated tactic for solving reduction sequences,
which we'll call normalize:
syntax"normalize"" using "ident,+:tacticmacro_rules|`(tactic|normalizeusing$xs,*)=>`(tactic|first|applyMulti.refl|(applyMulti.step·solve_by_elim(maxDepth:=15)(constructor:=false)onlyusing$xs,*·normalizeusing$xs,*))