Interpreter 8: Capstone
Table of Contents
This assignment is the final phase of your interpreter! It's larger than the previous interpreter milestones, and has two parts. (It is also graded as if it is two homework assignments, with the grading being evenly split between the two parts.)
For Part 1, you will implement several additional special forms and primitive functions that I have pre-specified. After this, most of the code we wrote at the beginning of term will be able to be run in your interpreter!
For Part 2, you will implement a number of additional extensions of your choice. I describe many possible later extensions later on this page. For Part 2, you will also need to write tests (similar to my M and E tests, although you don't need the multiple levels) and a short summary of which enhancements you did. More details on both these requirements are provided below!
This assignment is due at 10pm on the last day of class. Due to college policy, I cannot accept late work, nor revisions past that time. If you need an exception to this policy due to extreme circumstances, reach out to your class dean and me (Anna).
For this final assignment, please note that if you haven't already, you must fill out the project reflection form (linked on Moodle) in order to earn an E or an M. The deadline for this for is the last day of classes.
This assignment is to be done individually. You should not work with your interpreter partner on this assignment. You can talk to other people in the class to come up with ideas and get help with general concepts, but you should not show others your code, nor see others' code. You can get direct help debugging from Anna and any of the course staff (graders, lab assistants, prefects).
Both you and your interpreter project partner can access your code from the previous part by downloading it from Gradescope.
1. Get started
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. 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.
2. Part 1 - Required functionality
Required additions to your interpreter are as follows:
2.1. Some missing special forms
Implement the following special forms: letrec and set!.
letrec: Recall that letrec allows you to define (mutually) recursive procedures. Take a look at the specification in Dybvig. The key difference from let is that the bound variables can be accessed from other variables bound in the same let, but unlikelet*, the values can't be accessed until all expressions have been evaluated. For instance, consider:
(letrec ((x 3) (y x)) x)
This violates the description in Dybvig that "The order of evaluation of the expressions expr … is unspecified, so a program must not evaluate a reference to any of the variables bound by the letrec expression before all of the values have been computed." The specification says that if this restriction is violated, an exception (i.e., evaluation error) should be raised. However, Guile doesn't handle it correctly! It prints 3. Your interpreter should handle this correctly by raising an evaluation error.
Similarly, consider:
(define y 5)
(letrec ((x y) (y x))
x)
Again, the value of a variable is accessed before all variables are bound, so your interpreter should raise an evaluation error. This differs from Guile, which appears to print nothing in response to this.
Still not quite sure how to interpret letrec? Here's a rough guide
to evaluating (letrec ((x1 e1) ... (xn en)) body1 ... bodyn),
although it doesn't inclue all needed error checking:
- Create a new frame env’ with parent env.
- Create each of the bindings, and set the to
UNSPECIFIED_TYPE(this is inschemeitem.h) - Evaluate each of e1, …, en in environment env’.
- After all of these evaluations are complete, replace bindings for each xi to the evaluated result of ei (from step 2) in environment env’.
- Evaluate body1, …, bodyn sequentially in env’, returning the result of evaluating bodyn.
set!: Read the specification in Dybvig. This should be similar todefine, except that it always modifies an existing variable, and may be used in contexts outside of the global environment (unlike how we've restricted the testing of yourdefineimplementation). We'll haveset!return aVOID_TYPE, similar todefine.
2.2. Two more primitive functions
Implement < (the less-than function) and equal?. equal? only needs to work for numbers, strings, and symbols;
it does not need to perform any sort of deep equality test on a nested structure. Also, for symbols you should feel free to
just check whether the symbols are the same (e.g., <code>(define a 1) (equal? a a)</code> would return true,
while <code>(define a 1) (define b 1) (equal? a b)</code> would return false).
2.3. Notes on testing your code
The tests for this portion use both your old functionality and this new
functionality together, which may mean lead to finding some bugs in
your old code. Part of completing this assignment may involve some
debugging that is not directly related to the new functionality. One
note from past experience is that until now, we have had limited
ability to test if you are doing the right thing when you have a let
or lambda with multiple bodies. In both cases, each body should be
evaluated (starting from the first), and the result of evaluating the
final body should be returned.
I strongly encourage you to implement one new piece of functionality at a time, testing with small test cases as you go, and to use a debugger for debugging. This can be visually in VSCode or using gdb on the command line.
3. Part 2: Extensions of your choice
For this part, you choose what additional functionality you want your interpreter to have. I've included suggestions in many of the past assignments (copied below, for convenience) so some of you may have already gotten started. In any case, now there are many available options for you to choose from! If you have an idea for an extension that is not on this list, feel free to implement it (and reach out to Anna to discuss whether it's a small, medium, or large extension).
Extensions are organized into small (1 point), medium (2 points), and large (8 points). Small and medium extensions
are relatively straightforward. Large extensions are more ambitious, and may require changing how the interpreter runs
(e.g., by editing main.c or the makefile). They are generally more than 8x as hard as the small extensions, but are great
options if you're looking for a challenge!
3.1. Small Extensions
Each of these extensions counts towards one point.
- Implement additional primitives
*should be able to take any number of numeric arguments >= 1. Note that (* n) returns n.-should be able to take any number of numeric arguments >= 1. Note that (- n) returns the number -n, while (- n1 n2 n3) returns the result of ((n1 - n2) - n3)./should return a real (i.e., double) if the numbers do not divide evenly. (Guile returns a fraction; don't do this.) It's fine to only implement the two-argument case.code>modulo. I recommend implementing a version where you assume that the arguments are integers.
>,<\=,>\=and\=. As these functions all use similar logic, you may earn at most two points from these functions, even if you implement all 4. Note that the difference between\=andequal?is that\=only works with numbers.
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
when(described further down in this section of Dybvig) - Implement
unless(described further down in this section of Dybvig)
- Implement additional special forms
-
andis a special form.andis technically defined for any number of arguments (including 0) and for any type of arguments; however, I recommend you focus on the case where you have 1 or more arguments of Boolean type. More details are available in the Dybvig specification. -
oris a special form.oris technically defined for any number of arguments (including 0) and for any type of arguments; however, I recommend you focus on the case where you have 1 or more arguments of Boolean type. More details are available in the Dybvig specification.
Note that these are special forms, not primitive functions. You should be able to think of test cases
(e.g., ones that use set!) where this distinction matters.
- Format your output so that it resembles Guile output as closely as possible. Specifically, don't have an extraneous space after the last item in a list before the closing parenthesis. You don't need to write tests for this case! (But should include it in your write up.)
- Handle additional syntax (improvements to tokenization and parsing)
- Correctly handle square brackets. See the Parser assignment for more details on this extension.
3.2. Medium Extensions
Each of these counts for two points.
- Implement additional primitives
listtakes any number of arguments and returns a (proper) list containing the arguments. For example,(list)returns(),(list 3)returns(3), and(list 3 4 5)returns(3 4 5).maptakes two arguments, a procedure and a list. It returns a new list, where the procedure has been applied to each element of the original list.
- Implement additional special forms
cond. It turns outcond's specification is more complex than how we used it in class. Don't worry about the non-standard syntax options, you may assume that each clause has exactly the form(bool expr), even thoughcondtechnically allows things like(bool)or(bool expr1 expr2). However, you should implementcondso that either#torelseare allowed as the default case (#tshould work automatically,elsewill need to be handled as a special case). If there is nothing to return forcond(no true cases), returnVOID_TYPE.let*Create a new frame for each binding pair.
- Implement the third version of define shown in Dybvig,
which is the one that lets you use define alone as shorthand for both define and lambda:
(define (myfunction x y z) (+ x y z))
The above works, but is really shorthand for
(define myfunction (lambda (x y z) (+ x y z)))
- Handle additional syntax (improvements to tokenization and parsing)
- Correctly parse dotted pairs, e.g.
(a . b). Make sure that you also account for this case in the evaluation.
- Correctly parse dotted pairs, e.g.
3.3. Large Extensions
These ideas may require changes in what happens when you run ./interpreter,
and you might wonder how to do that given that you also don't want to
break the tests. One option is to make a separate C file that will
just hold your main function, with the different functionality
(e.g., the simple interface). You can then use an alternative Makefile
that doesn't compile main.c but does compile your separate C
file. You can then compile this via: make -f ReplMakefile,
where ReplMakefile is the name of the new Makefile. You should give the new
executable a different name than interpreter (e.g., interpreter2) and then
run it by similarly on the command line (e.g., ./interpreter2).
For these extensions, you don't need to write tests, but you should describe in the write-up how I can run your extensions and check manually that they work.
If you create new files, you will need to edit ./zipitup to ensure your files are
included in the Gradescope submission. After submitting, please also double-check that all necessary
files are on Gradescope (switch from the autograder tab to the files tab on Gradescope).
- Garbage collection. A simple mark-and-sweep collector is relatively easy to implement and should improve your memory usage. Have some way of demonstrating to the user that this is really working. One approach might be to have some demonstration code that displays a count of the number of SchemeItems freed every time the garbage collector is run, and also display some information to help the viewer know that the number of SchemeItems being freed is correct. Please make sure that the displays happen only with your demonstration code, not when running the M and E tests.
(load "file.scm"). This reads in a file and executes the code in the file as if it were typed in.A simple interface. The classic core of an interpreter is the read-eval-print loop, a.k.a. REPL. You can add this functionality to your code so that you can more easily use your interpreter interactively. For example:
$ ./interpreterRepl > (define factorial . (lambda (n) . (if (= n 0) . 1 . (* n (factorial (- n 1)))))) > (factorial 5) 120 >
One issue you will run into with this is you get unnecessary printouts of prompts when you are using a file for input instead of typing the code in manually. You can figure out whether the interpreter is being called interactively using the
isattyandfilenofunctions (man isattyin the terminal to learn more). You should only print the prompts ifstdinis interactive.Note that you can end an interactive session by typing Ctrl-D (sending an "end-of-file" character), so there is no need for a "quit" command.
If you really want to dive into this approach, optionally figure out how to use the
readlinelibrary to provide things like history and editing for your REPL.
3.4. Required: tests and a short write-up of what extensions you implemented.
Most of the extensions require you to write tests to check that they work. These should be in a similar format
to my M and E tests and you should put the code (.scm file) and correct output (.output file) in the user-tests
directory. To run your tests, run ./user-tests from the command line. Your tests should be thorough: think about the
expected input, but also weird edge cases. You are not required to write tests for cases that will fail (i.e., the ones
where your interpreter will correctly print "Evaluation error") but you may do so if you like.
Please name each test file in a descriptive way (e.g., and-test1 or let-star-3).
You should list the extensions you implemented in contributions.txt. It's fine if this is a bulleted list. In this file,
please also indicate if there are any edge cases that you know your code fails on. If you implemented any of the extensions
that don't have a testing component, provide a description of how I can run your code to see the extensions in action.
Again, if there are any edge cases that don't work, please let me know that as well.
3.5. Grading for Part 2
To receive an M:
- Complete extensions totaling at least 5 points.
- Write tests for the new functionality. The tests should have comprehensive coverage of the functional code.
- Describe the new functionality in
contributions.txt. - In
contributions.txt, be upfront about any edge cases that still fail. - Write the code so that it does not contain significant stylistic or efficiency problems
- Fill out the interpreter reflection form, linked from Moodle.
To receive an E:
- Fulfill all the criteria for an M.
- Complete extensions totaling at least 10 points.
- Use good coding style and include comments on each function that you write.
4. Starter files and your code
The files follow the same structure that they have for the previous assignments. You'll then need to add in the files from your previous version, and/or switch over to using our binaries. But note that the binaries I provide only go through part 4 of the project; I am not providing a working if/let interpreter. (The project gets too intermingled at this point for me to be able to supply complete partial work.)
Building and testing your code will work precisely the same as on the previous assignment.
5. How to test and submit your work
This section describes the grading for Part 1 – it's similar to the previous assignments! Please fill out
CollaborationsAndSources.txt according to help you received on both parts of this assignment.
5.1. Testing your work
As with previous assignments, there are M tests and E tests. See the section of Scheme Intro 2 labeled "How to test your work" if you need a refresher on how to run these tests. Note that the interpreter tests will fail if your code has compiler warnings and/or valgrind errors. Make sure to run the tests in the Docker environment.
5.2. Submitting your work
5.2.1. Acknowledging help
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.
Did you share strategies with anyone else? Talk about any annoying errors and get advice? These are
fine things to do, and you should note them in the CollaborationsAndSources.txt
file. Give the names of people you talked with and some description of
your interactions. If you used any resources outside of notes taken in class or the homework/lab starter files that
Anna provided, that is also something to note.
If you didn't talk with anyone or use any outside sources, please note
that explicitly in CollaborationsAndSources.txt.
5.2.2. Generating the submission file and uploading to Gradescope
After completing CollaborationsAndSources.txt and finishing the
assignment, run ./zipitup (if you get a permissions error, run chmod u+x ./zipitup first).
This script will create a zip file XXX_submission.zip where XXX is the assignment name or number.
Note that this zip file with include your C files, any new header files you created,
and CollaborationsAndSources.txt. We will re-copy in our version of the tests and of the provided
header files.
Upload the zip file to Gradescope.
On Gradescope, you should see autograder output for the same tests as you ran locally. There is a small chance that the tests will fail on Gradescope, even if they passed on your own computer. Usually this means that you coded something in a different way than we expected when writing the autograders. We will be grading based on the results of the tests on Gradescope, so make sure to pay attention to the results of the tests there. (If you run into this issue, check that you are submitting to the correct assignment on Gradescope. Then try resubmitting and rerunning the autograder. If the error messages are legible, feel free to debug further, but it's also fine to reach out to Anna or the graders at this point.)
5.2.3. 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.
- You have a
CollaborationsAndSources.txtfile with the information described above. - Your code has significant stylistic or efficiency issues that we have not explicitly covered in class or on the feedback for a previous assignment.
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 …").
- Your code follows good style convention.
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.
You may turn in revisions to this assignment until the last day of classes.
Good luck, ask questions, and have fun! When you've completed this assignment, you'll be able to run pretty complicated Scheme code, including code from earlier assignments.
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!