CS691F Programming Languages

| Introduction | Schedule | Software | OCaml Standard Library | Course Libraries |

Type Inference

Due date: Tuesday, October 28th, 11:59PM

You must update the course software to get the support code for this assignment. From the terminal:

$ opam update
$ opam install cs691f.1.4.1

Introduction

In this assignment, you will implement type inference for a language that is almost identical to the earlier type-checker assignment. The support code has a parser for the implicitly-typed syntax and a pretty-printer for the explicitly-typed syntax. Your will write all the intermediate stages and put them together.

Requirements

Submit a file named Typeinf.ml with the following function:

val typeinf : Typeinf_syntax.Implicit.exp -> Typeinf_syntax.Explicit.exp

The Typeinf_syntax module has two sub-modules that define the implicitly-typed and explicitly-typed syntaxes and the Typeinf_util module has parsers and printers to help you test your code.

The support code does not include an evaluator, though you should easily be able to write one if you like. Feel free to adapt the evaluator provided for the type-checking assignment.

Directions

1. Placeholder Type Identifiers

Write a function to create an explicit-typed AST with fresh identifiers; these identifiers will be substituted with concrete types at a later stage. In earlier assignments, we used strings to represent identifiers, but it is cumbersome to generate fresh strings correctly.

This assignment uses Identifier.t instead of string to represent identifiers. Identifier.t is an opaque type which has a function to create new, fresh identifiers that do not clash with anything defined before.

2. Constraint Generation

Write a function to calculate types and generate constraints. A constraint equates two types, so you can easily represent a constraint as a pair of types:

(* Examples of constraints *)
type constraint = typ * typ

let eg1 : constraint = (TInt, TInt)
let eg2 : constraint = (TInt, TBool)
let eg2 : constraint = (TInt, TId (Identifier.fresh "x"))
let eg3 : constraint = (TFun (TId (Identifier.fresh "x"), TInt), 
                        TId (Identifier.fresh "y"))

If you adhere to the subset of OCaml you've used so far, you'll have to thread the set of constraints throughout your program:

(* Threading a set of constraints might be cumbersome. *)
open Typeinf_syntax

let cgen (env : env) (exp : Explicit.exp) : Explicit.typ * constraints =
  ...

You may find it easier to store the constraints in a global variable by using a mutable references:

(* It may be easier to store constraints in an updatable reference. *)
open Typeinf_syntax

let cs : constraint ref = ...

let cgen (env : env) (exp : Explicit.exp) : Explicit.typ =
  ...

3. Substitution

The key to solving constraints by unification is the substitution data structure, which maps type identifiers to types. As discussed in class, you need to carefully define substitution composition, substitution application, and the substitution constructors. To ensure you use substitutions correctly, we recommend creating an abstract data type for substitutions with the following signature:

module type SUBST = sig
  type t

  val empty : t
  val singleton : Id.t -> typ -> t
  val apply : t -> typ -> typ
  val compose : t -> t -> t
  val to_list : t -> (Id.t * typ) list (* for debugging *)
end

Given this signature, you can implement a substitution module as follows:

(* The SUBST constraint ensures that t is opaque outside the module *)
module Subst : SUBST = struct
  type t = ...

  ...
end

You may represent a substitution in several ways. For example, if you use a list:

(* Substitutions as an association list *)
module Subst : SUBST = struct
  type t = (Identifier.t * Typeinf_syntax.Explicit.typ) list

  ...
end
You may find the standard library functions for manipulating association lists helpful. You may find it convenient to use a finite map instead:
(* Substitutions as a finite map *)
module Subst : SUBST = struct
  module IdMap = Map.Make (Identifier)
  type t = Typeinf_syntax.Explicit.typ IdMap.t

  ...
end
In the code above, the IdMap module has the following signature: Map.S.

To help you get started, here are some tests that demonstrate simple properties of substitutions:

(* Some examples of operations on substitutions *)
let x = Identifier.fresh "x"
let y = Identifier.fresh "y"
TEST "Subst.apply should replace x with TInt" =
  let s = Subst.singleton x TInt in
  Subst.apply s (TId x) = TInt

