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 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.
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
IdMapmodule 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
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.