About a year ago, I took Stanford CS242 (fall 2019) course and did the assignment to complete an interpreter for a simple ML-like language (it doesn’t have a name, and I’ll call it “Lam” as the file extension is .lam). It’s a bit disappointed that the assignment didn’t mention Hindley-Milner type inference algorithm, so I implemented one in my Rust version of the interpreter.

The implementation is based on chapter 10.6 of Cornell CS3110 textbook. I’ll recommend you to read that chapter first since:

  • It explains HM algorithm in detail, with step-by-step examples.
  • You don’t need to read other chapters of the book to understand everything (if you are familiar with OCaml).
  • I may not explain things that are already described in the book.

I’ll also assume that you have took the CS242 course and have done the assignment.

Type Inference Rules

The CS3110 book introduces some symbols to represent the constraints. I’ll translate them in a style that is more consistent with CS242’s type checking rules. For example, the “if” part of the rule is written below the “then” part in CS3110 while in CS242 we prefer to write “if” above the “then”.

The basic idea of HM algorithm is: instead of checking every type in an expression eagerly, we generate a set of type constraint which is similar to a set of equations, and solve them for the unknown types (in a process called unification).

A constraint is just a claim that two types are equal: Constraint c::= τ=τType τ::= num bool \begin{aligned} \mathsf{Constraint}\ c ::=\ & \tau = \tau \\ \mathsf{Type}\ \tau ::=\ & \mathsf{num} \\ |\ & \mathsf{bool} \\ |\ & \dots \\ \end{aligned}

The following are some example of rules with the format type checking rules => type inference rules. I’ll leave other rules as exercises for readers.

Basic Types

Constants, variables and lambdas add nothing to the constraints: n:numn:num,(T-Num)\frac{}{\varnothing\vdash n:\mathsf{num}}\Rightarrow\frac{}{\varnothing\vdash n:\mathsf{num}\dashv\varnothing},\tag{T-Num} x:τΓΓx:τx:τΓΓx:τ(T-Var)\frac{x:\tau\in\Gamma}{\Gamma\vdash x:\tau}\Rightarrow\frac{x:\tau\in\Gamma}{\Gamma\vdash x:\tau\dashv\varnothing}\tag{T-Var} Γ,x:τarge:τretΓ(λ(x:τarg).e):τargτretfresh τΓ,x:τe:τretCΓ(λx.e):ττretC,(T-Lam)\frac{\Gamma, x : \tau_{\mathsf{arg}} \vdash e : \tau_{\mathsf{ret}}}{\Gamma \vdash (\lambda\,(x : \tau_{\mathsf{arg}})\, .\, e) : \tau_{\mathsf{arg}} \to \tau_{\mathsf{ret}}}\Rightarrow\frac{\text{fresh}\ \tau\quad\Gamma, x : \tau \vdash e : \tau_{\mathsf{ret}}\dashv C}{\Gamma \vdash (\lambda\,x\, .\, e) : \tau \to \tau_{\mathsf{ret}}\dashv C},\tag{T-Lam} where fresh τ\text{fresh}\ \tau means τ\tau is a fresh new type variable that haven’t been used. 1

Binary operations add a constraint that the both sides of the operation and the result should be num. ΓeL:numΓeR:numΓeLeR:num ΓeL:τLCLΓeR:τRCRΓeLeR:numCL,CR,τL=num,τR=num(T-Binom)\begin{aligned} & \frac{\Gamma\vdash e_L:\mathsf{num}\quad\Gamma\vdash e_R:\mathsf{num}}{\Gamma\vdash e_L\oplus e_R:\mathsf{num}} \\ \Rightarrow\ & \frac{\quad\Gamma\vdash e_L:\tau_L\dashv C_L\quad\Gamma\vdash e_R:\tau_R\dashv C_R}{\Gamma\vdash e_L\oplus e_R:\mathsf{num}\dashv C_L,C_R,\tau_L=\mathsf{num},\tau_R=\mathsf{num}} \end{aligned}\tag{T-Binom}

