Lambda Calculus in Lean

1. Untyped Lambda Calculus: De Bruijn Terms🔗

import Mathlib.Logic.Relation import Mathlib.Data.Set.Basic import LeanLambda.Corenamespace Untyped

1.1. Lambda Terms Without Types🔗

The lambda calculus is a very small language for describing functions. It has no numbers, booleans, loops, or function declarations built in. There are only three ways to make a term:

  • a variable, such as x;

  • a function λx. e, whose argument is called x and whose body is e;

  • an application e₁ e₂, which applies the function e₁ to the argument e₂.

The word untyped means that a lambda does not say what kind of argument it expects. For now, λx. x, λx. x x, and even nonsensical-looking applications are all legitimate terms. Types will be introduced only in the next chapter.

Application associates to the left, so f x y means (f x) y. The body of a lambda extends as far to the right as possible, so λx. f x means λx. (f x).

A lambda binds occurrences of its variable in its body. In λx. x y, the occurrence of x is bound, while y is free. Names can be changed without changing the function: λx. x and λy. y both return their argument. This harmless change of bound names is called alpha-renaming.

Binder names are pleasant for humans but awkward for an implementation. Lean would normally regard the two identity functions above as different syntax trees, and substitution would constantly have to choose fresh names. We avoid that problem by implementing bound variables with de Bruijn indices first. Ordinary named notation will be added on top afterwards.

1.2. De Bruijn Terms🔗

A de Bruijn index tells us how far away a variable's binder is. Index 0 refers to the nearest surrounding lambda, index 1 skips that lambda and refers to the next one, and so on. Because the index records the binder rather than its name, alpha-renaming disappears automatically.

For example:

  • λx. x becomes λ 0;

  • λx. λy. y becomes λ λ 0;

  • λx. λy. x becomes λ λ 1.

The two occurrences of 0 in the first two examples refer to different lambdas: in each case they select the nearest one. To find the binder of an index, start at its occurrence and count lambdas while moving outwards.

We keep names for variables that have no binder inside the term. Thus the implementation distinguishes a bound index 0 from a genuinely free name such as x. This mixed representation lets us work with open terms without introducing a separate representation for their surrounding context.

e ::= n \mid x \mid \lambda.\ e \mid e\ e

The datatype below follows this grammar exactly. The quotation DB[...] is only convenient notation for constructing the corresponding syntax tree.

inductive DB where | bound (index : Nat) | free (x : String) | lam (body : DB) | app (fn arg : DB) deriving DecidableEq, Repr, Lean.ToExprsyntax_rules dbTerm quoted_by "DB[" dbTerm "]" where | "{" t:term "}" => t | "(" t:dbTerm ")" => parse t | n:num => DB.bound n | x:ident => DB.free x | "λ" body:dbTerm => DB.lam (parse body) | f:dbTerm:70 a:dbTerm:71 => DB.app (parse f) (parse a)example : DB[x] = .free "x" := DB.free "x" = DB.free "x" All goals completed! 🐙example : DB[λ λ 1] = .lam (.lam (.bound 1)) := (DB.bound 1).lam.lam = (DB.bound 1).lam.lam All goals completed! 🐙example : DB[0 1 2] = .app (.app (.bound 0) (.bound 1)) (.bound 2) := ((DB.bound 0).app (DB.bound 1)).app (DB.bound 2) = ((DB.bound 0).app (DB.bound 1)).app (DB.bound 2) All goals completed! 🐙namespace DB

1.3. Free Variables and Scope🔗

In a well-scoped de Bruijn term, every index refers to a surrounding lambda, while every named variable is free. Thus FV collects precisely the names stored by DB.free. Passing through a de Bruijn lambda does not remove a name, because these lambdas bind indices rather than strings.

\begin{array}{rcl} FV(n) &=& \varnothing\\ FV(x) &=& \{x\}\\ FV(\lambda.\ e) &=& FV(e)\\ FV(e_1\ e_2) &=& FV(e_1) \cup FV(e_2) \end{array}

Our datatype also permits raw terms containing a dangling index. For example, the term 0 has no free name, but it has no lambda for its index to refer to either. We regard such a term as ill-scoped, not as a term with a free named variable. The proposition ScopedAt d t says that every index in t is smaller than the number d of binders supplied by its surroundings. A term is locally closed when it needs no surrounding binders.

