A Certified Type Checker

In this example, we build a certified type checker for a simple expression language.

Remark: this example is based on an example in the book Certified Programming with Dependent Types by Adam Chlipala.

inductive Expr where | nat : Nat Expr | plus : Expr Expr Expr | bool : Bool Expr | and : Expr Expr Expr

We define a simple language of types using the inductive datatype Ty, and its typing rules using the inductive predicate HasType.

inductive Ty where | nat | bool deriving DecidableEqinductive HasType : Expr Ty Prop | nat : HasType (.nat v) .nat | plus : HasType a .nat HasType b .nat HasType (.plus a b) .nat | bool : HasType (.bool v) .bool | and : HasType a .bool HasType b .bool HasType (.and a b) .bool

We can easily show that if e has type t₁ and type t₂, then t₁ and t₂ must be equal by using the cases tactic. This tactic creates a new subgoal for every constructor, and automatically discharges unreachable cases. The tactic combinator tac₁ <;> tac₂ applies tac₂ to each subgoal produced by tac₁. Then, the tactic rfl is used to close all produced goals using reflexivity.

theorem HasType.det (h₁ : HasType e t₁) (h₂ : HasType e t₂) : t₁ = t₂ := e:Exprt₁:Tyt₂:Tyh₁:HasType e t₁h₂:HasType e t₂t₁ = t₂ t₂:Tyv✝:Nath₂:HasType (Expr.nat v✝) t₂Ty.nat = t₂t₂:Tya✝²:Exprb✝:Expra✝¹:HasType a✝² Ty.nata✝:HasType b✝ Ty.nath₂:HasType (a✝².plus b✝) t₂Ty.nat = t₂t₂:Tyv✝:Boolh₂:HasType (Expr.bool v✝) t₂Ty.bool = t₂t₂:Tya✝²:Exprb✝:Expra✝¹:HasType a✝² Ty.boola✝:HasType b✝ Ty.boolh₂:HasType (a✝².and b✝) t₂Ty.bool = t₂ t₂:Tyv✝:Nath₂:HasType (Expr.nat v✝) t₂Ty.nat = t₂t₂:Tya✝²:Exprb✝:Expra✝¹:HasType a✝² Ty.nata✝:HasType b✝ Ty.nath₂:HasType (a✝².plus b✝) t₂Ty.nat = t₂t₂:Tyv✝:Boolh₂:HasType (Expr.bool v✝) t₂Ty.bool = t₂t₂:Tya✝²:Exprb✝:Expra✝¹:HasType a✝² Ty.boola✝:HasType b✝ Ty.boolh₂:HasType (a✝².and b✝) t₂Ty.bool = t₂ a✝⁴:Exprb✝:Expra✝³:HasType a✝⁴ Ty.boola✝²:HasType b✝ Ty.boola✝¹:HasType a✝⁴ Ty.boola✝:HasType b✝ Ty.boolTy.bool = Ty.bool v✝:NatTy.nat = Ty.nata✝⁴:Exprb✝:Expra✝³:HasType a✝⁴ Ty.nata✝²:HasType b✝ Ty.nata✝¹:HasType a✝⁴ Ty.nata✝:HasType b✝ Ty.natTy.nat = Ty.natv✝:BoolTy.bool = Ty.boola✝⁴:Exprb✝:Expra✝³:HasType a✝⁴ Ty.boola✝²:HasType b✝ Ty.boola✝¹:HasType a✝⁴ Ty.boola✝:HasType b✝ Ty.boolTy.bool = Ty.bool All goals completed! 🐙

The inductive type Maybe p has two constructors: found a h and unknown. The former contains an element a : α and a proof that a satisfies the predicate p. The constructor unknown is used to encode "failure".

inductive Maybe (p : α Prop) where | found : (a : α) p a Maybe p | unknown

We define a notation for Maybe that is similar to the builtin notation for the Lean builtin type Subtype.

notation "{{ " x " | " p " }}" => Maybe (fun x => p)