Fixpoints requires that the type of x,ex,e and the entire expression are the same: Γ,x:τe:τΓfix (x:τ).e:τfresh τΓ,x:τe:τCΓfix x.e:τC,τ=τ,(T-Fix)\frac{\Gamma, x : \tau \vdash e : \tau}{\Gamma \vdash \mathsf{fix}\ (x : \tau)\, .\, e : \tau}\Rightarrow\frac{\text{fresh}\ \tau'\quad\Gamma, x : \tau' \vdash e : \tau\dashv C}{\Gamma \vdash \mathsf{fix}\ x\, .\, e : \tau'\dashv C,\tau'=\tau},\tag{T-Fix}

If-statements add two constraints: the condition must be bool and both arm must have the same type. Γecond:boolΓethen:τΓeelse:τΓif econd then ethen else eelse:τ Γecond:τ1C1Γethen:τ2C2Γeelse:τ3C3Γif econd then ethen else eelse:τ2C1,C2,C3,τ1=bool,τ2=τ3(T-If)\begin{aligned} & \frac{\Gamma\vdash e_{\mathsf{cond}}:\mathsf{bool}\quad\Gamma\vdash e_{\mathsf{then}}:\tau\quad\Gamma\vdash e_{\mathsf{else}}:\tau}{\Gamma\vdash\mathsf{if}\ e_{\mathsf{cond}}\ \mathsf{then}\ e_{\mathsf{then}}\ \mathsf{else}\ e_{\mathsf{else}}:\tau} \\ \Rightarrow\ & \frac{\Gamma\vdash e_{\mathsf{cond}}:\tau_1\dashv C_1\quad\Gamma\vdash e_{\mathsf{then}}:\tau_2\dashv C_2\quad\Gamma\vdash e_{\mathsf{else}}:\tau_3\dashv C_3}{\Gamma\vdash\mathsf{if}\ e_{\mathsf{cond}}\ \mathsf{then}\ e_{\mathsf{then}}\ \mathsf{else}\ e_{\mathsf{else}}:\tau_2\dashv C_1,C_2,C_3,\tau_1=\mathsf{bool},\tau_2=\tau_3} \end{aligned}\tag{T-If}

For function applications, we define a new type τ\tau for the entire expression and add a constraint that the function must have type τ2τ\tau_2 \to \tau: Γelam:τ2τΓearg:τ2Γ(elam earg):τ fresh τΓelam:τ1C1Γearg:τ2C2Γ(elam earg):τC1,C2,τ1=τ2τ(T-App)\begin{aligned} & \frac{\Gamma \vdash e_{\mathsf{lam}} : \tau_2 \to \tau\quad \Gamma \vdash e_{\mathsf{arg}} : \tau_2}{\Gamma \vdash (e_{\mathsf{lam}}\ e_{\mathsf{arg}}) : \tau} \\ \Rightarrow\ & \frac{\text{fresh}\ \tau\quad\Gamma \vdash e_{\mathsf{lam}} : \tau_1\dashv C_1\quad \Gamma \vdash e_{\mathsf{arg}} : \tau_2\dashv C_2}{\Gamma \vdash (e_{\mathsf{lam}}\ e_{\mathsf{arg}}) : \tau\dashv C_1, C_2, \tau_1 = \tau_2 \to \tau} \end{aligned}\tag{T-App}

De Bruijn Indices

These rules work well until you decided to use the same variable in different expressions:

let f = fun x -> fun y -> ((fun x -> x == y) 2) || x
in f true 1

If we use a set to represent the context Γ\Gamma, then the type of the inner x (which is num) will overwrite the outer x (which is bool). This isn’t a problem when we can check the user-annotated type without moving out of the scope, but now we have to infer the type of the inner and outer x after visiting the whole expression.

If you have done the interpreter assignment, you should know that the solution is to convert the expression to nameless form using de Bruijn indices:

let f = fun _ -> fun _ -> ((fun _ -> <0> == <1>) 2) || <0>
in f true 1