Consequently, a closed de Bruijn term must satisfy two independent conditions: it is locally closed, and it contains no free names.

@[simp] def FV : DB -> Finset String | .bound _ => {} | .free x => {x} | .lam body => (FV body) | .app f a => (FV f) (FV a)@[simp] def ScopedAt : Nat DB Prop | depth, .bound n => n < depth | _, .free _ => True | depth, .lam body => ScopedAt (depth + 1) body | depth, .app fn arg => ScopedAt depth fn ScopedAt depth argdef LocallyClosed (t : DB) : Prop := ScopedAt 0 tdef NoFreeVars (t : DB) : Prop := x, x FV tdef Closed (t : DB) : Prop := LocallyClosed t NoFreeVars texample : "x" FV DB[x] := "x" (free "x").FV All goals completed! 🐙example : NoFreeVars DB[0] := (bound 0).NoFreeVars All goals completed! 🐙example : ¬ LocallyClosed DB[0] := ¬(bound 0).LocallyClosed All goals completed! 🐙example : Closed DB[λ 0] := (bound 0).lam.Closed All goals completed! 🐙

1.4. Capture-Avoiding Substitution🔗

Contracting a beta redex replaces the parameter of a lambda by its argument:

(\lambda x.\ e)\ s \longrightarrow e[x := s].

In ordinary named notation, e[x := s] means “replace the occurrences of x that are free in the body e by s.” This replacement must respect the lambdas already inside e. For example,

(\lambda x.\lambda y.\ x)\ y

must return a constant function whose result is the free variable y. It must not become λy. y, which would turn that free variable into a bound one. With de Bruijn indices the redex is (λ λ 1) y, and the correct result is unambiguously λ y.

The implementation never substitutes for the string x. Instead, beta reduction substitutes for index 0, which refers to the parameter of the lambda being removed. We first define shifting, which adjusts an index when a term passes beneath a binder, and then define substitution itself.

1.4.1. Shifting🔗

Suppose a term contains an index referring to a lambda outside that term. If we place the term beneath one more lambda, its old binder is now one step farther away, so the index must increase. Indices already bound inside the term must remain unchanged.

The cutoff separates these cases. Indices smaller than c are protected by lambdas already crossed during recursion. Indices at least c refer farther outward and are incremented.

\begin{array}{rcl} \uparrow_c(n) &=& \begin{cases} n & n < c\\ n+1 & n \ge c \end{cases}\\ \uparrow_c(x) &=& x\\ \uparrow_c(\lambda.\ e) &=& \lambda.\ \uparrow_{c+1}(e)\\ \uparrow_c(e_1\ e_2) &=& \uparrow_c(e_1)\ \uparrow_c(e_2) \end{array}

Under a lambda the cutoff increases, because index 0 is bound by that new lambda and must not move.

@[simp] def shiftRenaming (cutoff index : Nat) : Nat := if index < cutoff then index else index + 1@[simp] def shiftAbove (cutoff : Nat) : DB -> DB | .bound k => .bound (shiftRenaming cutoff k) | .free x => .free x | .lam body => .lam (shiftAbove (cutoff + 1) body) | .app f a => .app (shiftAbove cutoff f) (shiftAbove cutoff a)

At cutoff zero, a bare index moves outward. The bound variable of an existing lambda is protected, while an index referring past that lambda still moves.

example : shiftAbove 0 DB[0] = DB[1] := shiftAbove 0 (bound 0) = bound 1 All goals completed! 🐙example : shiftAbove 0 DB[λ 0] = DB[λ 0] := shiftAbove 0 (bound 0).lam = (bound 0).lam All goals completed! 🐙example : shiftAbove 0 DB[λ 1] = DB[λ 2] := shiftAbove 0 (bound 1).lam = (bound 2).lam All goals completed! 🐙

1.4.2. Substitution🔗

The expression subst j s t replaces index j in t by the term s. Because the binder for index j is removed at the same time, indices above j must decrease. The index case therefore has exactly three possibilities:

[s/j]k = \begin{cases} k & k < j,\\ s & k = j,\\ k - 1 & j < k. \end{cases}

