Type Systems

5. Stlc: The Simply Typed Lambda-Calculus🔗

The simply typed lambda-calculus (STLC) is a tiny core calculus embodying the key concept of functional abstraction. This concept shows up in pretty much every real-world programming language in some form (functions, procedures, methods, etc.).

We will follow exactly the same pattern as in the previous chapter when formalizing this calculus (syntax, small-step semantics, typing rules) and its main properties (progress and preservation). The new technical challenges arise from the mechanisms of variable binding and substitution. It will take some work to deal with these.

The STLC lives in the lower-left front corner of the famous lambda cube (also called the Barendregt Cube), which visualizes three sets of features that can be added to its simple core:

STLC dependent types polymorphism type operators Calculus of Constructions

Moving from bottom to top in the cube corresponds to adding polymorphic types like ∀ α : Type, α → α. Adding just polymorphism gives us the famous Girard-Reynolds calculus, System F.

Moving from front to back corresponds to adding type operators like List.

Moving from left to right corresponds to adding dependent types like ∀ n, ArrayOfSize n.

The top right corner on the back, which combines all three features, is called the Calculus of Constructions. First studied by Coquand and Huet, it forms the foundation of Lean's logic.

5.1. Overview🔗

The STLC is built on some collection of base types: booleans, numbers, strings, etc. The exact choice of base types doesn't matter much — the definition of the language as well as its theoretical properties work out the same no matter what we choose — so for the sake of brevity let's take just Bool for the moment. In the next chapter we'll see how to add more base types, and in later chapters we'll enrich the pure STLC with other useful constructs like pairs, records, subtyping, and mutable state.

Starting from boolean constants and conditionals, we add three things:

  • variables

  • function abstractions

  • application