We also need to change the rules to represent the context as a stack. The type of the variable with index n is the n-th last element of the stack: Γ,τ<0>:τ (T-Var-Term),Γ=τ,ττ<n>:τCΓ<n+1>:τ (T-Var-Next),\frac{}{\Gamma,\tau\vdash \left<0\right>:\tau\dashv\varnothing}\ (\text{T-Var-Term}),\quad\frac{\Gamma=\tau^*,\tau\quad\tau^*\vdash \left<n\right>:\tau'\dashv C}{\Gamma\vdash \left<n+1\right>:\tau'\dashv\varnothing}\ (\text{T-Var-Next}), fresh τΓ,τe:τretCΓ(λ_.e):ττretC (T-Lam),fresh τΓ,τe:τCΓfix _.e:τC,τ=τ,(T-Fix)\frac{\text{fresh}\ \tau\quad\Gamma,\tau \vdash e : \tau_{\mathsf{ret}}\dashv C}{\Gamma \vdash (\lambda\,\_\, .\, e) : \tau \to \tau_{\mathsf{ret}}\dashv C}\ (\text{T-Lam}),\quad\frac{\text{fresh}\ \tau'\quad\Gamma,\tau' \vdash e : \tau\dashv C}{\Gamma \vdash \mathsf{fix}\ \_\, .\, e : \tau'\dashv C,\tau'=\tau},(\text{T-Fix})

ADT

Product type constructors are similar to binary operations: ΓeL:τLΓeR:τRΓ(eL,eR):τL×τR ΓeL:τLCLΓeR:τRCRΓ(eL,eR):τL×τRCL,CR,\begin{aligned} & \frac{\Gamma \vdash e_{L} : \tau_{L} \quad \Gamma \vdash e_{R} : \tau_{R}}{\Gamma \vdash (e_{L}, e_{R}) : \tau_{L} \times \tau_{R}} \\ \Rightarrow\ & \frac{\Gamma\vdash e_L:\tau_L\dashv C_L\quad\Gamma\vdash e_R:\tau_R\dashv C_R}{\Gamma \vdash (e_{L}, e_{R}) : \tau_{L} \times \tau_{R}\dashv C_L,C_R}, \end{aligned}

We can use the same trick as function applications in product type destructors: Γe:τL×τRΓe.L:τLfresh τL,τRΓe:τCΓe.L:τLC,τ=τL×τR\frac{\Gamma \vdash e : \tau_{L} \times \tau_{R}}{\Gamma \vdash e.L : \tau_{L}}\Rightarrow\frac{\text{fresh}\ \tau_L,\tau_R\quad\Gamma \vdash e : \tau\dashv C}{\Gamma \vdash e.L : \tau_{L} \dashv C,\tau=\tau_{L} \times \tau_{R}}