An index below j refers to a nearer binder and is unchanged. Index j is the variable being replaced. An index above j is decremented because one binder has disappeared.

When substitution crosses a lambda, both sides of this description move one level outward: the target becomes j + 1, and the substituted term is shifted. This is the capture-avoiding step in the definition.

@[simp] def subst (j : Nat) (s : DB) : DB -> DB | .bound k => if j > k then .bound k else if j < k then .bound (k - 1) else s | .free x => .free x | .lam body => .lam (subst (j + 1) (shiftAbove 0 s) body) | .app f a => .app (subst j s f) (subst j s a)

The first example performs the replacement itself. In the second, removing binder 0 brings the old index 1 one level nearer. In the third, the inserted free name is unchanged when substitution crosses a lambda.

example : subst 0 DB[z] DB[0] = DB[z] := subst 0 (free "z") (bound 0) = free "z" All goals completed! 🐙example : subst 0 DB[z] DB[1] = DB[0] := subst 0 (free "z") (bound 1) = bound 0 All goals completed! 🐙example : subst 0 DB[z] DB[λ 1] = DB[λ z] := subst 0 (free "z") (bound 1).lam = (free "z").lam All goals completed! 🐙

For the outermost binder, the three variable cases simplify to the following calculation rules. These are the equations used most often in beta reduction.

\begin{array}{rcl} [s/0]0 &=& s\\ [s/0](n+1) &=& n\\ [s/0]x &=& x \end{array}

1.5. Beta Reduction🔗

An application whose function is a lambda is called a beta redex:

(\lambda.\ e)\ s.

Contracting the redex removes the lambda and substitutes the argument for index 0 in the body. The resulting term is called the contractum:

(\lambda.\ e)\ s \to_\beta [s/0]e.

A larger term can contain several redexes. We therefore define beta reduction as a relation rather than as a function: one step chooses a single redex. The first rule below contracts a redex at the root; the remaining rules allow one step in the function, the argument, or the body of a lambda.

\frac{}{(\lambda.\ e)\ e' \to_\beta [e'/0]e} \qquad \frac{e_1 \to_\beta e_1'}{e_1\ e_2 \to_\beta e_1'\ e_2} \qquad \frac{e_2 \to_\beta e_2'}{e_1\ e_2 \to_\beta e_1\ e_2'} \qquad \frac{e \to_\beta e'}{\lambda.\ e \to_\beta \lambda.\ e'}

This is full beta reduction. In particular, reduction is allowed beneath a lambda. We have not chosen an evaluation strategy such as call-by-value or call-by-name.

Most calculations require more than one contraction. We write e →βᵇ* e' for zero or more beta steps. “Zero or more” is important: every term reduces to itself.

\frac{}{e \to_\beta^* e} \qquad \frac{e \to_\beta^* e' \qquad e' \to_\beta e''}{e \to_\beta^* e''}

Finally, a term is in normal form if it has no possible beta step. Because we use full beta reduction, a lambda is normal only when its body is normal.

inductive BetaStep : DB -> DB -> Prop where | beta : BetaStep (.app (.lam body) arg) (subst 0 arg body) | app_left : BetaStep f f' -> BetaStep (.app f a) (.app f' a) | app_right : BetaStep a a' -> BetaStep (.app f a) (.app f a') | lam : BetaStep body body' -> BetaStep (.lam body) (.lam body')infix:50 " →βᵇ " => BetaStepabbrev BetaStar : DB -> DB -> Prop := Relation.ReflTransGen BetaStepinfix:50 " →βᵇ* " => BetaStardef Reducible (t : DB) : Prop := u, t →βᵇ u

1.5.1. Calculating with Beta Reduction🔗

For concrete calculations, beta_step constructs a single beta step and beta_steps searches for a finite sequence of beta steps.

