/-! # Dependent de Bruijn Indices In this example, we represent program syntax terms in a type family parameterized by a list of types, representing the typing context, or information on which free variables are in scope and what their types are. Remark: this example is based on an example in the book [Certified Programming with Dependent Types](http://adam.chlipala.net/cpdt/) by Adam Chlipala. -/ /-! Programmers who move to statically typed functional languages from scripting languages often complain about the requirement that every element of a list have the same type. With fancy type systems, we can partially lift this requirement. We can index a list type with a “type-level” list that explains what type each element of the list should have. This has been done in a variety of ways in Haskell using type classes, and we can do it much more cleanly and directly in Lean. We parameterize our heterogeneous lists by at type `α` and an `α`-indexed type `β`. -/ inductive HList {α : Type v} (β : α → Type u) : List α → Type (max u v) | nil : HList β [] | cons : β i → HList β is → HList β (i::is) /-! We overload the `List.cons` notation `::` so we can also use it to create heterogeneous lists. -/ infix:67 " :: " => HList.cons /-! We similarly overload the `List` notation `[]` for the empty heterogeneous list. -/ notation "[" "]" => HList.nil /-! Variables are represented in a way isomorphic to the natural numbers, where number 0 represents the first element in the context, number 1 the second element, and so on. Actually, instead of numbers, we use the `Member` inductive family. The value of type `Member a as` can be viewed as a certificate that `a` is an element of the list `as`. The constructor `Member.head` says that `a` is in the list if the list begins with it. The constructor `Member.tail` says that if `a` is in the list `bs`, it is also in the list `b::bs`. -/ inductive Member : α → List α → Type | head : Member a (a::as) | tail : Member a bs → Member a (b::bs) /-! Given a heterogeneous list `HList β is` and value of type `Member i is`, `HList.get` retrieves an element of type `β i` from the list. The pattern `.head` and `.tail h` are sugar for `Member.head` and `Member.tail h` respectively. Lean can infer the namespace using the expected type. -/ def HList.get : HList β is → Member i is → β i | a::as, .head => a | a::as, .tail h => as.get h /-! Here is the definition of the simple type system for our programming language, a simply typed lambda calculus with natural numbers as the base type. -/ inductive Ty where | nat | fn : Ty → Ty → Ty /-! We can write a function to translate `Ty` values to a Lean type — remember that types are first class, so can be calculated just like any other value. We mark `Ty.denote` as `[reducible]` to make sure the typeclass resolution procedure can unfold/reduce it. For example, suppose Lean is trying to synthesize a value for the instance `Add (Ty.denote Ty.nat)`. Since `Ty.denote` is marked as `[reducible]`, the typeclass resolution procedure can reduce `Ty.denote Ty.nat` to `Nat`, and use the builtin instance for `Add Nat` as the solution. Recall that the term `a.denote` is sugar for `denote a` where `denote` is the function being defined. We call it the "dot notation". -/ @[reducible] def Ty.denote : Ty → Type | nat => Nat | fn a b => a.denote → b.denote /-! Here is the definition of the `Term` type, including variables, constants, addition, function application and abstraction, and let binding of local variables. Since `let` is a keyword in Lean, we use the "escaped identifier" `«let»`. You can input the unicode (French double quotes) using `\f<<` (for `«`) and `\f>>` (for `»`). The term `Term ctx .nat` is sugar for `Term ctx Ty.nat`, Lean infers the namespace using the expected type. -/ inductive Term : List Ty → Ty → Type | var : Member ty ctx → Term ctx ty | const : Nat → Term ctx .nat | plus : Term ctx .nat → Term ctx .nat → Term ctx .nat | app : Term ctx (.fn dom ran) → Term ctx dom → Term ctx ran | lam : Term (dom :: ctx) ran → Term ctx (.fn dom ran) | «let» : Term ctx ty₁ → Term (ty₁ :: ctx) ty₂ → Term ctx ty₂ /-! Here are two example terms encoding, the first addition packaged as a two-argument curried function, and the second of a sample application of addition to constants. The command `open Ty Term Member` opens the namespaces `Ty`, `Term`, and `Member`. Thus, you can write `lam` instead of `Term.lam`. -/ open Ty Term Member def add : Term [] (fn nat (fn nat nat)) := lam (lam (plus (var (tail head)) (var head))) def three_the_hard_way : Term [] nat := app (app add (const 1)) (const 2) /-! Since dependent typing ensures that any term is well-formed in its context and has a particular type, it is easy to translate syntactic terms into Lean values. The attribute `[simp]` instructs Lean to always try to unfold `Term.denote` applications when one applies the `simp` tactic. We also say this is a hint for the Lean term simplifier. -/ @[simp] def Term.denote : Term ctx ty → HList Ty.denote ctx → ty.denote | var h, env => env.get h | const n, _ => n | plus a b, env => a.denote env + b.denote env | app f a, env => f.denote env (a.denote env) | lam b, env => fun x => b.denote (x :: env) | «let» a b, env => b.denote (a.denote env :: env) /-! You can show that the denotation of `three_the_hard_way` is indeed `3` using reflexivity. -/ example : three_the_hard_way.denote [] = 3 := rfl /-! We now define the constant folding optimization that traverses a term if replaces subterms such as `plus (const m) (const n)` with `const (n+m)`. -/ @[simp] def Term.constFold : Term ctx ty → Term ctx ty | const n => const n | var h => var h | app f a => app f.constFold a.constFold | lam b => lam b.constFold | «let» a b => «let» a.constFold b.constFold | plus a b => match a.constFold, b.constFold with | const n, const m => const (n+m) | a', b' => plus a' b' /-! The correctness of the `Term.constFold` is proved using induction, case-analysis, and the term simplifier. We prove all cases but the one for `plus` using `simp [*]`. This tactic instructs the term simplifier to use hypotheses such as `a = b` as rewriting/simplifications rules. We use the `split` to break the nested `match` expression in the `plus` case into two cases. The local variables `iha` and `ihb` are the induction hypotheses for `a` and `b`. The modifier `←` in a term simplifier argument instructs the term simplifier to use the equation as a rewriting rule in the "reverse direction". That is, given `h : a = b`, `← h` instructs the term simplifier to rewrite `b` subterms to `a`. -/ theorem Term.constFold_sound (e : Term ctx ty) : e.constFold.denote env = e.denote env := by induction e with simp [*] | plus a b iha ihb => split next he₁ he₂ => simp [← iha, ← ihb, he₁, he₂] next => simp [iha, ihb]