This gives us the following collection of abstract syntax constructors (written out first in informal BNF notation — we'll formalize it below) for STLC terms t.

t::=x(variable)
|λ x : T . t(abstraction)
|t t(application)
|true(constant true)
|false(constant false)
|if t then t else t(conditional)

The Greek letter λ ("lambda") in a function abstraction λx:T. t is what gives the calculus its name. The variable x is called the parameter to the function; the term t is its body. The annotation :T specifies the type of arguments that the function can be applied to.

The types of the STLC include Bool, which classifies the boolean constants true and false as well as more complex computations that yield booleans, plus arrow types that classify functions (as is the case in Lean).

T::=Bool
|T → T

Some examples of STLC terms:

λx:Bool. x

The identity function for booleans.

(λx:Bool. x) true

The identity function for booleans, applied to the boolean true.

λx:Bool. if x then false else true

The boolean "not" function.

λx:Bool. true

The constant function that takes every (boolean) argument to true.

λx:Bool. λy:Bool. x

A two-argument function that takes two booleans and returns the first one.

(As in Lean, a two-argument function in the lambda-calculus is really a one-argument function whose body is also a one-argument function.)

(λx:Bool. λy:Bool. x) false true

A two-argument function that takes two booleans and returns the first one, applied to the booleans false and true.

(As in Lean, application associates to the left — i.e., this expression is parsed as ((λx:Bool. λy:Bool. x) false) true.)

λf:Bool → Bool. f (f true)

A higher-order function that takes a function f (from booleans to booleans) as an argument, applies f to true, and applies f again to the result.

(λf:Bool → Bool. f (f true)) (λx:Bool. false)

The same higher-order function, applied to the constantly false function.

The last two examples show, the STLC is a language of higher-order functions: we can write down functions that take other functions as arguments and/or return other functions as results.

The STLC doesn't provide any primitive syntax for defining named functions: i.e., all functions are "anonymous." We'll see in chapter MoreStlc that it is easy to add named functions — indeed, the fundamental naming and binding mechanisms are exactly the same.

Now reconsider our examples, each along with its type:

  • λx:Bool. x has type Bool → Bool

  • (λx:Bool. x) true has type Bool

  • λx:Bool. if x then false else true has type Bool → Bool

  • λx:Bool. true has type Bool → Bool

  • λx:Bool. λy:Bool. x has type Bool → Bool → Bool (i.e., Bool → (Bool → Bool))

  • (λx:Bool. λy:Bool. x) false true has type Bool

The last two, higher-order examples are left off the list on purpose — working out their types is the subject of the quizzes that follow.

Quiz

What is the type of the following term?

λf:Bool → Bool. f (f true)

(A) Bool → (Bool → Bool)

(B) (Bool → Bool) → Bool

(C) Bool → Bool

(D) Bool

(E) none of the above

Quiz

How about the type of this one?

(λf:Bool → Bool. f (f true)) (λx:Bool. false)

(A) Bool → (Bool → Bool)

(B) (Bool → Bool) → Bool

(C) Bool → Bool

(D) Bool

(E) none of the above

5.2. Syntax🔗

We next formalize the syntax of the STLC.

namespace Stlc open scoped MyGetElem

5.2.1. Types🔗

inductive Ty where | bool | arrow (T₁ T₂ : Ty)

5.2.2. Terms🔗

inductive Tm where | var (x : String) | app (t₁ t₂ : Tm) | abs (x : String) (T : Ty) (t : Tm) | tru | fls | ite (c t e : Tm)

We need some notation magic to set up the concrete syntax, as we did in the Types chapter...

The upshot of this section is that STLC types and terms are both written inside one pair of brackets, <{ … }>, and that ~e inside the brackets escapes back to an arbitrary Lean expression:

  • <{ Bool → Bool }> is a type;

  • <{ λ x : Bool . x }> is a term — a bare identifier inside the brackets is the object-language variable of that name, so <{ x }> is the variable x;

  • <{ ~t₁ ~t₂ }> applies one Lean-level term to another.

Lean works out from context which of the two a given bracket holds, so the same brackets serve for types, for terms, and — when we come to typing — for typing judgments too. How that works is in the collapsed blocks below; nothing later in the chapter depends on it.

Notation encoding: types

The stlcTy grammar covers Bool, arrows (written → or ->, associating to the right), parentheses, and ~e. A bare identifier other than Bool is spliced in as a Lean term, so a local T — or any Lean expression of type Ty — can appear directly inside the brackets.

To extend the grammar, a later chapter adds a syntax line to the category and a matching macro_rules case; that is all it takes to add a new type construct.

declare_syntax_cat stlcTy syntax:max "~" term:max : stlcTy syntax:max "(" stlcTy ")" : stlcTy syntax:max ident : stlcTy syntax:50 stlcTy:51 " → " stlcTy:50 : stlcTy syntax:50 stlcTy:51 " -> " stlcTy:50 : stlcTy syntax:max (name := tyBracket) "<{ " stlcTy " }>" : term macro_rules (kind := tyBracket) | `(<{ ~$T:term }>) => pure T | `(<{ ($T:stlcTy) }>) => `(<{ $T:stlcTy }>) | `(<{ $x:ident }>) => match x.getId.toString with | "Bool" => `(Ty.bool) | _ => `(($x : Ty)) | `(<{ $T₁:stlcTy → $T₂:stlcTy }>) => `(Ty.arrow <{ $T₁:stlcTy }> <{ $T₂:stlcTy }>) | `(<{ $T₁:stlcTy -> $T₂:stlcTy }>) => `(Ty.arrow <{ $T₁:stlcTy }> <{ $T₂:stlcTy }>)

We'll write types inside of <{ ... }> brackets:

Ty.bool : Ty#check <{ Bool }> Ty.bool.arrow Ty.bool : Ty#check <{ Bool -> Bool }> (Ty.bool.arrow Ty.bool).arrow Ty.bool : Ty#check <{ (Bool -> Bool) -> Bool }>
Notation encoding: terms

Terms are built from variables, application (associating to the left), abstraction, the two boolean constants, and conditionals. A binding occurrence — the x in λ x : T . t — has a small grammar of its own, stlcVar, and varStr turns it into the string that Tm.abs stores.

Because types and terms share the brackets, each macro_rules group says which bracket it belongs to (kind := tyBracket, kind := tmBracket), and each antiquote in a nested quotation says which grammar it came from. A bare identifier is the one genuinely overlapping case: Bool in term position would otherwise quietly become a variable named Bool, so that rule rejects it, which also settles which grammar a lone <{ Bool }> belongs to.

The last production, [x := s] t, is the notation for substitution; we give it its meaning when we define substitution below. It binds tighter than application, so [x:=s] t₁ t₂ is the application of [x:=s] t₁ to t₂, and a λ or if body must be parenthesized: [x:=s] (λ y : Bool . x).

declare_syntax_cat stlcVar syntax:max ident : stlcVar syntax:max "~" term:max : stlcVar open Lean in /-- The string named by a variable in binding position. -/ def varStr (x : TSyntax `stlcVar) : MacroM Term := match x with | `(stlcVar| $i:ident) => pure (quote i.getId.toString : Term) | `(stlcVar| ~$e) => pure e | _ => Macro.throwUnsupported declare_syntax_cat stlcTm syntax:max "~" term:max : stlcTm syntax:max "(" stlcTm ")" : stlcTm syntax:max ident : stlcTm syntax:75 stlcTm:75 ppSpace stlcTm:76 : stlcTm syntax:50 "λ " stlcVar " : " stlcTy " . " stlcTm:50 : stlcTm syntax:50 "if " stlcTm:51 " then " stlcTm:50 " else " stlcTm:50 : stlcTm syntax:max "[" stlcVar " := " stlcTm "] " stlcTm:max : stlcTm syntax:max (name := tmBracket) "<{ " stlcTm " }>" : term open Lean in macro_rules (kind := tmBracket) | `(<{ ~$e:term }>) => pure e | `(<{ ($t:stlcTm) }>) => `(<{ $t:stlcTm }>) | `(<{ $x:ident }>) => match x.getId.toString with | "true" => `(Tm.tru) | "false" => `(Tm.fls) | "Bool" => Macro.throwErrorAt x "`Bool` is a type, not a term" | _ => `(Tm.var $(quote x.getId.toString)) | `(<{ $t₁:stlcTm $t₂:stlcTm }>) => `(Tm.app <{ $t₁:stlcTm }> <{ $t₂:stlcTm }>) | `(<{ λ $x : $T . $t }>) => do `(Tm.abs $(← varStr x) <{ $T:stlcTy }> <{ $t:stlcTm }>) | `(<{ if $c then $t else $e }>) => `(Tm.ite <{ $c:stlcTm }> <{ $t:stlcTm }> <{ $e:stlcTm }>)
Notation encoding: printing it back

A delaborator runs the grammar backwards: it rebuilds the concrete syntax from a Ty or Tm value, so that types and terms appearing in goals and in #check output print as <{ λ x : Bool . x }> rather than as a pile of constructors. (Setting pp.notation false turns it off, revealing the underlying representation.)

open Lean PrettyPrinter Delaborator SubExpr Parenthesizer in /-- Re-inserts parentheses in `stlcTy` output according to the grammar's precedences. -/ @[category_parenthesizer stlcTy] def stlcTy.parenthesizer : CategoryParenthesizer | prec => do maybeParenthesize `stlcTy true wrapParens prec <| parenthesizeCategoryCore `stlcTy prec where wrapParens (stx : Syntax) : Syntax := Unhygienic.run do let pstx ← `(stlcTy| ($(⟨stx⟩))) return pstx.raw.setInfo (SourceInfo.fromRef stx) open Lean PrettyPrinter Delaborator SubExpr Parenthesizer in /-- Re-inserts parentheses in `stlcTm` output according to the grammar's precedences. -/ @[category_parenthesizer stlcTm] def stlcTm.parenthesizer : CategoryParenthesizer | prec => do maybeParenthesize `stlcTm true wrapParens prec <| parenthesizeCategoryCore `stlcTm prec where wrapParens (stx : Syntax) : Syntax := Unhygienic.run do let pstx ← `(stlcTm| ($(⟨stx⟩))) return pstx.raw.setInfo (SourceInfo.fromRef stx) open Lean PrettyPrinter Delaborator SubExpr in /-- Rebuild `stlcTy` concrete syntax from a `Ty` value. -/ partial def delabTyInner : DelabM (TSyntax `stlcTy) := do let stx ← match_expr ← getExpr with | Ty.bool => `(stlcTy| $(mkIdent `Bool):ident) | Ty.arrow _ _ => do let a ← withAppFn <| withAppArg delabTyInner let b ← withAppArg delabTyInner `(stlcTy| $a → $b) | _ => do match ← delab with | `($i:ident) => `(stlcTy| $i:ident) | e => `(stlcTy| ~$e) (⟨·⟩) <$> annotateTermInfo ⟨stx.raw⟩ open Lean in /-- Is `s` usable as a bare identifier in the object syntax? -/ def isPlainName (s : String) : Bool := !s.isEmpty && s != "_" && !s.front.isDigit && s.all fun c => c.isAlphanum || c == '_' open Lean in /-- Is `s` usable as a bare variable in `stlcTm` rather than as reserved syntax? -/ def isPlainTmVarName (s : String) : Bool := isPlainName s && s != "true" && s != "false" && s != "Bool" open Lean PrettyPrinter Delaborator SubExpr in /-- Rebuild `stlcVar` concrete syntax from the string in a binding position. -/ def delabVarInner : DelabM (TSyntax `stlcVar) := do match ← delab with | `($s:str) => if isPlainName s.getString then `(stlcVar| $(mkIdent (Name.mkSimple s.getString)):ident) else `(stlcVar| ~$s) | e => `(stlcVar| ~$e) open Lean PrettyPrinter Delaborator SubExpr in /-- Rebuild `stlcTm` concrete syntax from a `Tm` value. -/ partial def delabTmInner : DelabM (TSyntax `stlcTm) := do let stx ← match_expr ← getExpr with | Tm.tru => `(stlcTm| $(mkIdent `true):ident) | Tm.fls => `(stlcTm| $(mkIdent `false):ident) | Tm.var _ => do let x ← withAppArg delab match x with | `($s:str) => if isPlainTmVarName s.getString then `(stlcTm| $(mkIdent (Name.mkSimple s.getString)):ident) else let var : Term := mkIdent ``Stlc.Tm.var `(stlcTm| ~($var $x)) | _ => let var : Term := mkIdent ``Stlc.Tm.var `(stlcTm| ~($var $x)) | Tm.app _ _ => do let f ← withAppFn <| withAppArg delabTmInner let a ← withAppArg delabTmInner `(stlcTm| $f $a) | Tm.abs _ _ _ => do let x ← withAppFn <| withAppFn <| withAppArg delabVarInner let T ← withAppFn <| withAppArg delabTyInner let t ← withAppArg delabTmInner `(stlcTm| λ $x : $T . $t) | Tm.ite _ _ _ => do let c ← withAppFn <| withAppFn <| withAppArg delabTmInner let t ← withAppFn <| withAppArg delabTmInner let e ← withAppArg delabTmInner `(stlcTm| if $c then $t else $e) | _ => do -- `subst` is defined below, so it is matched by name rather than with -- `match_expr`; a substitution prints in its own bracket notation. let e ← getExpr if e.getAppFn.constName? == some `Stlc.subst && e.getAppNumArgs == 3 then let x ← withAppFn <| withAppFn <| withAppArg delabVarInner let s ← withAppFn <| withAppArg delabTmInner let t ← withAppArg delabTmInner `(stlcTm| [$x := $s] $t) else match ← delab with | `($i:ident) => `(stlcTm| $i:ident) | e => `(stlcTm| ~$e) (⟨·⟩) <$> annotateTermInfo ⟨stx.raw⟩ open Lean PrettyPrinter Delaborator SubExpr in @[delab app.Stlc.Ty.bool, delab app.Stlc.Ty.arrow] def delabTy : Delab := whenPPOption getPPNotation do guard <| match_expr ← getExpr with | Ty.bool => true | Ty.arrow _ _ => true | _ => false match ← delabTyInner with | `(stlcTy| ~$e) => pure e | e => `(<{ $e:stlcTy }>) open Lean PrettyPrinter Delaborator SubExpr in @[delab app.Stlc.Tm.var, delab app.Stlc.Tm.app, delab app.Stlc.Tm.abs, delab app.Stlc.Tm.tru, delab app.Stlc.Tm.fls, delab app.Stlc.Tm.ite] def delabTm : Delab := whenPPOption getPPNotation do guard <| match_expr ← getExpr with | Tm.var _ => true | Tm.app _ _ => true | Tm.abs _ _ _ => true | Tm.tru => true | Tm.fls => true | Tm.ite _ _ _ => true | _ => false match ← delabTmInner with | `(stlcTm| ~($e)) => pure e | `(stlcTm| ~$e) => pure e | e => `(<{ $e:stlcTm }>)

A few checks that the grammar parses the way it should — application associating to the left, conditionals nesting without parentheses, and ~ escaping to Lean:

Here are the terms we will use as running examples, written in the new notation:

abbrev idB := <{ λ x : Bool . x }> abbrev idBB := <{ λ x : Bool → Bool . x }> abbrev idBBBB := <{ λ x : (Bool → Bool) → (Bool → Bool) . x }> abbrev k := <{ λ x : Bool . λ y : Bool . x }> abbrev notB := <{ λ x : Bool . if x then false else true }>

Note that an abstraction λ x : T . t (formally, Tm.abs applied to x, T, and t) is always annotated with the type T of its parameter, in contrast to Lean (and other functional languages like ML, Haskell, etc.), which use type inference to fill in missing annotations. We're not considering type inference at all here.

5.3. Operational Semantics🔗

To define the small-step semantics of STLC terms, we begin, as always, by defining the set of values. Next, we define the critical notions of free variables and substitution, which are used in the reduction rule for application expressions. And finally we give the small-step relation itself.

5.3.1. Values🔗

To define the values of the STLC, we have a few cases to consider.

First, for the boolean part of the language, the situation is clear: true and false are the only values. An if expression is never a value.

Second, an application is not a value: it represents a function being invoked on some argument, which clearly still has work left to do.

Third, for abstractions, we have a choice:

  • We can say that λx:T. t is a value only when t is a value — i.e., only if the function's body has been reduced (as much as it can be without knowing what argument it is going to be applied to).

  • Or we can say that λx:T. t is always a value, no matter whether t is one or not — in other words, we can say that reduction stops at abstractions.

Our usual way of evaluating expressions in Lean makes the first choice — for example,

fun _x => 7#reduce fun _x : Bool => 3 + 4

yields:

fun _x => 7

But Lean is rather unusual in this respect. Most functional programming languages make the second choice — reduction of a function's body only begins when the function is actually applied to an argument.

We also make the second choice here.

inductive Tm.IsValue : Tm → Prop where | abs (x : String) (T₂ : Ty) (t₁ : Tm) : Tm.IsValue <{ λ ~x : ~T₂ . ~t₁ }> | tru : Tm.IsValue <{ true }> | fls : Tm.IsValue <{ false }> attribute [StlcEval] Tm.IsValue.abs Tm.IsValue.tru Tm.IsValue.fls

The example terms named above are all abstractions, hence all values. We record that once each, so that the reduction examples can cite the fact by name instead of unfolding the definition again at every use.

theorem idB_value : idB.IsValue := .abs .. theorem idBB_value : idBB.IsValue := .abs .. theorem notB_value : notB.IsValue := .abs ..
Note to developers

The Rocq source follows each inductive definition in this chapter with a Hint Constructors … : core, registering the constructors with auto; the proofs then lean on auto/eauto to assemble derivations. We have no counterpart here: the proofs below name their constructors explicitly, in the style of the Types chapter. Lean's grind would be the closest analogue if a later pass wants automation.

5.3.2. STLC Programs🔗

Finally, we must consider what constitutes a complete program.

Intuitively, a "complete program" must not refer to any undefined variables. We'll see shortly how to define the free variables in a STLC term. A complete program, then, is one that is closed — that is, that contains no free variables.

(Conversely, a term that may contain free variables is often called an open term.)

Note to developers (Chris Henson @chenson2018, before next release)

Is the "shortly" above setting wrong expectations? Where exactly are we defining the free variables in a STLC term? BCP 25: Indeed, we need to define "free"!

Having made the choice not to reduce under abstractions, we don't need to worry about whether variables are values, since we'll always be reducing programs "from the outside in," and that means the step relation will always be working with closed terms.

5.3.3. Substitution🔗

Now we come to the heart of the STLC: the operation of substituting one term for a variable in another term. This operation is used below to define the operational semantics of function application, where we will need to substitute the argument term for the function parameter in the function's body. For example, we reduce

(λx:Bool. if x then true else x) false

to

if false then true else false

by substituting false for the parameter x in the body of the function.

In general, we need to be able to substitute some given term s for occurrences of some variable x in another term t. Informally, this is written [x:=s]t and pronounced "substitute s for x in t."

Here are some examples:

  • [x:=true] (if x then true else false) yields if true then true else false

  • [x:=true] x yields true

  • [x:=true] (if x then x else y) yields if true then true else y

  • [x:=true] y yields y

  • [x:=true] false yields false (vacuous substitution)

  • [x:=true] (λy:Bool. if y then x else false) yields λy:Bool. if y then true else false

  • [x:=true] (λy:Bool. x) yields λy:Bool. true

  • [x:=true] (λy:Bool. y) yields λy:Bool. y

  • [x:=true] (λx:Bool. x) yields λx:Bool. x

The last example is illuminating: substituting x with true in λx:Bool. x does not yield λx:Bool. true! The reason for this is that the x in the body of λx:Bool. x is bound by the abstraction: it is a new, local name that just happens to be spelled the same as some global name x.

Here is the definition, informally...

[x:=s]x               = s
[x:=s]y               = y                     if x ≠ y
[x:=s](λx:T. t)       = λx:T. t
[x:=s](λy:T. t)       = λy:T. [x:=s]t         if x ≠ y
[x:=s](t₁ t₂)         = ([x:=s]t₁) ([x:=s]t₂)
[x:=s]true            = true
[x:=s]false           = false
[x:=s](if t₁ then t₂ else t₃) =
                if [x:=s]t₁ then [x:=s]t₂ else [x:=s]t₃

... and formally:

section set_option hygiene false in local macro_rules (kind := tmBracket) | `(<{ [$x := $s] $t }>) => do `(subst $(← varStr x) <{ $s:stlcTm }> <{ $t:stlcTm }>) def subst (x : String) (s : Tm) (t : Tm) : Tm := match t with -- `.var y`, not `<{ ~y }>`: `y` is the variable's *name*, a `String` -- (see the note below the definition). | .var y => if x = y then s else t | <{ λ ~y : ~T . ~t₁ }> => if x = y then t else <{ λ ~y : ~T . [~x := ~s] ~t₁ }> | <{ ~t₁ ~t₂ }> => <{ ([~x := ~s] ~t₁) ([~x := ~s] ~t₂) }> | <{ true }> => <{ true }> | <{ false }> => <{ false }> | <{ if ~t₁ then ~t₂ else ~t₃ }> => <{ if [~x := ~s] ~t₁ then [~x := ~s] ~t₂ else [~x := ~s] ~t₃ }> end macro_rules (kind := tmBracket) | `(<{ [$x := $s] $t }>) => do `(subst $(← varStr x) <{ $s:stlcTm }> <{ $t:stlcTm }>)
Notation encoding: substitution

One more line registers substitutions with the printer, so that a goal mentioning one reads as [x := s] t rather than as a subst application.

open Lean PrettyPrinter Delaborator SubExpr in @[delab app.Stlc.subst] def delabSubst : Delab := whenPPOption getPPNotation do match ← delabTmInner with | `(stlcTm| ~$e) => pure e | e => `(<{ $e:stlcTm }>)

As we did for the evaluators in the Slang chapter, we pair the definition with one simplification lemma per constructor, saying how subst behaves on that constructor. The variable and abstraction cases each need two lemmas, since substitution treats a bound name differently depending on whether it is the name being substituted for.

variable (x y : String) (s t t₁ t₂ t₃ : Tm) (T : Ty) @[simp] theorem subst_var_eq : <{ [~x := ~s] ~(Tm.var x) }> = s := x:Strings:Tm⊢ <{ [~x := s] ~(Stlc.Tm.var x) }> = s All goals completed! 🐙 @[simp] theorem subst_var_ne (h : x ≠ y) : <{ [~x := ~s] ~(Tm.var y) }> = .var y := x:Stringy:Strings:Tmh:x ≠ y⊢ <{ [~x := s] ~(Stlc.Tm.var y) }> = Stlc.Tm.var y All goals completed! 🐙 @[simp] theorem subst_abs_eq : <{ [~x := ~s] (λ ~x : ~T . ~t) }> = <{ λ ~x : ~T . ~t }> := x:Strings:Tmt:TmT:Ty⊢ <{ [~x := s] (λ ~x : T . t) }> = <{ λ ~x : T . t }> All goals completed! 🐙 @[simp] theorem subst_abs_ne (h : x ≠ y) : <{ [~x := ~s] (λ ~y : ~T . ~t) }> = <{ λ ~y : ~T . [~x := ~s] ~t }> := x:Stringy:Strings:Tmt:TmT:Tyh:x ≠ y⊢ <{ [~x := s] (λ ~y : T . t) }> = <{ λ ~y : T . [~x := s] t }> All goals completed! 🐙 @[simp] theorem subst_app : <{ [~x := ~s] (~t₁ ~t₂) }> = <{ ([~x := ~s] ~t₁) ([~x := ~s] ~t₂) }> := rfl @[simp] theorem subst_tru : <{ [~x := ~s] true }> = <{ true }> := rfl @[simp] theorem subst_fls : <{ [~x := ~s] false }> = <{ false }> := rfl @[simp] theorem subst_ite : <{ [~x := ~s] (if ~t₁ then ~t₂ else ~t₃) }> = <{ if [~x := ~s] ~t₁ then [~x := ~s] ~t₂ else [~x := ~s] ~t₃ }> := rfl

Checks that the substitution notation parses and nests as intended.

Quiz

What is the result of the following substitution?

[x:=s](λy:T₁. x (λx:T₂. x))

(1) (λy:T₁. x (λx:T₂. x))

(2) (λy:T₁. s (λx:T₂. s))

(3) (λy:T₁. s (λx:T₂. x))

(4) none of the above

Technical note: Substitution becomes trickier to define if we consider the case where s, the term being substituted for a variable in some other term, may itself contain free variables. We say that s is an open term.

Here is an example. Using the above definition to substitute the open term

s = λx:Bool. r

(where r is a free reference to some global resource) for the free variable z in the term

t = λr:Bool. z

where r is a bound variable, we would get

λr:Bool. λx:Bool. r

where the free reference to r in s has been "captured" by the binder at the beginning of t.

Why would this be bad? Because it violates the principle that the names of bound variables do not matter. For example, if we rename the bound variable in t, e.g., let

t' = λw:Bool. z

then [z:=s]t' is

λw:Bool. λx:Bool. r

which does not behave the same as the substituting in the original t:

[z:=s]t = λr:Bool. λx:Bool. r

That is, renaming a bound variable in t would change how t behaves under our simple substitution. So substitution gets more complicated in that setting, but fortunately we don't have that problem in our STLC variant.

Fortunately, since we are only interested here in defining the step relation on closed terms (i.e., terms like λx:Bool. x that include binders for all of the variables they mention), we can sidestep this extra complexity, but it must be dealt with when formalizing richer languages.

Exercise★★★(substi_correct)

The definition that we gave above defines substitution as a function. Suppose, instead, we wanted to define substitution as an inductive relation Substi. We've begun the definition by providing the inductive header and one of the constructors; your job is to fill in the rest of the constructors and prove that the relation you've defined coincides with the function given above.

inductive Substi (s : Tm) (x : String) : Tm → Tm → Prop where | var1 : Substi s x (.var x) s | var2 (x' : String) (h : x ≠ x') : Substi s x (.var x') (.var x') | abs1 (T₂ : Ty) (t₁ : Tm) : Substi s x <{ λ ~x : ~T₂ . ~t₁ }> <{ λ ~x : ~T₂ . ~t₁ }> | abs2 (x' : String) (T₁ : Ty) (t₁ t₁' : Tm) (hx : x ≠ x') (h : Substi s x t₁ t₁') : Substi s x <{ λ ~x' : ~T₁ . ~t₁ }> <{ λ ~x' : ~T₁ . ~t₁' }> | app (t₁ t₂ t₁' t₂' : Tm) (h₁ : Substi s x t₁ t₁') (h₂ : Substi s x t₂ t₂') : Substi s x <{ ~t₁ ~t₂ }> <{ ~t₁' ~t₂' }> | tru : Substi s x <{ true }> <{ true }> | fls : Substi s x <{ false }> <{ false }> | ite (t₁ t₂ t₃ t₁' t₂' t₃' : Tm) (h₁ : Substi s x t₁ t₁') (h₂ : Substi s x t₂ t₂') (h₃ : Substi s x t₃ t₃') : Substi s x <{ if ~t₁ then ~t₂ else ~t₃ }> <{ if ~t₁' then ~t₂' else ~t₃' }> theorem substi_correct (s : Tm) (x : String) (t t' : Tm) : <{ [~x := ~s] ~t }> = t' ↔ Substi s x t t' := s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] t }> = t' ↔ Substi s x t t' solution! s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] t }> = t' → Substi s x t t's:Tmx:Stringt:Tmt':Tm⊢ Substi s x t t' → <{ [~x := s] t }> = t' s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] t }> = t' → Substi s x t t' -- → s:Tmx:Stringt:Tmt':Tmh:<{ [~x := s] t }> = t'⊢ Substi s x t t' s:Tmx:Stringt:Tm⊢ Substi s x t <{ [~x := s] t }> induction t with s:Tmx:Stringy:String⊢ Substi s x (Stlc.Tm.var y) <{ [~x := s] ~(Stlc.Tm.var y) }> s:Tmx:Stringy:Stringhxy:x = y⊢ Substi s x (Stlc.Tm.var y) <{ [~x := s] ~(Stlc.Tm.var y) }>s:Tmx:Stringy:Stringhxy:¬x = y⊢ Substi s x (Stlc.Tm.var y) <{ [~x := s] ~(Stlc.Tm.var y) }> s:Tmx:Stringy:Stringhxy:x = y⊢ Substi s x (Stlc.Tm.var y) <{ [~x := s] ~(Stlc.Tm.var y) }> s:Tmx:String⊢ Substi s x (Stlc.Tm.var x) <{ [~x := s] ~(Stlc.Tm.var x) }>; s:Tmx:String⊢ Substi s x (Stlc.Tm.var x) s; All goals completed! 🐙 s:Tmx:Stringy:Stringhxy:¬x = y⊢ Substi s x (Stlc.Tm.var y) <{ [~x := s] ~(Stlc.Tm.var y) }> s:Tmx:Stringy:Stringhxy:¬x = y⊢ Substi s x (Stlc.Tm.var y) (Stlc.Tm.var y); All goals completed! 🐙 s:Tmx:Stringt₁:Tmt₂:Tmih₁:Substi s x t₁ <{ [~x := s] t₁ }>ih₂:Substi s x t₂ <{ [~x := s] t₂ }>⊢ Substi s x <{ t₁ t₂ }> <{ [~x := s] (t₁ t₂) }> All goals completed! 🐙 s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>⊢ Substi s x <{ λ ~y : T . t₁ }> <{ [~x := s] (λ ~y : T . t₁) }> s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>hxy:x = y⊢ Substi s x <{ λ ~y : T . t₁ }> <{ [~x := s] (λ ~y : T . t₁) }>s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>hxy:¬x = y⊢ Substi s x <{ λ ~y : T . t₁ }> <{ [~x := s] (λ ~y : T . t₁) }> s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>hxy:x = y⊢ Substi s x <{ λ ~y : T . t₁ }> <{ [~x := s] (λ ~y : T . t₁) }> s:Tmx:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>⊢ Substi s x <{ λ ~x : T . t₁ }> <{ [~x := s] (λ ~x : T . t₁) }>; s:Tmx:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>⊢ Substi s x <{ λ ~x : T . t₁ }> <{ λ ~x : T . t₁ }>; All goals completed! 🐙 s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>hxy:¬x = y⊢ Substi s x <{ λ ~y : T . t₁ }> <{ [~x := s] (λ ~y : T . t₁) }> s:Tmx:Stringy:StringT:Tyt₁:Tmih:Substi s x t₁ <{ [~x := s] t₁ }>hxy:¬x = y⊢ Substi s x <{ λ ~y : T . t₁ }> <{ λ ~y : T . [~x := s] t₁ }>; All goals completed! 🐙 s:Tmx:String⊢ Substi s x <{ true }> <{ [~x := s] true }> All goals completed! 🐙 s:Tmx:String⊢ Substi s x <{ false }> <{ [~x := s] false }> All goals completed! 🐙 s:Tmx:Stringt₁:Tmt₂:Tmt₃:Tmih₁:Substi s x t₁ <{ [~x := s] t₁ }>ih₂:Substi s x t₂ <{ [~x := s] t₂ }>ih₃:Substi s x t₃ <{ [~x := s] t₃ }>⊢ Substi s x <{ if t₁ then t₂ else t₃ }> <{ [~x := s] (if t₁ then t₂ else t₃) }> All goals completed! 🐙 s:Tmx:Stringt:Tmt':Tm⊢ Substi s x t t' → <{ [~x := s] t }> = t' -- ← s:Tmx:Stringt:Tmt':Tmh:Substi s x t t'⊢ <{ [~x := s] t }> = t' s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] ~(Stlc.Tm.var x) }> = ss:Tmx:Stringt:Tmt':Tmx'✝:Stringh✝:x ≠ x'✝⊢ <{ [~x := s] ~(Stlc.Tm.var x'✝) }> = Stlc.Tm.var x'✝s:Tmx:Stringt:Tmt':TmT₂✝:Tyt₁✝:Tm⊢ <{ [~x := s] (λ ~x : T₂✝ . t₁✝) }> = <{ λ ~x : T₂✝ . t₁✝ }>s:Tmx:Stringt:Tmt':Tmx'✝:StringT₁✝:Tyt₁✝:Tmt₁'✝:Tmhx✝:x ≠ x'✝h✝:Substi s x t₁✝ t₁'✝h_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝⊢ <{ [~x := s] (λ ~x'✝ : T₁✝ . t₁✝) }> = <{ λ ~x'✝ : T₁✝ . t₁'✝ }>s:Tmx:Stringt:Tmt':Tmt₁✝:Tmt₂✝:Tmt₁'✝:Tmt₂'✝:Tmh₁✝:Substi s x t₁✝ t₁'✝h₂✝:Substi s x t₂✝ t₂'✝h₁_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝h₂_ih✝:<{ [~x := s] t₂✝ }> = t₂'✝⊢ <{ [~x := s] (t₁✝ t₂✝) }> = <{ t₁'✝ t₂'✝ }>s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] true }> = <{ true }>s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] false }> = <{ false }>s:Tmx:Stringt:Tmt':Tmt₁✝:Tmt₂✝:Tmt₃✝:Tmt₁'✝:Tmt₂'✝:Tmt₃'✝:Tmh₁✝:Substi s x t₁✝ t₁'✝h₂✝:Substi s x t₂✝ t₂'✝h₃✝:Substi s x t₃✝ t₃'✝h₁_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝h₂_ih✝:<{ [~x := s] t₂✝ }> = t₂'✝h₃_ih✝:<{ [~x := s] t₃✝ }> = t₃'✝⊢ <{ [~x := s] (if t₁✝ then t₂✝ else t₃✝) }> = <{ if t₁'✝ then t₂'✝ else t₃'✝ }> s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] ~(Stlc.Tm.var x) }> = ss:Tmx:Stringt:Tmt':Tmx'✝:Stringh✝:x ≠ x'✝⊢ <{ [~x := s] ~(Stlc.Tm.var x'✝) }> = Stlc.Tm.var x'✝s:Tmx:Stringt:Tmt':TmT₂✝:Tyt₁✝:Tm⊢ <{ [~x := s] (λ ~x : T₂✝ . t₁✝) }> = <{ λ ~x : T₂✝ . t₁✝ }>s:Tmx:Stringt:Tmt':Tmx'✝:StringT₁✝:Tyt₁✝:Tmt₁'✝:Tmhx✝:x ≠ x'✝h✝:Substi s x t₁✝ t₁'✝h_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝⊢ <{ [~x := s] (λ ~x'✝ : T₁✝ . t₁✝) }> = <{ λ ~x'✝ : T₁✝ . t₁'✝ }>s:Tmx:Stringt:Tmt':Tmt₁✝:Tmt₂✝:Tmt₁'✝:Tmt₂'✝:Tmh₁✝:Substi s x t₁✝ t₁'✝h₂✝:Substi s x t₂✝ t₂'✝h₁_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝h₂_ih✝:<{ [~x := s] t₂✝ }> = t₂'✝⊢ <{ [~x := s] (t₁✝ t₂✝) }> = <{ t₁'✝ t₂'✝ }>s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] true }> = <{ true }>s:Tmx:Stringt:Tmt':Tm⊢ <{ [~x := s] false }> = <{ false }>s:Tmx:Stringt:Tmt':Tmt₁✝:Tmt₂✝:Tmt₃✝:Tmt₁'✝:Tmt₂'✝:Tmt₃'✝:Tmh₁✝:Substi s x t₁✝ t₁'✝h₂✝:Substi s x t₂✝ t₂'✝h₃✝:Substi s x t₃✝ t₃'✝h₁_ih✝:<{ [~x := s] t₁✝ }> = t₁'✝h₂_ih✝:<{ [~x := s] t₂✝ }> = t₂'✝h₃_ih✝:<{ [~x := s] t₃✝ }> = t₃'✝⊢ <{ [~x := s] (if t₁✝ then t₂✝ else t₃✝) }> = <{ if t₁'✝ then t₂'✝ else t₃'✝ }> All goals completed! 🐙

5.3.4. Reduction🔗

The small-step reduction relation for STLC now follows the same pattern as the ones we have seen before. Intuitively, to reduce a function application, we first reduce its left-hand side (the function) until it becomes an abstraction; then we reduce its right-hand side (the argument) until it is also a value; and finally we substitute the argument for the bound variable in the body of the abstraction. This last rule, written informally as

(λx:T. t₁₂) v₂ ⟶ [x:=v₂] t₁₂

is traditionally called beta-reduction.

                              v.IsValue
                       -----------------------      (appAbs)
                        (λx:T. t) v ⟶ [x:=v]t

                              t₁ ⟶ t₁'
                          ----------------          (app1)
                           t₁ t₂ ⟶ t₁' t₂

                              v.IsValue
                              t₂ ⟶ t₂'
                          ----------------          (app2)
                           v₁ t₂ ⟶ v₁ t₂'

... plus the usual rules for conditionals:

                  --------------------------------                (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₃)

This is call by value reduction: to reduce an application (t₁ t₂), we

  • first reduce t₁ to a value: a function λx:T. t

  • then reduce the argument t₂ to a value v

  • then reduce the application itself by substituting v for the bound variable x in the body t.

Formally:

section set_option hygiene false in local notation:40 t:41 " ⟶ " t':41 => Step t t' inductive Step : Tm → Tm → Prop where | appAbs (x : String) (T : Ty) (t v : Tm) (hv : v.IsValue) : <{ (λ ~x : ~T . ~t) ~v }> ⟶ <{ [~x := ~v] ~t }> | app1 (t₁ t₁' t₂ : Tm) (h : t₁ ⟶ t₁') : <{ ~t₁ ~t₂ }> ⟶ <{ ~t₁' ~t₂ }> | app2 (v₁ t₂ t₂' : Tm) (hv : v₁.IsValue) (h : t₂ ⟶ t₂') : <{ ~v₁ ~t₂ }> ⟶ <{ ~v₁ ~t₂' }> | 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) (h : t₁ ⟶ t₁') : <{ if ~t₁ then ~t₂ else ~t₃ }> ⟶ <{ if ~t₁' then ~t₂ else ~t₃ }> end scoped notation:40 t:41 " ⟶ " t':41 => Step t t' scoped notation:40 t:41 " ⟶* " t':41 => Multi Step t t' -- for later use with `normalize` attribute [StlcEval] Step.appAbs Step.app1 Step.app2 Step.ifTrue Step.ifFalse Step.ifStep

As in the Smallstep chapter, ⟶* is the multi-step closure of ⟶ — that is, Multi applied to this chapter's step relation. We inherit its reflexivity lemma along with it, so a zero-step execution goal t ⟶* t is closed by rfl.

Quiz

What does the following term step to?

(λx:Bool → Bool. x) (λx:Bool. x) ⟶ ???

(A) λx:Bool. x

(B) λx:Bool → Bool. x

(C) (λx:Bool → Bool. x) (λx:Bool. x)

(D) none of the above

Quiz

What does the following term step to?

(λx:Bool → Bool. x)
    ((λx:Bool → Bool. x) (λx:Bool. x))
⟶ ???

(A) λx:Bool. x

(B) λx:Bool → Bool. x

(C) (λx:Bool → Bool. x) (λx:Bool. x)

(D) (λx:Bool → Bool. x) ((λx:Bool → Bool. x) (λx:Bool. x))

(E) none of the above

Quiz

What does the following term normalize to?

(λx:Bool → Bool. x) notB true  ⟶* ???

where notB abbreviates λx:Bool. if x then false else true

(A) λx:Bool. x

(B) true

(C) false

(D) notB

(E) none of the above

Quiz

What does the following term normalize to?

(λx:Bool. x) (notB true) ⟶* ???

(A) λx:Bool. x

(B) true

(C) false

(D) notB true

(E) none of the above

5.3.5. Examples🔗

Example:

(λx:Bool → Bool. x) (λx:Bool. x) ⟶* λx:Bool. x

i.e.,

idBB idB ⟶* idB
example : <{ ~idBB ~idB }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB idB }> ⟶* idB x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB idB }> ⟶ idBx:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ idB ⟶* idB x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB idB }> ⟶ idB All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ idB ⟶* idB All goals completed! 🐙

Example:

(λx:Bool → Bool. x) ((λx:Bool → Bool. x) (λx:Bool. x))
      ⟶* λx:Bool. x

i.e.,

(idBB (idBB idB)) ⟶* idB.
example : <{ ~idBB (~idBB ~idB) }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (idBB idB) }> ⟶* idB -- the same reduction happens twice, so we name it x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶* idB x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶ <{ idBB idB }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶* idB x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB (idBB idB) }> ⟶ <{ idBB idB }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶ idBx:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ idB ⟶* idB x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ <{ idBB idB }> ⟶ idB All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tystep₁:<{ idBB idB }> ⟶ idB⊢ idB ⟶* idB All goals completed! 🐙

Example:

(λx:Bool → Bool. x)
   (λx:Bool. if x then false else true)
   true
      ⟶* false

i.e.,

(idBB notB) true ⟶* false.
example : <{ ~idBB ~notB true }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB notB true }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB notB true }> ⟶ <{ notB true }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ notB true }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB notB true }> ⟶ <{ notB true }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ notB true }> ⟶ <{ if true then false else true }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ if true then false else true }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ notB true }> ⟶ <{ if true then false else true }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ if true then false else true }> ⟶ <{ false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ false }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ if true then false else true }> ⟶ <{ false }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ false }> ⟶* <{ false }> All goals completed! 🐙

Example:

(λx:Bool → Bool. x)
   ((λx:Bool. if x then false else true) true)
      ⟶* false

i.e.,

idBB (notB true) ⟶* false.

(Note that this term doesn't actually typecheck; even so, we can ask how it reduces.)

example : <{ ~idBB (~notB true) }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (notB true) }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (notB true) }> ⟶ <{ idBB (if true then false else true) }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (if true then false else true) }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (notB true) }> ⟶ <{ idBB (if true then false else true) }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (if true then false else true) }> ⟶ <{ idBB false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB false }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (if true then false else true) }> ⟶ <{ idBB false }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB false }> ⟶ <{ false }>x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ false }> ⟶* <{ false }> x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB false }> ⟶ <{ false }> All goals completed! 🐙 x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ false }> ⟶* <{ false }> All goals completed! 🐙

As in the Smallstep chapter, we can use the normalize tactic to simplify these proofs:

example : <{ ~idBB ~idB }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB idB }> ⟶* idB All goals completed! 🐙 example : <{ ~idBB (~idBB ~idB) }> ⟶* idB := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (idBB idB) }> ⟶* idB All goals completed! 🐙 example : <{ ~idBB ~notB true }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB notB true }> ⟶* <{ false }> All goals completed! 🐙 example : <{ ~idBB (~notB true) }> ⟶* <{ false }> := x:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Ty⊢ <{ idBB (notB true) }> ⟶* <{ false }> All goals completed! 🐙
Quiz

Do values and normal forms coincide in the language presented so far?

(A) yes

(B) no

Exercise★★(step_example5)

Try to do this one both with and without normalize.

theorem stepExample5 : <{ ~idBBBB ~idBB ~idB }> ⟶* idB := ⊢ <{ idBBBB idBB idB }> ⟶* idB solution! ⊢ <{ idBBBB idBB idB }> ⟶ <{ idBB idB }>⊢ <{ idBB idB }> ⟶* idB ⊢ <{ idBBBB idBB idB }> ⟶ <{ idBB idB }> All goals completed! 🐙 ⊢ <{ idBB idB }> ⟶ idB⊢ idB ⟶* idB ⊢ <{ idBB idB }> ⟶ idB All goals completed! 🐙 ⊢ idB ⟶* idB All goals completed! 🐙 theorem stepExample5' : <{ ~idBBBB ~idBB ~idB }> ⟶* idB := ⊢ <{ idBBBB idBB idB }> ⟶* idB solution! All goals completed! 🐙

5.4. Typing🔗

Next we consider the typing relation of the STLC, which is meant to prevent reduction from getting stuck.

For instance, the following two STLC terms are both stuck:

if λx:Bool. x then true else false

Here we branch on a function as though it were a boolean.

true false

Here we apply a boolean as though it were a function.

5.4.1. Contexts🔗

Although we are primarily interested in the binary relation ⊢ t ⦂ T, relating a closed term t to its type T, we need to generalize a bit to make the definitions work.

Consider checking that λx:T₁₁. t₁₂ has type T₁₁ → T₁₂. Intuitively, we need to check that t₁₂ has type T₁₂. However, we have removed the binder λx, so x may occur free in t₁₂ (that is, t₁₂ may be open). While checking that t₁₂ has type T₁₂, we must remember that x has type T₁₁, in order to deal with these free occurrences of x. Similarly, t₁₂ itself could contain abstractions, and typechecking their bodies could require looking up the declared types of yet more free variables.

To keep track of all this, we add a third element to the relation, a typing context Γ, which records the types of the variables that may occur free in a term — that is, Γ is a partial map from variables to types.

The new typing judgment is written Γ ⊢ t ⦂ T and informally read as "term t has type T, given the types of free variables in t as specified by Γ".

We'll also write x ↦ T ; Γ for "update the partial map Γ so that it maps x to T," following the notation from the Typeclasses chapter.

With these refinements, we are ready to give informal and formal specifications of the typing relation.

Note to developers (Chris Henson @chenson2018, before next release)

I find the FULL explanation above much better than the TERSE one below, since the question below seems ill-posed without extra context. Why would one want to type a term x y if we've just said that we will just look at closed terms as our programs?

abbrev Context := PartialMap String Ty

A context is a PartialMap from variable names to types — the partial maps of the Typeclasses chapter, which are total maps whose values are optional, so that none at a variable means "not bound here".

5.4.2. Typing Relation🔗

                              Γ x = T₁
                            ------------                       (var)
                             Γ ⊢ x ⦂ T₁

                        x ↦ T₂ ; Γ ⊢ t₁ ⦂ T₁
                      -------------------------                (abs)
                       Γ ⊢ λx:T₂. t₁ ⦂ T₂ → T₁

                          Γ ⊢ t₁ ⦂ T₂ → T₁
                            Γ ⊢ t₂ ⦂ T₂
                         ------------------                    (app)
                           Γ ⊢ t₁ t₂ ⦂ T₁

                          -----------------                    (tru)
                           Γ ⊢ true ⦂ Bool

                         ------------------                    (fls)
                          Γ ⊢ false ⦂ Bool

             Γ ⊢ t₁ ⦂ Bool    Γ ⊢ t₂ ⦂ T₁    Γ ⊢ t₃ ⦂ T₁
            ---------------------------------------------      (ite)
                   Γ ⊢ if t₁ then t₂ else t₃ ⦂ T₁

We can read the three-place relation Γ ⊢ t ⦂ T as: "under the assumptions in Γ, the term t has the type T."

In the formal development, we write this judgment inside the same <{ .. }> brackets we use for types and terms, as introduced by the following notational conventions.

A context is written ∅ when empty and x ↦ T ; Γ when extended with a binding, and ~e escapes to a Lean expression of type Context. The whole judgment then goes inside the same <{ … }> brackets as terms, written with the turnstile and colon of the Types chapter: <{ Γ ⊢ t ⦂ T }>.

Notation encoding: contexts and judgments

Contexts get a grammar of their own, stlcCtx. The meaning is the map update we already have — x ↦ T ; Γ expands to exactly the Typeclasses chapter's partial-map update on Γ — but its surface syntax has to be our own, because inside these brackets all three positions are in object syntax. Writing the map notation directly would mean writing the binding as "x" →ₚ <{ Bool → Bool }> ; Γ: the name quoted, and the type escaped back out of the brackets it belongs in. The grammar hides those two encoding details, and nothing else.

declare_syntax_cat stlcCtx syntax:max "∅" : stlcCtx syntax:max "~" term:max : stlcCtx syntax:max stlcVar " ↦ " stlcTy " ; " stlcCtx : stlcCtx syntax:max (name := judgeBracket) "<{ " stlcCtx " ⊢ " stlcTm " ⦂ " stlcTy " }>" : term open Lean in /-- The `Context` denoted by a context expression. -/ partial def ctxTerm (G : TSyntax `stlcCtx) : MacroM Term := match G with | `(stlcCtx| ∅) => `((∅ : Context)) | `(stlcCtx| ~$e) => pure e | `(stlcCtx| $x:stlcVar ↦ $T:stlcTy ; $G:stlcCtx) => do `(PartialMap.update $(← ctxTerm G) $(← varStr x) <{ $T:stlcTy }>) | _ => Macro.throwUnsupported

As with subst, the judgment notation is used inside the definition it names, so it is introduced in two steps: the rule below is declared local with hygiene off, so the HasType in its expansion resolves to the relation being declared, and after the section closes it is declared again for real use.

section set_option hygiene false in local macro_rules (kind := judgeBracket) | `(<{ $G:stlcCtx ⊢ $t:stlcTm ⦂ $T:stlcTy }>) => do `(HasType $(← ctxTerm G) <{ $t:stlcTm }> <{ $T:stlcTy }>)
inductive HasType : Context → Tm → Ty → Prop where | var (Γ : Context) (x : String) (T₁ : Ty) (h : Γ[x] = some T₁) : <{ ~Γ ⊢ ~(Tm.var x) ⦂ ~T₁ }> | abs (Γ : Context) (x : String) (T₁ T₂ : Ty) (t₁ : Tm) (h : <{ ~x ↦ ~T₂ ; ~Γ ⊢ ~t₁ ⦂ ~T₁ }>) : <{ ~Γ ⊢ λ ~x : ~T₂ . ~t₁ ⦂ ~T₂ → ~T₁ }> | app (Γ : Context) (T₁ T₂ : Ty) (t₁ t₂ : Tm) (h₁ : <{ ~Γ ⊢ ~t₁ ⦂ ~T₂ → ~T₁ }>) (h₂ : <{ ~Γ ⊢ ~t₂ ⦂ ~T₂ }>) : <{ ~Γ ⊢ ~t₁ ~t₂ ⦂ ~T₁ }> | tru (Γ : Context) : <{ ~Γ ⊢ true ⦂ Bool }> | fls (Γ : Context) : <{ ~Γ ⊢ false ⦂ Bool }> | ite (Γ : Context) (t₁ t₂ t₃ : Tm) (T₁ : Ty) (h₁ : <{ ~Γ ⊢ ~t₁ ⦂ Bool }>) (h₂ : <{ ~Γ ⊢ ~t₂ ⦂ ~T₁ }>) (h₃ : <{ ~Γ ⊢ ~t₃ ⦂ ~T₁ }>) : <{ ~Γ ⊢ if ~t₁ then ~t₂ else ~t₃ ⦂ ~T₁ }> attribute [StlcTyping] HasType.var HasType.abs HasType.app HasType.tru HasType.fls HasType.ite
Notation encoding: the judgment, for real

Closing the section retires the hygiene-free rule; the same rule is then declared again, hygienically, for every later use.

end macro_rules (kind := judgeBracket) | `(<{ $G:stlcCtx ⊢ $t:stlcTm ⦂ $T:stlcTy }>) => do `(HasType $(← ctxTerm G) <{ $t:stlcTm }> <{ $T:stlcTy }>)
Notation encoding: printing judgments back

As with terms, a judgment prints back in its own notation, so that a goal reads as <{ x ↦ Bool ; ∅ ⊢ x ⦂ Bool }> rather than as a HasType applied to a chain of map updates.

open Lean PrettyPrinter in /-- Rebuild `stlcCtx` syntax from the term syntax of a `Context`, so that a context prints as `x ↦ Bool ; Γ` rather than as a chain of map updates. -/ partial def unexpandCtx : Term → UnexpandM (TSyntax `stlcCtx) | `(∅) => `(stlcCtx| ∅) | `($x:str →ₚ $T) => do unexpandCtx (← `($x →ₚ $T ; ∅)) | `($x:str →ₚ $T ; $G) => do let G' ← unexpandCtx G let x' : TSyntax `stlcVar ← if isPlainName x.getString then `(stlcVar| $(mkIdent (Name.mkSimple x.getString)):ident) else `(stlcVar| ~$x) match T with | `(<{ $T':stlcTy }>) => `(stlcCtx| $x':stlcVar ↦ $T' ; $G') | _ => `(stlcCtx| $x':stlcVar ↦ ~($T) ; $G') | G => `(stlcCtx| ~($G)) open Lean PrettyPrinter in @[app_unexpander Stlc.HasType] def HasType.unexpand : Unexpander | `($_ $G <{ $t:stlcTm }> <{ $T:stlcTy }>) => do `(<{ $(← unexpandCtx G) ⊢ $t ⦂ $T }>) | `($_ $G <{ $t:stlcTm }> $T) => do `(<{ $(← unexpandCtx G) ⊢ $t ⦂ ~($T) }>) | `($_ $G $t <{ $T:stlcTy }>) => do `(<{ $(← unexpandCtx G) ⊢ ~($t) ⦂ $T }>) | `($_ $G $t $T) => do `(<{ $(← unexpandCtx G) ⊢ ~($t) ⦂ ~($T) }>) | _ => throw ()

5.4.3. Examples🔗

example : <{ ∅ ⊢ λ x : Bool . x ⦂ Bool → Bool }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ ∅ ⊢ λ x : Bool . x ⦂ Bool → Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ x ↦ Bool ; ∅ ⊢ x ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ("x" →ₚ <{ Bool }>)["x"] = some <{ Bool }>; All goals completed! 🐙

The derivation is small enough to write out directly: an abstraction rule whose premise is the variable rule, and the variable rule's premise — that the extended context maps x to Bool — holds by computation, hence rfl.

Much like reduction sequences, long derivations of typing rules can grow quite tedious to prove. Luckily, we can have Lean automate proofs of this sort, using another tactic: apply_rules. This tactic works much like normalize, but is more efficient and will make progress even if it cannot solve the goal outright. Like normalize, apply_rules also takes a using argument which tells Lean which set of constructors to draw from.

∅ ⊢ λx:Bool. λy:Bool → Bool. y (y x)
      ⦂ Bool → (Bool → Bool) → Bool.
example : <{ ∅ ⊢ λ x : Bool . λ y : Bool → Bool . y (y x) ⦂ Bool → (Bool → Bool) → Bool }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ ∅ ⊢ λ x : Bool . λ y : Bool → Bool . y (y x) ⦂ Bool → (Bool → Bool) → Bool }> All goals completed! 🐙