syntax "beta_step" : tacticmacro_rules | `(tactic| beta_step) => `(tactic| repeat' constructor)syntax "beta_steps" : tacticsyntax "aux_beta_steps" : tacticmacro_rules | `(tactic| beta_steps) => `(tactic| first | exact Relation.ReflTransGen.refl | apply Relation.ReflTransGen.head aux_beta_steps) | `(tactic| aux_beta_steps) => `(tactic| first | apply BetaStep.beta; beta_steps | apply BetaStep.app_left; aux_beta_steps | apply BetaStep.app_right; aux_beta_steps | apply BetaStep.lam; aux_beta_steps)

The first calculation contracts an identity function. The second is the capture-avoidance example from substitution: the free name y remains free.

example : DB[(λ 0) x] →βᵇ DB[x] := (bound 0).lam.app (free "x") →βᵇ free "x" All goals completed! 🐙example : DB[(λ λ 1) y] →βᵇ DB[λ y] := (bound 1).lam.lam.app (free "y") →βᵇ (free "y").lam All goals completed! 🐙

The next term contains two redexes, so either may be selected first. Both choices can be continued to the same result; confluence will later show that such agreement can always be found.

example : DB[((λ 0) x) ((λ 0) y)] →βᵇ DB[x ((λ 0) y)] := ((bound 0).lam.app (free "x")).app ((bound 0).lam.app (free "y")) →βᵇ (free "x").app ((bound 0).lam.app (free "y")) All goals completed! 🐙example : DB[((λ 0) x) ((λ 0) y)] →βᵇ DB[((λ 0) x) y] := ((bound 0).lam.app (free "x")).app ((bound 0).lam.app (free "y")) →βᵇ ((bound 0).lam.app (free "x")).app (free "y") All goals completed! 🐙example : DB[((λ 0) x) ((λ 0) y)] →βᵇ* DB[x y] := ((bound 0).lam.app (free "x")).app ((bound 0).lam.app (free "y")) →βᵇ* (free "x").app (free "y") All goals completed! 🐙example : DB[((λ 0) x) ((λ 0) y)] →βᵇ* DB[x y] := ((bound 0).lam.app (free "x")).app ((bound 0).lam.app (free "y")) →βᵇ* (free "x").app (free "y") calc _ →βᵇ DB[x ((λ 0) y)] := ((bound 0).lam.app (free "x")).app ((bound 0).lam.app (free "y")) →βᵇ (free "x").app ((bound 0).lam.app (free "y")) All goals completed! 🐙 _ →βᵇ DB[x y] := (free "x").app ((bound 0).lam.app (free "y")) →βᵇ (free "x").app (free "y") All goals completed! 🐙

Multi-step reduction is preserved by lambda and application. The final lemma combines reduction inside a redex with contraction of that redex.

@[grind .] theorem BetaStar.lam (hsteps : body →βᵇ* body') : .lam body →βᵇ* .lam body' := body:DBbody':DBhsteps:body →βᵇ* body'body.lam →βᵇ* body'.lam body:DBbody':DBbody.lam →βᵇ* body.lambody:DBbody':DBb✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep body b✝a✝:b✝ →βᵇ c✝a_ih✝:body.lam →βᵇ* b✝.lambody.lam →βᵇ* c✝.lam body:DBbody':DBbody.lam →βᵇ* body.lambody:DBbody':DBb✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep body b✝a✝:b✝ →βᵇ c✝a_ih✝:body.lam →βᵇ* b✝.lambody.lam →βᵇ* c✝.lam All goals completed! 🐙@[grind .] theorem BetaStar.app : (fn →βᵇ* fn') (arg →βᵇ* arg') (.app fn arg →βᵇ* .app fn' arg') := fn:DBfn':DBarg:DBarg':DBfn →βᵇ* fn' arg →βᵇ* arg' fn.app arg →βᵇ* fn'.app arg' intro hfn fn:DBfn':DBarg:DBarg':DBhfn:fn →βᵇ* fn'harg:arg →βᵇ* arg'fn.app arg →βᵇ* fn'.app arg'; fn:DBfn':DBarg:DBarg':DBharg:arg →βᵇ* arg'fn.app arg →βᵇ* fn.app arg'fn:DBfn':DBarg:DBarg':DBharg:arg →βᵇ* arg'b✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep fn b✝a✝:b✝ →βᵇ c✝a_ih✝:fn.app arg →βᵇ* b✝.app arg'fn.app arg →βᵇ* c✝.app arg' fn:DBfn':DBarg:DBarg':DBharg:arg →βᵇ* arg'fn.app arg →βᵇ* fn.app arg'fn:DBfn':DBarg:DBarg':DBharg:arg →βᵇ* arg'b✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep fn b✝a✝:b✝ →βᵇ c✝a_ih✝:fn.app arg →βᵇ* b✝.app arg'fn.app arg →βᵇ* c✝.app arg' fn:DBfn':DBarg:DBarg':DBb✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep fn b✝a✝:b✝ →βᵇ c✝a_ih✝:fn.app arg →βᵇ* b✝.app argfn.app arg →βᵇ* c✝.app argfn:DBfn':DBarg:DBarg':DBb✝¹:DBc✝¹:DBa✝³:Relation.ReflTransGen BetaStep fn b✝a✝²:b✝ →βᵇ c✝b✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep arg b✝a✝:b✝ →βᵇ c✝a_ih✝¹:fn.app arg →βᵇ* b✝¹.app b✝ fn.app arg →βᵇ* c✝¹.app b✝a_ih✝:fn.app arg →βᵇ* b✝¹.app c✝fn.app arg →βᵇ* c✝¹.app c✝ fn:DBfn':DBarg:DBarg':DBfn.app arg →βᵇ* fn.app argfn:DBfn':DBarg:DBarg':DBb✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep arg b✝a✝:b✝ →βᵇ c✝a_ih✝:fn.app arg →βᵇ* fn.app b✝fn.app arg →βᵇ* fn.app c✝fn:DBfn':DBarg:DBarg':DBb✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep fn b✝a✝:b✝ →βᵇ c✝a_ih✝:fn.app arg →βᵇ* b✝.app argfn.app arg →βᵇ* c✝.app argfn:DBfn':DBarg:DBarg':DBb✝¹:DBc✝¹:DBa✝³:Relation.ReflTransGen BetaStep fn b✝a✝²:b✝ →βᵇ c✝b✝:DBc✝:DBa✝¹:Relation.ReflTransGen BetaStep arg b✝a✝:b✝ →βᵇ c✝a_ih✝¹:fn.app arg →βᵇ* b✝¹.app b✝ fn.app arg →βᵇ* c✝¹.app b✝a_ih✝:fn.app arg →βᵇ* b✝¹.app c✝fn.app arg →βᵇ* c✝¹.app c✝ All goals completed! 🐙@[grind .] theorem BetaStar.beta : (body →βᵇ* body') (arg →βᵇ* arg') .app (.lam body) arg →βᵇ* subst 0 arg' body' := body:DBbody':DBarg:DBarg':DBbody →βᵇ* body' arg →βᵇ* arg' body.lam.app arg →βᵇ* subst 0 arg' body' intro hbody body:DBbody':DBarg:DBarg':DBhbody:body →βᵇ* body'harg:arg →βᵇ* arg'body.lam.app arg →βᵇ* subst 0 arg' body'; calc _ = .app (.lam body) arg := body:DBbody':DBarg:DBarg':DBhbody:body →βᵇ* body'harg:arg →βᵇ* arg'body.lam.app arg = body.lam.app arg All goals completed! 🐙 _ →βᵇ* .app (.lam body') arg' := body:DBbody':DBarg:DBarg':DBhbody:body →βᵇ* body'harg:arg →βᵇ* arg'body.lam.app arg →βᵇ* body'.lam.app arg' All goals completed! 🐙 _ →βᵇ subst 0 arg' body' := body:DBbody':DBarg:DBarg':DBhbody:body →βᵇ* body'harg:arg →βᵇ* arg'body'.lam.app arg' →βᵇ subst 0 arg' body' All goals completed! 🐙

1.5.2. Normal and Neutral Terms🔗

A term is in normal form when no beta step is possible. We express this positively by describing the possible shapes of such terms. A neutral term is a variable, possibly applied to normal arguments. A normal term is either a lambda with a normal body or a neutral term.

\frac{}{n\ \mathsf{neutral}} \qquad \frac{}{x\ \mathsf{neutral}} \qquad \frac{e_1\ \mathsf{neutral}\qquad e_2\ \mathsf{normal}} {e_1\ e_2\ \mathsf{neutral}}

\frac{e\ \mathsf{normal}}{\lambda.\,e\ \mathsf{normal}} \qquad \frac{e\ \mathsf{neutral}}{e\ \mathsf{normal}}

mutual inductive Normal : DB Prop where | lam : Normal body Normal (.lam body) | neutral : Neutral t Normal t inductive Neutral : DB Prop where | bound : Neutral (.bound n) | free : Neutral (.free x) | app : Neutral fn Normal arg Neutral (.app fn arg) end

For concrete terms, normal repeatedly applies the rules defining normal and neutral terms.

syntax "normal" : tacticmacro_rules | `(tactic| normal) => `(tactic| repeat' constructor)example : Normal DB[x] := (free "x").Normal All goals completed! 🐙example : Normal DB[λ 0] := (bound 0).lam.Normal All goals completed! 🐙example : Normal DB[x (λ 0)] := ((free "x").app (bound 0).lam).Normal All goals completed! 🐙example : Neutral DB[x (λ 0)] := ((free "x").app (bound 0).lam).Neutral All goals completed! 🐙

1.5.3. Progress and Finality🔗

1.5.3.1. Progress🔗

Progress. Every term either takes a beta step or is normal. This is a property of lambda terms themselves; no typing assumption is needed.

theorem progress : Reducible t Normal t := t:DBt.Reducible t.Normal induction t with x:String(free x).Reducible (free x).Normal x:String(free x).Normal; have : Neutral (.free x) := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Normal (.free x) := x:Stringthis:(free x).Neutral(free x).Normal All goals completed! 🐙 n:(bound n).Reducible (bound n).Normal n:(bound n).Normal; have : Neutral (.bound n) := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Normal (.bound n) := n:this:(bound n).Neutral(bound n).Normal All goals completed! 🐙 body:DBih:body.Reducible body.Normalbody.lam.Reducible body.lam.Normal obtain _ | _ : (Reducible body) (Normal body) := body:DBih:body.Reducible body.Normalbody.Reducible body.Normal All goals completed! 🐙 body:DBih:body.Reducible body.Normalh✝:body.Reduciblebody.lam.Reducible body.lam.Normal body:DBih:body.Reducible body.Normalh✝:body.Reduciblebody.lam.Reducible; obtain_by body' : body →βᵇ body' := body:DBih:body.Reducible body.Normalh✝:body.Reducible body', body →βᵇ body' All goals completed! 🐙 have : .lam body →βᵇ .lam body' := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Reducible (.lam body) := body:DBih:body.Reducible body.Normalh✝¹:body.Reduciblebody':DBh✝:body →βᵇ body'this:body.lam →βᵇ body'.lambody.lam.Reducible body:DBih:body.Reducible body.Normalh✝¹:body.Reduciblebody':DBh✝:body →βᵇ body'this:body.lam →βᵇ body'.lambody.lam →βᵇ body'.lam; All goals completed! 🐙 body:DBih:body.Reducible body.Normalh✝:body.Normalbody.lam.Reducible body.lam.Normal body:DBih:body.Reducible body.Normalh✝:body.Normalbody.lam.Normal; have : Normal body := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Normal (.lam body) := body:DBih:body.Reducible body.Normalh✝:body.Normalthis:body.Normalbody.lam.Normal All goals completed! 🐙 fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normal(fn.app arg).Reducible (fn.app arg).Normal obtain _ | _ : (Reducible fn) (Normal fn) := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalfn.Reducible fn.Normal All goals completed! 🐙 fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:fn.Reducible(fn.app arg).Reducible (fn.app arg).Normal fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:fn.Reducible(fn.app arg).Reducible; obtain_by fn' : fn →βᵇ fn' := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:fn.Reducible fn', fn →βᵇ fn' All goals completed! 🐙 have : .app fn arg →βᵇ .app fn' arg := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Reducible (.app fn arg) := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Reduciblefn':DBh✝:fn →βᵇ fn'this:fn.app arg →βᵇ fn'.app arg(fn.app arg).Reducible fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Reduciblefn':DBh✝:fn →βᵇ fn'this:fn.app arg →βᵇ fn'.app argfn.app arg →βᵇ fn'.app arg; All goals completed! 🐙 obtain _ | _ : (Reducible arg) (Normal arg) := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:fn.Normalarg.Reducible arg.Normal All goals completed! 🐙 fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Normalh✝:arg.Reducible(fn.app arg).Reducible (fn.app arg).Normal fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Normalh✝:arg.Reducible(fn.app arg).Reducible; obtain_by arg' : arg →βᵇ arg' := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Normalh✝:arg.Reducible arg', arg →βᵇ arg' All goals completed! 🐙 have : .app fn arg →βᵇ .app fn arg' := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Reducible (.app fn arg) := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝²:fn.Normalh✝¹:arg.Reduciblearg':DBh✝:arg →βᵇ arg'this:fn.app arg →βᵇ fn.app arg'(fn.app arg).Reducible fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝²:fn.Normalh✝¹:arg.Reduciblearg':DBh✝:arg →βᵇ arg'this:fn.app arg →βᵇ fn.app arg'fn.app arg →βᵇ fn.app arg'; All goals completed! 🐙 fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝¹:fn.Normalh✝:arg.Normal(fn.app arg).Reducible (fn.app arg).Normal cases Normal fn with arg:DBiharg:arg.Reducible arg.Normalh✝:arg.Normalbody:DBa✝:body.Normalihfn:body.lam.Reducible body.lam.Normal(body.lam.app arg).Reducible (body.lam.app arg).Normal arg:DBiharg:arg.Reducible arg.Normalh✝:arg.Normalbody:DBa✝:body.Normalihfn:body.lam.Reducible body.lam.Normal(body.lam.app arg).Reducible; have : .app (.lam body) arg →βᵇ .subst 0 arg body := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Reducible (.app (.lam body) arg) := arg:DBiharg:arg.Reducible arg.Normalh✝:arg.Normalbody:DBa✝:body.Normalihfn:body.lam.Reducible body.lam.Normalthis:body.lam.app arg →βᵇ subst 0 arg body(body.lam.app arg).Reducible arg:DBiharg:arg.Reducible arg.Normalh✝:arg.Normalbody:DBa✝:body.Normalihfn:body.lam.Reducible body.lam.Normalthis:body.lam.app arg →βᵇ subst 0 arg bodybody.lam.app arg →βᵇ subst 0 arg body; All goals completed! 🐙 fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:arg.Normala✝:fn.Neutral(fn.app arg).Reducible (fn.app arg).Normal fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:arg.Normala✝:fn.Neutral(fn.app arg).Normal; have : Neutral (.app fn arg) := t:DBt.Reducible t.Normal All goals completed! 🐙 show : Normal (.app fn arg) := fn:DBarg:DBihfn:fn.Reducible fn.Normaliharg:arg.Reducible arg.Normalh✝:arg.Normala✝:fn.Neutralthis:(fn.app arg).Neutral(fn.app arg).Normal All goals completed! 🐙

1.5.3.2. Finality🔗

Finality of normal forms. A normal term is not reducible. The proof follows the structure of the term: variables cannot step, a lambda can only step in its normal body, and an application headed by a neutral term is not a redex and cannot step inside either of its already-final components.

@[grind .] theorem Neutral.not_lam : ¬ Neutral (.lam body) := body:DB¬body.lam.Neutral body:DBhneutral:body.lam.NeutralFalse; All goals completed! 🐙@[grind .] theorem finality (hnormal : Normal t) : ¬ Reducible t := t:DBhnormal:t.Normal¬t.Reducible t:DBhnormal:t.Normala✝:t.ReducibleFalse; obtain_by t' : t →βᵇ t' := t:DBhnormal:t.Normala✝:t.Reducible t', t →βᵇ t' All goals completed! 🐙 induction t generalizing t' with n:hnormal:(bound n).Normala✝:(bound n).Reduciblet':DBh✝:bound n →βᵇ t'False All goals completed! 🐙 x:Stringhnormal:(free x).Normala✝:(free x).Reduciblet':DBh✝:free x →βᵇ t'False All goals completed! 🐙 body:DBih:body.Normal body.Reducible (t' : DB), body →βᵇ t' Falsehnormal:body.lam.Normala✝:body.lam.Reduciblet':DBh✝:body.lam →βᵇ t'False cases Normal (.lam body) with body:DBih:body.Normal body.Reducible (t' : DB), body →βᵇ t' Falsea✝¹:body.lam.Reduciblet':DBh✝:body.lam →βᵇ t'a✝:body.lam.NeutralFalse All goals completed! 🐙 body:DBih:body.Normal body.Reducible (t' : DB), body →βᵇ t' Falsea✝¹:body.lam.Reduciblet':DBh✝:body.lam →βᵇ t'a✝:body.NormalFalse cases .lam body →βᵇ t' with body:DBih:body.Normal body.Reducible (t' : DB), body →βᵇ t' Falsea✝²:body.lam.Reduciblea✝¹:body.Normalbody':DBa✝:body →βᵇ body'False have : Reducible body := t:DBhnormal:t.Normal¬t.Reducible body:DBih:body.Normal body.Reducible (t' : DB), body →βᵇ t' Falsea✝²:body.lam.Reduciblea✝¹:body.Normalbody':DBa✝:body →βᵇ body'body →βᵇ body'; All goals completed! 🐙 All goals completed! 🐙 fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝:(fn.app arg).Reduciblet':DBh✝:fn.app arg →βᵇ t'False have : Normal (.app fn arg) := t:DBhnormal:t.Normal¬t.Reducible All goals completed! 🐙 have : Neutral (.app fn arg) := t:DBhnormal:t.Normal¬t.Reducible fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝¹:(fn.app arg).Reduciblet':DBh✝:fn.app arg →βᵇ t'a✝:(fn.app arg).Neutral(fn.app arg).Neutral; All goals completed! 🐙 have : Neutral fn Normal arg := t:DBhnormal:t.Normal¬t.Reducible fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝²:(fn.app arg).Reduciblet':DBh✝:fn.app arg →βᵇ t'this:(fn.app arg).Normala✝¹:fn.Neutrala✝:arg.Normalfn.Neutral arg.Normal; All goals completed! 🐙 cases .app fn arg →βᵇ t' with arg:DBarg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsebody✝:DBfn_ih✝:body✝.lam.Normal body✝.lam.Reducible (t' : DB), body✝.lam →βᵇ t' Falsehnormal:(body✝.lam.app arg).Normala✝:(body✝.lam.app arg).Reduciblethis✝¹:(body✝.lam.app arg).Normalthis✝:(body✝.lam.app arg).Neutralthis:body✝.lam.Neutral arg.NormalFalse All goals completed! 🐙 fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝¹:(fn.app arg).Reduciblethis✝¹:(fn.app arg).Normalthis✝:(fn.app arg).Neutralthis:fn.Neutral arg.Normalfn':DBa✝:fn →βᵇ fn'False have : Reducible fn := t:DBhnormal:t.Normal¬t.Reducible fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝¹:(fn.app arg).Reduciblethis✝¹:(fn.app arg).Normalthis✝:(fn.app arg).Neutralthis:fn.Neutral arg.Normalfn':DBa✝:fn →βᵇ fn'fn →βᵇ fn'; All goals completed! 🐙 All goals completed! 🐙 fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝¹:(fn.app arg).Reduciblethis✝¹:(fn.app arg).Normalthis✝:(fn.app arg).Neutralthis:fn.Neutral arg.Normalarg':DBa✝:arg →βᵇ arg'False have : Reducible arg := t:DBhnormal:t.Normal¬t.Reducible fn:DBarg:DBfn_ih✝:fn.Normal fn.Reducible (t' : DB), fn →βᵇ t' Falsearg_ih✝:arg.Normal arg.Reducible (t' : DB), arg →βᵇ t' Falsehnormal:(fn.app arg).Normala✝¹:(fn.app arg).Reduciblethis✝¹:(fn.app arg).Normalthis✝:(fn.app arg).Neutralthis:fn.Neutral arg.Normalarg':DBa✝:arg →βᵇ arg'arg →βᵇ arg'; All goals completed! 🐙 All goals completed! 🐙

1.5.3.3. Characterization of Normality🔗

The judgment Normal was defined by the shape of a term, while Reducible says that a beta step is possible. Progress and finality show that these two possibilities are exact complements.

theorem normal_iff_not_reducible : Normal t ¬ Reducible t := t:DBt.Normal ¬t.Reducible t:DBt.Normal ¬t.Reduciblet:DB¬t.Reducible t.Normal t:DBt.Normal ¬t.Reducible All goals completed! 🐙 t:DB¬t.Reducible t.Normal All goals completed! 🐙end DBend Untyped