The type annotation of sum types can also be omitted. The type will be known the time user uses it: Γe:τLΓinj e=L as τL+τR:τL+τRfresh τRΓe:τLCΓinj e=L:τL+τRC,(T-Inject-L)\frac{\Gamma \vdash e : \tau_{L}}{\Gamma \vdash \mathsf{inj}\ e = L\ \mathsf{as}\ \tau_{L} + \tau_{R} : \tau_{L} + \tau_{R}}\Rightarrow\frac{\text{fresh}\ \tau_R\quad\Gamma \vdash e : \tau_{L}\dashv C}{\Gamma \vdash \mathsf{inj}\ e = L : \tau_{L} + \tau_{R}\dashv C},\tag{T-Inject-L} Γe:τL+τRΓ,xL:τLeL:τΓ,xR:τReR:τΓcase e{L(xL)eLR(xR)eR}:τ fresh τL,τR,Γe:τsumCsumΓ,τLeL:τLCLΓ,τReR:τRCRΓcase e{L(_)eLR(_)eR}:τLCsum,CL,CR,τsum=τL+τR,τL=τR.(T-Case)\begin{aligned} & \frac{\Gamma \vdash e : \tau_{L} + \tau_{R} \quad \Gamma, x_{L} : \tau_{L} \vdash e_{L} : \tau \quad \Gamma, x_{R} : \tau_{R} \vdash e_{R} : \tau}{\Gamma \vdash \mathsf{case}\ e\,\{L(x_{L}) \to e_{L} \mid R(x_{R}) \to e_{R}\} : \tau} \\ \Rightarrow &\ \frac{\text{fresh}\ \tau_L,\tau_R,\quad\Gamma \vdash e : \tau_{\mathsf{sum}} \dashv C_{\mathsf{sum}} \quad \Gamma,\tau_{L} \vdash e_{L} : \tau_L'\dashv C_L \quad \Gamma,\tau_{R} \vdash e_{R} : \tau_R'\dashv C_R}{\Gamma \vdash \mathsf{case}\ e\,\{L(\_) \to e_{L} \mid R(\_) \to e_{R}\} : \tau_L'\dashv C_{\mathsf{sum}},C_L,C_R,\tau_{\mathsf{sum}}=\tau_{L} + \tau_{R},\tau_L'=\tau_R'}. \end{aligned}\tag{T-Case}

How to Use the Rules

We should know how these rules are used before learning polymorphism.

Collecting Constraints and Type

Remember how we used the type checking rule: for an expression ee, first we find a rule whose “then” part matches ee, check whether the conditions in its “if” part are satisfied (which are also type checks), then we know that Γe:τ\Gamma\vdash e:\tau.

fn type_check_expr(ast: &Expr, ctx: HashMap<Variable, Type>) -> Result<Type, String> {
    match ast {
        Expr::Num(_) => Ok(Type::Num),
        Expr::Addop { binop, left, right } => {
            let tau_left = type_check_expr(left, ctx.clone())?;
            let tau_right = type_check_expr(right, ctx.clone())?;
            match (tau_left.clone(), tau_right.clone()) {
                (Type::Num, Type::Num) => Ok(Type::Num),
                _ => type_mismatch!(tau_left, tau_right, binop),
            }
        }
        // and other rules
    }
}

The first part of type inference works essentially the same, except that we don’t really check the type, only maintain the set of constraints and a type:

pub fn get_constraints(&self, ctx: &mut Vec<Type>) -> Result<(Type, Vec<Constraint>), String> {
    let (tau_result, c_result) = match self {
        Expr::Num(_) => Ok((Type::Num, vec![])),
        Expr::Addop { left, right, .. } | Expr::Mulop { left, right, .. } => {
            let (tau_left, c_left) = left.get_constraints(ctx)?;
            let (tau_right, c_right) = right.get_constraints(ctx)?;
            let constraints = flat!(vec![
                c_left,
                c_right,
                vec![
                    Constraint {
                        type_l: tau_left,
                        type_r: Type::Num,
                    },
                    Constraint {
                        type_l: tau_right,
                        type_r: Type::Num,
                    },
                ]
            ]);
            Ok((Type::Num, constraints))
        }
        // and other rules
    }
}

Unification

Now we got a set of constraints that looks like C={τ1=num,τ2×τ1=(numbool)×τ3,}C=\{\tau_1=\mathsf{num},\tau_2\times\tau_1=(\mathsf{num}\to\mathsf{bool})\times\tau_3,\dots\} and a type that probabily has some unknown variables, like α.τ1(α×bool)\forall\alpha.\tau_1\to(\alpha\times\mathsf{bool}). The next step is to unify these constraints to know what the types τ1,τ2,\tau_1,\tau_2,\dots are.

The unification algorithm is straightforward since the constraint and types has a simple, tree-like structure. Unsurprisingly, a union-find set is involved in the algorithm:

  1. For every “forall” types α.τ\forall\alpha.\tau in CC, introduce a fresh variable τ\tau' and remove the quantifier so it becomes [ατ]τ[\alpha\to\tau']\tau.
  2. Initialize the union-find set SS with every variable in CC.
  3. Initialize a map T={}T=\{\}. TT stores the type of variables we have already known. Its keys are the roots in SS and its values are types.
  4. While CC\neq\varnothing:
    1. Pop the first constraint τ1=τ2\tau_1=\tau_2 from CC.
    2. If both τ1\tau_1 and τ2\tau_2 are the same atom types like bool and num, then continue.
    3. If τ1\tau_1 or τ2\tau_2 are variables:
      1. If the variable is not the root in SS, find and use its root in SS.
      2. If both τ1\tau_1 and τ2\tau_2 are variables, we union these two variables in SS. If (τ1,τ1)T(\tau_1,\tau_1')\in T or (τ2,τ2)T(\tau_2,\tau_2')\in T, we change the key of the items and add τ1=τ2\tau_1'=\tau_2' to CC (if both items exists in TT).
      3. If τ1\tau_1 is variable and τ2\tau_2 does not contain τ1\tau_1, we add (τ1,τ2)(\tau_1,\tau_2) to the map TT.
      4. If τ2\tau_2 is variable and τ1\tau_1 does not contain τ2\tau_2, we add (τ2,τ1)(\tau_2,\tau_1) to the map TT.
    4. If τ1\tau_1 is τ11τ12\tau_{11}\to\tau_{12} and τ2\tau_2 is τ21τ22\tau_{21}\to\tau_{22}, then add τ11=τ21\tau_{11}=\tau_{21} and τ12=τ22\tau_{12}=\tau_{22} to CC.
    5. Similar to 7, if τ1\tau_1 and τ2\tau_2 falls in the same category of the type’s BNF, then we decompose them into smaller types and add constraints to CC.
    6. Otherwise, the expression doesn’t type check.

