Homework 7
Typed lambda calculus
Please submit homeworks via the new submission page.
In this homework, you’ll implement an interpreter for the typed lambda calculus. I strongly recommend adapting your solutions for HW06. If you aren’t satisfied with your solutions, I am willing to offer you my code; however, taking my code will preclude you from resubmitting HW06. I think you will learn more by not taking my code.
Your submission should be a zipfile including:
- a
Makefile
, with a default target that builds an executable namedinterp
; - your source files;
- a file
README.md
, which lists the collaborators on this assignment (not more than three) and any other sources you got help from (other students, websites, hepatomancy, etc.); and - a file
fact.lc
, which computes the factorial of 5.
Please submit a clean zipfile, i.e., you should include just your Makefile
, source, README.md
, and fact.lc
.
I will grade your homework by unzipping the zipfile and running ‘make’. If that doesn’t work and the cause is something that is not my fault (like needing to install library you used), you will get a zero.
Do not:
- have your files inside a directory
- include an existing
interp
executable - include any
.hi
or.o
files - include weirdness like
__MACOSX
or.DS_Store
files - include version control information like
.git
directories
To avoid getting a zero, test out your zipfile by unzipping it in a new directory and running make. Does it build your interpreter correctly?
Two part submissions
You’ll submit this homework twice. The first checkpoint is Thursday, April 12th Sunday, April 15th; the second (and final!) checkpoint is Thursday, April 19th Sunday, April 22nd.
I’m going to run the same grader on your code twice: once at the first checkpoint, once at the second. Your grade on this assignment will be the better of the two.
Approach
I recommend that you start by adapting the parser and then the interpreter—you don’t need to know anything about type checking to do that. As we discuss type checking on Tuesday and Thursday, you’ll be able to fill in the type checker. Start early and ask questions often.
I recommend an “iterated” approach to development. Each step belowshould have “and test” after it, with your test suite growing each time:
- Clear out the -c and -n flags.
- Add types to the parser, AST, and evaluator.
- Add a type checker.
- Add bools, the boolean operators, etc.
- Add ints, etc.
- Add pairs.
- Add let rec.
Trying to do everything at once will only confuse you and make it harder to debug what’s going on. At a minimum, I’d shoot for step (4) by the first checkpoint.
The language
You will be implementing the typed lambda calculus. Top level programs will just be expressions; our particular language will be defined by the following mutually recursive grammars for types, expressions, and unary and binary operators, where n
refers to an arbitrary natural number:
t ::= int | bool | t1 -> t2 | (t1,t2)
e ::= x | e1 e2 | lambda x : t. e
| if e1 then e2 else e3 | let x = e1 in e2 | let rec x : t = e1 in e2 | (e : t)
| n | true | false | (e1, e2)
| unop e | e1 binop e2
unop ::= - | not | fst | snd
binop ::= + | - | * | / | and | or | == | <
The abstract syntax listed above is in some sense the minimum—your AST can of course have more nodes than I list there.
Your concrete syntax should allow arbitrary whitespace between tokens (like lambda
or .
) and parenthesization for disambiguation (lambda x : int. x (x x)
is different from lambda x : int. x x x
).
Variable names should begin with an alphabetical character followed by zero or more alphanumeric or single-quote ('
) characters, i.e., x
and foo3'5bar
are valid variable names, but 12
and 'quoted'
and lambda
are not. As for the While language, please be careful to ensure that the identifiers and keywords are kept distinct.
Application should be left associative, i.e., x y z
should parse like (x y) z
. Arithmetic operations should be left associative.
Lambda expressions should be allowed to have more than one argument, in which case the types should be ascripted under parens, i.e., lambda (s : int -> int) (z : int). s z
should parse like lambda s : int -> int. (lambda z : int. (s z))
. The parentheses around the argument and its type should be optional when there’s only one argument.
You should allow type ascriptions at non-recursive let expressions, though they’re not necessary. You should parse let x : t = e1 in e2
as let x = (e1 : t) in e2
.
The operators of the language should bind in the following order, from loosest to tightest:
or
and
== != < > <= >=
+ -
* /
- not fst snd application
So y == m * x + b
should yield a conditional that checks that x and y are on a given line; I find in my interpreter:
$ echo 'let x = 5 in -x + 3' | ./interp
-2
$ echo '(lambda x:int. x) (10 * 20)' | ./interp
200
$ echo '(lambda x:int. x) 10 * 20' | ./interp
200
$ echo '(1,2) == (0+1,3-1)' | ./interp
true
$ echo '1 < 2' | ./interp
true
$ echo 'true < false' | ./interp
Type error: type mismatch
expected: int
got: bool
in: false
Note that equality and inequality should be defined on all types, but comparators should only be defined on ints. Division should be truncating integer division (div
in Haskell).
The interpreter
Like last time, your interpreter should be in an executable named interp
generated by a Makefile
. Your interpreter should use a call-by-value semantics, i.e., you only apply a beta rule when the argument has fully reduced to a lambda.
As your interpreter runs, it should, by default, print out final result of the expression. You do not need to print out functions. For example, my interpreter runs as follows:
$ echo lambda x:int. x | ./interp
<function>
But if I run a program that results in a more conventional value (an integer, a boolean, or a pair), I get meaningful output:
$ echo '(lambda x:int. x) (10 * 20)' | ./interp
200
Your interpreter should signal errors appropriately. The precise content of your error message isn’t the most important thing—though the more detailed they can be, the better!—but it is critical that interp
exits with a non-zero exit code when there is an error. For example, when I run my version of interp
on lambda. lambda lambda
, I get the following error message:
Parse error: "parse.lc" (line 1, column 7):
unexpected keyword in place of variable (lambda)
expecting letter or digit, space or "'"
There are other errors that can occur beyond parsing, like failure to type check:
$ echo '1 + true' | ./interp
Type error: type mismatch
expected: int
got: bool
in: true
Or trying to give a recursive definition for something other than a function:
$ echo 'let rec x : int = x + 1 in x' | ./interp -u
Error: recursion error defining x
If you don’t encounter an error, your program should exit with a zero exit code.
Type system
The type system for our language extends the simply-typed lambda calculus we’ll discuss in class on Tuesday and Thursday. I’ll give the trickiest rules here:
------------
G |- n : Int
G |- e1 : Int G |- e2 : Int
------------------------------
G |- e1 + e2 : Int
G,x:t1 |- e : t2
------------------------------
G |- lambda x:t1. e : t1 -> t2
G |- e1 : t1 G |- e2 : t2
----------------------------
G |- (e1,e2) : (t1,t2)
G |- e : (t1,t2)
---------------
G |- fst e : t1
G |- e1 : t G |- e2 : t t /= t1 -> t2
-------------------------------------------
G |- e1 == e2 : bool
G |- e1 : Int G |- e2 : Int
------------------------------
G |- e1 < e2 : bool
G |- e1 : bool G |- e2 : t G |- e3 : t
--------------------------------------------
G |- if e1 then e2 else e3 : t
G |- e1 : t1 G,x:t1 |- e2 : t2
---------------------------------
G |- let x = e1 in e2 : t2
G,x:t1 |- e1 : t1 G,x:t1 |- e2 : t2
--------------------------------------
G |- let rec x : t1 = e1 in e2 : t2
G |- e : t
----------------
G |- (e : t) : t
Note that (a) equality is defined on all non-functional types, and works as long as its arguments have the same type; (b) let expressions don’t bind the variable recursively, but let rec
does; and (c) type ascription is a way of “forcing” a term to have a given type. Hopefully these rules are enough to determine which terms are well typed and how. If you’re unsure, please post to Piazza; I encourage you to share tricky examples!
Command-line arguments
Your interpreter should, when run without arguments, read all of the input from standard input, parse the input as a program, and then evaluate the program, pretty printing the final result.
When given an argument, your interpreter should read the file as input (and then proceed to parse, evaluate, and print as above). If the file is specified as -
, then you should follow UNIX convention and read from standard input. (Pro tip: never name a file -
.) I don’t care what your program does when given more than one argument; mine uses the rightmost file given.
You should also implement one flag for your interpreter: -u
for ‘unsafe’ mode, which disables type checking. For example:
$ echo '(lambda x : int. if x then 0 else 1) 5' >bad.lc
$ ./interp bad.lc
Type error: type mismatch
expected: bool
got: int
in: x
$ ./interp -u bad.lc
Error: expected bool, got 5
$ echo '(lambda x:int. x) true' | ./interp
Type error: type mismatch
expected: int
got: bool
in: true
$ echo '(lambda x:int. x) true' | ./interp -u
true
$ echo 'false < true' | ./interp -u
Error: expected int, got false
$ echo 'false < 1' | ./interp -u
Error: expected int, got false
Conservativity
You are free to write whatever error messages you like, though please be sure (a) that they go to stderr (not stdout) and (b) that you exit with a non-zero exit code.
Do not output unnecessary text when a program succeeds. While such debugging information may be valuable when you’re programming it’s (a) not part of the specification in this document and (b) will confuse my grader. If you’re not sure what output to give, please ask on Piazza.
Testing
I strongly recommend that you build a test suite. You might even be able to adapt some tests from HW06! Include short programs and long programs; programs that fail and programs that succeed. Now that our language is starting to resemble a real programming language, you should be able to write lots of interesting programs!
At a minimum, I expect you turn in a file fact.lc
which computes factorial of 5. (The type discipline won’t let you define all of the Church numerals, but who cares… we have real integers and recursive functions!) We should have:
$ ./interp fact.lc
120