A Scheme Interpreter
Part Three: A Turing-Complete Evaluator
In this final part, you will complete your Scheme interpreter
based on the starter code and your solutions to Part One and
Part Two. Make all edits to SchemeEvaluator.java
.
Evaluation is always done in the contex of
some environment. The file Primitives.java
provides values for the global environment, such as +
,
display
, cons
, and =
. New
environments are created when a lambda
is applied
(including the implicit applications, such as when using the
let
special form).
Evaluation Rules
Your evaluator must support the following forms and rules:
-
A symbol expression (that is, an expression that is just a symbol) is evaluated by looking up that name in the environment in which the expression is being evaluated. For example, the expression
x
would evaluate to whateverenvironment.lookupVariable("x")
returns. -
A self-evaluating expression-- like a boolean, character, string, or number-- already represents a value and can just be directly returned.
-
A list expression that doesn't start with the name of a special form is a "combination" (recall 4.1.1 of SICP). This is the application of a function: First, evaluate the operator (the expression that will be the function applied) and evaluate the operands (the arguments passed to the function by that call). Next, for a non-primitive procedure, create a new environment that extends the environment in which the
lambda
was originally created-- matching the names of the parameters with the right corresponding arguments. Then, in that new environment, evaluate the body of thelambda
. -
A list expression that starts with
quote
is the quote special form, which evaluates to the (uninterpreted) datum after it. -
The
set!
special form evaluates the value to be assigned and updates the environment so that the assigned variable gets that new value.The following example shows you how to express that process in code:
static Value evaluateAssignment(LinkedList<Value> body, Environment env) { matchSymbol(body, "set!"); SymbolDatum variable = next(body, LexemeDatum.SymbolDatum.class); Value value = evaluate(next(body, Datum.class), env); noExtras(body, "set!"); env.setVariable(variable.toString(), value); return RuntimeValue.newUnspecified(); }
Where
next
andmatchSymbol
are helper methods (not provided) that remove and check values from thebody
queue, and wherenoExtras
checks that thebody
queue is then empty. -
The
define
special form defines a variable in the environment (usingenv.defineVariable(.., ..)
, in constrast to theenv.setVariable(.., ..)
used byset!
.)The syntactic sugar forms of
define
must be supported, where the following short-hands are translated into the equivalentlambda
s:(define (f) ...) => (define f (lambda () ... )) (define (f . all) ... ) => (define f (lambda all ...)) (define (f x) ... ) => (define f (lambda (x) ...)) (define (f x y . rest) ... ) => (define f (lambda (x y . rest) ...))
-
The
let
special form is sugar for creating new names bound to some given expressions:(let ([n e] [m f]) body) => ((lambda (n m) body) e f)
Where
n
andm
are names (identifiers), ande
andf
are arbitrary expressions (including even otherlet
expressions!). You do not need to implement the named-let
form for this project. -
The
if
special form conditionally executes its consequent expression or its (optional) alternative expression. For example:static Value evaluateIf(LinkedList<Value> body, Environment env) { matchSymbol(body, "if"); Value condition = evaluate(next(body, Datum.class), env); Datum consequent = next(body, Datum.class); Datum alternative = (!body.isEmpty()) ? next(body, Datum.class) : null; noExtras(body, "if"); Datum target = (condition.isTrue()) ? consequent : alternative; if (target == null) { return RuntimeValue.newUnspecified(); } return evaluate(target, env); }
-
Your evaluator must support the
cond
special form. -
Your evaluator must support the
begin
special form. -
Last, but not least, your evaluator must support the
lambda
special form, including the various ways to express variable-arity procedures. The starter code includes a commented exampleLambda
class that extendsRuntimeValue.Procedure
. Extracting the names of the formal parameters, and determining if alambda
accepts a variable number of arguments, and keeping a reference to the current environment are all duties yourlambda
must handle.