Get the Type

You don’t need to know the type of the expression in general: if the unification succeeded, then the program is type checked. In some cases (like the polymorphism below), we still need to get the type.

We can’t directly use the type τ\tau generated in the first step because it may contain free type variables (the variable without a quantifier) and whether these variables can be reduced is not known. For example, every fun x -> x in the expressions below generates type a -> a, but their types are different:

(fun x -> x) 1 (* int -> int *)
fun x -> x (* forall a . a -> a *)

The following algorithm removes every free variables in τ\tau:

  1. While there is free type variable:
    1. Get the first free variable xx.
    2. If SS doesn’t contain xx, then τx.τ\tau\gets\forall x.\tau and continue. Otherwise, let rr be the root of xx.
    3. If rxr\neq x, then τ[xr]τ\tau\gets[x\to r]\tau and continue.
    4. If TT doesn’t contain rr, then τx.τ\tau\gets\forall x.\tau and continue. Otherwise, Let τr\tau_r be the value of rr.
    5. τ[xτr]τ\tau\gets[x\to\tau_r]\tau.
  2. Return τ\tau.

Polymorphism

In previous versions of Lam, the user had to explicitly say that there is polymorphism using the Λα.e\Lambda\alpha.e syntax, and annotate the type of α\alpha. It’s impossible to keep this syntax: we won’t know what the type indicated by the α\alpha in Λα.e\Lambda\alpha.e means if the user doesn’t use α\alpha in their program.

In OCaml, polymorphism is obtained by let expression: when binding a name to an expression with forall type, the variable should be able to instantiate to different types in different contexts. For example, the id in the following expression will have type bool -> bool in (id false) and num -> num in (id 1):

let id = fun x -> x
in if (id false) then (id 1) else 1

As a result, let is no longer a syntax sugar of function application (when inferencing types. You can still treat it as function application when evaluating it). If you “desugar” the expression above, you will get num = bool and the unification will fail.

The inferencing rule of let is a bit involved: Γevar:τvarCvarΓ,generalize(τvar,Cvar)ein:τinCinΓlet _=evar in ein:τinCin,Cvar,(T-Let)\frac{\Gamma\vdash e_{\mathsf{var}}:\tau_{\mathsf{var}}\dashv C_{\mathsf{var}}\quad\Gamma,\mathrm{generalize}(\tau_{\mathsf{var}},C_{\mathsf{var}})\vdash e_{\mathsf{in}}:\tau_{\mathsf{in}}\dashv C_{\mathsf{in}}}{\Gamma\vdash\mathsf{let}\ \_=e_{\mathsf{var}}\ \mathsf{in}\ e_{\mathsf{in}}:\tau_{\mathsf{in}}\dashv C_{\mathsf{in}},C_{\mathsf{var}}},\tag{T-Let} where generalize(τ,C)\mathrm{generalize}(\tau,C) unifies CC and remove free variables in τ\tau using the algorithm in the previous section.

The instantiation type annotations and the T-Poly-App rule are replaced by instantiating arguments in forall types as fresh variables: fresh τ1,,τnΓ,α1αn.τ<0>:[α1τ1,αnτn]τ.(T-Poly-Inst)\frac{\text{fresh}\ \tau_1',\dots,\tau_n'}{\Gamma,\forall\,\alpha_1\dots\forall\,\alpha_n\, .\, \tau\vdash\left<0\right> : [\alpha_1\to\tau_1',\dots\alpha_n\to\tau_n']\tau\dashv\varnothing}.\tag{T-Poly-Inst}