TEST "Subst.apply should recur into type constructors" =
  let s = Subst.singleton x TInt in
  Subst.apply s (TFun (TId x, TBool)) = (TFun (TInt, TBool))

TEST "Subst.compose should distribute over Subst.apply (1)" =
  let s1 = Subst.singleton x TInt in
  let s2 = Subst.singleton y TBool in
  Subst.apply (Subst.compose s1 s2) (TFun (TId x, TId y)) =
  Subst.apply s1 (Subst.apply s2 (TFun (TId x, TId y)))

TEST "Subst.compose should distribute over Subst.apply (2)" =
  let s1 = Subst.singleton x TBool in
  let s2 = Subst.singleton y (TId x) in
  Subst.apply (Subst.compose s1 s2) (TFun (TId x, TId y)) =
  Subst.apply s1 (Subst.apply s2 (TFun (TId x, TId y)))

4. Unification

Unification is a function that takes two types as arguments and produces a substitution that maps type identifiers to types:

val unify : Typeinf_syntax.Explicit.typ -> Typeinf_syntax.Explicit.typ -> Subst.t

To help you get started, here is a small test suite that tests some key features of unification.

(* An incomplete suite of tests for unification *)
TEST "unifying identical base types should return the empty substitution" =
  Subst.to_list (unify TInt TInt) = []

TEST "unifying distinct base types should fail" =
  try let _ = unify TInt TBool in false
  with Failure "unification failed" -> true

TEST "unifying with a variable should produce a singleton substitution" =
  let x = Identifier.fresh "myvar" in
  Subst.to_list (unify TInt (TId x)) = [(x, TInt)]

TEST "unification should recur into type constructors" =
  let x = Identifier.fresh "myvar" in
  Subst.to_list (unify (TFun (TInt, TInt)) 
                       (TFun (TId x, TInt))) = 
  [(x, TInt)]

TEST "unification failures may occur across recursive cases" =
  try
    let x = Identifier.fresh "myvar" in  
    let _ = unify (TFun (TInt, TId x)) 
                  (TFun (TId x, TBool)) in
    false
  with Failure "unification failed" -> true

TEST "unification should produce a substitution that is transitively closed" =
  let x = Identifier.fresh "myvar1" in  
  let y = Identifier.fresh "myvar2" in  
  let z = Identifier.fresh "myvar3" in  
  let subst = unify (TFun (TFun (TInt, TId x), TId y))
                    (TFun (TFun (TId x, TId y), TId z)) in
  Subst.to_list subst = [ (z, TInt); (y, TInt); (x, TInt) ]

TEST "unification should detect constraint violations that require transitive
      closure" =
  try
    let x = Identifier.fresh "myvar1" in  
    let y = Identifier.fresh "myvar2" in  
    let _ = unify (TFun (TFun (TInt, TId x), TId y))
                      (TFun (TFun (TId x, TId y), TBool)) in
    false
  with Failure "unification failed" -> true

TEST "unification should implement the occurs check (to avoid infinite loops)" =
  try
    let x = Identifier.fresh "myvar" in  
    let _ = unify (TFun (TInt, TId x)) (TId x) in
    false (* a bug is likely to cause an infinite loop *)
  with Failure "occurs check failed" -> true

This test suite is not complete. In particular, it doesn't test unification on any constructors other than TFun.

5. Constraint Solving

Using the unify function you wrote above, write a function to solve a list of constraints by repeatedly applying unification to the pair of types in each constraint. Remember to apply the substitution you produce at each step to the as-yet unified constraints.

6. Type Annotation

Write a function that substitutes the type identifiers in the explicitly-typed AST with concrete types that you calcuated in the last step:

val annotate_exp : Subst.t -> Typeinf_syntax.Explicit.exp -> Typeinf_syntax.Explicit.exp

7. Type Checking

This step isn't strictly required, but we strongly recommend you type-check the programs produced by the previous step. With some light modifications, you can reuse the type checker you wrote earlier. Checking the annotated code will help you catch bugs.

8. Finish

Finally, put the pieces above together to build the inference function:

val typeinf : Typeinf_syntax.Implicit.exp -> Typeinf_syntax.Explicit.exp

This function shouldn't do very much more than apply the functions above in the right order.