Interpreter 7: Primitives
Table of Contents
In the last assignment, you implemented lambda, which let you create functions
within Scheme. Scheme also has primitive functions, which are "built-in"
functions that are not written in Scheme themselves. You'll implement primitive
functions for this assignment.
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. Functionality
In this assignment, you'll implement the following functions: +, null?,
car, cdr, cons, and equal?.
Primitives are functions not implemented in Scheme; you'll implement them as C functions that get called by your interpreter directly. More details follow in the section below on primitive functions. A few comments on these specific primitives:
+should be able to handle any number of integer or real arguments (if 0 arguments, return 0). If any of the arguments are reals (double), return a real; else return an integer.null?,car, andcdrshould take one argument each, and you should have error checking to make sure that's the case. Similarly, you need to check to make sure that the argument is appropriately typed.consshould take two arguments, and you should have error checking to make sure that's the case. You'll also need to modify the code you have that handles output, becauseconsallows you to create non-list pairs. You'll need to add functionality to use a dot to separate such pairs. This portion of the Dybvig Scheme book does a decent job at describing how such output should work. (Note that thedisplayfunction in our linkedlist binaries won't work on pairs such as this one. If you're using our binaries and using the linkedlistdisplayfunction, you'll need to write your own instead.)appendshould take two arguments, and you should have error checking to make sure that's the case; we will not supportappendwith varying numbers of arguments even though Guile does this. The first argument must be a list (do error checking to make sure that's the case; note that an empty list does count as a list for these purposes), and the second can be any type. Like withcons, this will mean needing to be able to display non-list pairs. (E.g.,(append (quote (1 2 3)) 4)evaluates to(1 2 3 . 4)in Guile.) Recall thatappendworks by making a copy of its first argument but not its second (copying versus not copying won't matter much at the moment, but it will matter when we add inset!in the final part of the interpreter). This should be a shallow copy, meaning that we create newconscells to represent the outermost list layer, but nested lists will still point to shared memory.
3. Sample tests
$ cat test-in-01.scm
(define length
(lambda (L)
(if (null? L)
0
(+ 1 (length (cdr L))))))
(length (quote ()))
(length (quote (4 5 6)))
$ ./interpreter < test-in-01.scm
0
3
$ cat test-in-02.scm
(append (quote (4 5)) (quote (6 7)))
(define reverse-list
(lambda (L)
(if (null? L)
L
(append (reverse-list (cdr L)) (cons (car L) (quote ()))))))
(reverse-list (quote ()))
(reverse-list (quote (1 2 3 4)))
(reverse-list (quote (("computer" "science") "is" "awesome")))
$ ./interpreter < test-in-02.scm
(4 5 6 7)
()
(4 3 2 1)
("awesome" "is" ("computer" "science"))
4. Primitive functions
There's one bit of trickiness that will come up: you're going to want
to have both functions-as-closures and functions-as-primitives. Here's
how to do this. In schemeitem.h, we've added a PRIMITIVE_TYPE. This will
be for a SchemeItem that represents a pointer to a function. Here's how the additional portion appears:
typedef enum {INT_TYPE, ..., PRIMITIVE_TYPE, ...} itemType; struct SchemeItem { itemType type; union { int i; ... // A primitive style function; just a pointer to it, with the right // signature (pf = my chosen variable for a primitive function) struct SchemeItem *(*pf)(struct SchemeItem *); } }
To show how I'm using primitive functions, here are some relevant functions and snippets of code from scattered spots in my implementation for an exponential function that you're not writing unless choose to implement it as part of the final project submission.
/* * Given two int or double items in args, * returns an item representing argument 1 * raised to the power of argument 2. * Raises an error with incorrect args length * or types. */ SchemeItem *primitiveExp(SchemeItem *args) { //Code omitted } /* * Adds a binding between the given name * and the input function. Used to add * bindings for primitive funtions to the top-level * bindings list. */ void bind(char *name, SchemeItem *(*function)(SchemeItem *), Frame *frame) { //Code omitted } void interpret(SchemeItem *tree) { ... // Make top-level bindings list: // First, initialize frame (code omitted) // Then, bind primitive functions bind("exp", primitiveExp, frame); ... }
Note that it really is important that you bind the functions and not
just that you look for a primitive function name occurring as the
first thing after parentheses. Primitive functions aren't special
forms like if or lambda, and treating them in the same way where
you look for them to appear directly after parentheses will lead to
problems if these functions are passed as inputs to other functions
or returned as outputs.
5. Optional Extensions
For the final project submission, you will get to choose what additional functionality you want your interpreter to have, and you'll implement (several small, a few medium, or 1-2 large) enhancements. It's not required to get started on these extensions now, but if you'd like to, here are some ideas:
- Implement the following additional special forms:
list,map, andequal?.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. Each of these counts as a small extension. - Implement the following primitives:
-,*, and/. Each of these count as a small extension.
6. Starter files and your code
The files follow the same structure that they have for the previous
assignments. The only change in the files I'm providing is the change
to add a primitive function in schemeitem.h, as described above. 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. Note that the tokenizer assignment gives you instructions for how to run a single file (which can either be one you create or one of our tests). If your code is not passing the tests, I strongly encourage you to try running on one test (of your creation or of ours) and figure out what's going wrong there before moving on. You can run valgrind as well as use gdb; if you get stuck, reach out for help!
7. How to test and submit your work
7.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.
7.2. Submitting your work
7.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.
7.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.)
7.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!
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!