4. Types: Type Systems
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!).
4.1. Typed Arithmetic Expressions
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.
The language definition is completely routine.
4.1.1. Syntax
Here is the syntax of program terms, informally:
| t | ::= | true | |
| | | false | ||
| | | if t then t else t | ||
| | | 0 | ||
| | | succ t | ||
| | | pred t | ||
| | | iszero t |
And here it is formally:
namespace TM
inductive Tm where
| tru
| fls
| ite (c t e : Tm)
| zero
| succ (t : Tm)
| pred (t : Tm)
| isZero (t : Tm)
4.1.1.1. Notation
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_catadds a new non-terminal to Lean's grammar — heretm, the terms of this chapter's language. -
Each
syntaxdirective declares one production of that non-terminal, with annotations fixing precedence, and the last one declares the<{ … }>brackets that let atmappear where Lean expects a term. -
macro_rulesthen translates the resulting syntax forms into the corresponding constructors ofTm.
declare_syntax_cat tm
-- 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:max num : tm
syntax:max ident : tm
syntax:75 ident ppHardSpace tm:76 : tm
syntax:max "(" tm ")" : tm
syntax:max "~" term:max : tm
syntax:50 "if " tm:51 " then " tm:51 " else " tm:51 : tm
syntax:max "<{ " tm " }>" : term
open Lean in
macro_rules
| `(<{ $n:num }>) =>
if n.getNat == 0 then `(Tm.zero)
else Macro.throwErrorAt n "the only numeric literal in this language is 0"
| `(<{ $x:ident }>) =>
match x.getId.toString with
| "true" => `(Tm.tru)
| "false" => `(Tm.fls)
| _ => `($x) -- a bare identifier is a spliced Lean term (usually a variable)
| `(<{ $f:ident $e:tm }>) =>
match f.getId.toString with
| "succ" => `(Tm.succ <{ $e }>)
| "pred" => `(Tm.pred <{ $e }>)
| "iszero" => `(Tm.isZero <{ $e }>)
| _ => Macro.throwErrorAt f s!"unknown operator `{f.getId}`"
| `(<{ ($e) }>) => `(<{ $e }>)
| `(<{ ~$e }>) => pure e
| `(<{ if $c then $t else $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 back
open Lean PrettyPrinter Delaborator SubExpr Parenthesizer in
/-- Re-inserts parentheses in `tm` output according to the grammar's precedences. -/
@[category_parenthesizer tm]
def tm.parenthesizer : CategoryParenthesizer | prec => do
maybeParenthesize `tm true wrapParens prec <|
parenthesizeCategoryCore `tm prec
where
wrapParens (stx : Syntax) : Syntax := Unhygienic.run do
let pstx ← `(tm| ($(⟨stx⟩)))
return pstx.raw.setInfo (SourceInfo.fromRef stx)
open Lean PrettyPrinter Delaborator SubExpr in
/-- Rebuild `tm` concrete syntax from a `Tm` term. -/
partial def delabTmInner : DelabM (TSyntax `tm) := do
let stx ←
match_expr ← getExpr with
| Tm.tru => `(tm| $(mkIdent `true):ident)
| Tm.fls => `(tm| $(mkIdent `false):ident)
| Tm.zero => `(tm| 0)
| Tm.succ _ => do `(tm| $(mkIdent `succ):ident $(← withAppArg delabTmInner))
| Tm.pred _ => do `(tm| $(mkIdent `pred):ident $(← withAppArg delabTmInner))
| Tm.isZero _ => do `(tm| $(mkIdent `iszero):ident $(← withAppArg delabTmInner))
| Tm.ite _ _ _ => do
let c ← withAppFn <| withAppFn <| withAppArg delabTmInner
let t ← withAppFn <| withAppArg delabTmInner
let e ← withAppArg delabTmInner
`(tm| if $c then $t else $e)
| _ => do
-- A bare variable prints without the `~` escape; anything else keeps it.
match ← delab with
| `($i:ident) => `(tm| $i:ident)
| e => `(tm| ~$e)
(⟨·⟩) <$> annotateTermInfo ⟨stx.raw⟩
open Lean PrettyPrinter Delaborator SubExpr in
-- 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.
@[delab app.TM.Tm.tru, delab app.TM.Tm.fls, delab app.TM.Tm.zero, delab app.TM.Tm.succ,
delab app.TM.Tm.pred, delab app.TM.Tm.isZero, delab app.TM.Tm.ite]
partial def delabTm : Delab := whenPPOption getPPNotation do
guard <| match_expr ← getExpr with
| Tm.tru => true | Tm.fls => true | Tm.zero => true
| Tm.succ _ => true | Tm.pred _ => true | Tm.isZero _ => true
| Tm.ite _ _ _ => true | _ => false
match ← delabTmInner with
| `(tm| ~$e) => pure e
| e => `(<{ $e }>)
4.1.1.2. Values
Values are true, false, and numeric values (0, and succ of a numeric value).
inductive Tm.IsBValue : Tm → Prop where
| tru : Tm.IsBValue <{ true }>
| fls : Tm.IsBValue <{ false }>
inductive Tm.IsNValue : Tm → Prop where
| zero : Tm.IsNValue <{ 0 }>
| succ (t : Tm) (h : Tm.IsNValue t) : Tm.IsNValue <{ succ t }>
def Tm.IsValue (t : Tm) : Prop := Tm.IsBValue t ∨ Tm.IsNValue t
4.1.2. Operational Semantics
Here is the single-step relation, informally.
----------------------------- (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.
section
set_option hygiene false in
local notation:40 t:41 " ⟶ " t':41 => Tm.Step t t'
inductive Tm.Step : Tm → Tm → Prop where
| ifTrue (t₁ t₂ : Tm) : <{ if true then t₁ else t₂ }> ⟶ t₁
| ifFalse (t₁ t₂ : Tm) : <{ if false then t₁ else t₂ }> ⟶ t₂
| ifStep (c c' t₂ t₃ : Tm) (h : c ⟶ c') :
<{ if c then t₂ else t₃ }> ⟶ <{ if c' then t₂ else t₃ }>
| succStep (t₁ t₁' : Tm) (h : t₁ ⟶ t₁') : <{ succ t₁ }> ⟶ <{ succ t₁' }>
| predZero : <{ pred 0 }> ⟶ <{ 0 }>
| predSucc (v : Tm) (hv : Tm.IsNValue v) : <{ pred (succ v) }> ⟶ v
| predStep (t₁ t₁' : Tm) (h : t₁ ⟶ t₁') : <{ pred t₁ }> ⟶ <{ pred t₁' }>
| isZeroZero : <{ iszero 0 }> ⟶ <{ true }>
| isZeroSucc (v : Tm) (hv : Tm.IsNValue v) : <{ iszero (succ v) }> ⟶ <{ false }>
| isZeroStep (t₁ t₁' : Tm) (h : t₁ ⟶ t₁') : <{ iszero t₁ }> ⟶ <{ iszero t₁' }>
end
scoped notation:40 t:41 " ⟶ " t':41 => Tm.Step t t'
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
succ (if true then true else true)
can take a step (once, before becoming stuck).
4.1.3. Normal Forms and Values
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").
Such terms are stuck.
def Tm.IsNormalForm (t : Tm) : Prop := _root_.IsNormalForm Tm.Step t
def Tm.IsStuck (t : Tm) : Prop := Tm.IsNormalForm t ∧ ¬ Tm.IsValue t
theorem some_term_is_stuck : ∃ t, Tm.IsStuck t := ⊢ ∃ t, t.IsStuck
All goals completed! 🐙
However, although values and normal forms are not the same in this language, the set of values is a subset of the set of normal forms.
This is important because it shows we did not accidentally define things so that some value could still take a step.
theorem nvalue_is_nf (t : Tm) (h : Tm.IsNValue t) : Tm.IsNormalForm t := t:Tmh:t.IsNValue⊢ t.IsNormalForm
induction h with
t:Tm⊢ <{ 0 }>.IsNormalForm t:Tmhc:∃ t', <{ 0 }> ⟶ t'⊢ False; t:Tmt':Tmhstp:<{ 0 }> ⟶ t'⊢ False; All goals completed! 🐙
t:Tmt₀:Tmhn₀:t₀.IsNValueih:t₀.IsNormalForm⊢ <{ succ t₀ }>.IsNormalForm
t:Tmt₀:Tmhn₀:t₀.IsNValueih:t₀.IsNormalFormhc:∃ t', <{ succ t₀ }> ⟶ t'⊢ False; t:Tmt₀:Tmhn₀:t₀.IsNValueih:t₀.IsNormalFormt':Tmhstp:<{ succ t₀ }> ⟶ t'⊢ False
cases hstp with
t:Tmt₀:Tmhn₀:t₀.IsNValueih:t₀.IsNormalFormt₁':Tmh:t₀ ⟶ t₁'⊢ False All goals completed! 🐙
(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.)
theorem value_is_nf (t : Tm) (h : Tm.IsValue t) : Tm.IsNormalForm t := t:Tmh:t.IsValue⊢ t.IsNormalForm
All goals completed! 🐙
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.
theorem value_is_nf' (t : Tm) (h : Tm.IsValue t) : Tm.IsNormalForm t := t:Tmh:t.IsValue⊢ t.IsNormalForm
All goals completed! 🐙
Use value_is_nf (here, nvalue_is_nf) to show that the Tm.Step relation
is also deterministic.
theorem step_deterministic : Deterministic Tm.Step := ⊢ Deterministic Tm.Step
All goals completed! 🐙
Is the following term stuck?
iszero (if true then (succ 0) else 0)
(A) Yes (B) No
Show solution
(B) No
What about this one? Is it stuck?
if (succ 0) then true else false
(A) Yes (B) No
Show solution
(A) Yes
What about this one? Is it stuck?
succ (succ 0)
(A) Yes (B) No
Show solution
(B) No
What about this one? Is it stuck?
succ (if true then true else true)
(A) Yes (B) No
(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.
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.)
section
set_option hygiene false in
local notation:40 t:41 " ⇢ " t':41 => Tm.AltStep t t'
inductive Tm.AltStep : Tm → Tm → Prop where
| ifTrue (t₁ t₂ : Tm) : <{ if true then t₁ else t₂ }> ⇢ t₁
| ifFalse (t₁ t₂ : Tm) : <{ if false then t₁ else t₂ }> ⇢ t₂
| ifStep (t₁ t₁' t₂ t₃ : Tm) : t₁ ⇢ t₁' →
<{ if t₁ then t₂ else t₃ }> ⇢ <{ if t₁' then t₂ else t₃ }>
| succStep (t₁ t₁' : Tm) : t₁ ⇢ t₁' → <{ succ t₁ }> ⇢ <{ succ t₁' }>
| predZero : <{ pred 0 }> ⇢ <{ 0 }>
| predSucc (t₁ : Tm) : <{ pred (succ t₁) }> ⇢ t₁
| predStep (t₁ t₁' : Tm) : t₁ ⇢ t₁' → <{ pred t₁ }> ⇢ <{ pred t₁' }>
| isZeroZero : <{ iszero 0 }> ⇢ <{ true }>
| isZeroSucc (t₁ : Tm) : <{ iszero (succ t₁) }> ⇢ <{ false }>
| isZeroStep (t₁ t₁' : Tm) : t₁ ⇢ t₁' → <{ iszero t₁ }> ⇢ <{ iszero t₁' }>
end
scoped notation:40 t:41 " ⇢ " t':41 => Tm.AltStep t t'
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 bothpred 0(bypredSucc) andpred (succ 0)(bypredStep, sincepred 0 ⇢ 0). -
Is every
Tm.Stepnormal form also a⇢normal form? No:pred (succ true)is stuck forTm.Stepbut steps under⇢(totrue, bypredSucc, now that theTm.IsNValuepremise is gone). -
Is every
⇢normal form also aTm.Stepnormal form? Yes —Tm.Stepis a subrelation of⇢, so anything stuck for⇢is stuck forTm.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 valuefalseunder⇢but is stuck underTm.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:
def alt_simplify_step (t : Tm) : Option Tm :=
match t with
| <{ if t₁ then t₂ else t₃ }> =>
match alt_simplify_step t₁ with
| some t₁' => some <{ if t₁' then t₂ else t₃ }>
| none =>
match t₁ with
| <{ true }> => some t₂
| <{ false }> => some t₃
| _ => none
| <{ succ t₁ }> =>
match alt_simplify_step t₁ with
| some t₁' => some <{ succ t₁' }>
| none => none
| <{ pred t₁ }> =>
match alt_simplify_step t₁ with
| some t₁' => some <{ pred t₁' }>
| none =>
match t₁ with
| <{ 0 }> => some <{ 0 }>
| <{ succ t₂ }> => some t₂
| _ => none
| <{ iszero t₁ }> =>
match alt_simplify_step t₁ with
| some t₁' => some <{ iszero t₁' }>
| none =>
match t₁ with
| <{ 0 }> => some <{ true }>
| <{ succ 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 (succ true) }> = some <{ true }> := rfl
example : alt_simplify_step <{ if true then 0 else succ 0 }> = some <{ 0 }> := rfl
example : alt_simplify_step <{ 0 }> = none := rfl
4.1.4. Typing
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.
------------- (tru)
⊢ true ⦂ Bool
-------------- (fls)
⊢ false ⦂ Bool
⊢ t₁ ⦂ Bool ⊢ t₂ ⦂ τ ⊢ t₃ ⦂ τ
----------------------------------- (ite)
⊢ if t₁ then t₂ else t₃ ⦂ τ
--------- (zero)
⊢ 0 ⦂ Nat
⊢ t₁ ⦂ Nat
--------------- (succ)
⊢ succ t₁ ⦂ Nat
⊢ t₁ ⦂ Nat
--------------- (pred)
⊢ pred t₁ ⦂ Nat
⊢ t₁ ⦂ Nat
------------------ (isZero)
⊢ iszero t₁ ⦂ Bool
Here are the formal rules.
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.
inductive Ty where
| bool
| nat
syntax:max "<{ " "⊢ " tm " ⦂ " ident " }>" : term
syntax:max "<{ " "⊢ " tm " ⦂ " "~" term:max " }>" : term
Notation encoding: typing relation
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.
section
set_option hygiene false in
local macro_rules
| `(<{ ⊢ $t ⦂ $T:ident }>) =>
match T.getId.toString with
| "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.
inductive Tm.HasType : Tm → Ty → Prop where
| tru : <{ ⊢ true ⦂ Bool }>
| fls : <{ ⊢ false ⦂ Bool }>
| ite (t₁ t₂ t₃ : Tm) (τ : Ty)
(h₁ : <{ ⊢ t₁ ⦂ Bool }>) (h₂ : <{ ⊢ t₂ ⦂ τ }>) (h₃ : <{ ⊢ t₃ ⦂ τ }>) :
<{ ⊢ if t₁ then t₂ else t₃ ⦂ τ }>
| zero : <{ ⊢ 0 ⦂ Nat }>
| succ (t₁ : Tm) (h : <{ ⊢ t₁ ⦂ Nat }>) : <{ ⊢ succ t₁ ⦂ Nat }>
| pred (t₁ : Tm) (h : <{ ⊢ t₁ ⦂ Nat }>) : <{ ⊢ pred t₁ ⦂ Nat }>
| isZero (t₁ : Tm) (h : <{ ⊢ t₁ ⦂ Nat }>) : <{ ⊢ iszero t₁ ⦂ Bool }>
Notation encoding: typing relation
-- The same rules repeated with hygiene enabled, for use after the section.
macro_rules
| `(<{ ⊢ $t ⦂ $T:ident }>) =>
match T.getId.toString with
| "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`
open Lean PrettyPrinter Delaborator SubExpr in
@[delab app.TM.Ty.bool, delab app.TM.Ty.nat]
def delabTy : Delab := whenPPOption getPPNotation do
match_expr ← getExpr with
| Ty.bool => `($(mkIdent `Bool):ident)
| Ty.nat => `($(mkIdent `Nat):ident)
| _ => failure
@[app_unexpander Tm.HasType]
def Tm.HasType.unexpand : Lean.PrettyPrinter.Unexpander
| `($_ <{ $t }> $T:ident) => `(<{ ⊢ $t ⦂ $T }>)
| `($_ $t:ident $T:ident) => `(<{ ⊢ $(⟨t.raw⟩) ⦂ $T }>)
| `($_ $t $T) => `(<{ ⊢ ~$t ⦂ ~$T }>)
| _ => throw ()
end
example : <{ ⊢ if false then 0 else succ 0 ⦂ 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.
example : ¬ <{ ⊢ if false then 0 else true ⦂ Bool }> := ⊢ ¬<{ ⊢ if false then 0 else true ⦂ Bool }>
hc:<{ ⊢ if false then 0 else true ⦂ Bool }>⊢ False; cases hc with h₁:<{ ⊢ false ⦂ Bool }>h₂:<{ ⊢ 0 ⦂ Bool }>h₃:<{ ⊢ true ⦂ Bool }>⊢ False All goals completed! 🐙
example :
¬ <{ ⊢ if iszero (succ 0) then succ false else true ⦂ Bool }> := ⊢ ¬<{ ⊢ if iszero (succ 0) then succ false else true ⦂ Bool }>
hc:<{ ⊢ if iszero (succ 0) then succ false else true ⦂ Bool }>⊢ False; cases hc with h₁:<{ ⊢ iszero (succ 0) ⦂ Bool }>h₂:<{ ⊢ succ false ⦂ Bool }>h₃:<{ ⊢ true ⦂ Bool }>⊢ False All goals completed! 🐙
4.1.5. Canonical forms
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.
theorem bool_canonical (t : Tm) (hT : <{ ⊢ t ⦂ Bool }>) (hv : Tm.IsValue t) : Tm.IsBValue t := t:TmhT:<{ ⊢ t ⦂ Bool }>hv:t.IsValue⊢ t.IsBValue
cases hv with
t:TmhT:<{ ⊢ t ⦂ Bool }>hb:t.IsBValue⊢ t.IsBValue All goals completed! 🐙
t:TmhT:<{ ⊢ t ⦂ Bool }>hn:t.IsNValue⊢ t.IsBValue cases hn with
hT:<{ ⊢ 0 ⦂ Bool }>⊢ <{ 0 }>.IsBValue All goals completed! 🐙
t₀:Tmh:t₀.IsNValuehT:<{ ⊢ succ t₀ ⦂ Bool }>⊢ <{ succ t₀ }>.IsBValue All goals completed! 🐙
theorem nat_canonical (t : Tm) (hT : <{ ⊢ t ⦂ Nat }>) (hv : Tm.IsValue t) : Tm.IsNValue t := t:TmhT:<{ ⊢ t ⦂ Nat }>hv:t.IsValue⊢ t.IsNValue
cases hv with
t:TmhT:<{ ⊢ t ⦂ Nat }>hb:t.IsBValue⊢ t.IsNValue hT:<{ ⊢ true ⦂ Nat }>⊢ <{ true }>.IsNValuehT:<{ ⊢ false ⦂ Nat }>⊢ <{ false }>.IsNValue hT:<{ ⊢ true ⦂ Nat }>⊢ <{ true }>.IsNValuehT:<{ ⊢ false ⦂ Nat }>⊢ <{ false }>.IsNValue All goals completed! 🐙
t:TmhT:<{ ⊢ t ⦂ Nat }>hn:t.IsNValue⊢ t.IsNValue All goals completed! 🐙
4.1.6. Progress
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.
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 progress (t : Tm) (τ : Ty) (hT : <{ ⊢ t ⦂ τ }>) : Tm.IsValue t ∨ ∃ t', t ⟶ t' := t:Tmτ:TyhT:<{ ⊢ t ⦂ τ }>⊢ t.IsValue ∨ ∃ t', t ⟶ t'
All goals completed! 🐙
What is the relation between the progress property defined here and the strong progress from the Smallstep chapter?
(A) No difference — they mean the same thing
(B) Progress implies strong progress
(C) Strong progress implies progress
(D) No relationship
(E) Dunno
Show solution
(C) Strong progress implies progress
Complete the corresponding informal proof.
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, thent = if t₁ then t₂ else t₃, with⊢ t₁ ⦂ Bool,⊢ t₂ ⦂ Tand⊢ t₃ ⦂ T. By the IH, eithert₁is a value or elset₁can step to somet₁'.-
If
t₁is a value, then by the canonical forms lemmas and the fact that⊢ t₁ ⦂ Boolwe have thatt₁is a boolean value (Tm.IsBValue) — i.e., it is eithertrueorfalse. Ift₁ = true, thentsteps tot₂byifTrue, while ift₁ = false, thentsteps tot₃byifFalse. Either way,tcan step, which is what we wanted to show. -
If
t₁itself can take a step, then, byifStep, so cant.
-
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.
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.
In this language...
-
Every value is a normal form.
(A) True (B) False
Show solution
TRUE: This can be proved by induction on values.
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.
In this language...
-
The single-step reduction relation is a total function.
(A) True (B) False
Show solution
FALSE: normal forms do not reduce to anything.
4.1.7. Type Preservation
The second critical property of typing is that, when a well-typed term takes a step, the result is a well-typed term (of the same type).
Complete the formal proof of the preservation property. (Again, make
sure you understand the informal proof fragment in the following exercise
first.)
theorem preservation (t t' : Tm) (τ : Ty) (hT : <{ ⊢ t ⦂ τ }>) (he : t ⟶ t') : <{ ⊢ t' ⦂ τ }> := t:Tmt':Tmτ:TyhT:<{ ⊢ t ⦂ τ }>he:t ⟶ t'⊢ <{ ⊢ t' ⦂ τ }>
All goals completed! 🐙
Complete the following informal proof.
Theorem: If ⊢ t ⦂ T and t ⟶ t', then ⊢ t' ⦂ T.
Proof: By induction on a derivation of ⊢ t ⦂ T.
-
If the last rule in the derivation is
ite, thent = if t₁ then t₂ else t₃, with⊢ t₁ ⦂ Bool,⊢ t₂ ⦂ Tand⊢ t₃ ⦂ T.Inspecting the rules for the small-step reduction relation and remembering that
thas the formif ..., we see that the only ones that could have been used to provet ⟶ t'areifTrue,ifFalse, orifStep.-
If the last rule was
ifTrue, thent' = t₂. But we know that⊢ t₂ ⦂ T, so we are done. -
If the last rule was
ifFalse, thent' = t₃. But we know that⊢ t₃ ⦂ T, so we are done. -
If the last rule was
ifStep, thent' = if t₁' then t₂ else t₃, wheret₁ ⟶ t₁'. We know⊢ t₁ ⦂ Boolso, by the IH,⊢ t₁' ⦂ Bool. Theiterule then gives us⊢ if t₁' then t₂ else t₃ ⦂ T, as required.
-
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.
theorem preservation' (t t' : Tm) (τ : Ty) (hT : <{ ⊢ t ⦂ τ }>) (he : t ⟶ t') : <{ ⊢ t' ⦂ τ }> := t:Tmt':Tmτ:TyhT:<{ ⊢ t ⦂ τ }>he:t ⟶ t'⊢ <{ ⊢ t' ⦂ τ }>
All goals completed! 🐙
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.
4.1.8. Type Soundness
Putting progress and preservation together, we see that a well-typed term can never reach a stuck state.
def Tm.MultiStep (t₁ t₂ : Tm) : Prop := Multi Tm.Step t₁ t₂
scoped notation:40 t₁:41 " ⟶* " t₂:41 => Tm.MultiStep t₁ t₂
theorem soundness (t t' : Tm) (τ : Ty) (hT : <{ ⊢ t ⦂ τ }>) (hm : t ⟶* t') : ¬ Tm.IsStuck t' := t:Tmt':Tmτ:TyhT:<{ ⊢ t ⦂ τ }>hm:t ⟶* t'⊢ ¬t'.IsStuck
induction hm generalizing τ with
t:Tmt':Tma:Tmτ:TyhT:<{ ⊢ a ⦂ τ }>⊢ ¬a.IsStuck
t:Tmt':Tma:Tmτ:TyhT:<{ ⊢ a ⦂ τ }>hst:a.IsStuck⊢ False; t:Tmt':Tma:Tmτ:TyhT:<{ ⊢ a ⦂ τ }>hnf:a.IsNormalFormhnv:¬a.IsValue⊢ False
cases progress a τ hT with
t:Tmt':Tma:Tmτ:TyhT:<{ ⊢ a ⦂ τ }>hnf:a.IsNormalFormhnv:¬a.IsValuehv:a.IsValue⊢ False All goals completed! 🐙
t:Tmt':Tma:Tmτ:TyhT:<{ ⊢ a ⦂ τ }>hnf:a.IsNormalFormhnv:¬a.IsValuehs:∃ t', a ⟶ t'⊢ False All goals completed! 🐙
t:Tmt':Tma:Tmb:Tmc:Tmh₁:a ⟶ bh₂:Multi Tm.Step b cih:∀ (τ : Ty), <{ ⊢ b ⦂ τ }> → ¬c.IsStuckτ:TyhT:<{ ⊢ a ⦂ τ }>⊢ ¬c.IsStuck All goals completed! 🐙
Suppose we add the following two new rules to the reduction relation:
| predTrue : pred true ⟶ pred false | predFalse : pred false ⟶ pred true
Which of the following properties remain true in the presence of these rules? (Choose 1 for yes, 2 for no.)
-
Determinism of
Tm.Step -
Progress
-
Preservation
Show solution
All three remain true.
Suppose, instead, that we add this new rule to the typing relation:
| ifFunny : ⊢ t₂ ⦂ Nat → ⊢ if true then t₂ else t₃ ⦂ Nat
Which of the following properties remain true in the presence of this rule?
-
Determinism of
Tm.Step -
Progress
-
Preservation
Show solution
All three remain true.
4.2. Additional Exercises
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.
theorem subject_expansion :
(∀ (t t' : Tm) (τ : Ty), t ⟶ t' ∧ <{ ⊢ t' ⦂ τ }> → <{ ⊢ t ⦂ τ }>)
∨ ¬ (∀ (t t' : Tm) (τ : Ty), t ⟶ t' ∧ <{ ⊢ t' ⦂ τ }> → <{ ⊢ t ⦂ τ }>) := ⊢ (∀ (t t' : Tm) (τ : Ty), t ⟶ t' ∧ <{ ⊢ t' ⦂ τ }> → <{ ⊢ t ⦂ τ }>) ∨
¬∀ (t t' : Tm) (τ : Ty), t ⟶ t' ∧ <{ ⊢ t' ⦂ τ }> → <{ ⊢ t ⦂ τ }>
All goals completed! 🐙
end TM
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.)
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
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.
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.
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.
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.
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.
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.
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?
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?