It's worth noting that apply_rules relies on an important property of our typing rules - namely, that they are syntax directed. A syntax directed judgment is one where the syntax of a term completely determines which rule can be applied at any given time; only one rule can be applied to each term. This is important because apply_rules just applies the first rule in its set of constructors or lemmas that it can - it doesn't backtrack if that rule isn't correct. So, making sure that only one rule can apply to any given term is important to ensure that apply_rules always discovers a valid derivation, if one exists.

Exercise★★(typing_example_2_full) (Optional)

Prove the same result, applying one rule at a time and naming the argument type of each application explicitly.

example : <{ ∅ ⊢ λ x : Bool . λ y : Bool → Bool . y (y x) ⦂ Bool → (Bool → Bool) → Bool }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ ∅ ⊢ λ x : Bool . λ y : Bool → Bool . y (y x) ⦂ Bool → (Bool → Bool) → Bool }> solution! x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ x ↦ Bool ; ∅ ⊢ λ y : Bool → Bool . y (y x) ⦂ (Bool → Bool) → Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y (y x) ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ Bool → Bool }>x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y x ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ Bool → Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ("y" →ₚ <{ Bool → Bool }> ; "x" →ₚ <{ Bool }>)["y"] = some <{ Bool → Bool }>; All goals completed! 🐙 x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y x ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ Bool → Bool }>x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ x ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ Bool → Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ("y" →ₚ <{ Bool → Bool }> ; "x" →ₚ <{ Bool }>)["y"] = some <{ Bool → Bool }>; All goals completed! 🐙 x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ y ↦ Bool → Bool ; x ↦ Bool ; ∅ ⊢ x ⦂ Bool }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ("y" →ₚ <{ Bool → Bool }> ; "x" →ₚ <{ Bool }>)["x"] = some <{ Bool }>; All goals completed! 🐙
Exercise★★(typing_example_3)

