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
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.
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.
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.
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 =
...
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
(* Substitutions as a finite map *)
module Subst : SUBST = struct
module IdMap = Map.Make (Identifier)
type t = Typeinf_syntax.Explicit.typ IdMap.t
...
end
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)))
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
.
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.
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
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.
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.