Interpreter 5: if/let
Table of Contents
It's time to start actually implementing the evaluation portion of the project!
1. Pair programming structure
This is a "pair" assignment, which means that if you are working on a team with someone else, you and your partner should engage in the pair programming model. You should be physically together, sharing a computer and both looking at the same screen. Each partner has an active role to play during pair programming - please review the instructions on Moodle about pair programming, and make sure you understand what you should be doing when you are the navigator and when you are the driver.
You should make sure that over the course of an assignment that you spend roughly the same amount of time each "driving." My recommendation is to take turns approximately every 15 minutes or so. Set a timer to help you remember. I will also ask you to fill out a survey about your partnership at the end of the term.
For the interpreter project, you are permitted to do a limited amount of debugging separate from your partner. If you do this, you should be sure that you're each contributing similar amounts: it's not fair, either in terms of time or in terms of learning, if one person does all the debugging. Before debuggging on your own, make sure that your partner is onboard with that. Also, make sure to share what you found with your partner afterwards.
If pair programming in real-time just doesn't work for you and your partner, then you will need to amicably split up and work individually. If you choose this option, you must communicate that to me [Anna] and send me an email with a zip file containing the code checkpoint of any work you did together (if applicable).
If things are not going well with working with your partner or you find yourselves tempted to split up the work, please talk to me. While doing a limited amount of debugging separately is fine, you should be doing the initial design and coding together - if you or your partner cannot explain how part of your interpreter works, then you have not followed the pair programming policies for this class.
2. Get started
You'll download the folder with the starter code from the link
below. Like for previous homeworks, unzip the folder and move it to
where you'd like: most likely the folder for the Docker container.
Copy over CollaborationsAndSources.txt and your .c files from previous stages of
the project (unless you'd like to use the binaries that I provide, see towards the end of the assignment).
Then get started in VS Code.
If when running tests you run into a permissions error, you can fix this by running the following code in the terminal:
chmod a+x test-e test-m
This tells the computer that all users should be permitted to execute (run)
the files test-e and test-m.
Note 10/28 9AM: If you are planning to use my binaries, rather than your own parser code, proceed with caution. I just discovered a bug in my parser code and am working on a fix. (It won't affect the M or E tests for this assignment, but may affect additional test cases that you write.)
3. Functionality
For this phase of the project, you must support the correct evaluation of the following types of Scheme expressions:
- Boolean, integer, real, and string literals. (They evaluate to themselves.)
(if test trueExpr falseExpr)Usually,
testwill evaluate to a Boolean, but that is not strictly required in Scheme. So, your code should evaluatefalseExpriftestevaluates to#f, and should evaluatetrueExprotherwise. In other words, we will treat anything that is not#fas true.(let list-of-bindings body)You will need to implement the creation of new frames. Much more on this in a bit.
- Variables. Now that you've implemented
letand frames, you should be able to evaluate a bound symbol.
When you're done with this part of the project, you should be able to evaluate very simple programs like this:
(let ((x #t)) (if x 3 5))
This should evaluate to 3. Your programs won't yet be able to handle functions or the quote symbol; that's coming later.
4. Core functionality
At the core of your interpreter, you will need to implement a function to evaluate Scheme code:
SchemeItem *eval(SchemeItem *tree, Frame *frame) { ... }
Given an expression tree and a frame (aka environment) in which to evaluate that expression,
eval returns the value of the expression. I'll explain more about the "frame" in a
moment. Here's a skeleton of my eval function, to give you an idea of where to
start:
SchemeItem *eval(SchemeItem *tree, Frame *frame) {
switch (tree->type) {
case INT_TYPE: {
...
break;
}
case ......: {
...
break;
}
case SYMBOL_TYPE: {
... //Code to implement what should happen if we have a symbol
break;
}
case CONS_TYPE: {
SchemeItem *first = car(tree);
SchemeItem *args = cdr(tree);
// Some error checking here (omitted from sample code) before moving to cases
if (!strcmp(first->s,"if")) {
result = evalIf(args, frame); //Helper functions can make your code easier to navigate!
}
// .. other special forms here...
else {
// not a recognized special form
evaluationError();
}
break;
}
....
}
....
}
5. Frames, and bindings
We started discussing this topic on Friday, and will get much farther into the details this week. But in the meantime, here's a brief overview of how to evaluate Scheme code:
The eval function mentioned above takes a pointer to a "frame"; what is a
frame? A frame is a collection of bindings. OK, what's a binding? A binding is a
mapping from a variable name (i.e. a symbol) to a value. Frames are created
whenever we introduce new variable names. For example, in the program
(let ((x 5) (y "hello")) (if #t x y))
the bindings for x and y are stored in a single frame. You will have to
construct a frame whenever eval encounters a let. This frame should be
passed in when calling eval on the body of the let expression. The frame is
used to resolve (find the value of) each variable encountered during
evaluation. When eval tries to evaluate x, it looks in the current
frame to find a value for x.
There's one other crucial detail here: frames can be chained together to create a larger environment consisting of multiple frames. For example, in the program
(let ((x "hello")) (let ((y "goodbye")) (if #t x y)))
each of the two let expressions creates a separate frame. When eval evaluates
x, it first checks the innermost let's frame; since that frame only contains a
binding for y, it then checks the outer let's frame, where it finds a binding
for x with value "hello".
To summarize, evaluating a let expression of the form:
(let ((var1 expr1) (var2 expr2) ... (varn exprn)) body)
consists of the following steps:
- Let
ebe a pointer to the current frame. Create a new framefwhose parent frame ise. - For i = 1 through n:
- Let
valibe the result of evaluatingexpriin framee. - Add a binding from
varitovalitof.
- Let
- Evaluate
bodyusing framefand return the result.
You will need to implement data structures for frames and bindings. The easiest
approach is to use linked lists. The linked list of frames essentially forms a
stack: you push on a new frame just before evaluating the body of the let
expression, and pop the frame off before returning (although the "pop" really
happens automatically when you return from eval). Within each frame you should
store a list of variable/value pairs (i.e. bindings), using another linked
list. When you need to resolve a variable, check the current frame first, then
its parent if that variable is not in the current frame, then its parent, and so
on until you reach a frame with no parent.
6. Evaluation errors
There are going to be many cases in which evaluation is not possible. For example:
- When an
ifexpression has fewer than 3 arguments (yes, there are two syntaxes available forifin full Scheme, but we're only going to support the one we've been using in whichifalways has three arguments) - When an
ifexpression has more than 3 arguments. - When the
list-of-bindingsforletdoes not contain a nested list - When you encounter a variable that is not bound in the current frame or any of its ancestors
- etc…
In any of these cases, you should print "Evaluation error" at the start of your error message so that the tests will pass. You might enhance these messages by adding more text to it, like "Evaluation error: if has fewer than 3 arguments" or the like. While these more detailed error messages are not required, writing better error messages may help you now or later in the project with debugging, as if things go wrong when they shouldn't, you'll have a better sense for what happened.
You should then
immediately quit, and make sure to clean up memory on your way out; use texit for this.
7. Multiple S-expressions
As discussed in the parser assignment, a Scheme program is a list of
S-expressions. You should call eval on each S-expression. This is what the
function interpret, that you need to write, should do: it is a wrapper
that calls eval for each top-level S-expression in the program. You should
print out any necessary results before moving on to the next
S-expression. (The interpreter follows a read-evaluate-print loop:
after you evaluate a top level S-expression, the result of that
evaluation is printed.)
Some other parts of the code may also have multiple S-expressions,
such as the body of let. If the body of let has multiple
expressions, you should evaluate each one of them, and return the
result of the last one. See the last test below for an example of
this.
8. Sample output
$ cat test-in-01.scm
3
5
(if #f 7 12)
$ ./interpreter < test-in-01.scm
3
5
12
$ cat test-in-02.scm
(let ((x 5)) x)
(if #t x y)
$ ./interpreter < test-in-02.scm
5
Evaluation error: unbound variable x.
$ cat test-in-03.scm
(let ((foo "hello")
(bar 7)
(baz #f))
(if baz foo bar))
$ ./interpreter < test-in-03.scm
7
$ cat test-in-04.scm
(let ((x 3)) 2 4 x)
$ ./interpreter < test-in-04.scm
3
9. Optional Extensions
Work in this section is 100% optional, and not worth any points. Nonetheless, if you're looking for an extra challenge, these are fun additional exercises to try.
Here are some additional details in the Scheme language that you can integrate if you wish; they will be unnecessary for the core part of the project going forward, though they may play into later optional extensions:
Implement the Scheme function
display(described further down in this section of Dybvig). The simplest cases, which are those I'd focus on, are the following:(display x) ;; displays value of a particular variable (display "hello world") ;; displays a particular string without the quotes
You can handle non-printable characters (newlines, etc) inside the strings in any reasonable way, so long as the code doesn't crash.
- Implement the Scheme functions
whenandunless(described further down in this section of Dybvig).
10. The starter files
Your repository will follow the same structure that it has for the previous assignments. The key changes are in schemeitem.h, which now includes a struct for a frame, and interpreter.h, which is
the header file for the new functionality. You'll need to add interpreter.c, which implements the functions specified in interpreter.h.
Again as usual, you may use my binaries if you wish instead of using
your own previous code, and the directions on how to do so are the
same. I continue to encourage you enthusiastically to use your own
code! To do so, copy in the .c files from your last assignment. Note
that I will not able to provide binaries for parts beyond the parser -
the code for evaluation gets too intertwined to be able to do so effectively.
If you're working on a team, you and your partner each wrote your own parser for the previous assignment. If you are going to continue building on your own code, you'll need to choose one of the two parsers to use going forward. Have a rock-paper-scissors tournament with your teammate to decide which, or choose the one that passed more tests. If you have time and want to work with your partner on merging the two together to make an even better parser, feel free.
Building and testing your code will work precisely the same as on the
previous assignment (just build, test-m, and test-e). Gradescope
will copy in fresh copies of the tester files and the supporting
architecture when running the tests.
11. How to test and submit your work
11.1. Testing your work
Go back and look at the sections at the end of Scheme Intro 2 labeled "How to test your work". Everything there about how to run M tests and E tests applies identically here. Note that the tests will fail if your code has compiler warnings and/or valgrind errors, in addition to if its behavior is observably incorrect.
11.2. Submitting your work
You'll submit your work on Gradescope. You should already have a file
CollaborationsAndSources.txt
in your working directory (if not, copy it over from the previous assignment). Go to the header
for this portion of the assignment and fill in the document. Indicate
in what ways (if any) you collaborated with other people on this
assignment (did you talk with anyone other than Anna or our prefect? Did you share strategies with anyone?
Get or give advice on debugging? These are
fine things to do, and you should note them in the CollaborationsAndSources.txt
file). If you used any resources outside of your notes from class, that is
also something to note in CollaborationsAndSources.txt. If
you didn't talk with anyone or use any outside sources, please note
that explicitly. Look back at the
Scheme Intro 1 instructions if you want more details on the
CollaborationsAndSources.txt file.
After updating CollaborationsAndSources.txt and finishing the
assignment, run ./zipitup
This will zip up your C files, header files, and collaborations and sources. We will be re-copying in our version of the tests and of the included header files.
Then, upload the created zip to Gradescope.
On Gradescope, make sure to add your partner if you worked with a partner. This page has detailed instructions for how to do that.
On Gradescope, you should see autograder output for the same tests as you ran locally. There is a small but nonzero chance that you have coded something unexpected that cause the tests pass on the computer you’re working on, but not on Gradescope. We will be grading this assignment based on the results of the tests on Gradescope, so you should make sure to pay attention to how the tests run there as well when you submit.
For grading, you'll earn at least an M if the following conditions are all met:
- All M tests pass
- A visual inspection of your code shows that you have not hyper-tailored your code to pass the tests. Hyper-tailoring your code would be doing things like checking for the exact input from the test, meaning you haven't written your code in a way that would work for similar inputs (see Homework policies for more on this).
- You have a
CollaborationsAndSources.txtwith the information described above. - The work is submitted by the late deadline.
You'll earn an E if:
- All conditions above for an M are met.
- All the E tests pass.
- Your code is not significantly more complex than needed to accomplish the task.
- You have a comment before each function describing what the function does (its input/output behavior, not a play-by-play of "first checks… then makes a recursive call …").
- The work is submitted by the regular deadline or it is submitted by the late deadline and you elect to use a late token by submmitting a regrading request.
You may earn a grade of M or SP (Some Progress) and be offered to resubmit for an improved grade if:
- All tests pass (all E tests if you can resubmit for an E, and all M tests if you can resubmit for an M)
- Your code has significant stylistic or efficiency issues that we have not explicitly covered in class or on the feedback for a previous assignment.
If your code does not meet the criteria for an M or an E, then you'll earn a
grade of NA (Not Assessable) or SP (Some Progress) for the assignment. Not Assessable means that your submission does not
have an attempt at one or more functions in the assignment. Some Progress means
that your submission has some code (in the correct language) for all functions in the
assignment, but that the code does not pass all of the M tests, is
hyper-tailored to the tests, and/or is missing a CollaborationsAndSources.txt
file.
Good luck, ask questions, and have fun!
This assignment was originally created by David Liben-Nowell and has since been updated by Dave Musicant, Jed Yang, and Laura Effinger-Dean. Thanks for sharing!