Formally prove the following typing derivation holds:

∃ T,
   ∅ ⊢ λ x : Bool → Bool . λ y : Bool → Bool . λ z : Bool .
               y (x z)
         ⦂ T
example : ∃ T, <{ ∅ ⊢ λ x : Bool → Bool . λ y : Bool → Bool . λ z : Bool . y (x z) ⦂ ~T }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ∃ T, <{ ∅ ⊢ λ x : Bool → Bool . λ y : Bool → Bool . λ z : Bool . y (x z) ⦂ ~(T) }> solution! x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ <{ ∅ ⊢ λ x : Bool → Bool . λ y : Bool → Bool . λ z : Bool . y (x z) ⦂ (Bool → Bool) → (Bool → Bool) → Bool → Bool }> All goals completed! 🐙

We can also show that some terms are not typable. For example, we can check that there is no typing derivation assigning a type to the term λx:Bool. λy:Bool. x y — i.e.,

¬ ∃ T, ∅ ⊢ λx:Bool. λy:Bool. x y ⦂ T
example : ¬ ∃ T, <{ ∅ ⊢ λ x : Bool . λ y : Bool . x y ⦂ ~T }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ¬∃ T, <{ ∅ ⊢ λ x : Bool . λ y : Bool . x y ⦂ ~(T) }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT✝:Tyx:StringT:Tyhc:<{ ∅ ⊢ λ x : Bool . λ y : Bool . x y ⦂ ~(T) }>⊢ False -- Each `cases` peels off one rule of the derivation, naming the premise it -- leaves behind; the context stays small because the old hypothesis goes away. cases hc with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:StringT₁✝:Tyh₁:<{ x ↦ Bool ; ∅ ⊢ λ y : Bool . x y ⦂ ~(T₁✝) }>⊢ False cases h₁ with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:StringT₁✝:Tyh₂:<{ y ↦ Bool ; x ↦ Bool ; ∅ ⊢ x y ⦂ ~(T₁✝) }>⊢ False cases h₂ with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:StringT₁✝:TyT₂✝:Tyh₂✝:<{ y ↦ Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ ~(T₂✝) }>hf:<{ y ↦ Bool ; x ↦ Bool ; ∅ ⊢ x ⦂ T₂✝ → T₁✝ }>⊢ False cases hf with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:StringT₁✝:TyT₂✝:Tyh₂✝:<{ y ↦ Bool ; x ↦ Bool ; ∅ ⊢ y ⦂ ~(T₂✝) }>hx:("y" →ₚ <{ Bool }> ; "x" →ₚ <{ Bool }>)["x"] = some <{ T₂✝ → T₁✝ }>⊢ False -- `x` is bound to `Bool` in the context, but the application rule -- needs it to have an arrow type. All goals completed! 🐙

