Get the startup code here. Unzip the file and drag the entire cs360 folder into the "src" directory of a new NetBeans project.
For this project, you will write a Racket interpreter in Java. Your interpreter will support a larger subset of Racket than our in-class version (Mini-Racket), but you will not implement every feature of the entire language. We will call this language JRacket.
This project is structured similarly to our in-class Mini-Racket interpreter, but
since we're in Java, we're using an OOP style. In other words, instead of having
a single eval()
function, we have a RacketExpression
class with an eval()
method that
other expression classes will override.
The types of expressions you must support are:
RacketInteger
class.
RacketBoolean
class.
RacketSymbol
class.
RacketList
class. Note that lists in JRacket serve
two purposes (just like in regular Racket): they are used as a data structure, e.g.,
'(1 2 3)
, but also to represent statements in the language, such as '(define x 3)
.
RacketFunction
class. There are two subtypes:
RacketPrimitiveFunction
(a function defined in terms of Java code, similar to
add/sub/mul in Mini-Racket, in that we had to hard-code them in the interpreter),
and RacketClosure
(a function defined in terms of a lambda expression).
main()
method in the Main
class. It should run
"out of the box," though it's not particularly powerful at the start.
You can type end at the prompt to quit the interpreter. You should
be able to interact with the interpreter by typing in integers, booleans, or
quoted expressions (these are already implemented for you):
>>> 4 ==> 4 >>> #t ==> #t >>> '(1 2 3) Evaluating: (quote (1 2 3)) ==> (1 2 3) >>> 'x Evaluating: (quote x) ==> xThe basic interpreter is flexible enough that if you make a parenthetical or syntactical mistake, it will detect it and not kick you out of the interpreter:
>>> '(3 4)) cs360.ParsingException: Too many closing parens in '(3 4)). Already parsed [', [3, 4]] >>> #tf cs360.ParsingException: Cannot parse boolean value: #tf
eval()
and one
apply()
, we have a RacketExpression
class that has an abstract method called eval()
that
subclasses will override. Furthermore, the RacketFunction
class has an apply()
method
that its subclasses will override.
The major difference between this interpreter and Mini-Racket's interpreter is that
we have multiple eval()
methods, since each subtype of RacketExpression
is its own class. The first thing
you should do is take a look at the eval()
methods in RacketInteger
and RacketBoolean
and notice how they work (they're very simple). For instance, RacketInteger
's eval()
method reflects how in Mini-Racket we tested if an expression was a number and if so,
we just returned the expression itself (because numbers, when evaluated, return
themselves).
I suggest you add things in a slightly different order than we did in class:
Frame
class,
which stores a single frame of an environment, and has a pointer to a parent Frame
.
(There is no Environment class; a Frame
suffices to represent an entire environment
because it has a pointer to its parent Frame
). In Frame
, implement the lookupVariableValue()
and defineVariable()
methods. The comments in the code should tell you what to do.
You can ignore setVariable()
.
Now, go to RacketSymbol and edit the eval() method to call lookupVariableValue() just like Mini-Racket does when it sees a symbol.
Next, you now need to edit RacketList's eval() method to support expressions of the type (define x 3). Notice how RacketList's eval() dispatches to evalDefine(), evalLambda(), etc, just like Mini-Racket. Take a look at evalQuote() first. It's already written for you, but it will guide you in writing the other evalXYZ() methods.
Write evalDefine(). This method should call defineVariable().
You now should be able to do the following:
>>> (define x 3) Evaluating: (define x 3) ==> done >>> x Evaluating: x ==> 3 >>> (define blah '(1 2 3)) Evaluating: (define blah (quote (1 2 3))) Evaluating: (quote (1 2 3)) ==> done >>> blah Evaluating: blah ==> (1 2 3) >>> (define y blah) Evaluating: (define y blah) Evaluating: blah ==> done >>> y Evaluating: y ==> (1 2 3)
Once you write this function, you will be able to call the primitive (built-in, non-user-defined) functions in JRacket, which are defined in RacketPrimitiveFunction.java. Note that RacketPrimitiveFunction and RacketClosure both have an apply() method, but the one for PrimitiveFunction is already written for you.
Hint: The code already written for you tests whether or not funcObj is a RacketFunction. If it is, you can cast funcObj to a RacketFunction to gain access to its apply() method.
You now should be able to do the following:
>>> (+ 3 3) Evaluating: (+ 3 3) Evaluating: + ==> 6 >>> (define z 42) Evaluating: (define z 42) ==> done >>> (define q 5) Evaluating: (define q 5) ==> done >>> (* (- 2 q) z) Evaluating: (* (- 2 q) z) Evaluating: * Evaluating: (- 2 q) Evaluating: - Evaluating: q Evaluating: z ==> -126 >>> (cons 1 '()) Evaluating: (cons 1 (quote ())) Evaluating: cons Evaluating: (quote ()) ==> (1) >>> (define L (cons 1 '(2 3))) Evaluating: (define L (cons 1 (quote (2 3)))) Evaluating: (cons 1 (quote (2 3))) Evaluating: cons Evaluating: (quote (2 3)) ==> done >>> L Evaluating: L ==> (1 2 3) >>> l Evaluating: l cs360.InterpreterException: Cannot find variable l >>> (= 3 4) Evaluating: (= 3 4) Evaluating: = ==> #f >>> (cons (car L) L) Evaluating: (cons (car L) L) Evaluating: cons Evaluating: (car L) Evaluating: car Evaluating: L Evaluating: L ==> (1 1 2 3)Important: Unlike in Mini-Racket, JRacket does not need separate tests for each primitive function (e.g., add?, subtract?, multiply?, etc). Our interpreter knows whether or not a function is a primitive because every object knows what class it belongs to. Therefore, as long as inside evalCall() you call apply() on the appropriate object, it will get dispatched correctly.
Once completed, you now should be able to do this:
>>> (if (= 3 4) 1 2) Evaluating: (if (= 3 4) 1 2) Evaluating: (= 3 4) Evaluating: = ==> 2 >>> (if (equal? '(1 2) '(1 2)) (cons 'a '(b)) 'kablooie) Evaluating: (if (equal? (quote (1 2)) (quote (1 2))) (cons (quote a) (quote (b))) (quote kablooie)) Evaluating: (equal? (quote (1 2)) (quote (1 2))) Evaluating: equal? Evaluating: (quote (1 2)) Evaluating: (quote (1 2)) Evaluating: (cons (quote a) (quote (b))) Evaluating: cons Evaluating: (quote a) Evaluating: (quote (b)) ==> (a b)
Hint: Creating a List of RacketSymbols filled with the argument names is a little tricky, because you know that in a lambda expression, such as (lambda (x y) (+ x y)), the 2nd component of that expression is a sub-list of argument names [e.g., (x y)], but Java does not know that. Java just knows each component is itself a RacketExpression. What I suggest doing is to cast the argument list (a RacketExpression) to a RacketList, then you can iterate through it, get each individual variable name, and add them to a newly-created List of RacketSymbols.
Then, finally, create a new closure of the argument names, the body of the lambda, and the environment. Return this new closure. (You may assume the body of the lambda has only one expression.)
>>> (lambda (x y) (+ x y)) Evaluating: (lambda (x y) (+ x y)) ==> #[function:anonymous] >>> (lambda () 'p) Evaluating: (lambda () (quote p)) ==> #[function:anonymous] >>> (lambda (lst) (cons 1 lst)) Evaluating: (lambda (lst) (cons 1 lst)) ==> #[function:anonymous]
Pseudocode: Make a new frame whose parent is this closure's environment (remember, apply() is a method inside a closure object!). Then, use defineVariable() on the new frame to bind all the arguments to their proper values. Then, call eval() on the body of the closure and return whatever it returns.
You're done! You can now write anything:
>>> (define add1 (lambda (x) (+ x 1))) Evaluating: (define add1 (lambda (x) (+ x 1))) Evaluating: (lambda (x) (+ x 1)) ==> done >>> (add1 5) Evaluating: (add1 5) Evaluating: add1 Applying: [Func:add1 [x] ... Evaluating: (+ x 1) Evaluating: + Evaluating: x ==> 6 >>> (define make-adder (lambda (x) (lambda (y) (+ x y)))) Evaluating: (define make-adder (lambda (x) (lambda (y) (+ x y)))) Evaluating: (lambda (x) (lambda (y) (+ x y))) ==> done >>> (define add2 (make-adder 2)) Evaluating: (define add2 (make-adder 2)) Evaluating: (make-adder 2) Evaluating: make-adder Applying: [Func:make-adder [x] ... Evaluating: (lambda (y) (+ x y)) ==> done >>> (add2 19) Evaluating: (add2 19) Evaluating: add2 Applying: [Func:add2 [y] ... Evaluating: (+ x y) Evaluating: + Evaluating: x Evaluating: y ==> 21 >>> (define fact (lambda (n) (if (= n 0) 1 (* n (fact (- n 1)))))) Evaluating: (define fact (lambda (n) (if (= n 0) 1 (* n (fact (- n 1)))))) Evaluating: (lambda (n) (if (= n 0) 1 (* n (fact (- n 1))))) ==> done >>> (fact 3) Evaluating: (fact 3) Evaluating: fact Applying: [Func:fact [n] ... Evaluating: (if (= n 0) 1 (* n (fact (- n 1)))) Evaluating: (= n 0) Evaluating: = Evaluating: n Evaluating: (* n (fact (- n 1))) Evaluating: * Evaluating: n Evaluating: (fact (- n 1)) Evaluating: fact Evaluating: (- n 1) Evaluating: - Evaluating: n Applying: [Func:fact [n] ... Evaluating: (if (= n 0) 1 (* n (fact (- n 1)))) Evaluating: (= n 0) Evaluating: = Evaluating: n Evaluating: (* n (fact (- n 1))) Evaluating: * Evaluating: n Evaluating: (fact (- n 1)) Evaluating: fact Evaluating: (- n 1) Evaluating: - Evaluating: n Applying: [Func:fact [n] ... Evaluating: (if (= n 0) 1 (* n (fact (- n 1)))) Evaluating: (= n 0) Evaluating: = Evaluating: n Evaluating: (* n (fact (- n 1))) Evaluating: * Evaluating: n Evaluating: (fact (- n 1)) Evaluating: fact Evaluating: (- n 1) Evaluating: - Evaluating: n Applying: [Func:fact [n] ... Evaluating: (if (= n 0) 1 (* n (fact (- n 1)))) Evaluating: (= n 0) Evaluating: = Evaluating: n ==> 6