For example, let’s get the type of <0> (fun _ -> <0>) in the program

let _ = fun _ -> fun _ -> <1> <0> in
  let _ = <0> (fun _ -> <0>) in <0> 1  

The context is α.β.(αβ)αβ\forall\alpha.\forall\beta.(\alpha\to\beta)\to\alpha\to\beta (which is the type of fun _ -> fun _ -> <1> <0>) and the expression is <0> (fun _ -> <0>). We can use the rules to get the type τ\tau and constraints CC: fresh τfresh α,fresh βα.β.(αβ)αβ<0>:(αβ)αβ (T-Poly-Inst)fresh γγ<0>:γ (T-Var-Term)(λ_.<0>):γγ (T-Lam)α.β.(αβ)αβ(<0> (λ_<0>)):τ(αβ)αβ=(γγ)τ (T-App)\frac{\text{fresh}\ \tau\quad\dfrac{\text{fresh}\ \alpha',\text{fresh}\ \beta'}{\forall\alpha.\forall\beta.(\alpha\to\beta)\to\alpha\to\beta\vdash\left<0\right> : (\alpha'\to\beta')\to\alpha'\to\beta'\dashv\varnothing}\ (\text{T-Poly-Inst})\quad\dfrac{\text{fresh}\ \gamma\quad\dfrac{}{\gamma \vdash\left<0\right> : \gamma\dashv\varnothing}\ (\text{T-Var-Term})}{\varnothing \vdash (\lambda\,\_\, .\, \left<0\right>) : \gamma \to \gamma\dashv\varnothing}\ (\text{T-Lam})}{\forall\alpha.\forall\beta.(\alpha\to\beta)\to\alpha\to\beta \vdash (\left<0\right>\ (\lambda\,\_\to\left<0\right>)) : \tau\dashv (\alpha'\to\beta')\to\alpha'\to\beta' = (\gamma\to\gamma) \to \tau}\ (\text{T-App})

We can get {α=β=γ,τ=αα}\{\alpha=\beta=\gamma,\tau=\alpha\to\alpha\} from the constraint (αβ)αβ=(γγ)τ(\alpha'\to\beta')\to\alpha'\to\beta' = (\gamma\to\gamma) \to \tau (try to simulate the unification algorithm yourself by hand!), so generalize(τ,{(αβ)αβ=(γγ)τ})=τ.ττ\mathrm{generalize}(\tau,\{(\alpha'\to\beta')\to\alpha'\to\beta' = (\gamma\to\gamma) \to \tau\})=\forall\tau'.\tau'\to\tau'.

Let’s go through the whole process of another example (in de Bruijn form):

let _ = fun _ -> <0>
in if (<0> false) then (<0> 1) else 1

