For the remainder of the semester, we will be working towards building an interpreter for a large subset of Racket, covering most of the language that we have used previously in class. The interpreter we will write is constructed in 6 required components and 3 optional components, each which builds upon the last. It will not be possible to start a required component until you have completed the previous one. For this reason, it is very important that you complete each component in a timely fashion. Each week we will work on two required component and one optional component of the interpreter. You are strongly encouraged to finish each week's required components before the following week.
While I will not strictly enforce the date that each component is due, you must turn in each component in its entirety when you have completed it: turning in only the final version of the project is not sufficient. I will be looking for the progress you make from class to class, and the incremental testing that you do. This will make up a portion of my final evaluation of your project. The final version of the project (all six components completed) will be due on Sunday, May 12th at 12:00 noon.
You are strongly encouraged to work with a partner for the duration of this project. You may not switch partners or drop your partner part-way through the project. You and your partner will receive identical grades, and this project is worth 20% of your grade, so be sure you have chosen a partner wisely.
The textbook focuses on creating an interpreter in Section 4.1. While many of the concepts we discuss in our interpreter are similar to those discussed in the textbook, we will not design our interpreter in the same way. You should read the text for a more complete understanding of the interpreter you are building, but you will be able to complete the interpreter project based on previous material you have learned in this class and through the directed steps I will provide for you in the each of the parts of this project.
We will be building what is known as a "bottom-up" interpreter. This means that we will start at the "bottom", getting the lowest-level components working first before moving "up" to higher-level components. Extensive testing of each component is extremely important as small mistakes early on will make successful completion of this project much more difficult.
Below is an outline of the steps we will take in implementing our interpreter:
|Variables and Environments||* binding, frame and environment abstractions|
* quote, define, set!
* simple syntax checking
|(extra credit)||* quasiquote, unquote|
|Primitive Procedures||* primitive procedure abstraction
* car, cdr, cons, null?
* +, -, *, /, <, >, =, equal?, eq?
* display, newline
* set-mcar!, set-mcdr!
|Special Forms||* if, begin, cond|
|(extra credit)||* trace, untrace|
|Week 3 (4/29‑5/2)||Part 5
|User-defined procedures||* lambda, let, let*|
|Added special forms
interpret your interpreter
|* new special forms
|(extra credit)|| * define-syntax-rule
* "shortcut notation" functions
* functions with variable arity
* ...and more!
This interpreter will be one large program. To be sure you keep yourself organized, you can always refer to this summary page which details each of the major functions you will write, along with the order of their parameters. You will want to run update37 before you begin each new part.
Extensive testing of your interpreter will clutter the code you have written and make it difficult for both you and me to examine. To alleviate this, you will keep all the tests you write in a separate file. Your interpreter should be kept in a file called interpreter.rkt and your tests should be kept in a file called i‑tests.rkt. I have provided you with a skeleton file to help you get started in writing your tests.
You should familiarize yourself with the search function built into DrRacket: under the Edit menu, choose Find. Also, a feature you may find useful is the (define ...) button located near the top left of the window. It displays every defined variable and procedure, and if you click on one, you will be taken directly to its definition in the file.
After you have completed each component, use handin37 to turn in the completed part.
Throughout the project, we will emphasize the idea of data abstraction. This means that we will create abstraction barriers between the data manipulated by the interpreter and the actual way in which the data is represented. This will be similar to what you have done in past assignments. Be sure to use constructor, selector, and mutator procedures whenever you want to create, access, or update data. For ease of writing your interpreter, you should not use object-oriented techniques for this project.
The first component of the interpreter begins by developing environments. Environments will be used by the interpreter when evaluating Racket expressions. Every expression in Racket is evaluated within some environment. Often it is a special environment called the global-env (defined in Part 2).
At the lowest level of an environment are variable bindings. A binding is the joining of a symbol to a value. We will use mcons pairs to represent bindings. For example, the binding (mcons 'x 3) indicates that the symbol x is bound to the value 3, and (mcons 'lst '(1 2)) indicates that the symbol lst is bound to the list (1 2).
These are the functions that you will use to create, access and change variable bindings:
(make-binding var val) ; constructor (binding-variable binding) ; selector (binding-value binding) ; selector (set-binding-value! binding val) ; mutator
I have included tests for each of these functions in the i‑tests.rkt file. You may also want to see the summary page for more details. Example:
> (define sample (make-binding 'today 'monday)) > sample (mcons 'today 'monday) > (binding-variable sample) 'today > (binding-value sample) 'monday > (set-binding-value! sample 'tuesday) > sample (mcons 'today 'tuesday) > (define sample2 (make-binding 'a-pair (cons 1 2))) > sample2 (mcons 'a-pair '(1 . 2)) > (binding-variable sample2) 'a-pair > (binding-value sample2) '(1 . 2) > (define sample3 (make-binding 'a-list (cons 1 (cons 2 null)))) > sample3 (mcons 'a-list '(1 2)) > (binding-variable sample3) 'a-list > (binding-value sample3) '(1 2)
Next we need to implement frames, which are (immutable, ie. "normal") lists of bindings. The procedure make-frame takes a list of variables and a list of values, both of which should be the same length, and creates a frame containing bindings of the variables to the values. If the variable and value lists are of different lengths, make-frame should generate an appropriate error message (using the error primitive). For example:
> (make-frame '(a b c) '(6 7 8)) (list (mcons 'a 6) (mcons 'b 7) (mcons 'c 8)) > (make-frame '(d e) '((1 . 2) (3 4))) (list (mcons 'd '(1 . 2)) (mcons 'e '(3 4))) > (make-frame '(x y z) '(#t #f)) make-frame::too many variables > (make-frame '(today tomorrow) '(mon tue wed)) make-frame::too many values
After completing and testing make-frame, write and test the remaining frame functions. (See the summary page for more details.)
(make-frame vars vals) ; constructor (empty-frame? frame) (first-binding frame) ; selector (rest-of-bindings frame) ; selector (adjoin-binding binding frame) (binding-in-frame var frame)
Note that the function adjoin-binding does not have a "!" in its name. Therefore it should not use set!, set-mcar!, or set-mcdr!. It should simply cons a new binding onto an existing frame and return the new frame.
Also, be sure you retain all of your tests in the i‑tests.rkt file since you will be graded on your tests as well as your code.
Here are some examples of the above mentioned functions:
> (define frame (make-frame '(a b c) '(6 7 8))) > (first-binding frame) (mcons 'a 6) > (rest-of-bindings frame) (list (mcons 'b 7) (mcons 'c 8)) > (empty-frame? frame) #f > (binding-in-frame 'a frame) (mcons 'a 6) > (binding-in-frame 'c frame) (mcons 'c 8) > (binding-in-frame 'x frame) #f
Now that frames have been implemented, we can implement environments, which are represented as a mutable list of frames. An empty environment is represented as the empty list. Test all of these environment functions thoroughly before going on. (See the summary page for more details.)
(define empty-env null) ; constructor (empty-env? env) (first-frame env) ; selector (rest-of-frames env) ; selector (set-first-frame! env new-frame) ; mutator (adjoin-frame frame env) (extend-env vars vals base-env) (binding-in-env var env) (lookup-variable var env) ; outside of the 'environment' abstraction
A variable may appear in several different frames of an environment. For example:
(define env1 (extend-env '(a b c) '(1 2 3) empty-env)) (define env2 (extend-env '(a c d e) '(red blue green yellow) env1)) (define env3 (extend-env '(a f) '(#t #f) env2))In env3, there are three different bindings for the variable a and two different bindings for the variable c.
Write the procedure binding-in-env that will take a variable name and search through the frames of an environment until it finds the first binding for that variable. In this example, the first binding for a would be (mcons 'a #t), the first binding for c would be (mcons 'c 'blue) and the first binding for b would be (mcons 'b 2). It should return #f if no binding is found. Be sure to use any appropriate abstraction functions, such as empty-env? and binding-in-frame.
> (binding-in-env 'c env3) (mcons 'c 'blue)
Finally, use binding-in-env to write the procedure lookup-variable to find the value of a variable in a given environment. If no binding is found, an error message should be generated. For example:
> (lookup-variable 'c env3) blue > (lookup-variable 'g env3) lookup-variable::unbound variable g