12 - Type Inference
From Coq Require Import Strings.String.
Import Nat.
Require Import Maps.
From Coq Require Import Arith.Arith.
From Coq Require Import Arith.PeanoNat.
From Coq Require Import Bool.Bool.
Require Import lec_11_stlc.
References
- https://softwarefoundations.cis.upenn.edu/plf-current/MoreStlc.html
- https://softwarefoundations.cis.upenn.edu/plf-current/Typechecking.html
- https://softwarefoundations.cis.upenn.edu/plf-current/References.html
Recap
- Last time, we introduced STLC which took the Lambda calculus and layered a type-system on top.
- The advantage was that we could get rid of stuck terms.
- The disadvantage was that we lost expressive power.
- Today, we'll introduce more types and also look into how to make type-systems practical by introducing algorithmic versions a la type-checking and type-inference.
Goal for today
- Introduce more simple types: let, unit, products/sums, and records.
- Introduce reference types.
- Introduce more type system extensions: parameteric polymorphism and subtyping.
- Give type-checking and type-inference algorithms.
More Simple Types
Fix
Print tm.
(*
t ::= ... (same as before and ...)
| fix t (fix-points)
*)
t ::= ... (same as before and ...)
| fix t (fix-points)
*)
(*
same operational rules and ...
---------------------------------------------- ST_fix
fix (\x: T, t) --> x := fix (\x: T, t) t
*)
same operational rules and ...
---------------------------------------------- ST_fix
fix (\x: T, t) --> x := fix (\x: T, t) t
*)
(*
same typing rules and ...
Γ ⊢ t : T -> T
------------------ T_fix
Γ ⊢ fix t : T
*)
same typing rules and ...
Γ ⊢ t : T -> T
------------------ T_fix
Γ ⊢ fix t : T
*)
(*
Γ ⊢ (\f: nat -> nat, \n: nat, if n == 0 then 0 else f (n-1)) : (nat -> nat) -> (nat -> nat)
-------------------------------------------------------------------------------------- T_fix
Γ ⊢ fix (\f: nat -> nat, \n: nat, if n == 0 then 0 else f (n-1)) : nat -> nat
*)
Γ ⊢ (\f: nat -> nat, \n: nat, if n == 0 then 0 else f (n-1)) : (nat -> nat) -> (nat -> nat)
-------------------------------------------------------------------------------------- T_fix
Γ ⊢ fix (\f: nat -> nat, \n: nat, if n == 0 then 0 else f (n-1)) : nat -> nat
*)
(*
Γ ⊢ (\f: nat -> nat, \n: nat, f n) : (nat -> nat) -> (nat -> nat)
----------------------------------------------------------------------- T_fix
Γ ⊢ fix (\f: nat -> nat, \n: nat, f n) : nat -> nat
*)
Γ ⊢ (\f: nat -> nat, \n: nat, f n) : (nat -> nat) -> (nat -> nat)
----------------------------------------------------------------------- T_fix
Γ ⊢ fix (\f: nat -> nat, \n: nat, f n) : nat -> nat
*)
(*
t ::= ... (same as before and ...)
| let x=t in t (let bindings)
*)
t ::= ... (same as before and ...)
| let x=t in t (let bindings)
*)
Definition let_ex1: nat :=
let x: nat := 1 in
x + 1.
Eval compute in let_ex1.
Definition let_ex2: nat :=
let x: bool := true in
(let y: nat := if x then 1 else 2 in
y).
Eval compute in let_ex2.
Definition let_ex3: bool :=
let x: nat := 1 in
let x: bool := true in (* shadowing *)
x.
Eval compute in let_ex3.
(*
same operational rules and ...
t1 --> t1'
------------------------------------------ ST_Let1
let x = t1 in t2 --> let x = t1' in t2
value v1
------------------------------------------ ST_LetValue
let x = t1 in t2 --> x:= v1 t2
*)
same operational rules and ...
t1 --> t1'
------------------------------------------ ST_Let1
let x = t1 in t2 --> let x = t1' in t2
value v1
------------------------------------------ ST_LetValue
let x = t1 in t2 --> x:= v1 t2
*)
(*
same typing rules and ...
Γ ⊢ t1 : T1 Γ, x : T1 ⊢ t2 : T2
------------------------------------- T_Let
Γ ⊢ let x = t1 in t2 : T2
*)
same typing rules and ...
Γ ⊢ t1 : T1 Γ, x : T1 ⊢ t2 : T2
------------------------------------- T_Let
Γ ⊢ let x = t1 in t2 : T2
*)
(*
t ::= ... (same as before and ...)
| () (unit)
*)
t ::= ... (same as before and ...)
| () (unit)
*)
(*
T ::= ... (same as before and ...)
| unit (unit type)
*)
Print unit.
Definition unit: unit := tt.
T ::= ... (same as before and ...)
| unit (unit type)
*)
Print unit.
Definition unit: unit := tt.
(*
v ::= ... (same as before and ...)
| () (unit)
*)
v ::= ... (same as before and ...)
| () (unit)
*)
(*
same typing rules and ...
------------------------------ T_Unit
Γ ⊢ () : unit
*)
same typing rules and ...
------------------------------ T_Unit
Γ ⊢ () : unit
*)
(*
t ::= ... (same as before and ...)
| (t, t) (pair)
| fst t (first projection)
| snd t (second projection)
*)
t ::= ... (same as before and ...)
| (t, t) (pair)
| fst t (first projection)
| snd t (second projection)
*)
(*
T ::= ... (same as before and ...)
| T * T (product type)
*)
Definition prod1 : nat * bool := (1, true).
Definition prod2 : ((nat * bool) * nat) := ((1, true), 2).
T ::= ... (same as before and ...)
| T * T (product type)
*)
Definition prod1 : nat * bool := (1, true).
Definition prod2 : ((nat * bool) * nat) := ((1, true), 2).
(*
v ::= ... (same as before and ...)
| (v, v) (pair)
*)
v ::= ... (same as before and ...)
| (v, v) (pair)
*)
(*
same operational rules and ...
t1 --> t1'
--------------------------- ST_Pair1
(t1, t2) --> (t1', t2)
value v1 t2 --> t2'
--------------------------- ST_Pair2
(v1, t2) --> (v1, t2')
t --> t'
--------------------------- ST_Fst1
fst t --> fst t'
value v1 value v2
--------------------------- ST_FstPair
fst (v1, v2) --> v1
t --> t'
--------------------------- ST_Snd1
snd t --> snd t'
value v1 value v2
--------------------------- ST_SndPair
snd (v1, v2) --> v1
*)
same operational rules and ...
t1 --> t1'
--------------------------- ST_Pair1
(t1, t2) --> (t1', t2)
value v1 t2 --> t2'
--------------------------- ST_Pair2
(v1, t2) --> (v1, t2')
t --> t'
--------------------------- ST_Fst1
fst t --> fst t'
value v1 value v2
--------------------------- ST_FstPair
fst (v1, v2) --> v1
t --> t'
--------------------------- ST_Snd1
snd t --> snd t'
value v1 value v2
--------------------------- ST_SndPair
snd (v1, v2) --> v1
*)
(*
same typing rules and ...
Γ ⊢ t1 : T1 Γ ⊢ t2 : T2
--------------------------------- T_Pair
Γ ⊢ (t1, t2) : T1 * T2
Γ ⊢ t : T1 * T2
------------------------- T_Fst
Γ ⊢ fst t : T1
Γ ⊢ t : T1 * T2
------------------------- T_Fst
Γ ⊢ snd t : T2
*)
Eval compute in fst ((1 + 1, true), 2).
same typing rules and ...
Γ ⊢ t1 : T1 Γ ⊢ t2 : T2
--------------------------------- T_Pair
Γ ⊢ (t1, t2) : T1 * T2
Γ ⊢ t : T1 * T2
------------------------- T_Fst
Γ ⊢ fst t : T1
Γ ⊢ t : T1 * T2
------------------------- T_Fst
Γ ⊢ snd t : T2
*)
Eval compute in fst ((1 + 1, true), 2).
(*
t ::= ... (same as before and ...)
| inl T t (inject left)
| inr T t (inject right)
| case t of
| inl x => t
| inr y => t (case)
*)
t ::= ... (same as before and ...)
| inl T t (inject left)
| inr T t (inject right)
| case t of
| inl x => t
| inr y => t (case)
*)
Definition sum1 : nat + bool := inl 1.
Definition sum2 : nat + bool := inr true.
Definition sum3 (x: nat + bool) : bool + nat :=
match x with
| inl n => inr n
| inr b => inl b
end.
Values
Operational semantics
(*
same operational rules and ...
t1 --> t1'
---------------------------- ST_Inl
inl T2 t1 --> inl T2 t1'
t2 --> t2'
---------------------------- ST_Inr
inr T1 t2 --> inr T1 t2'
t0 --> t0'
---------------------------------------------- ST_Case
case t0 of inl x1 => t1 | inr x2 => t2 -->
case t0' of inl x1 => t1 | inr x2 => t2
value v1
--------------------------------------------------------------------- ST_Case
case (inl T2 v1) of inl x1 => t1 | inr x2 => t2 --> x1 := v1t1
value v2
--------------------------------------------------------------------- ST_Case
case (inr T1 v2) of inl x1 => t1 | inr x2 => t2 --> x2 := v2t2
*)
same operational rules and ...
t1 --> t1'
---------------------------- ST_Inl
inl T2 t1 --> inl T2 t1'
t2 --> t2'
---------------------------- ST_Inr
inr T1 t2 --> inr T1 t2'
t0 --> t0'
---------------------------------------------- ST_Case
case t0 of inl x1 => t1 | inr x2 => t2 -->
case t0' of inl x1 => t1 | inr x2 => t2
value v1
--------------------------------------------------------------------- ST_Case
case (inl T2 v1) of inl x1 => t1 | inr x2 => t2 --> x1 := v1t1
value v2
--------------------------------------------------------------------- ST_Case
case (inr T1 v2) of inl x1 => t1 | inr x2 => t2 --> x2 := v2t2
*)
(*
same typing rules and ...
Γ ⊢ t1 : T1
--------------------------------- T_Inl
Γ ⊢ inl T2 t1 : T1 + T2
Γ ⊢ t2 : T2
--------------------------------- T_Inr
Γ ⊢ inr T1 t2 : T1 + T2
Γ ⊢ t0 : T1 + T2 Γ, x1: T1 ⊢ t1 : T3 Γ, x2: T2 ⊢ t3 : T3
------------------------------------------------------------------ T_Case
Γ ⊢ case t0 of inl x1 => t1 | inr x2 => t2 : T3
*)
same typing rules and ...
Γ ⊢ t1 : T1
--------------------------------- T_Inl
Γ ⊢ inl T2 t1 : T1 + T2
Γ ⊢ t2 : T2
--------------------------------- T_Inr
Γ ⊢ inr T1 t2 : T1 + T2
Γ ⊢ t0 : T1 + T2 Γ, x1: T1 ⊢ t1 : T3 Γ, x2: T2 ⊢ t3 : T3
------------------------------------------------------------------ T_Case
Γ ⊢ case t0 of inl x1 => t1 | inr x2 => t2 : T3
*)
(*
t ::= ... (same as before and ...)
| {x=t, ..., x=t} (record)
| t.x (projection)
*)
t ::= ... (same as before and ...)
| {x=t, ..., x=t} (record)
| t.x (projection)
*)
(*
T ::= ... (same as before and ...)
| {x: T, ..., x: T} (record type)
*)
Record recTy1 : Set :=
mkRecTy1
{
field1: nat
}.
Definition rec1 : recTy1 :=
{|
field1 := 1
|}.
Record recTy2 : Set :=
mkRecTy2
{
field2: nat;
field3: bool
}.
Definition rec2 : recTy2 :=
{|
field2 := 1;
field3 := true;
|}.
T ::= ... (same as before and ...)
| {x: T, ..., x: T} (record type)
*)
Record recTy1 : Set :=
mkRecTy1
{
field1: nat
}.
Definition rec1 : recTy1 :=
{|
field1 := 1
|}.
Record recTy2 : Set :=
mkRecTy2
{
field2: nat;
field3: bool
}.
Definition rec2 : recTy2 :=
{|
field2 := 1;
field3 := true;
|}.
(*
v ::= ... (same as before and ...)
| {x=v, ..., x=v} (unit)
*)
v ::= ... (same as before and ...)
| {x=v, ..., x=v} (unit)
*)
(*
same operational rules and ...
value v_1 ... value v_{i-1} t_i --> t_i'
---------------------------------------------------------------------------- ST_Rec
{x_1=v_1, ..., x_i=v_{i-1}, x_i=t_i, x_{i+1}=t_{i+1}, ... x_n=t_n} -->
{x_1=v_1, ..., x_i=v_{i-1}, x_i=t_i', x_{i+1}=t_{i+1}, ... x_n=t_n}
t --> t'
-------------------- ST_ProjRec1
t.x --> t'.x
value v_1 ... value v_n
---------------------------------------------------- ST_ProjRec2
{x_1=v_1, ..., x_i=v_i, ... x_n=v_n}.x_i --> v_i
*)
same operational rules and ...
value v_1 ... value v_{i-1} t_i --> t_i'
---------------------------------------------------------------------------- ST_Rec
{x_1=v_1, ..., x_i=v_{i-1}, x_i=t_i, x_{i+1}=t_{i+1}, ... x_n=t_n} -->
{x_1=v_1, ..., x_i=v_{i-1}, x_i=t_i', x_{i+1}=t_{i+1}, ... x_n=t_n}
t --> t'
-------------------- ST_ProjRec1
t.x --> t'.x
value v_1 ... value v_n
---------------------------------------------------- ST_ProjRec2
{x_1=v_1, ..., x_i=v_i, ... x_n=v_n}.x_i --> v_i
*)
(*
same typing rules and ...
Γ ⊢ t_1 : T_1 ... Γ ⊢ t_n : T_n
--------------------------------------------------------- T_Rec
Γ ⊢ {x_1=t_1, ..., x_n=t_n} : {x_1:T_1, ..., x_n: T_n}
*)
same typing rules and ...
Γ ⊢ t_1 : T_1 ... Γ ⊢ t_n : T_n
--------------------------------------------------------- T_Rec
Γ ⊢ {x_1=t_1, ..., x_n=t_n} : {x_1:T_1, ..., x_n: T_n}
*)
(*
t ::= ... (same as before and ...)
| ref t (create reference)
| !t (read reference)
| t := t (write reference)
*)
t ::= ... (same as before and ...)
| ref t (create reference)
| !t (read reference)
| t := t (write reference)
*)
(*
T ::= ... (same as before and ...)
| Ref t (reference type)
*)
T ::= ... (same as before and ...)
| Ref t (reference type)
*)
Examples?
- Coq does not have references!
- We can use a programming model called monads to model state.
- This is a programming model that is used in Haskell.
Stores
- A store, or computer memory, is modeled as a list of terms.
- Each cell can be accessed by referencing it's location.
- Thus a location can be modeled as a natural number.
- This should remind of us the identifiers from IMP which were modeled as strings.
(*
---------------
| | | | | | | |
---------------
^
|
location
*)
---------------
| | | | | | | |
---------------
^
|
location
*)
(*
v ::= ... (same as before and ...)
| loc l (location)
*)
v ::= ... (same as before and ...)
| loc l (location)
*)
Operational semantics
- Just like IMP, we now need to define operational semantics involving transformations on state.
- With STLC + references, we will also need to involve the term reduction.
- Thus every operational rule will transform pairs of states and terms to pairs of states and terms.
(*
(σ, t) --> (σ' ,t')
-------------------------------- ST_Ref1
(σ, ref t) --> (σ', ref t')
value v σ' = σ ++ v
------------------------------------------- ST_Ref2
(σ, ref v) --> (σ', loc (length σ'))
(σ, t) --> (σ ,t')
-------------------------------- ST_Deref1
(σ, !t) --> (σ, !t')
l < length σ
-------------------------------- ST_Deref2
(σ, !loc l) --> (σ, σl)
(σ, t1) --> (σ, t1')
------------------------------------ ST_Assign1
(σ, t1 := t2) --> (σ, t1' := t2)
value v1 (σ, t2) --> (σ, t2')
------------------------------------ ST_Assign2
(σ, v1 := t2) --> (σ, v1 := t2')
value v2 σ' = update σ l v2
------------------------------------ ST_Assign3
(σ, loc l := v2) --> (σ', ())
*)
(σ, t) --> (σ' ,t')
-------------------------------- ST_Ref1
(σ, ref t) --> (σ', ref t')
value v σ' = σ ++ v
------------------------------------------- ST_Ref2
(σ, ref v) --> (σ', loc (length σ'))
(σ, t) --> (σ ,t')
-------------------------------- ST_Deref1
(σ, !t) --> (σ, !t')
l < length σ
-------------------------------- ST_Deref2
(σ, !loc l) --> (σ, σl)
(σ, t1) --> (σ, t1')
------------------------------------ ST_Assign1
(σ, t1 := t2) --> (σ, t1' := t2)
value v1 (σ, t2) --> (σ, t2')
------------------------------------ ST_Assign2
(σ, v1 := t2) --> (σ, v1 := t2')
value v2 σ' = update σ l v2
------------------------------------ ST_Assign3
(σ, loc l := v2) --> (σ', ())
*)
(*
Γ; σ ⊢ σl : T
-------------------------- WrongRef
Γ; σ ⊢ loc l : Ref T
*)
Γ; σ ⊢ σl : T
-------------------------- WrongRef
Γ; σ ⊢ loc l : Ref T
*)
- Intuitively, we might want to say that the type of a location is the the type of the term stored in that cell.
- However, this rule fails to deal with cycles.
Infinite Derivation with Cycles
(*
Suppose σ = \x:unit. !(loc 1), \x:unit. !(loc 0)
.
.
.
------------------------------------------------------------------- WrongRef
Γ, x: unit, x: unit; σ ⊢ !loc 0 : T3
------------------------------------------------------------------- Abs
Γ, x: unit; σ ⊢ \x:unit. !(loc 0) : unit -> T3
------------------------------------------------------------------- WrongRef
Γ, x: unit; σ ⊢ !(loc 1) x : T2
------------------------------------------------------------------- Abs
Γ; σ ⊢ \x:unit. !(loc 1) : unit -> T2
------------------------------------------------------------------- WrongRef
Γ; σ ⊢ !(loc 0) : Ref T1
*)
Suppose σ = \x:unit. !(loc 1), \x:unit. !(loc 0)
.
.
.
------------------------------------------------------------------- WrongRef
Γ, x: unit, x: unit; σ ⊢ !loc 0 : T3
------------------------------------------------------------------- Abs
Γ, x: unit; σ ⊢ \x:unit. !(loc 0) : unit -> T3
------------------------------------------------------------------- WrongRef
Γ, x: unit; σ ⊢ !(loc 1) x : T2
------------------------------------------------------------------- Abs
Γ; σ ⊢ \x:unit. !(loc 1) : unit -> T2
------------------------------------------------------------------- WrongRef
Γ; σ ⊢ !(loc 0) : Ref T1
*)
Store Typing
- We will need to introduce an idea called a *store typing* Σ.
- A store typing maps each location to a type.
- This breaks the cyclic self-reference with using a concrete store σ.
(*
Γ; Σ ⊢ Σl : T
-------------------------- TLoc
Γ; Σ ⊢ loc l : Ref T
Γ; Σ ⊢ t : T
-------------------------- TRef
Γ; Σ ⊢ ref t : Ref T
Γ; Σ ⊢ t : Ref T
-------------------------- TDeref
Γ; Σ ⊢ !t : T
Γ; Σ ⊢ t1 : Ref T Γ; Σ ⊢ t2 : T
------------------------------------------ TAssign
Γ; Σ ⊢ t1 := t2 : unit
*)
Γ; Σ ⊢ Σl : T
-------------------------- TLoc
Γ; Σ ⊢ loc l : Ref T
Γ; Σ ⊢ t : T
-------------------------- TRef
Γ; Σ ⊢ ref t : Ref T
Γ; Σ ⊢ t : Ref T
-------------------------- TDeref
Γ; Σ ⊢ !t : T
Γ; Σ ⊢ t1 : Ref T Γ; Σ ⊢ t2 : T
------------------------------------------ TAssign
Γ; Σ ⊢ t1 := t2 : unit
*)
Fixing Infinite Derivations with Store Typings
- Suppose σ = \x:unit. !(loc 1), \x:unit. !(loc 0)
- We can use Σ = unit -> unit -> unit, unit -> unit -> unit
(*
Γ; Σ ⊢ Σ0 : unit -> unit -> unit
------------------------------------------------------------------- WrongRef
Γ; Σ ⊢ !(loc 0) : Ref (unit -> unit -> unit)
*)
Γ; Σ ⊢ Σ0 : unit -> unit -> unit
------------------------------------------------------------------- WrongRef
Γ; Σ ⊢ !(loc 0) : Ref (unit -> unit -> unit)
*)
Progress and Preservation?
- Unlike all the previous extensions, references change our operational semantics and typing relation substantially.
- Consequently, we might wonder if progress and preservation still hold.
- First, how do we even state progress and preservation?
Preservation Wrong
- A first attempt at phrasing preservation might look like the following:
- This isn't quite right because we are not relating the store typing Σ and a concrete store σ.
- We will need an extra condition that relates the two.
Well-Formed Stores
(*
dom(Σ) = dom(σ) ∀ l \in dom(σ), empty; Σ ⊢ σl : Σl
-------------------------------------------------------------
Σ ⊢ σ
*)
dom(Σ) = dom(σ) ∀ l \in dom(σ), empty; Σ ⊢ σl : Σl
-------------------------------------------------------------
Σ ⊢ σ
*)
Preservation Wrong 2
- A second attempt at phrasing preservation might look like the following:
- The reason this is wrong is because ref t creates a larger store.
- Similar to a "weakening" lemma on contexts, we need to define extensions of store typings.
Preservation Right
- The correct statement of preservation is given below:
Complex Types
Parametric Polymorphism
Syntax
(*
t ::= ... (same as before and ...)
| ΛX.t (type abstraction)
| tT (type application)
*)
t ::= ... (same as before and ...)
| ΛX.t (type abstraction)
| tT (type application)
*)
(*
T ::= ... (same as before and ...)
| ∀X.T (polymorphic type)
*)
Definition id: forall A: Type, A -> A :=
fun (A: Type) => fun (x: A) => x.
Definition id' (A: Type) (x: A): A :=
x.
Definition swap: forall A: Type, forall B: Type, A * B -> B * A :=
fun (A: Type) => fun (B: Type) => fun (p: A * B) => (snd p, fst p).
Definition swap' (A B: Type) (p: A * B): B * A :=
(snd p, fst p).
T ::= ... (same as before and ...)
| ∀X.T (polymorphic type)
*)
Definition id: forall A: Type, A -> A :=
fun (A: Type) => fun (x: A) => x.
Definition id' (A: Type) (x: A): A :=
x.
Definition swap: forall A: Type, forall B: Type, A * B -> B * A :=
fun (A: Type) => fun (B: Type) => fun (p: A * B) => (snd p, fst p).
Definition swap' (A B: Type) (p: A * B): B * A :=
(snd p, fst p).
(*
v ::= ... (same as before and ...)
| ΛX.t (type abstraction)
*)
v ::= ... (same as before and ...)
| ΛX.t (type abstraction)
*)
(*
same operational rules and ...
---------------------------- ST_TypeRed
(ΛX.t)T --> X:=Tt
*)
same operational rules and ...
---------------------------- ST_TypeRed
(ΛX.t)T --> X:=Tt
*)
Type abstraction contexts
- Similar to lambda abstractions, we need to ensure that our type variables are well-formed.
- We do this with type variable contexts Δ, which is a set of type variables in scope.
Typing rules
(*
similar typing rules where we use Δ and
Δ, X; Γ ⊢ t : T fv(T) ⊆ Δ, X
------------------------------------ T_TypeAbs
Δ; Γ ⊢ ΛX.t : ∀X.T
Δ; Γ ⊢ t : ∀X.T1 fv(T1) ⊆ Δ
------------------------------------ T_TypeApp
Δ; Γ ⊢ tT2 : X := T2T1
*)
similar typing rules where we use Δ and
Δ, X; Γ ⊢ t : T fv(T) ⊆ Δ, X
------------------------------------ T_TypeAbs
Δ; Γ ⊢ ΛX.t : ∀X.T
Δ; Γ ⊢ t : ∀X.T1 fv(T1) ⊆ Δ
------------------------------------ T_TypeApp
Δ; Γ ⊢ tT2 : X := T2T1
*)
(*
Γ ⊢ t : T1 T1 ≤ T2
-------------------------------- T_Subsume
Γ ⊢ t: T2
*)
Γ ⊢ t : T1 T1 ≤ T2
-------------------------------- T_Subsume
Γ ⊢ t: T2
*)
(*
-------------- Refl
T ≤ T
T1 ≤ T2 T2 ≤ T3
------------------------- Trans
T1 ≤ T3
*)
-------------- Refl
T ≤ T
T1 ≤ T2 T2 ≤ T3
------------------------- Trans
T1 ≤ T3
*)
(*
perm is permutation
-------------------------------------------------------------------- Sub_REc
{x_1=t_1, ..., x_{n+k}=t_{n+k}} ≤ perm({x_1:T_1, ..., x_n: T_n})
*)
perm is permutation
-------------------------------------------------------------------- Sub_REc
{x_1=t_1, ..., x_{n+k}=t_{n+k}} ≤ perm({x_1:T_1, ..., x_n: T_n})
*)
(*
T1 ≤ T1' T2 ≤ T2'
----------------------------- Sub_Prod
(T1, T2) ≤ (T1', T2')
*)
T1 ≤ T1' T2 ≤ T2'
----------------------------- Sub_Prod
(T1, T2) ≤ (T1', T2')
*)
(*
T1' ≤ T1 T2 ≤ T2'
----------------------------- Sub_Prod
T1 -> T2 ≤ T1' -> T2'
*)
T1' ≤ T1 T2 ≤ T2'
----------------------------- Sub_Prod
T1 -> T2 ≤ T1' -> T2'
*)
- Contravariant in input type
- Covariant in return type
Type-Checking
- The typing rules give us a way to separate well-typed terms from stuck terms.
- But we don't have an algorithm.
- We now show that we can make this algorithmic by giving a type-checking algorithm
Fixpoint eqb_ty (T1 T2:ty) : bool :=
match T1,T2 with
| <{ Bool }> , <{ Bool }> =>
true
| <{ T11 -> T12 }>, <{ T21 -> T22 }> =>
andb (eqb_ty T11 T21) (eqb_ty T12 T22)
| _,_ =>
false
end.
Lemma eqb_ty_refl : forall T,
eqb_ty T T = true.
Proof.
intros T. induction T; simpl.
reflexivity.
rewrite IHT1. rewrite IHT2. reflexivity.
Qed.
Lemma eqb_ty__eq : forall T1 T2,
eqb_ty T1 T2 = true -> T1 = T2.
Proof with auto.
intros T1. induction T1; intros T2 Hbeq; destruct T2; inversion Hbeq.
- (* T1=Bool *)
reflexivity.
- (* T1 = T1_1->T1_2 *)
rewrite andb_true_iff in H0.
inversion H0 as [Hbeq1 Hbeq2].
apply IHT1_1 in Hbeq1.
apply IHT1_2 in Hbeq2.
subst...
Qed.
Fixpoint type_check (Gamma : context) (t : tm) : option ty :=
match t with
| tm_var x =>
Gamma x
| <{\x:T2, t1}> =>
match type_check (x |-> T2 ; Gamma) t1 with
| Some T1 => Some <{T2 -> T1}>
| _ => None
end
| <{t1 t2}> =>
match type_check Gamma t1, type_check Gamma t2 with
| Some <{T11 -> T12}>, Some T2 =>
if eqb_ty T11 T2 then Some T12 else None
| _,_ => None
end
| <{true}> =>
Some <{Bool}>
| <{false}> =>
Some <{Bool}>
| <{if guard then t else f}> =>
match type_check Gamma guard with
| Some <{Bool}> =>
match type_check Gamma t, type_check Gamma f with
| Some T1, Some T2 =>
if eqb_ty T1 T2 then Some T1 else None
| _,_ => None
end
| _ => None
end
end.
Eval compute in type_check empty <{true}>.
(* ==> Some <{ Bool }> *)
Eval compute in type_check empty <{\x: Bool, x}>.
(* ==> Some <{ Bool -> Bool }> *)
Eval compute in type_check empty <{\x: Bool, \x: Bool, x x}>.
(* ==> None *)
Eval compute in type_check empty <{\f: Bool -> Bool, \x: Bool, f x}>.
(* ==> Some <{ (Bool -> Bool) -> Bool -> Bool }> *)
- We should check that our algorithm and our typing rules match.
- We do this by showing that our type-checker is sound for the given typing rules.
Theorem type_checking_sound : forall Gamma t T,
type_check Gamma t = Some T ->
Gamma ⊢ t \in T.
Proof with eauto.
intros Gamma t.
generalize dependent Gamma.
induction t; intros Gamma T Htc; inversion Htc.
{
(* var *)
rename s into x.
destruct (Gamma x) eqn:H.
rename t into T'.
inversion H0; subst.
eauto.
inversion H0.
}
{
(* app *)
remember (type_check Gamma t1) as TO1.
destruct TO1 as [T1|].
destruct T1 as [|T11 T12].
inversion H0.
remember (type_check Gamma t2) as TO2.
destruct TO2.
destruct (eqb_ty T11 t) eqn: Heqb.
apply eqb_ty__eq in Heqb; subst.
inversion H0; subst; eauto.
inversion H0.
inversion H0.
inversion H0.
}
{
(* abs *)
rename s into x, t into T1.
remember (x |-> T1 ; Gamma) as G'.
remember (type_check G' t0) as TO2.
destruct TO2.
inversion H0; subst. auto.
inversion H0.
}
{
(* tru *)
eauto.
}
{
(* fls *)
eauto.
}
{
(* test *)
remember (type_check Gamma t1) as TOc.
remember (type_check Gamma t2) as TO1.
remember (type_check Gamma t3) as TO2.
destruct TOc as [Tc|].
destruct Tc.
destruct TO1 as [T1|].
destruct TO2 as [T2|].
destruct (eqb_ty T1 T2) eqn:Heqb. inversion H0; subst.
apply eqb_ty__eq in Heqb; subst. auto.
inversion H0.
inversion H0.
inversion H0.
inversion H0.
inversion H0.
}
Qed.
type_check Gamma t = Some T ->
Gamma ⊢ t \in T.
Proof with eauto.
intros Gamma t.
generalize dependent Gamma.
induction t; intros Gamma T Htc; inversion Htc.
{
(* var *)
rename s into x.
destruct (Gamma x) eqn:H.
rename t into T'.
inversion H0; subst.
eauto.
inversion H0.
}
{
(* app *)
remember (type_check Gamma t1) as TO1.
destruct TO1 as [T1|].
destruct T1 as [|T11 T12].
inversion H0.
remember (type_check Gamma t2) as TO2.
destruct TO2.
destruct (eqb_ty T11 t) eqn: Heqb.
apply eqb_ty__eq in Heqb; subst.
inversion H0; subst; eauto.
inversion H0.
inversion H0.
inversion H0.
}
{
(* abs *)
rename s into x, t into T1.
remember (x |-> T1 ; Gamma) as G'.
remember (type_check G' t0) as TO2.
destruct TO2.
inversion H0; subst. auto.
inversion H0.
}
{
(* tru *)
eauto.
}
{
(* fls *)
eauto.
}
{
(* test *)
remember (type_check Gamma t1) as TOc.
remember (type_check Gamma t2) as TO1.
remember (type_check Gamma t3) as TO2.
destruct TOc as [Tc|].
destruct Tc.
destruct TO1 as [T1|].
destruct TO2 as [T2|].
destruct (eqb_ty T1 T2) eqn:Heqb. inversion H0; subst.
apply eqb_ty__eq in Heqb; subst. auto.
inversion H0.
inversion H0.
inversion H0.
inversion H0.
inversion H0.
}
Qed.
- We can also show completeness!
- That is, if there exists a derivation, our type-checker will verify it.
Theorem type_checking_complete : forall Gamma t T,
Gamma ⊢ t \in T ->
type_check Gamma t = Some T.
Proof.
intros Gamma t T Hty.
(* Question: what should we do induction on? *)
induction Hty; simpl.
{
(* T_Var *)
destruct (Gamma x) eqn:H0; assumption.
}
{
(* T_Abs *)
rewrite IHHty; auto.
}
{ (* T_App *)
rewrite IHHty1.
rewrite IHHty2.
rewrite (eqb_ty_refl T2); auto.
}
{
(* T_True *)
eauto.
}
{
(* T_False *)
eauto.
}
{
(* T_If *)
rewrite IHHty1.
rewrite IHHty2.
rewrite IHHty3.
rewrite (eqb_ty_refl T1); auto.
}
Qed.
Gamma ⊢ t \in T ->
type_check Gamma t = Some T.
Proof.
intros Gamma t T Hty.
(* Question: what should we do induction on? *)
induction Hty; simpl.
{
(* T_Var *)
destruct (Gamma x) eqn:H0; assumption.
}
{
(* T_Abs *)
rewrite IHHty; auto.
}
{ (* T_App *)
rewrite IHHty1.
rewrite IHHty2.
rewrite (eqb_ty_refl T2); auto.
}
{
(* T_True *)
eauto.
}
{
(* T_False *)
eauto.
}
{
(* T_If *)
rewrite IHHty1.
rewrite IHHty2.
rewrite IHHty3.
rewrite (eqb_ty_refl T1); auto.
}
Qed.
Type-Inference
- The goal of type-inference is to have an algorithm that gives us the types of terms without us having to write them down.
- We define a new syntax of types that contains guesses.
- We define a typing relation that generates *constraints* involving guesses.
- We solve for the constraints using *unification* producing a type substitution.
- The resulting type substitution gives us the types.
(*
T ::= Bool
| T -> T
| X (* guess *)
*)
Inductive ty : Type :=
| Ty_Bool : ty (* for the boolean values *)
| Ty_Arrow : ty -> ty -> ty (* t1 -> t2, for the abstraction values *)
| Ty_Var : string -> ty (* guess *)
.
T ::= Bool
| T -> T
| X (* guess *)
*)
Inductive ty : Type :=
| Ty_Bool : ty (* for the boolean values *)
| Ty_Arrow : ty -> ty -> ty (* t1 -> t2, for the abstraction values *)
| Ty_Var : string -> ty (* guess *)
.
Types now have free variables
- We could compute free variables in terms.
- Now that types have variables, we have a similar notion.
Fixpoint free_var (t: ty) : set string :=
match t with
| Ty_Bool =>
empty_set string
| Ty_Arrow t1 t2 =>
set_union string_dec (free_var t1) (free_var t2)
| Ty_Var X =>
set_add string_dec X (empty_set string)
end.
(*
t ::= x
| t t
| \x: T. t
| true
| false
| if t t t
*)
Inductive tm : Type :=
| tm_var : string -> tm
| tm_app : tm -> tm -> tm
| tm_abs : string -> ty -> tm -> tm (* parameter augmented with type *)
| tm_true : tm
| tm_false : tm
| tm_if : tm -> tm -> tm -> tm
.
t ::= x
| t t
| \x: T. t
| true
| false
| if t t t
*)
Inductive tm : Type :=
| tm_var : string -> tm
| tm_app : tm -> tm -> tm
| tm_abs : string -> ty -> tm -> tm (* parameter augmented with type *)
| tm_true : tm
| tm_false : tm
| tm_if : tm -> tm -> tm -> tm
.
Free variables for constraints
- We can collect the free variables in the types in a constraint. and lists of constraints.
Fixpoint free_var_constraint (c: constraint) : set string :=
match c with
| Ty_eq t t' => set_union string_dec (free_var t) (free_var t')
end.
Definition free_var_constraints (cs: list constraint) : set string :=
List.fold_left (fun acc x => set_union string_dec acc x) (List.map free_var_constraint cs) (empty_set string).
(*
Γ(x) = T
-------------------- T_Var
Γ ⊢ x: T ~> {}
Γ, x: T2 ⊢ t: T1 ~> C
------------------------------- T_Abs
Γ ⊢ \x: T2, t: T2 -> T1 ~> C
Γ ⊢ t1: T2 ~> C1 Γ ⊢ t2: T1 ~> C2 X ∉ fv(C1 ∪ C2)
----------------------------------------------------------- T_App
Γ ⊢ t1 t2: T1 ~> C1 ∪ C2 ∪ {T2 = T1 -> X}
--------------------------- T_True
Γ ⊢ true: Bool ~> {}
--------------------------- T_False
Γ ⊢ false: Bool ~> {}
Γ ⊢ t1 : Bool ~> C1 Γ ⊢ t2 : T ~> C2 Γ ⊢ t3 : T ~> C3
---------------------------------------------------------------- T_False
Γ ⊢ if t1 t2 t3: T ~> C1 ∪ C2 ∪ C3
*)
Definition context := partial_map ty.
Inductive has_typeC : context -> tm -> ty -> list constraint -> Prop :=
| T_Var : forall Gamma a T1,
Gamma a = Some T1 ->
has_typeC Gamma (tm_var a) T1 nil
| T_Abs : forall Gamma x T1 T2 t1 C,
has_typeC (x |-> T2 ; Gamma) t1 T1 C ->
has_typeC Gamma (tm_abs x T2 t1) (Ty_Arrow T2 T1) C
| T_App : forall T1 T2 Gamma t1 t2 C1 C2 X,
has_typeC Gamma t1 (Ty_Arrow T2 T1) C1 ->
has_typeC Gamma t2 T2 C2 ->
set_mem string_dec X (free_var_constraints (C1 ++ C2)) = false ->
has_typeC Gamma (tm_app t1 t2) T1 (C1 ++ C2 ++ (Ty_eq T2 (Ty_Arrow T1 (Ty_Var X)) :: nil))
| T_True : forall Gamma,
has_typeC Gamma tm_true Ty_Bool nil
| T_False : forall Gamma,
has_typeC Gamma tm_false Ty_Bool nil
| T_If : forall t1 t2 t3 T1 Gamma C1 C2 C3,
has_typeC Gamma t1 Ty_Bool C1 ->
has_typeC Gamma t2 T1 C2 ->
has_typeC Gamma t3 T1 C3 ->
has_typeC Gamma (tm_if t1 t2 t3) T1 (C1 ++ C2 ++ C3).
Γ(x) = T
-------------------- T_Var
Γ ⊢ x: T ~> {}
Γ, x: T2 ⊢ t: T1 ~> C
------------------------------- T_Abs
Γ ⊢ \x: T2, t: T2 -> T1 ~> C
Γ ⊢ t1: T2 ~> C1 Γ ⊢ t2: T1 ~> C2 X ∉ fv(C1 ∪ C2)
----------------------------------------------------------- T_App
Γ ⊢ t1 t2: T1 ~> C1 ∪ C2 ∪ {T2 = T1 -> X}
--------------------------- T_True
Γ ⊢ true: Bool ~> {}
--------------------------- T_False
Γ ⊢ false: Bool ~> {}
Γ ⊢ t1 : Bool ~> C1 Γ ⊢ t2 : T ~> C2 Γ ⊢ t3 : T ~> C3
---------------------------------------------------------------- T_False
Γ ⊢ if t1 t2 t3: T ~> C1 ∪ C2 ∪ C3
*)
Definition context := partial_map ty.
Inductive has_typeC : context -> tm -> ty -> list constraint -> Prop :=
| T_Var : forall Gamma a T1,
Gamma a = Some T1 ->
has_typeC Gamma (tm_var a) T1 nil
| T_Abs : forall Gamma x T1 T2 t1 C,
has_typeC (x |-> T2 ; Gamma) t1 T1 C ->
has_typeC Gamma (tm_abs x T2 t1) (Ty_Arrow T2 T1) C
| T_App : forall T1 T2 Gamma t1 t2 C1 C2 X,
has_typeC Gamma t1 (Ty_Arrow T2 T1) C1 ->
has_typeC Gamma t2 T2 C2 ->
set_mem string_dec X (free_var_constraints (C1 ++ C2)) = false ->
has_typeC Gamma (tm_app t1 t2) T1 (C1 ++ C2 ++ (Ty_eq T2 (Ty_Arrow T1 (Ty_Var X)) :: nil))
| T_True : forall Gamma,
has_typeC Gamma tm_true Ty_Bool nil
| T_False : forall Gamma,
has_typeC Gamma tm_false Ty_Bool nil
| T_If : forall t1 t2 t3 T1 Gamma C1 C2 C3,
has_typeC Gamma t1 Ty_Bool C1 ->
has_typeC Gamma t2 T1 C2 ->
has_typeC Gamma t3 T1 C3 ->
has_typeC Gamma (tm_if t1 t2 t3) T1 (C1 ++ C2 ++ C3).
Interpreting Type Variables
- Just like we might interpret variables in a term with an environment, we can interpret type variables in a type variable with an environment.
Definition type_context := partial_map ty.
Fixpoint type_substitution (ty_ctx: type_context) (t: ty) :=
match t with
| Ty_Bool => Ty_Bool
| Ty_Arrow t1 t2 => Ty_Arrow (type_substitution ty_ctx t1) (type_substitution ty_ctx t2)
| Ty_Var x =>
match (ty_ctx x) with
| Some ty => ty
| None => Ty_Var x
end
end.
Definition type_substitution_constraints (ty_ctx: type_context) (constraints: list constraint) :=
List.map
(fun c =>
match c with
| Ty_eq t1 t2 => Ty_eq (type_substitution ty_ctx t1) (type_substitution ty_ctx t2)
end) constraints.
Unification
- Now we reach the famous Hindley-Milner unification algorithm.
- If a type substitution exists, then unification finds it, and the term with that type under that type substitution has a typing derivation.
- If a type substitution does not exist, then unification will return None, and the term does not have a typing derivation.
- It is a remarkably simple algorithm.
- Note that it is not easy to show that this algorithm terminates in Coq, so we pass in some fuel.
Fixpoint unify (n: nat) (constraints: list constraint) (ty_ctx: type_context): option type_context :=
match n with
| O => None
| S n' =>
match constraints with
| nil => Some ty_ctx
| c::constraints' =>
match c with
| Ty_eq Ty_Bool Ty_Bool =>
unify n' constraints' ty_ctx
| Ty_eq (Ty_Var X) t' =>
if set_mem string_dec X (free_var t')
then None
else
let ty_ctx' := X |-> t' ; ty_ctx
in
unify n' (type_substitution_constraints ty_ctx' constraints') ty_ctx'
| Ty_eq t (Ty_Var X) =>
if set_mem string_dec X (free_var t)
then None
else
let ty_ctx' := X |-> t ; ty_ctx
in
unify n' (type_substitution_constraints ty_ctx' constraints') ty_ctx'
| Ty_eq (Ty_Arrow t1 t2) (Ty_Arrow t1' t2') =>
match n with
| O => None
| S n' =>
match constraints with
| nil => Some ty_ctx
| c::constraints' =>
match c with
| Ty_eq Ty_Bool Ty_Bool =>
unify n' constraints' ty_ctx
| Ty_eq (Ty_Var X) t' =>
if set_mem string_dec X (free_var t')
then None
else
let ty_ctx' := X |-> t' ; ty_ctx
in
unify n' (type_substitution_constraints ty_ctx' constraints') ty_ctx'
| Ty_eq t (Ty_Var X) =>
if set_mem string_dec X (free_var t)
then None
else
let ty_ctx' := X |-> t ; ty_ctx
in
unify n' (type_substitution_constraints ty_ctx' constraints') ty_ctx'
| Ty_eq (Ty_Arrow t1 t2) (Ty_Arrow t1' t2') =>
- This case increases the constraints lists.
- Hence we need to do the "fuel" trick.
unify n' (constraints' ++ (Ty_eq t1 t1'::Ty_eq t2 t2'::nil)) ty_ctx
| Ty_eq _ _ =>
None
end
end
end.
| Ty_eq _ _ =>
None
end
end
end.
Definition u2 := unify 100 (Ty_eq (Ty_Var "X") Ty_Bool::nil) empty.
Eval compute in
(match u2 with
| Some ctx => ctx "X"%string
| None => None
end).
Eval compute in
(match u2 with
| Some ctx => ctx "Y"%string
| None => None
end).
Eval compute in
(match u2 with
| Some ctx => ctx "X"%string
| None => None
end).
Eval compute in
(match u2 with
| Some ctx => ctx "Y"%string
| None => None
end).
Definition constraints3 :=
Ty_eq (Ty_Var "X") Ty_Bool::
Ty_eq (Ty_Arrow Ty_Bool Ty_Bool) (Ty_Arrow Ty_Bool (Ty_Var "X"))::
nil.
Definition u3 := unify 100 constraints3 empty.
Eval compute in
(match u3 with
| Some ctx => ctx "X"%string
| None => None
end).
Eval compute in
(match u3 with
| Some ctx => ctx "Y"%string
| None => None
end).
Ty_eq (Ty_Var "X") Ty_Bool::
Ty_eq (Ty_Arrow Ty_Bool Ty_Bool) (Ty_Arrow Ty_Bool (Ty_Var "X"))::
nil.
Definition u3 := unify 100 constraints3 empty.
Eval compute in
(match u3 with
| Some ctx => ctx "X"%string
| None => None
end).
Eval compute in
(match u3 with
| Some ctx => ctx "Y"%string
| None => None
end).
Definition constraints4 :=
Ty_eq (Ty_Var "X") Ty_Bool::
Ty_eq (Ty_Arrow Ty_Bool Ty_Bool) (Ty_Arrow Ty_Bool (Ty_Arrow Ty_Bool (Ty_Var "X")))::
nil.
Definition u4 := unify 100 constraints4 empty.
Eval compute in u4.
Ty_eq (Ty_Var "X") Ty_Bool::
Ty_eq (Ty_Arrow Ty_Bool Ty_Bool) (Ty_Arrow Ty_Bool (Ty_Arrow Ty_Bool (Ty_Var "X")))::
nil.
Definition u4 := unify 100 constraints4 empty.
Eval compute in u4.
- Hindley-Milner type inference has the property that it returns the minimal substitution that makes the term typeable.
- And, if the term is not typeable, then it returns None.
Summary
- More simple types * FIx * Let / Unit * Products / Sums * Records
- Reference Types (problem of cycles)
- Complex extensions * Parametric Polymorphism * Subtyping
- Type-Checking
- Type-Inference