Another nonexample:

¬ ∃ S T, ∅ ⊢ λx:S. x x ⦂ T
example : ¬ ∃ S T, <{ ∅ ⊢ λ x : ~S . x x ⦂ ~T }> := x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:String⊢ ¬∃ S T, <{ ∅ ⊢ λ x : S . x x ⦂ ~(T) }> solution! -- The two occurrences of `x` force its type `S` to satisfy `S = S → T`, -- and no (finite) type does. x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>⊢ ¬∃ S T, <{ ∅ ⊢ λ x : S . x x ⦂ ~(T) }> x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT✝:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>S:TyT:Tyhc:<{ ∅ ⊢ λ x : S . x x ⦂ ~(T) }>⊢ False cases hc with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>S:TyT₁✝:Tyh₁:<{ x ↦ ~(S) ; ∅ ⊢ x x ⦂ ~(T₁✝) }>⊢ False cases h₁ with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>S:TyT₁✝:TyT₂✝:Tyha:<{ x ↦ ~(S) ; ∅ ⊢ x ⦂ ~(T₂✝) }>hf:<{ x ↦ ~(S) ; ∅ ⊢ x ⦂ T₂✝ → T₁✝ }>⊢ False cases hf with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>S:TyT₁✝:TyT₂✝:Tyha:<{ x ↦ ~(S) ; ∅ ⊢ x ⦂ ~(T₂✝) }>hx:("x" →ₚ S)["x"] = some <{ T₂✝ → T₁✝ }>⊢ False cases ha with x✝:Stringy:Strings:Tmt:Tmt₁:Tmt₂:Tmt₃:TmT:Tyx:Stringarrow_ne:∀ (T₁ T₂ : Ty), T₁ ≠ <{ T₁ → T₂ }>S:TyT₁✝:TyT₂✝:Tyhx:("x" →ₚ S)["x"] = some <{ T₂✝ → T₁✝ }>hy:("x" →ₚ S)["x"] = some T₂✝⊢ False All goals completed! 🐙
Note to developers