The function Expr.typeCheck e returns a type ty and a proof that e has type ty, or unknown. Recall that, def Expr.typeCheck ... in Lean is notation for namespace Expr def typeCheck ... end Expr. The term .found .nat .nat is sugar for Maybe.found Ty.nat HasType.nat. Lean can infer the namespaces using the expected types.

def Expr.typeCheck (e : Expr) : {{ ty | HasType e ty }} := match e with | nat .. => .found .nat .nat | bool .. => .found .bool .bool | plus a b => match a.typeCheck, b.typeCheck with | .found .nat h₁, .found .nat h₂ => .found .nat (.plus h₁ h₂) | _, _ => .unknown | and a b => match a.typeCheck, b.typeCheck with | .found .bool h₁, .found .bool h₂ => .found .bool (.and h₁ h₂) | _, _ => .unknowntheorem Expr.typeCheck_correct (h₁ : HasType e ty) (h₂ : e.typeCheck .unknown) : e.typeCheck = .found ty h := e:Exprty:Tyh:HasType e tyh₁:HasType e tyh₂:e.typeCheckMaybe.unknowne.typeCheck = Maybe.found ty h e:Exprty:Tyh:HasType e tyh₁:HasType e tye.typeCheckMaybe.unknowne.typeCheck = Maybe.found ty h cases typeCheck e with | found ty' h' => e:Exprty:Tyh:HasType e tyh₁:HasType e tyty':Tyh':HasType e ty'h₂✝:Maybe.found ty' h'Maybe.unknownMaybe.found ty' h' = Maybe.found ty h; e:Exprty:Tyh:HasType e tyh₁:HasType e tyty':Tyh':HasType e ty'h₂✝:Maybe.found ty' h'Maybe.unknownthis:ty = ty'Maybe.found ty' h' = Maybe.found ty h; e:Exprty:Tyh:HasType e tyh₁:HasType e tyh':HasType e tyh₂✝:Maybe.found ty h'Maybe.unknownMaybe.found ty h' = Maybe.found ty h; All goals completed! 🐙 | unknown => e:Exprty:Tyh:HasType e tyh₁:HasType e tyh₂✝:Maybe.unknownMaybe.unknownMaybe.unknown = Maybe.found ty h; All goals completed! 🐙

Now, we prove that if Expr.typeCheck e returns Maybe.unknown, then forall ty, HasType e ty does not hold. The notation e.typeCheck is sugar for Expr.typeCheck e. Lean can infer this because we explicitly said that e has type Expr. The proof is by induction on e and case analysis. Note that the tactic simp [typeCheck] is applied to all goal generated by the induction tactic, and closes the cases corresponding to the constructors Expr.nat and Expr.bool.

