22c:054 Team Projects
Scheme Interpreter (Prolog, Oz)
Write an interpreter in any of the languages seen in class
for a Scheme-like language.
Refer to Chapter 10.3 of the textbook for a discussion of Scheme.
Implement the interpreter as the function TopEval
which,
takes a well-formed Scheme expression
and
returns the value of the expression,
or a undefined value error
if some identifier in the expression in unbound.
TopEval
is defined in terms of the function Eval which,
takes as input a Scheme expression
and
environment.
Environments
An environment is essentially a lookup table that
contains value bindings for a certain number of variables.
It can be conceptually described as an abstract datatype envir
with the following signature and specification
(given here in Miranda syntax).
abstype |
| envir |
with |
| EmptyEnv:: envir |
|
| LookUp:: (schemeVar, envir) -> schemeVal |
|
| AddBinding:: (schemeVar, schemeVal, envir) -> envir |
where
-
EmptyEnv is the empty environment.
-
LookUp takes
a variable v and an environment env
and
returns the value that v has in env.
If v is not present in env
it returns an "unbound variable" error.
-
AddBinding takes
a variable v,
an expression e,
and an environment env
and
adds v to env with value e.
Closures
The evaluation of a lambda expression
(the equivalent of anonymous functions in SML)
produces an internal data structure called closure.
In essence,
a closure encapsulates the current environment into a lambda expression in
order to preserve the values of non-local variables in
the body of the lambda expression.
(It is really closures that are passed around in functional programming,
not plain anonymous functions.)
Closures can be conceptually described as an abstract datatype of the form:
abstype |
| closure |
with |
| NewClos:: (schemeVar, schemeExp, envir) -> closure |
|
| ClosVar:: closure -> schemeVar |
|
| ClosExp:: closure -> schemeExp |
|
| ClosEnv:: closure -> envir |
where
-
NewClos
takes a Scheme variable (the argument of some lambda expression),
a Scheme expression (the body of the same lambda expression),
and an environment (the current one)
and
encapsulates them into a closure.
-
ClosVar, ClosExp, and ClosEnv
take a closure and respectively return the variable, expression, and environment encapsulated in it.
Scheme's Operational Semantics
The operational semantics of Scheme is defined by the following
assertions describing the behavior of the Scheme interpreter.
(The interpreter's funtions are in boldface to improve readability.)
TopEval
-
for all expressions e,
TopEval( e ) = Eval( e, EmptyEnv )
Eval
-
For all predefined constant or function symbols c and
environments env,
Eval( c, env ) = c .
Every predefined constant or function symbol evaluates to itself.
-
For all variables v and environments env,
Eval( v, env ) = Lookup( v, env )
Every variable evaluates to the value it is bound to in the current environment.
-
For all expressions e and environments env,
Eval( (quote e), env ) = e
Every "quoted" expression evaluates to itself.
-
For all expressions e1 distinct from quote,
expressions e2
and environments env,
Eval( (e1 e2), env ) =
Apply( Eval( e1, env ), Eval( e2, env ) ).
The evaluation of the application of an expression to another is provided
by the function Apply,
once both expressions have been fully evaluated in the current environment.
(Like SML, evaluation in Scheme is eager.)
-
For all variables v, expressions e,
and environments env,
Eval( (lambda (v) e), env ) =
NewClos(v, e, env)
Every lambda expression evaluates to a closure
encapsulating the expression and the current environment.
-
For all expressions e, e1, e2 and environments env,
Eval( (if e e1 e2), env )
|
= |
Eval( e2, env ),
if
Eval( e, env ) = nil
|
|
|
= |
Eval( e1, env ),
otherwise
|
The conditional expression is evaluatuated lazily.
If the test expression evaluates to anything other then nil,
its value is the value of the "then" expression;
otherwise, it is the value of the "else" expression.
-
For all expressions e1, e2,..., e_n with n>2
and environments env,
Eval( (e1 e2 e3 ... e_n), env ) =
Eval( ((e1 e2) e3 ... e_n), env )
The notation (e1 e2 e3) is just syntactic sugar for
((e1 e2) e3) (and so on).
-
For all variables v1, v2, ..., v_n,
expressions e, e1, e2, ..., e_n,
and environments env,
Eval( (let ((v1 e1) (v2 e2) ... (v_n e_n))   e), env ) =
Eval( (let ((v1 e1)) (let ((v2 e2) ... (v_n e_n))   e)), env )
A let expression with more than one local variable is really
a series of nested let espressions with just one local variable.
-
For all variables v1, expressions e, e1,
and environments env,
Eval( (let ((v1 e1)) e), env ) =
Eval( ((lambda (v1) e) e1), env)
let expressions themselves are not primitive.
They can be given in terms of lambda expressions.
Apply
-
For all predefined function symbols c and
irreducible expressions i,
Apply( c, i ) = ApplyPredef( c, i )
The result of applying a predefined functions symbol to a completely evaluated expression is provided by ApplyPredef
(which implements the predefined functions directly in the interpreter's code).
-
For all closures clos
and
irreducible expressions i,
Apply( clos, i ) =
Eval( e , AddBinding( v , i, env ) )
where
e = ClosExp( clos ),
v = ClosVar( clos ),
and
env = CloseEnv( clos ).
The value of applying a closure with variable v and expression e to a completely evaluated expression i
is given by evaluating e in an environment
that extends the closure's own
with the binding of v to i.
Implementation Details (Oz)
For simplicity:
- implement only the integer and list datatypes
providing a basic set of predefined functions for them;
- use Oz's list notation in place of Scheme's list notation
(e.g.
[1 2 3],
[lambda [x] [plus x 3]],
[if [greater x y] x y]
instead of
(1 2 3),
(lambda (x) (plus x 3)),
(if (greater x y) x y)
,
respectively);
in particular, always use nil for () ;
- use the names plus, mul, etc. for
the arithmetic operators +, *, etc.
- do not support the abbreviation of the form '(1 2 3)
for the expression (quote (1 2 3));
- do not implement the forms
define, letrec, set!, read, display, cond.
- do not implement lambda expressions with more than one argument.
- add your implementation the file scheme.oz
which provides a simple GUI for entering Scheme expressions into
the interpreter and printing their final value.
Hints:
Be careful in implementing AddBinding.
Expressions with occurrences of different local variables with the same
name
such as
((lambda (x) (plus x ((lambda (x) (times x x)) 4))) 3)
or
(plus ((lambda (x) (plus x 1)) 2) ((lambda (x) (plus x 1)) 4))
are legal in Scheme and behave as you would expect
because variables are lexically scoped.
Your implementation too must provide lexical scoping for variables.
(See Chapter 5.2 of the textbook for more details on lexical scoping).
Implementation Details (Prolog)
As for Oz, with the following differences:
-
use Prolog's list notation in place of Scheme's list notation
(e.g.
[1, 2, 3],
[lambda,[x],[plus, x, 3]],
[if, [greater,x,y],x,y]
instead of
(1 2 3),
(lambda (x) (plus x 3)),
(if (greater x y) x y)
,
respectively);
in particular, always use [] for ();
- with the syntactic conventions above,
Scheme programs are legal Prolog terms that
you can feed directly into Prolog predicates.
This will save you the effort of writing a parser for Scheme.
Implement the interpreter then as the binary Prolog predicate
TopEval(E,V) which
succeds iff V is the value of the Scheme expression E.
Last Updated: Mar 6, 2000