The Rocq proof gets to the same contradiction through a chain of inversions and then an induction on the offending type; the LATER note there asks why eauto 30 makes no progress on the previous example, and a NOTATION note from Ori reports an error with the associativity of the arrow in one of the inversion hypotheses. Neither issue arises in this encoding.

Quiz

Which of the following propositions is not provable?

(A) y ↦ Bool ; ∅ ⊢ λx:Bool. x ⦂ Bool → Bool

(B) ∃ T, ∅ ⊢ λy:Bool → Bool. λx:Bool. y x ⦂ T

(C) ∃ T, ∅ ⊢ λy:Bool → Bool. λx:Bool. x y ⦂ T

(D) ∃ S, x ↦ S ; ∅ ⊢ λy:Bool → Bool. y x ⦂ (Bool → Bool) → S

Quiz

Which of these is not provable?

(A) ∃ T, ∅ ⊢ λy:Bool → Bool → Bool. λx:Bool. y x ⦂ T

(B) ∃ S T, x ↦ S ; ∅ ⊢ x x x ⦂ T

(C) ∃ S U T, x ↦ S ; y ↦ U ; ∅ ⊢ λz:Bool. x (y z) ⦂ T

(D) ∃ S T, x ↦ S ; ∅ ⊢ λy:Bool. x (x y) ⦂ T

end Stlc
Source revision: 9e5dba0, committed 2026-09-25 03:12 UTC