theorem Expr.typeCheck_complete {e : Expr} : e.typeCheck = .unknown ¬ HasType e ty := ty:Tye:Expre.typeCheck = Maybe.unknown¬HasType e ty induction e with All goals completed! 🐙 | plus a b iha ihb => ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.nath₂✝:HasType b Ty.natheq✝¹:a.typeCheck = Maybe.found Ty.nat h₁✝heq✝:b.typeCheck = Maybe.found Ty.nat h₂✝Maybe.found Ty.nat = Maybe.unknown¬HasType (a.plus b) tyty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝²:{{ ty | HasType a ty }}x✝¹:{{ ty | HasType b ty }}x✝:∀ (h₁ : HasType a Ty.nat) (h₂ : HasType b Ty.nat), a.typeCheck = Maybe.found Ty.nat h₁b.typeCheck = Maybe.found Ty.nat h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.plus b) ty next ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.nath₂✝:HasType b Ty.natheq✝¹:a.typeCheck = Maybe.found Ty.nat h₁✝heq✝:b.typeCheck = Maybe.found Ty.nat h₂✝Maybe.found Ty.nat = Maybe.unknown¬HasType (a.plus b) ty ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.nath₂✝:HasType b Ty.natheq✝¹:a.typeCheck = Maybe.found Ty.nat h₁✝heq✝:b.typeCheck = Maybe.found Ty.nat h₂✝a✝:Maybe.found Ty.nat = Maybe.unknown¬HasType (a.plus b) ty; All goals completed! 🐙 next ra rb hnp ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.nat) (h₂ : HasType b Ty.nat), a.typeCheck = Maybe.found Ty.nat h₁b.typeCheck = Maybe.found Ty.nat h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.plus b) ty -- Recall that `hnp` is a hypothesis generated by the `split` tactic -- that asserts the previous case was not taken ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.nat) (h₂ : HasType b Ty.nat), a.typeCheck = Maybe.found Ty.nat h₁b.typeCheck = Maybe.found Ty.nat h₂Falseh:Maybe.unknown = Maybe.unknownht:HasType (a.plus b) tyFalse cases ht with | plus h₁ h₂ ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.nat) (h₂ : HasType b Ty.nat), a.typeCheck = Maybe.found Ty.nat h₁b.typeCheck = Maybe.found Ty.nat h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.plus b) ty All goals completed! 🐙 | and a b iha ihb => ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.boolh₂✝:HasType b Ty.boolheq✝¹:a.typeCheck = Maybe.found Ty.bool h₁✝heq✝:b.typeCheck = Maybe.found Ty.bool h₂✝Maybe.found Ty.bool = Maybe.unknown¬HasType (a.and b) tyty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝²:{{ ty | HasType a ty }}x✝¹:{{ ty | HasType b ty }}x✝:∀ (h₁ : HasType a Ty.bool) (h₂ : HasType b Ty.bool), a.typeCheck = Maybe.found Ty.bool h₁b.typeCheck = Maybe.found Ty.bool h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.and b) ty next ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.boolh₂✝:HasType b Ty.boolheq✝¹:a.typeCheck = Maybe.found Ty.bool h₁✝heq✝:b.typeCheck = Maybe.found Ty.bool h₂✝Maybe.found Ty.bool = Maybe.unknown¬HasType (a.and b) ty ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyx✝¹:{{ ty | HasType a ty }}x✝:{{ ty | HasType b ty }}h₁✝:HasType a Ty.boolh₂✝:HasType b Ty.boolheq✝¹:a.typeCheck = Maybe.found Ty.bool h₁✝heq✝:b.typeCheck = Maybe.found Ty.bool h₂✝a✝:Maybe.found Ty.bool = Maybe.unknown¬HasType (a.and b) ty; All goals completed! 🐙 next ra rb hnp ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.bool) (h₂ : HasType b Ty.bool), a.typeCheck = Maybe.found Ty.bool h₁b.typeCheck = Maybe.found Ty.bool h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.and b) ty ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.bool) (h₂ : HasType b Ty.bool), a.typeCheck = Maybe.found Ty.bool h₁b.typeCheck = Maybe.found Ty.bool h₂Falseh:Maybe.unknown = Maybe.unknownht:HasType (a.and b) tyFalse cases ht with | and h₁ h₂ ty:Tya:Exprb:Expriha:a.typeCheck = Maybe.unknown¬HasType a tyihb:b.typeCheck = Maybe.unknown¬HasType b tyra:{{ ty | HasType a ty }}rb:{{ ty | HasType b ty }}hnp:∀ (h₁ : HasType a Ty.bool) (h₂ : HasType b Ty.bool), a.typeCheck = Maybe.found Ty.bool h₁b.typeCheck = Maybe.found Ty.bool h₂FalseMaybe.unknown = Maybe.unknown¬HasType (a.and b) ty All goals completed! 🐙

Finally, we show that type checking for e can be decided using Expr.typeCheck.

instance (e : Expr) (t : Ty) : Decidable (HasType e t) := match h' : e.typeCheck with | .found t' ht' => if heq : t = t' then isTrue (heq ht') else isFalse fun ht => heq (HasType.det ht ht') | .unknown => isFalse (Expr.typeCheck_complete h')