2025-11-13 Andreas Abel, transcript of lecture by Magnus Myreen
IDEs complain about parts of programs, e.g.:
z = x / y
~~~
Why is type checking important?
Compiler might not know what to do, e.g. integer division vs. floating point division.
Type checking proves certain properties of your program.
Type soundness in strongly-typed programming languages:
typeable p ==> semantics p ≠ crashSpecification:
A type system is defined by inference rules defining a relation that is a type judgement.
Γ ⊢ e : t
Implementation:
Format:
J₀ J₁ ... Jₙ
------------ C
J
even n
------ ------------
even 0 even (n + 2)
Claim: even 6. Has to be proven with the rules.
------
even 0
------
even 2
------
even 4
------
even 6
More rules, e.g.
even n even m
------------------
even (n + m)
Allows more proofs
------ ------
even 0 even 0
------ ------
even 2 even 2
----------------
even 4
n+1 ↝ m
------ -------
n ↝ n n ↝ m
We can derive 1 ↝ 3 but not 4 ↝ 3.
-----
3 ↝ 3
-----
2 ↝ 3
-----
1 ↝ 3
So n ↝ m is n ≤ m.
Example: rules for divison
e₁ : int e₂ : int
----------------------
e₁ / e₂ : int
e₁ : double e₂ : double
-------------------------
e₁ / e₂ : double
Generically:
e₁ : t e₂ : t
------------------ t ∈ {int, double}
e₁ / e₂ : t
Writing it properly with ASTs:
e₁ : t e₂ : t
------------------ t ∈ {int, double}
EDiv e₁ e₂ : t
E.g. 2 / 3 is abstract syntax tree
EDiv (ELitInt 2) (ELitInt 3).
---------------
ELitInt i : int
With concrete syntax, this gets a bit ambiguous:
-------
i : int
Needs a side condition that i is an integer
literal.
Convention:
i stands for integer literalse stands for expressionsChecking: given an expression e and a type
t, answer yes (e : t) or no.
check : (Exp, Typ) → Bool
Inference: given an expression e, produce its type
t if it has one.
infer : Exp → Maybe Typ
What is the connection to the typing rules?
Write some pseudo-code...
check (ELitInt i, t): return (t == int)
infer (ELitInt x): return (Just int)
check (EDiv e₁ e₂, t): return check (e₁, t) && check (e₂, t) && t ∈ {int, double}
infer (EDic e₁, e₂):
t₁ ← infer e₁
t₂ ← infer e₂
assert (t₁ == t₂)
assert (t₁ ∈ {int,double})
return t₁
Type checker transforms
EDiv (EVar "y") (EVar "z")
to type-annotated expressions
EDivT int (EVarT "y" int) (EVarT "z" int)
Example:
int k = 5;
double d = k / 2.0;
This would be rejected by the type checker.
New rule:
e : t₁
------ t₁ ≤ t₂
e : t₂
Subtyping t₁ ≤ t₂, e.g. int ≤ double.
Derivation:
k : int
---------- ------------
k : double 2.0 : double
---------------------------
k / 2.0 : double
It is not syntactically clear when we should flip the type
from int to double.
The rule allows it at any point.
The type annotated expressions need to retain evidence of the use of the subtyping rule.
ECoerceT int double e
e₁ : t e₂ : t
---------------- t ∈ {int, double, bool}
e₁ == e₂ : bool
So what is the type of assignment statement, e.g.
x = 5;?
For statements we write ⊢ s to mean statement
s is type-correct.
How to we indicate that something has a specific return type?
E.g.
if (...) { return 5; } else { return 2.5; }
Type correct:
|-^double if (...) { return 5; } else { return 2.5; }
Not correct:
|-^int if (...) { return 5; } else { return 2.5; }
Rule:
e : t
------------
⊢ᵗ return e;
We are still missing the typing of variables.
This is where the environment Γ comes into play.
Example program:
int f (double x) {
int i = 2;
int j = 5;
// int i = 3; // this would not be allowed, since i is already declared in this block
{
int x = 0; // ok to overwrite x because we are in a different scope
double i = 3; // ok to overwrite i because we are in a different scope
{
... i + x ... // these refer to i and x of the inner scope
// Context here:
// (x:double, i:int, j:int), (x:int, i:double)
// consists of 2 blocks
}
int k;
}
i++;
}
Rules with context:
lookup x in Γ gives t
---------------------
Γ ⊢ x : t
Example:
Γ(x) = int
----------- -----------
Γ ⊢ x : int Γ ⊢ 1 : int
--------------------------------------
Γ ⊢ x + 1 : int
Γ ⊢ e : t -- Γ context, e expression, t type
Γ ⊢ᵗ s ⇒ Γ' -- Γ initial context, s statement, Γ' post-context, t return type
Example: rule for variable declaration
-------------------
Γ ⊢ int i ⇒ Γ,i:int
Sequence
Γ ⊢ s₁ ⇒ Γ' Γ' ⊢ s₂ ⇒ Γ''
-----------------------------
Γ ⊢ s₁ s₂ ⇒ Γ''
Assignment statement
x:t ∈ Γ Γ ⊢ e : t
----------------------
Γ ⊢ x = e; ⇒ Γ
If-statement
Γ ⊢ e : bool Γ ⊢ s₁ ⇒ Γ₁ Γ ⊢ s₂ ⇒ Γ₂
------------------------------------------
Γ ⊢ if (e) s₁ else s₂ ⇒ Γ
While-statement
Γ ⊢ e : bool Γ ⊢ s ⇒ Γ₁
-------------------------
Γ ⊢ while (e) s ⇒ Γ
Function calls
Σ; Γ ⊢ eᵢ : tᵢ (i=1..n)
------------------------------ f : (t₁, t₂, ..., tₙ) → t ∈ Σ
Σ; Γ ⊢ f (e₁, e₂, ..., eₙ) : t
Σ is the function context mapping function names
f to function types (t₁, t₂, ..., tₙ) → t.
To check a program consisting of function definitions:
Construct function context Σ
Check all function bodies
Σ; Γ ⊢ body ⇒ Γ'
where Γ contains the formal parameters of the function
(and Γ' does not matter).