Due Tuesday, Feb 24th at 11:59PM (Pacific Time)
In this assignment, we will continue to augment
Outline:
- Part 1 gives you lots of practice with the syntax and the operational semantics of the augmented
$\lambda^+$ language. - Part 2 is where you will implement the language extensions for the
$\lambda^+$ interpreter. - Part 3 contains extra-credit problems where you will infer the semantics of two mystery language features by playing with the reference
$\lambda^+$ interpreter.
- Make sure you download the latest version of the reference manual using this link.
- Download a zip of the starter code using this link.
- Run
opam install . --deps-onlyin the root directory of this homework to install the necessary dependencies. You need to run this command again even if you did so for HW2, since this assignment has new dependencies that were not used in HW2. - You will be modifying
lamp/eval.mlby replacing the placeholders denoted bytodowith your own code. - You must not change the type signatures of the original functions. Otherwise, your program will not compile on gradescope. If you accidentally change the type signatures, you can refer to the corresponding
.mlifile to see what the expected type signatures are. - Once you're done, submit
lamp/eval.mlto gradescope.
Please post your questions in the #hw3 Slack channel or come to office hours.
We have provided one unit test for each programming problem. To run all unit tests, simply run
dune runtestin the root directory of this homework. This will compile your programs and report tests that fail. You do not need to recompile your code after each change; dune runtest will do that for you.
We highly encourage you to add your own tests, since the autograder won't show you what the official test cases are (only the identifiers of passed/failed cases will be shown). You can locate the provided test cases in test/test_lamp.ml.
Let's augment
Several things to note:
-
Recursive function definitions are de-sugared into a combination of
let,lambda, andfix. For example, the following recursive definitionfun rec fact with n = if n = 0 then 1 else n * fact (n-1) in fact 5
will be de-sugared into
let fact = fix fact is lambda n. if n = 0 then 1 else n * fact (n-1) in fact 5
The de-sugaring procedure works as follows:
- First, we ignore the
reckeyword, and de-sugar the function definition as if it were non-recursive into a combination ofletandlambda. That is, we de-sugarintofun fact with n = if n = 0 then 1 else n * fact (n-1) in ...
let fact = lambda n. if n = 0 then 1 else n * fact (n-1) in ...
- Then, we wrap the lambda function with
fix f is .... For the example above, we replacelambda n. if n = 0 then 1 else n * fact (n-1)with a wrapped version usingfix fact is ..., which gives uslet fact = fix fact is lambda n. if n = 0 then 1 else n * fact(n-1) in ...
- First, we ignore the
-
Pattern-match on lists must end with the
endkeyword, unlike in OCaml. -
Equality comparisons in
$\lambda^+$ are between integers. Boolean and list equality can easily be implemented with custom functions, so we don't include them as built-in features to make the core language as compact as possible.
We will augment the AST of
type 'a binder = string * 'a
type expr =
(* as before *)
| ...
(* booleans *)
| True
| False
| IfThenElse of expr * expr * expr
| Comp of relop * expr * expr
(* lists *)
| Nil
| Cons of expr * expr
| Match of expr * expr * expr binder binder
(* recursion *)
| Fix of expr binder-
For booleans,
TrueandFalserepresent the boolean constants "true" and "false" respectivelyIfThenElse(e1, e2, e3)represents the if-then-else expressionif e1 then e2 else e3Comp(op, e1, e2)represents the comparisone1 op e2, whereopis a comparison operator. The comparison operators areEq,Lt, andGt.
-
For lists,
Nilrepresents the empty listCons(e1, e2)represents the cons celle1 :: e2, i.e., making a new list whose head ise1and whose tail ise2Match(e1, e2, ("x", ("xs", e3)))represents list pattern-matching as inmatch e1 with Nil -> e2 | x::xs -> e3 end. Note that the second branch of the match involves binding: the head of the list is bound to name "x" and the tail bound to "xs". Therefore, the third argument of theMatchconstructor will always have typeexpr binder binder, which by definition is(string * (string * expr)). For example, the concrete syntaxmatch e1 with Nil -> e2 | x::xs -> e3 endis represented abstractly asMatch(e1, e2, ("x", ("xs", e3)))
-
For recursion, the
Fixconstructor represents the fixed-point operator.Fixalso has a binding structure: it declares the name of the recursive function to be in-scope in the function definition. This is why theFixconstructor takes anexpr binderas its argument. For example,fix f is lambda x. f xis represented abstract asFix ("f", Lambda ("x", App (Var "f", Var "x")))
Problem (📝): Manually parse the following expressions in concrete syntax into ASTs. Use the reference interpreter on CSIL (~hanzhi/lamp) to check your answers.
if if 3=10 then Nil else false then if true then lambda x.x else false else 11::(Nil::true)::lambda x.xfun rec length with xs = match xs with Nil -> 0 | _::xs' -> 1 + length xs' end in length (1::2::3::Nil)
Programming languages and their operational semantics are designed by humans. As such, the human designer will always face a load of design choices that may lead to systems with different theoretical properties and practical trade-offs.
In this part, we will explore some of the "road not taken" in the design of
Problem (📝): App rule :
e1 ↓↓ \lambda x.e1'
e2 ↓↓ v
e1'[x |-> v] ↓↓ v'
------------------------ (App)
(e1 e2) ↓↓ v'
An alternative is to have the following App rule:
e1 = \lambda x.e1'
e2 ↓↓ v
e1'[x |-> v] ↓↓ v'
------------------------ (App-Alt1)
(e1 e2) ↓↓ v'
where we simply changed the first premise to an equality instead of an evaluation relation.
Let us denote the original evaluation relation as App is replaced by App-Alt1 as
Exhibit an expression
Hint: Consider the application of "multi-argument" functions.
Problem (📝): Another alternative to the App rule is to have the following rule:
e1 ↓↓ \lambda x.e1'
e1'[x |-> e2] ↓↓ v
-------------------------- (App-Alt2)
(e1 e2) ↓↓ v
Let us denote the original evaluation relation as App is replaced by App-Alt2 as
Exhibit an expression
Hint: What is call-by-value, and what is call-by-name?
Problem (🧑💻, 40 points) Augment the interpreter you wrote for HW2 to support booleans, lists, and recursion according to the operational semantics specified in the language reference manual. Specifically, for the free_vars, subst and eval functions:
- Copy and paste the code you wrote in the previous assignment for
free_vars,substandevalintolamp/eval.ml. - Replace
todo ()with your own code. Hint: the new cases offree_varsandsubstshould be trivial based on what you wrote in HW2.
There are two ways to test your interpreter:
-
We included some unit tests as well as a couple of realistic
$\lambda^+$ programs in test/examples/ that you can use to test your interpreter. Simply rundune runtest. -
You can also run the interpreter interactively (REPL) or in file mode as described in the previous assignments. For file mode, do
dune exec bin/repl.exe -- <filename>. For REPL, simply run the following command:dune exec bin/repl.exeWe added some convenience commands to the REPL:
-
<expr>triggers your interpreter to evaluate the expression, as usual. - CTRL-C interrupts the REPL and initiate a new prompt. This is useful if your interpreter enters an infinite loop, or you simply want to throw away the current input expression and start anew.
- CTRL-D quits the REPL.
-
#let <var> = <expr>evaluates the right-hand-side expression and adds the binding to the environment. Subsequent expressions can refer to this binding. For exampleNote that> #let x = 10 x = 10 > x + 1 <== x + 1 [eval] ==> 11 > #let x = 20 x = 20 > x + 1 <== x + 1 [eval] ==> 21- This syntax is only available in the REPL mode. Do not confuse it with
let-expressions in the language itself, which you implemented in HW2. - Later bindings may shadow earlier ones, just like in OCaml.
- This syntax is only available in the REPL mode. Do not confuse it with
-
#printshows the current binding environment. -
#clearresets the binding environment. -
#save <filename>saves the current history of commands to a file -
#load <filename>loads a file containing a list of commands and replays them. Binding commands are also replayed, so you can create a file that contains a sequence of#letcommands and replay them to set up a particular environment. You can load multiple files by doing multiple#loadcommands.
-
Click here to show extra-credit problems for Part 3
In this part of the homework, you will be inferring the semantics of two mystery language features by playing with the reference
External Choice (⭐️bonus⭐️, 2 points)
The first mystery language feature is called external choice.
Concretely, there are two ways in which an external choice can be made between two things: $1 e makes the first choice, while $2 e makes the second choice. Given an already-made choice e, the only thing we can do about it is to analyze it and branch accordingly, and we don't get to influence the choice in any way (hence the word "external"):
match e with
| $1 x -> e1
| $2 y -> e2
end
In the AST,
-
$1 eand$2 eare represented asE1 of exprandE2 of exprrespectively. - The case analysis is represented as
Either of expr * expr binder * expr binder. For example,match e with $1 x -> e1 | $2 y -> e2 endis represented asEither(e, ("x", e1), ("y", e2)).
Your task is to implement E1, E2, and Either cases of your eval function. You will be scored based on whether the semantics you infer is identical to the semantics of external choice implemented by the reference interpreter. The autograder output will be hidden until the submission portal is closed, so you will not be able to see how many autograder tests you passed.
Hint: This example solves the max problem from HW1 using external choice.
Internal Choice (⭐️bonus⭐️, 2 points)
The second language extension is internal choice, which is dual to external choice. Previously, when given an unknown external choice, we don't get to choose or influence the choice. In contrast, when given an internal choice expression e, we get to decide which choice to make, using the syntax e.1 or e.2. To construct an internal choice object for someone else, we must present both choices to them, using the syntax (e1, e2) where e1 and e2 are expressions. In the AST, these syntactic forms are represented as I1 of expr, I2 of expr, and Both of expr * expr. As evident from the type, none of those constructors has any binding structure.
The reference interpreter on CSIL (located at ~hanzhi/lamp) implements internal choice. You will reverse-engineer their operational semantics by experimenting with different example expressions and observing the behavior of the interpreter.
Your task is to implement Both, I1, and I2 cases of your eval function. You will be scored based on whether the semantics you infer is identical to the semantics of internal choice implemented by the reference interpreter. The autograder output will be hidden until the submission portal is closed, so you will not be able to see how many autograder tests you passed.