Assignment 7: Type Checking & Subtyping
Overview
In this assignment, you’ll implement most of the typechecker for Moses
, a functional
programming language with subtypes.
The goal of this assignment is to understand type systems.1 To do so, you will need to:
- Understand the rules for a type system and how they work together to provide a formal specification (in other words, “a description in math”) for a typechecker.
- Implement typechecking, by translating the rules of a type system into code.
As with many programming-languages assignments, this one asks you to read and understand much more than it asks you to write code. You might end up writing no more than 60 lines of code (plus, of course, lots of comments), but you should first be sure to understand the rules that you are implementing.
Getting Started
Start by reading the overview of the Moses language. Just skim this file for now, to reinforce your experience from the lab and to notice what is in the file, so you can come back to it later.
Materials
Use the assignment workflow and the link provided on Piazza to get access to this week’s files.
As always, your final submission cannot have syntax or type errors. Functions must have the exact names and types given in the assignment (though you are always encouraged to define other helper functions).
Subtyping [25%]
Pull up the description of Moses
’s type system,
and read the introductory section and the section on subtypes.
Now you have read the specification of the type system. The implementation of the
type system is in Typecheck.hs
, included with this assignment. It is incomplete, and it
is up to us to finish it.
Our first task will be to finish the code for subtyping, which we will need before we can implement the rest of the type checker.
Your task: In Typecheck.hs
, finish the
implementations of commonSupertype
and commonSubtype
.
Implementation Tips
-
Be sure to look in
MosesAST.hs
for the definition of the abstract syntax for types. You can also look at the autogenerated documentation. for this file. -
isSubtype
returns true when $x \sqsubseteq y$. -
commonSupertype x y
returns the most specific type $t$ that is a supertype of $x$ and $y$. Formally:Another way of saying this is that it returns the “least upper bound” of $x$ and $y$ (denoted $x \sqcup y$). Such a $t$ always exists—in the worst case
ANY
is the least upper bound. -
commonSubtype x y
returns the most general type $t$ that is a subtype of $x$ and $y$. Formally:Another way of saying this is that it returns the “greatest lower bound” of $x$ and $y$ (denoted $x \sqcap y$). Such a $t$ always exists—in the worst case,
NONE
is the greatest lower bound. -
When you read the code for
isSubtype
(which is strongly recommended even though this function is complete), notice the “backwardness” of the subtyping rule for function arguments. Similar “backwardness” applies when writingcommonSupertype
—there is a reason to definecommonSubtype
as well (they’ll end up mutually recursive!).
Testing
We’ve provided a few automated tests (review the how-to article for information on running tests). You can also add to those tests, and you can manually test your code.
- Automated tests:
test/SubtypeSpec.hs
contains a small number of automated tests for subtyping. - Manual testing: You can also manually test your code by loading
Typecheck.hs
inghci
and callingcommonSubtype
andcommonSupertype
directly.
Type Checking [75%]
The main functionality of the type
system is defined by the function typeofExpr
in Typecheck.hs
. This
unfinished function checks that
an expression is well-typed and returns its type. You should finish the
definition of typeofExpr
.
Your task: Finish the typechecker, using the type rules described in the type-checking document.
Implementation Tips
- You should always return the most useful and specific type reasonably possible—
ANY
might be a valid type for everything but it isn’t very useful. (Also, remember that the mythicalNONE
value is allowed wherever you need it.)
-
Sometimes, a type rule requires that a subexpression has a particular type (for example, the subexpression must be a number or a list). There are several useful helper functions that you can use when you recurse in
typeofExpr
to check subexpressions, includingtypeofExprWantNumber
,typeofExprWantList
, etc. which each calltypeofExpr
but then make sure that the resulting type is of a particular form. -
The code also uses a custom Monad called
Fallible
which is like theMaybe
type, but instead ofNothing
can provide a reason why typechecking failed. As a result, your code will probably want to usedo
notation. -
As you write rules for the different expression cases, you’ll find yourself wanting to do many of the same things each time. Rather than copy and paste code, try to make your life easier by abstracting out common functionality into helper functions.
Testing
There are several kinds of tests you can use to check your code.
-
Automated tests:
test/TypecheckSpec.hs
contains a few tests. -
Manual testing: You can also manually test your code by loading
Typecheck.hs
inghci
and callingcheckTypeof
, like so:
checkTypeof "lambda x => x"
-
Integration tests:
test/ProgramSpec.hs
contains tests for several Moses programs, located in thetest
directory. Because these tests use the Moses executable, you need to recompile the executable each time you change your code before rerunning these tests. You can also manually run Moses on a file by (re)loadingMoses.hs
inghci
and callingrunfile
:runfile "test/okay-var1.mose"
Suggested implementation order
Proceed as follows:
-
Typecheck primitive values (
Exactly
…). This part is done already. Read the code and make sure you understand what it does. -
Typecheck variables (
Var
…) by looking them up in the type environment. This part is done already; but read and understand the code. -
Typecheck tuples (
Tuple
…). This one is already mostly written. The provided code uses Haskell’smapM
function, which you may want to look up. You just need to finish the code by making itreturn
the right type. -
Typecheck $\lambda$-expressions. Your code will need a
Map.insert
call and a recursive call back totypeofExpr
on the function body. -
Typecheck simple (non-tuple)
let
-expressions. This code isn’t much different from checking $\lambda$-expressions. -
Typecheck conditionals. We want a
BOOL
for the condition—one of the helper functions is likely to be handy here. For the result type,commonSupertype
will also be useful. -
Typecheck the list cons operator (
:
). We single this one out for special treatment because, unlike all the other operators, its arguments have different types. Once again,commonSupertype
is your friend (e.g.,1 : ["Foo"]
is legal, and has type[ANY]
). Also useful is one of the helper functions to make sure the right-hand side typechecks as a list of some kind. -
Typecheck
case
expressions. This code is similar to the code for simplelet
s and the code forif
statements. Again, the provided helper functions andcommonSupertype
are your friends. -
Typecheck tuple-match
let
-expressions. The right-hand side needs to be a tuple of the same size as the number of variables. You might find the Haskell functionszip
andfoldl
handy when extending the type environment. -
Typecheck all the remaining the binary operators. There are a lot, and some are similar, some different. The partial code gives a suggestion for how to handle the constraints and checking. Note that the relational operators are overloaded, they either take two
REAL
s, or they take twoSTRING
s. The typeFallible
is a member of theAlternative
typeclass, so the<|>
operator may be useful when handling this either/or situation. -
Typecheck function application. The first expression needs to be “some kind of function”, and the second expression needs to be compatible with the function argument.
You can stop now. All of the graded components are implemented.
However, there is one feature that the typechecker does not currently support: letrec
.
If you are interested in adding letrec
to the typechecker, keep going.
letrec
[ungraded & optional]
It’s best to try a couple of different ways to typecheck letrec
.
-
First try this: When typechecking the value of the variable, use
NONE
as the type of the variable. The type of the value will give you a new (hopefully better) type for the variable, which you should use when you typecheck the body of theletrec
. -
The suggested approach above for recursive declarations almost works, but has a subtle bug. It types the following declaration as
INT -> INT
:letrec silly = \n :: INT => if n == 42 then 54 else silly in silly
This type isn’t really correct, because if
silly
has typeINT -> INT
, then the type of theif
statement is the common supertype ofINT
andINT -> INT
, which isANY
, thus the real type of the expression ought to have beenINT -> ANY
. To address this bug, the easiest approach is relatively simple: do what you did in your original version multiple times.
-
This semester, we have scaled back this assignment in the following ways:
Normally, this assignment requires you to come up with some of the typing rules. This semester, we provide a description of the type rules, so you can focus on understanding them and translating them to code.
Also, normally you would have to implement the typing rule for
letrec
, but that is optional and ungraded this semester. ↩