We know that fun _ -> <0> has type ττ\tau\to\tau where τ\tau is a free variable: fresh ττ<0>:τ (T-Var-Term)(λ_.<0>):ττ (T-Lam)let _=λ_.<0> in ein (T-Let)\dfrac{\dfrac{\text{fresh}\ \tau\quad\dfrac{}{\tau \vdash\left<0\right> : \tau\dashv\varnothing}\ (\text{T-Var-Term})}{\varnothing \vdash (\lambda\,\_\, .\, \left<0\right>) : \tau \to \tau\dashv\varnothing}\ (\text{T-Lam})}{\varnothing\vdash\mathsf{let}\ \_=\lambda\,\_\, .\, \left<0\right>\ \mathsf{in}\ e_{\mathsf{in}}}\ (\text{T-Let}) so we generalize it to τ.ττ\forall\tau.\tau\to\tau and add it to the context when working on the if expression. Here’s the inference process for the predicate (<0> false): fresh τ1fresh τ1τ.ττ<0>:τ1τ1 (T-Poly-Inst)τ.ττfalse:bool (T-Bool)τ.ττ(<0> false):τ1τ1τ1=boolτ1 (T-App)\dfrac{\text{fresh}\ \tau_1\quad\dfrac{\text{fresh}\ \tau_1'}{\forall\tau.\tau\to\tau \vdash \left<0\right> : \tau_1'\to\tau_1'\dashv \varnothing}\ (\text{T-Poly-Inst})\quad \dfrac{}{\forall\tau.\tau\to\tau \vdash \mathsf{false} : \mathsf{bool}\dashv\varnothing}\ (\text{T-Bool})}{\forall\tau.\tau\to\tau \vdash (\left<0\right>\ \mathsf{false}) : \tau_1\dashv\tau_1'\to\tau_1' = \mathsf{bool} \to \tau_1}\ (\text{T-App})

The constraint and the type of (<0> 1) can be got using a similar process. Let’s complete the inference: τ.ττ(<0>false):τ1τ1τ1=boolτ1τ.ττ(<0>1):τ2τ2τ2=numτ2τ.ττ1:numτ.ττif (<0>false) then (<0>1) else 1:τ2τ1τ1=boolτ1,τ2τ2=numτ2,τ1=bool,τ2=num (T-If)let _=λ_.<0> in if (<0>false) then (<0>1) else 1:τ2τ1τ1=boolτ1,τ2τ2=numτ2,τ1=bool,τ2=num (T-Let)\dfrac{\dfrac{\dfrac{\dots}{\forall\tau.\tau\to\tau \vdash (\left<0\right>\mathsf{false}) : \tau_1\dashv\tau_1'\to\tau_1' = \mathsf{bool} \to \tau_1}\quad\dfrac{\dots}{\forall\tau.\tau\to\tau \vdash (\left<0\right>1) : \tau_2\dashv\tau_2'\to\tau_2' = \mathsf{num} \to \tau_2}\quad\dfrac{}{\forall\tau.\tau\to\tau\vdash 1:\mathsf{num}\dashv\varnothing}}{\forall\tau.\tau\to\tau\vdash\mathsf{if}\ (\left<0\right>\mathsf{false})\ \mathsf{then}\ (\left<0\right>1)\ \mathsf{else}\ 1:\tau_2\dashv \tau_1'\to\tau_1' = \mathsf{bool} \to \tau_1,\tau_2'\to\tau_2' = \mathsf{num} \to \tau_2,\tau_1=\mathsf{bool},\tau_2=\mathsf{num}}\ (\text{T-If})}{\varnothing\vdash\mathsf{let}\ \_=\lambda\,\_\, .\, \left<0\right>\ \mathsf{in}\ \mathsf{if}\ (\left<0\right>\mathsf{false})\ \mathsf{then}\ (\left<0\right>1)\ \mathsf{else}\ 1:\tau_2\dashv\tau_1'\to\tau_1' = \mathsf{bool} \to \tau_1,\tau_2'\to\tau_2' = \mathsf{num} \to \tau_2,\tau_1=\mathsf{bool},\tau_2=\mathsf{num}}\ (\text{T-Let})

Therefore the entire expression has type τ2\tau_2. The unification algorithm (again, try to simulate it by hand!) will generate a map TT where the root of τ2\tau_2 corresponds to num.

Type Safety Proofs

We need to proof the progress and preservation theorem of our type inference system:

  • Progress: if e:τ\varnothing\vdash e:\tau then either e vale\ \mathsf{val} or there exists ee' such that eee\mapsto e'.
  • Preservation: if e:τ\varnothing\vdash e:\tau and eee\mapsto e', then e:τ\varnothing\vdash e':\tau.

By saying Γe:τ\Gamma\vdash e:\tau, we mean that Γe:τC\Gamma\vdash e:\tau'\dashv C, and τ=generalize(τ,C)\tau=\mathrm{generalize}(\tau',C).

I’ll try to proof the theorems in later posts. This means that I didn’t have the proof yet, and everything presented in this blog post may be wrong. But I’m already exhausted after spending every weekend of the past month trying to make an interpreter that works 2.

  1. If you are looking at the markdown of this post, you may have notice the difference in the LaTeX code’s style. That’s because I found a LaTeX OCR website. The code is partly generated by OCRing a screenshot of the assignment’s webpage

  2. I haven’t implemented every feature in Lam for the same reason. Also, recursive types and modules aren’t interesting from type inference perspective.