Typing rules:
Judgment form: e : t
--------- ty/tt ----------- ty/ff ------------------ ty/n
tt : Bool ff : Bool (num n) : Number
e1 : Number e2 : Number
--------------------------- ty/+
(e1 + e2) : Number
e : Bool e1 : t e2 : t
------------------------------- ty/if
if e then e1 else e2 : t
Concept: these rules are syntax-directed. That is, there is exactly one rule matching every syntactic form in the language.
“Well-typed programs don’t go wrong” =>
“Well-typed programs evaluate to a value of the same type” =>
If e : t, then e ==> v and v : t
Proof: by induction over the derivation of e : t
Look at each inference rule for e ==> v one by one:
(num n) : Num, tt : Bool, and ff : Bool all work triviallye1 + e2 : Num when e1 : Num and e2 : Num:
e1 ==> v1, e2 ==> v2, v1 : Num and v2 : NumNum, v1 must be num n
and v2 must be num m for some numbers n and meval/plus, (e1 + e2) ==> num (n + m), and num (n + m)
is a value of type Numif e then e1 else e2 when e : Bool and there exists a type t such that e1 : t and e2 : t:
e ==> v and v : Bool.e1 ==> v1 and v1 : t.e2 ==> v2 and v2 : t.v are tt and ff.
v = tt. Then by rule eval/if, it suffices to show a v for which e1 ==> v and v : t. Let v be v1 and then we have what we need from IH.v = ff. Similar to previous case but for e2 and v2.Jugment: Γ ⊢ e ok – means e is well-scoped, i.e. all of its free variables are in Γ.
Examples:
y ⊢ (let x = 10 in x + y)⋅ ⊢ (let x = 10 in x + y)Goal property:
⋅ ⊢ e ok then running e will not result in any unbound variable errors.x ∈ Γ
--------- ok/var
Γ ⊢ x ok
---------------- ok/num
Γ ⊢ (num n) ok
Γ ⊢ e1 ok Γ, x ⊢ e2 ok
----------------------------- ok/let
Γ ⊢ (let x = e1 in e2) ok
Exercise: check the good/bad example above.
Type errors we want to rule out:
(10 3)(λx.x) + 3(λx. x + x) (λy.y)Inference rules*
Values:
v ::= num n | λx.e
Types:
t ::= Num | t -> t
Expressions:
e ::= num n | e + e | λx.e | e e | x
x : t ∈ Γ
------------ ty/var
Γ ⊢ x : t
------------------ ty/num
Γ ⊢ (num n) : Num
Γ ⊢ e1 : Num Γ ⊢ e2 : Num
------------------------------- ty/+
Γ ⊢ e1 + e2 : Num
Γ, x : t ⊢ e : s
--------------------- ty/λ
Γ ⊢ λx.e : t -> s
Γ ⊢ e1 : t -> s Γ ⊢ e2 : t1
--------------------------------- ty/app
Γ ⊢ (e1 e2) : s
Exercises
(λx.x) : Num -> Num
: and still have a derivation?((λx.x) (num 10)) : Num(λx.x) (λy.y) : Num -> Num(λf.f (num 10)) : (Num -> Num) -> Num(λx.x) (λx.x)
If we write a small step semantics, we can prove something easier:
If
e : t, then eithereis a value of typet, orecan take a step to somee' : t.
This formulation is sometimes broken out into two lemmas, known as progress and preservation:
e : t then either e is a value or can take a step.e : t and e steps to e', then e' : t.Some lemmas, representative of key ideas in designing type systems:
e : t and e is a value, then e is one of the canonical values of type t. For example, if t is an arrow (function) type, then e must be a function.G, x: t |- e : s and esub : t, then G |- [esub/x]e : s.e => v and e => v' with v and v' values, v = v'.. |- e : t and e is a value, it can’t take a step.Take a look at the rules and think about how to implement them.
Can we write a type checker?
app rule makes this tricky.Can we write a type synthesizer?
lam rule makes this tricky.There are a lot of approaches to resolving this, but one way is to ask the programmer to provide type annotations in certain places.
In particular, if we ask λ expressions to annotate their input type, we can write a type synthesizer.
See lec8-stlc.rkt for code.