LATER: For consistency with the STLC definitions in future chapters, it
would be better and simpler to represent numbers by a simple nat, rather
than as strings of succ applied to 0 (which also introduces
subtleties that are really not the main point here). But the present
formulation is not a big problem either.
LATER: Harper's lecture from the Milner symposium would make a good
support for this lecture.
LATER: There are a bunch of slides from earlier offerings of CIS500 that
might be wonderful additions to the TERSE notes.
https://www.seas.upenn.edu/~cis500/cis500-f06/lectures/1002.pdf
https://www.seas.upenn.edu/~cis500/cis500-f06/lectures/1004.pdf
Our next major topic is type systems — static program analyses that
classify expressions according to the "shapes" of their results. We'll
begin with a typed version of the simplest imaginable language, to
introduce the basic ideas of types and typing rules and the fundamental
theorems about type systems: type preservation and progress. In the
next chapter we'll move on to the simply typed lambda-calculus, which
lives at the core of every modern functional programming language
(including Lean!).
To motivate the discussion of type systems, let's begin as usual with a
tiny toy language. We want it to have the potential for programs to go
wrong because of runtime type errors, so we endow it with two kinds of
data — numbers and booleans — where not every operation is defined on
both types of data. For example, program terms like 5 + true and
if 42 then 0 else 1 use undefined operator/data-type combinations.
Writing terms as raw constructors (.ite .fls .zero (.succ .zero)) gets
unreadable quickly. We introduce a concrete syntax so that a term can be
written inside <{ … }> — for example <{ if false then 0 else succ 0 }> —
mirroring the informal grammar above. A bare identifier is spliced as a Lean
term (so a variable t is written just t); ~e escapes an arbitrary Lean
expression, and ( … ) groups.
You do not need to understand exactly how the declarations below work; every
object language in this book is given its syntax the same way, so it is worth
seeing the pattern once:
declare_syntax_cat adds a new non-terminal to Lean's grammar — here tm,
the terms of this chapter's language.
Each syntax directive declares one production of that non-terminal, with
annotations fixing precedence, and the last one declares the <{ … }>
brackets that let a tm appear where Lean expects a term.
macro_rules then translates the resulting syntax forms into the
corresponding constructors of Tm.
declare_syntax_cattm-- The keyword atoms (`true`/`false`/`succ`/`pred`/`iszero`) are parsed as bare-- identifiers and dispatched in the macro below, rather than declared as-- reserved symbols. Reserving them would break ordinary Lean uses of-- `true`/`false` and clash with the constructor/case names `succ`, `pred`.syntax:maxnum:tmsyntax:maxident:tmsyntax:75identppHardSpacetm:76:tmsyntax:max"("tm")":tmsyntax:max"~"term:max:tmsyntax:50"if "tm:51" then "tm:51" else "tm:51:tmsyntax:max"<{ "tm" }>":termopenLeaninmacro_rules|`(<{$n:num}>)=>ifn.getNat==0then`(Tm.zero)elseMacro.throwErrorAtn"the only numeric literal in this language is 0"|`(<{$x:ident}>)=>matchx.getId.toStringwith|"true"=>`(Tm.tru)|"false"=>`(Tm.fls)|_=>`($x)-- a bare identifier is a spliced Lean term (usually a variable)|`(<{$f:ident$e:tm}>)=>matchf.getId.toStringwith|"succ"=>`(Tm.succ<{$e}>)|"pred"=>`(Tm.pred<{$e}>)|"iszero"=>`(Tm.isZero<{$e}>)|_=>Macro.throwErrorAtfs!"unknown operator `{f.getId}`"|`(<{($e)}>)=>`(<{$e}>)|`(<{~$e}>)=>puree|`(<{if$cthen$telse$e}>)=>`(Tm.ite<{$c}><{$t}><{$e}>)
A delaborator closes the loop: it walks a Tm value and rebuilds the
concrete syntax, so that terms appearing in goals, #check, and #eval
output print as <{ … }> rather than as a pile of constructors. (Setting
pp.notation false turns it off, revealing the raw constructors.) A Ty
prints as Bool or Nat.
Notation encoding: printing terms backopenLeanPrettyPrinterDelaboratorSubExprParenthesizerin/-- Re-inserts parentheses in `tm` output according to the grammar's precedences. -/@[category_parenthesizertm]deftm.parenthesizer:CategoryParenthesizer|prec=>domaybeParenthesize`tmtruewrapParensprec<|parenthesizeCategoryCore`tmprecwherewrapParens(stx:Syntax):Syntax:=Unhygienic.rundoletpstx←`(tm|($(⟨stx⟩)))returnpstx.raw.setInfo(SourceInfo.fromRefstx)openLeanPrettyPrinterDelaboratorSubExprin/-- Rebuild `tm` concrete syntax from a `Tm` term. -/partialdefdelabTmInner:DelabM(TSyntax`tm):=doletstx←match_expr←getExprwith|Tm.tru=>`(tm|$(mkIdent`true):ident)|Tm.fls=>`(tm|$(mkIdent`false):ident)|Tm.zero=>`(tm|0)|Tm.succ_=>do`(tm|$(mkIdent`succ):ident$(←withAppArgdelabTmInner))|Tm.pred_=>do`(tm|$(mkIdent`pred):ident$(←withAppArgdelabTmInner))|Tm.isZero_=>do`(tm|$(mkIdent`iszero):ident$(←withAppArgdelabTmInner))|Tm.ite___=>doletc←withAppFn<|withAppFn<|withAppArgdelabTmInnerlett←withAppFn<|withAppArgdelabTmInnerlete←withAppArgdelabTmInner`(tm|if$cthen$telse$e)|_=>do-- A bare variable prints without the `~` escape; anything else keeps it.match←delabwith|`($i:ident)=>`(tm|$i:ident)|e=>`(tm|~$e)(⟨·⟩)<$>annotateTermInfo⟨stx.raw⟩openLeanPrettyPrinterDelaboratorSubExprin-- The keys are the constants' full names: `Tm` lives in namespace `TM`, and the-- `delab` attribute does not resolve its argument against the current namespace.@[delabapp.TM.Tm.tru,delabapp.TM.Tm.fls,delabapp.TM.Tm.zero,delabapp.TM.Tm.succ,delabapp.TM.Tm.pred,delabapp.TM.Tm.isZero,delabapp.TM.Tm.ite]partialdefdelabTm:Delab:=whenPPOptiongetPPNotationdoguard<|match_expr←getExprwith|Tm.tru=>true|Tm.fls=>true|Tm.zero=>true|Tm.succ_=>true|Tm.pred_=>true|Tm.isZero_=>true|Tm.ite___=>true|_=>falsematch←delabTmInnerwith|`(tm|~$e)=>puree|e=>`(<{$e}>)
----------------------------- (ifTrue)
if true then t₁ else t₂ ⟶ t₁
------------------------------ (ifFalse)
if false then t₁ else t₂ ⟶ t₂
t₁ ⟶ t₁'
----------------------------------------------- (ifStep)
if t₁ then t₂ else t₃ ⟶ if t₁' then t₂ else t₃
t₁ ⟶ t₁'
------------------- (succStep)
succ t₁ ⟶ succ t₁'
----------- (predZero)
pred 0 ⟶ 0
IsNValue v
------------------ (predSucc)
pred (succ v) ⟶ v
t₁ ⟶ t₁'
------------------- (predStep)
pred t₁ ⟶ pred t₁'
---------------- (isZeroZero)
iszero 0 ⟶ true
IsNValue v
------------------------ (isZeroSucc)
iszero (succ v) ⟶ false
t₁ ⟶ t₁'
----------------------- (isZeroStep)
iszero t₁ ⟶ iszero t₁'
The formal rules are below. We are defining them differently than we have
previously: We are using the relation's own notation (⟶) inside the
constructors. We achieve this by defining Tm.Step within a section,
and within that section using set_option hygiene false on ⟶ so that
the constructors read in this syntax; after the section
closes, we use a scoped notation as we have done before.
The Tm.IsNValue premises in predSucc and isZeroSucc are needed
for determinism (this will be proved in an optional exercise below).
Notice that the Tm.Step relation doesn't care about whether the
expression being stepped makes global sense — it just checks that the
operation in the next reduction step is being applied to the right
kinds of operands. For example, the term succ true cannot take a
step, but the almost as obviously nonsensical term
The first interesting thing to notice about this Tm.Step relation is that
the strong progress theorem from the Smallstep chapter fails here. That
is, there are terms that are normal forms (they can't take a step) but
not values (they are not included in our definition of possible "results
of reduction").
(Hint: You will reach a point in this proof where you need to use an
induction to reason about a term that is known to be a numeric value.
This induction can be performed either over the term itself or over the
evidence that it is a numeric value. The proof goes through in either
case, but you will find that one way is quite a bit shorter than the
other. For the sake of the exercise, try to complete the proof both
ways.)
The "other way" mentioned in the hint proves the same fact by induction on
the term itself rather than on the evidence that it is a numeric value. It
goes through, but is a bit longer than the nvalue_is_nf route above.
(Hint: Notice that the Tm.Step relation doesn't care about whether the
expression being stepped makes global sense — it just checks that the
operation in the next reduction step is being applied to the right
kinds of operands.)
Show solution
(B) No
Optional aside, good practice with step relations but tangential to the
main development. We define an alternate step relation ⇢ and a step
function for it.
Note to developers (mwhicks1)
In the Rocq source these were a hidden, never-uncommented draft
(with `eval` still to be renamed to `step`). Here they are activated as
live Lean. Not sure if we want to keep this.
Suppose we define an alternate single-step relation, written t ⇢ t',
that drops the Tm.IsNValue premise from the predSucc and isZeroSucc
rules — so pred (succ t) and iszero (succ t) may step even when t is
not a numeric value. (It is built with exactly the same notation
setup as Tm.Step; note predSucc/isZeroSucc no longer take a premise.)
Some questions about this relation (answers inline):
Is ⇢ deterministic (∀ t t' t'', t ⇢ t' → t ⇢ t'' → t' = t'')? No:
pred (succ (pred 0)) steps to both pred 0 (by predSucc) and
pred (succ 0) (by predStep, since pred 0 ⇢ 0).
Is every Tm.Step normal form also a ⇢ normal form? No:
pred (succ true) is stuck for Tm.Step but steps under ⇢ (to
true, by predSucc, now that the Tm.IsNValue premise is gone).
Is every ⇢ normal form also a Tm.Step normal form? Yes — Tm.Step
is a subrelation of ⇢, so anything stuck for ⇢ is stuck for Tm.Step.
Is every value reachable by Tm.Step (in many steps) also reachable by
⇢ (in many steps)? Yes, for the same subrelation reason.
Conversely? No: iszero (succ true) reaches the value false under
⇢ but is stuck under Tm.Step.
A functional version computes a single ⇢ step of a term, returning
none when the term is a ⇢ normal form. This is a nice chance to see a
step function, which the chapter otherwise gives only as a relation:
defalt_simplify_step(t:Tm):OptionTm:=matchtwith|<{ift₁thent₂elset₃}>=>matchalt_simplify_stept₁with|somet₁'=>some<{ift₁'thent₂elset₃}>|none=>matcht₁with|<{true}>=>somet₂|<{false}>=>somet₃|_=>none|<{succt₁}>=>matchalt_simplify_stept₁with|somet₁'=>some<{succt₁'}>|none=>none|<{predt₁}>=>matchalt_simplify_stept₁with|somet₁'=>some<{predt₁'}>|none=>matcht₁with|<{0}>=>some<{0}>|<{succt₂}>=>somet₂|_=>none|<{iszerot₁}>=>matchalt_simplify_stept₁with|somet₁'=>some<{iszerot₁'}>|none=>matcht₁with|<{0}>=>some<{true}>|<{succVariable name `t₂` 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]_t₂Note: This linter can be disabled with `set_option linter.unusedVariables false`t₂}>=>some<{false}>|_=>none|_=>none-- `pred (succ true)` steps under `⇢` (the dropped `IsNValue` premise) even-- though it is stuck under `Tm.Step`:example:alt_simplify_step<{pred(succtrue)}>=some<{true}>:=rflexample:alt_simplify_step<{iftruethen0elsesucc0}>=some<{0}>:=rflexample:alt_simplify_step<{0}>=none:=rfl
The next critical observation is that, although this language has stuck
terms, they are always nonsensical, mixing booleans and numbers in a way
that we don't even want to have a meaning. We can easily exclude such
ill-typed terms by defining a typing relation that relates terms to the
types (either numeric or boolean) of their final results.
The typing relation⊢ t ⦂ τ relates terms to the types of their
results. In informal notation it is often written ⊢ t ⦂ τ and
pronounced "t has type τ." The ⊢ symbol is called a "turnstile."
The ⦂ between the term and its type is a dedicated type-colon glyph
(distinct from an ordinary :); in the editor you enter it with the Lean
input abbreviation \tc followed by a space.
Below, we're going to see richer typing relations where one or more
additional "context" arguments are written to the left of the turnstile.
For the moment, the context is always empty.
The typing judgment is written <{ ⊢ t ⦂ T }>: the whole judgment is wrapped
in <{ … }>, the term is in the object grammar (bare variables, no inner
<{ }>) and the type as Bool/Nat (a type variable is spliced, ~T escapes
to a Lean Ty). As with Tm.Step, we define the relation using its own
notation. The detailed definition of the notation is folded into the box below.
The notation is defined inside a section with set_option hygiene false so the bare name
Tm.HasType in the expansion resolves to the relation being defined; after the
section we re-declare the same rules hygienically for real use. Unlike ⟶,
the judgment builds on the custom tm syntactic category, so it must use
syntax/macro_rules rather than notation — which is why it still needs the
app_unexpander to print the judgment back.
sectionset_optionhygienefalseinlocalmacro_rules|`(<{⊢$t⦂$T:ident}>)=>matchT.getId.toStringwith|"Bool"=>`(Tm.HasType<{$t}>Ty.bool)|"Nat"=>`(Tm.HasType<{$t}>Ty.nat)|_=>`(Tm.HasType<{$t}>$T)|`(<{⊢$t⦂~$T}>)=>`(Tm.HasType<{$t}>$T)-- The actual definition, written in the notation above.inductiveTm.HasType:Tm→Ty→Propwhere|tru:<{⊢true⦂Bool}>|fls:<{⊢false⦂Bool}>|ite(t₁t₂t₃:Tm)(τ:Ty)(h₁:<{⊢t₁⦂Bool}>)(h₂:<{⊢t₂⦂τ}>)(h₃:<{⊢t₃⦂τ}>):<{⊢ift₁thent₂elset₃⦂τ}>|zero:<{⊢0⦂Nat}>|succ(t₁:Tm)(h:<{⊢t₁⦂Nat}>):<{⊢succt₁⦂Nat}>|pred(t₁:Tm)(h:<{⊢t₁⦂Nat}>):<{⊢predt₁⦂Nat}>|isZero(t₁:Tm)(h:<{⊢t₁⦂Nat}>):<{⊢iszerot₁⦂Bool}>Notation encoding: typing relation-- The same rules repeated with hygiene enabled, for use after the section.macro_rules|`(<{⊢$t⦂$T:ident}>)=>matchT.getId.toStringwith|"Bool"=>`(Tm.HasType<{$t}>Ty.bool)|"Nat"=>`(Tm.HasType<{$t}>Ty.nat)|_=>`(Tm.HasType<{$t}>$T)|`(<{⊢$t⦂~$T}>)=>`(Tm.HasType<{$t}>$T)-- Print `Tm.HasType`/`Ty` values back as `<{ ⊢ … ⦂ … }>` notation: `delabTy`openLeanPrettyPrinterDelaboratorSubExprin@[delabapp.TM.Ty.bool,delabapp.TM.Ty.nat]defdelabTy:Delab:=whenPPOptiongetPPNotationdomatch_expr←getExprwith|Ty.bool=>`($(mkIdent`Bool):ident)|Ty.nat=>`($(mkIdent`Nat):ident)|_=>failure@[app_unexpanderTm.HasType]defTm.HasType.unexpand:Lean.PrettyPrinter.Unexpander|`($_<{$t}>$T:ident)=>`(<{⊢$t⦂$T}>)|`($_$t:ident$T:ident)=>`(<{⊢$(⟨t.raw⟩)⦂$T}>)|`($_$t$T)=>`(<{⊢~$t⦂~$T}>)|_=>throw()endexample:<{⊢iffalsethen0elsesucc0⦂Nat}>:=.ite____.fls.zero(.succ_.zero)
It's important to realize that the typing relation is a conservative
(or static) approximation: it does not consider what happens when the
term is reduced — in particular, it does not calculate the type of its
normal form.
The following two lemmas capture the fundamental fact that the
definitions of boolean and numeric values agree with the typing relation:
a well-typed value of type Bool is a boolean value, and of type Nat a
numeric value.
The typing relation enjoys two critical properties.
The first is that well-typed normal forms are not stuck — or conversely,
if a term is well typed, then either it is a value or it can take at
least one step. We call this progress.
Exercise★★★(finish_progress)
Complete the formal proof of the progress property. (Make sure you
understand the parts we've given of the informal proof in the following
exercise before starting — this will save you a lot of time.)
Theorem: If ⊢ t ⦂ T, then either t is a value or else t ⟶ t' for
some t'.
Proof: By induction on a derivation of ⊢ t ⦂ T.
If the last rule in the derivation is ite, then t = if t₁ then t₂
else t₃, with ⊢ t₁ ⦂ Bool, ⊢ t₂ ⦂ T and ⊢ t₃ ⦂ T. By the IH,
either t₁ is a value or else t₁ can step to some t₁'.
If t₁ is a value, then by the canonical forms lemmas and the fact
that ⊢ t₁ ⦂ Bool we have that t₁ is a boolean value
(Tm.IsBValue) — i.e., it is either true or false. If t₁ = true, then t steps to t₂ by
ifTrue, while if t₁ = false, then t steps to t₃ by
ifFalse. Either way, t can step, which is what we wanted to
show.
If t₁ itself can take a step, then, by ifStep, so can t.
- If the last rule in the derivation is `tru`, then `t = true`, which
is a boolean value and hence a value. The cases for `fls` and `zero`
are similar.
- If the last rule in the derivation is `succ`, then `t = succ t₁`, with
`⊢ t₁ ⦂ Nat`. By the IH, either `t₁` is a value or else `t₁` can step
to some `t₁'`.
- If `t₁` is a value, then by the canonical forms lemma `t₁` is an
`nvalue`, and hence `t` is also an `nvalue` (and hence a value) by
`succ`.
- If `t₁` can take a step, then by `succStep`, so can `t`.
- If the last rule in the derivation is `pred`, then `t = pred t₁`, with
`⊢ t₁ ⦂ Nat`. By the IH, either `t₁` is a value or else `t₁` can step
to some `t₁'`.
- If `t₁` is a value, then (by the same argument as in the previous
case) it must be an `nvalue`. By case analysis on the `nvalue`
judgement, there are two cases:
- If `t₁ = 0`, then `t` can take a step by `predZero`.
- Otherwise, `t₁ = succ t₁'`, with `t₁'` an `nvalue`. Hence `t` can
again take a step, this time by `predSucc`.
- Finally, if `t₁` can take a step, then by `predStep`, so can `t`.
- If the last rule in the derivation is `isZero`, then `t = iszero t₁`,
with `⊢ t₁ ⦂ Nat`. By the IH, either `t₁` is a value or else `t₁` steps
to some `t₁'`.
- If `t₁` is a value, it must be an `nvalue`, and there are two cases to
consider:
- If `t₁ = 0`, then `t` can take a step by `isZeroZero`.
- Otherwise, `t₁ = succ t₁'` where `t₁'` is an `nvalue`. Hence `t` can
take a step by `isZeroSucc`.
- If `t₁` can take a step, then so can `t`, by `isZeroStep`.
This theorem is more interesting than the strong progress theorem that we
saw in the Smallstep chapter, where all normal forms were values. Here
a term can be stuck, but only if it is ill typed.
Quiz
Quick review: in the language defined at the start of this chapter...
Every well-typed normal form is a value.
(A) True (B) False
Show solution
TRUE: This is the content of the progress theorem.
Quiz
In this language...
Every value is a normal form.
(A) True (B) False
Show solution
TRUE: This can be proved by induction on values.
Quiz
In this language...
The single-step reduction relation is a partial function (i.e., it is
deterministic).
(A) True (B) False
Show solution
TRUE: This is the determinism theorem.
Quiz
In this language...
The single-step reduction relation is a total function.
If the last rule in the derivation is ite, then t = if t₁ then t₂
else t₃, with ⊢ t₁ ⦂ Bool, ⊢ t₂ ⦂ T and ⊢ t₃ ⦂ T.
Inspecting the rules for the small-step reduction relation and
remembering that t has the form if ..., we see that the only ones
that could have been used to prove t ⟶ t' are ifTrue,
ifFalse, or ifStep.
If the last rule was ifTrue, then t' = t₂. But we know that
⊢ t₂ ⦂ T, so we are done.
If the last rule was ifFalse, then t' = t₃. But we know that
⊢ t₃ ⦂ T, so we are done.
If the last rule was ifStep, then t' = if t₁' then t₂ else t₃,
where t₁ ⟶ t₁'. We know ⊢ t₁ ⦂ Bool so, by the IH, ⊢ t₁' ⦂
Bool. The ite rule then gives us ⊢ if t₁' then t₂ else t₃ ⦂ T,
as required.
- If the last rule in the derivation were `tru`, then `t = true`.
However, `true` does not step to anything, so this case is vacuously
true.
- Similarly, neither `fls` nor `zero` could be the final rule in the
derivation.
- If the last rule in the derivation is `succ`, then `t = succ t₁` with
`⊢ t₁ ⦂ Nat` and `T = Nat`. The only rule which could have been used to
show that `t` steps is `succStep`, in which case `t₁` steps to some
`t₁'`. So, by the IH, `⊢ t₁' ⦂ Nat`, and hence `t' = succ t₁'` also has
type `Nat` by `succ`.
- If the last rule in the derivation is `pred`, then `t = pred t₁` with
`⊢ t₁ ⦂ Nat`. There are only three rules which could have been the last
rule in the derivation of `pred t₁ ⟶ t'`.
- If the last rule was `predZero`, then `t' = 0` which has type `Nat`.
- If the last rule was `predSucc`, then `t₁ = succ t'`; by inversion
on the fact that `⊢ t₁ ⦂ Nat` it follows that `⊢ t' ⦂ Nat` as well.
- If the last rule was `predStep`, then `t₁` steps to some `t₁'`; by the
IH `⊢ t₁' ⦂ Nat`, and so `pred t₁'` has type `Nat` as well by
`pred`.
- If the last rule in the derivation is `isZero`, then `t = iszero t₁`
with `⊢ t₁ ⦂ Nat` and `T = Bool`. There are only three rules which
could have been the last rule in the derivation of `iszero t₁ ⟶ t'`.
- If the last rule was `isZeroZero`, then `t' = true` which has type
`Bool`.
- If the last rule was `isZeroSucc`, then `t' = false` which has type
`Bool`.
- If the last rule was `isZeroStep`, then `t₁` steps to some `t₁'`. By
the IH, `⊢ t₁' ⦂ Nat` as well, and hence `t' = iszero t₁'` has type
`Bool` by `isZero`.
Exercise★★★(preservation_alternate_proof)
Now prove the same property again by induction on the evaluation
derivation instead of on the typing derivation. Begin by carefully
reading and thinking about the first few lines of the above proofs to
make sure you understand what each one is doing. The set-up for this
proof is similar, but not exactly the same.
The preservation theorem is often called subject reduction, because it
tells us what happens when the "subject" of the typing relation is
reduced. This terminology comes from thinking of typing statements as
sentences, where the term is the subject and the type is the predicate.
Having seen the subject reduction property, one might wonder whether the
opposite property — subject expansion — also holds. That is, is it
always the case that, if t ⟶ t' and ⊢ t' ⦂ T, then ⊢ t ⦂ T? If so,
prove it. If not, give a counter-example.
Subject expansion does not hold in this language (or most interesting
languages). For example, `if false then true else 0` is ill typed, but
it reduces to the well-typed term `0`.
The following are thought exercises: for each modification, say which
of determinism / progress / preservation still hold, with a
counterexample if one breaks. (These are graded manually; there is no
Lean code to complete.)
Note to developers
HIDE: Two further variations kept for the instructors only (they overlap
with the two quizzes in the Type Soundness section above).
variation1a (EX2M?): add the two step rules
predTrue : pred true ⟶ pred false
predFalse : pred false ⟶ pred true
-- Determinism, Progress, and Preservation all remain true.
variation1b (EX2M?): add the typing rule
ifFunny : ⊢ t₂ ⦂ Nat → ⊢ if true then t₂ else t₃ ⦂ Nat
-- Determinism, Progress, and Preservation all remain true.
Exercise★★(variation1) (Manually graded)
Suppose that we add this new rule to the typing relation:
succBool : ⊢ t ⦂ Bool → ⊢ succ t ⦂ Bool
Which of the following properties remain true in the presence of this
rule? For each one, write either "remains true" or else "becomes false."
If a property becomes false, give a counterexample.
Determinism of Tm.Step
Progress
Preservation
- Determinism: remains true (the `step` relation is unchanged).
- Progress: becomes false -- `succ true` is now well typed (`⊢ succ true ⦂
Bool`) but is stuck.
- Preservation: remains true.
Exercise★★(variation2) (Manually graded)
Suppose, instead, that we add this new rule to the Tm.Step relation:
funny1 : if true then t₂ else t₃ ⟶ t₃
Which of the above properties become false in the presence of this rule?
For each one that does, give a counter-example.
- Determinism: becomes false -- `if true then 0 else (succ 0)` can now step
to either `0` (ifTrue) or `succ 0` (funny1).
- Progress and preservation: remain true.
Exercise★★(variation3) (Optional)
Suppose instead that we add this rule:
funny2 : t₂ ⟶ t₂' → if t₁ then t₂ else t₃ ⟶ if t₁ then t₂' else t₃
Which of the above properties become false in the presence of this rule?
For each one that does, give a counter-example.
Determinism becomes false (e.g. `if false then (pred 0) else (succ 0)`
can step by either `ifFalse` or the new rule). Progress and
preservation remain true.
Exercise★★(variation4) (Optional)
Suppose instead that we add this rule:
funny3 : pred false ⟶ pred (pred false)
Which of the above properties become false in the presence of this rule?
For each one that does, give a counter-example.
All three properties remain true.
Exercise★★(variation5) (Optional)
Suppose instead that we add this rule:
funny4 : ⊢ 0 ⦂ Bool
Which of the above properties become false in the presence of this rule?
For each one that does, give a counter-example.
Progress becomes false: `if 0 then true else true` has type `Bool`, is a
normal form, and is not a value.
Exercise★★(variation6) (Optional)
Suppose instead that we add this rule:
funny5 : ⊢ pred 0 ⦂ Bool
Which of the above properties become false in the presence of this rule?
For each one that does, give a counter-example.
Preservation becomes false: `pred 0` has type `Bool` and steps to `0`,
which does not have type `Bool`.
Exercise★★★(more_variations) (Optional)
Make up some exercises of your own along the same lines as the ones
above. Try to find ways of selectively breaking properties — i.e., ways
of changing the definitions that break just one of the properties and
leave the others alone.
Exercise★(remove_pred0) (Manually graded)
The reduction rule predZero is a bit counter-intuitive: we might feel
that it makes more sense for the predecessor of 0 to be undefined,
rather than being defined to be 0. Can we achieve this simply by
removing the rule from the definition of Tm.Step? Would doing so create
any problems elsewhere?
Yes, but doing this would break the progress property. A better way would
be to raise an exception in this case, but this requires that we add
exceptions to the language we're formalizing!
Suppose our evaluation relation is defined in the big-step style. State
appropriate analogs of the progress and preservation properties. (You do
not need to prove them.)
Can you see any limitations of either of your properties? Do they allow
for nonterminating programs? Why might we prefer the small-step semantics
for stating preservation and progress?
The type preservation property for the big-step semantics is similar to
the one we gave for the small-step semantics: if a well-typed term
evaluates to some final value, then this value has the same type as the
original term. The proof is similar to the one we gave. However,
preservation for small-step semantics implies that all intermediate states
(i.e., all states reachable in multi-step) are well-typed, whereas big-step
semantics only relates a term to its final evaluation result, with no
notion of "intermediate state" about which preservation can make
guarantees.
The situation with the progress property is more interesting. A direct
analog (if a term is well typed then it evaluates to some other term)
makes a much stronger claim than the progress theorem we have given: it
says that every well-typed term can be evaluated to some final value---that
is, that evaluation always terminates on well-typed terms. For arithmetic
expressions, this happens to be the case, but for more interesting
languages (languages involving general recursion, for example) it will
often not be true. For such languages, we simply have no progress property
in the big-step style: in effect, there is no way to tell the difference
between reaching an error state and failing to terminate. This is one
reason that language theorists generally prefer the small-step style.
Note to developers (Benjamin Pierce @bcpierce00)
This next is not using the new conventions for grade blocks, which I thought
to_verso.py was now enforcing. Is that because this file was converted a while back,
before these improvements? (I suspect yes because the indentation is also
wonky and I improved that too.) Anyway, the grade block headers should be fixed, throughout (and maybe in Smallstep and Imp?)... dev block headers too, if we want
to be really consistent.
Source revision: 9e5dba0, committed 2026-09-25 03:12 UTC