Lab 06
Interpreters 2 – Interpreting Elementary Expressions
Due Friday April 10 at 11:59 PM
In this
lab you will build an interpreter for several increasingly powerful versions of
Scheme., starting with a “language” that just recognizes numbers, and moving
through variables, arithmetic and primitive procedures, conditional
expressions, and finally let-expressions.
For each version of the language we will create a grammar, a parser, and
an evaluator. Because we implement the
languages one small step at a time, you should never find yourself overwhelmed
with details. Check your work carefully
as you go. Writing all of the code and
then debugging is a receipe for disaster.
You
might want to download this file at the start: REP.rkt
Part 1: MiniSchemeA
We
will start with a very basic language called MiniSchemeA, and gradually add
language features. As we do so, we will update our parser and interpreter to implement
the new language features.
As a
convention, the parser and interpreter for MiniSchemeX will reside in
"parseX.rkt" and "interpX.rkt" respectively. We will use names "parse.rkt", and "interp.rkt" as the names of our working file. Each time we complete step X of the language we will save the working files as "parseX.rkt" and "interpX.rkt". This way you always have working code for the previous step of the interpreter to go back to if you get in trouble. You should hand in your last working version of env.rkt, parse.rkt and interp.rkt. Here is
what you should put in each file:
parseX.rkt |
|
Tree Datatypes
and parser definitions
|
interpX.rkt |
eval-exp definition |
|
env.rkt |
Environment
datatypes and lookup function |
As
you go, think about where to put new definitions. Racket has trouble with
circular requirements (such as parse.rkt requiring interp.rkt and interp.rkt
requiring parse.rkt). You probably want your parse and env files to not
require any other module, and your interp files to require both parseX.rkt and
env.rkt
Our
first MiniScheme, MiniSchemeA, will be specified by the following grammar::
EXP ::=
number
parse into lit-exp
Our
parse procedure will take an input expression and return a parse tree for an
EXP. The only expressions in MiniSchemeA are numbers. Admittedly, this is not
very exciting but it is a place to start. Our interpreter for Mini-Scheme-A will be very basic as well.
In
parseA we need a datatype to hold EXP nodes that represent numbers. An easy
solution is to make a list out of an atom (such as 'lit-exp) that identifies
the datatype and the numeric value being represented. There are other possible
representations and it doesn't matter which you choose as long as you have a
constructor (which I'll call new-lit-exp),
recognizer (lit-exp?) and getter
(lit-exp-num). You can use any
names you want; the only required names are parse (for the
parser), eval-exp (for the interpreter) and init-env
(for the initial environment).
The
parse function simply creates a lit-exp when it sees a number (and throws
an error otherwise). It looks like this
(define parse (lambda (input)
(cond
[(number? input) (new-lit-exp
input)]
[else
(error 'parse "Invalid syntax ~s" input)))))
Save this code and the code that
implements the lit-exp datatype, in
parseA.rkt
As for the interpreter, you know that Scheme evaluates all integers as
themselves. Our evaluation function will be very simple. It looks like this.
(require "env.rkt")
(require "parse.rkt"
(define eval-exp
(lambda (tree env)
(cond
[(lit-exp? tree) (lit-exp-num tree)]
(else (error 'eval-exp "Invalid tree: ~s" tree)))))
Save
this code as interp.rkt
We
can interpret expressions in the command interpreter of the interp.rkt file.
Run this file to load the interpreter and parser into memory, then type into
the command interpreter:
(define T (parse '23))
(eval-exp T init-ent)
This should print the value 23
It
quickly becomes tedious to always invoke your interpreter by specifically
calling the interpreter eval-exp after calling the parser on the
quoted expression. It would be nice if we could write a read-eval-print loop
for MiniScheme. This is very easily accomplished with the code found in
file REP.rkt. Save this file to your directory and edit
it to require your env.rkt, parse.rkt, and interp.rkt. Build a new fle
minischeme.rkt with the following:
#lang racket
(require "REP.rkt")
(read-eval-print)
Running
this program will give you an input box that allows you to type expressions and
get back their value as determined by your parse and interp modules. . For
example, if you enter the minischeme expression 23 this evaluates it and prints
its value, 23.
The
read-eval-print procedure assumes that your parse procedure is named parse and that your
evaluator is called eval-exp and takes as arguments a parse tree and an
environment, in that order.
Save your parse.rkt file as parseA.rkt and your interpreter as interpA.rkt, as these make up the interpreter for MiniSchemeA. Go back to the original files parse.rkt and interp.rkt to proceed on to the next step.
Part 2: Variables and Definitions;
MiniSchemeB
MiniSchemeA is somewhat lacking in utility. Our specification for MiniSchemeB will be only slightly more interesting.
We
will start with the following grammar for MiniSchemeB:
EXP
::= number parse into lit-exp
|
symbol parse into var-ref
The parser is a simple modification of our parse.rkt parser. You need to add a line to (parse input) to handle the case where (symbol? input) is #t. Of course, you need a var-ref datatype including a constructor (I call it new-var-ref), recognizer (var-ref?) and getter (var-ref-symbol ).
To
evaluate a variable expression, MiniSchemeB needs to be able to look up
references. We evaluate a var-ref tree node in a given environment by calling lookup in that environment on
the var-ref-symbol. Since we asked you to include
bindings for symbols x and y in the initial environment, you should be able to
evaluate the minischeme expressions x or y to
get their values. Any other symbol at this point should give you an error
message.
Part 3: Calls to primitive
functions; MiniSchemeC
This is a good point to add primitive arithmetic operators to our environment.
Nothing needs to be done for parsing-- operators like +, - and
so forth are symbols, so they will be parsed to var-ref nodes. Our environment
needs to associate these symbols to values. There are many ways to do this; the
way we will use will be easy to expand to procedures derived from
lambda expressions. We will first make a datatype prim-proc to
represent primitive procedures. This is simple; the only data this type needs
to carry is the symbol for the operator, so this looks just like the var-ref
type. Make a constructor (new-prim-proc
…)) and a getter for the datatype: (prim-proc-symbol
p).
Next,
we make a list of the primitive arithmetic operators. You can start with the following and later expand it:
(define
primitive-operators '(+ - * /)
and add the operators to the environment with
(define
init-env (extended-env primitive-operators
(map new-prim-proc primitive-operators)
(extended-env '(x y) '(10 23) (new-empty-env))))
This means that when we evaluate + by looking it up in the environment we will get the structure (prim-proc +).
We
will now extend the grammar to include applications so we can use our primitive
operators:
EXP ::= number parse
into lit-exp
| symbol parse into var-ref
This gives us a language that can do something. You need to implement an app-exp datatype that can hold a procedure (which is itself a tree) and a list of argument expressions (again, these are trees). The constructor for that might be (new-app-exp proc args). Update the parser to build an app-exp node when the expression being parsed is not an atom. This looks like
(define
parse
(lambda (input)
(cond
[(number? input) .....]
[(symbol? input) ....]
[(not (pair? input)) (error ......)]
[else (new-app-exp (parse (car input ......)))])))
Remember to parse
both the operator and the list of operands.
In the interp.rkt file we extend eval-exp to evaluate an app-exp node by calling a new function apply-proc with the evaluated operator and the list of evaluated arguments. Here is apply-proc:
(define
apply-proc (lambda (p arg-values)
(cond
[ (prim-proc? p)
(apply-primitive-op (prim-proc-symbol p) arg-values)]
[else (error 'apply-proc
"Bad procedure: ~s" p)])))
Here is apply-primitive-op:
(define apply-primitive-op (lambda (op
arg-values)
(cond
[(eq? op '+) (+
(car arg-values) (cadr arg-values))]
[(eq? op '-) (-
(car arg-values) (cadr arg-values))]
etc.
Our language now
handles calls to primitive operators, such as (+ 2 4) or (+
x y). We are getting somewhere!
Next
extend MiniSchemeC to support three new primitive procedures that each take one
argument: add1, sub1, and minus. The first
two should be obvious; the minus procedure negates its argument: (minus
6) is -6, and (minus (minus 6)) is 6.
What
kind of Scheme doesn't have list processing functions? Extend MiniSchemeC to
implement list, build, first, rest, and empty?.
(for list, cons, car, cdr, and null?). The initial environment should also
include a new variable, nil bound to the empty list.
Our methodology should now be pretty clear. At each step we have a new line in the grammar to handle a new kind of Scheme expression. We update the parser,which requires making a new tree datatype to handle the new parsed expression. We then update the eval-exp procedure to evaluate the new tree node. For the remaining steps we will be more brief.
Part 4: Conditionals; MiniSchemeD
Let's
update our language to include conditional evaluation. We will adopt the
convention that zero and False represent false, and everything else represents
true. Note this. #t and #f are not
values in MiniScheme. You should assign
the value ‘True and ‘False to the atoms True and False. True expressions, such
as (equals? 2 (+ 1 1)) should evaluate to True, not to #t.
Write
MiniSchemeD, which implements if-expressions. You will need to add False and True to
the initial environment. The meaning of (if foo bar baz) is:
If foo evaluates to False or 0, the value is obtained by
evaluating baz otherwise the value is obtained by evaluating bar
The
new grammar for our language will be:
EXP
::= number parse into
lit-exp
| symbol parse
into var-ref
| (if EXP EXP EXP) parse
into if-exp
| (EXP EXP*) parse into app-exp
You need to make a new
datatype and update the parser in parseD.rkt, and update the eval-exp procedure
in interpD.rkt. For the parser, note that both if expressions and application
expressions are lists. We know a list represents an if-expression if its first
element is the atom 'if. Put the test for this below the general test
(not(pair? exp)) -- we will assume a pair represents an application expression if we
don't recognize its first element as a keyword denoting a different kind of
expression.
Finally,
extend MiniSchemeD to implement the primitives equals?, lt?,gt?, leq? and geq?
equals? should behave just like Scheme's eqv? while lt?, gt?, leq? and geq? are
the usual inequality operators <. >, <= and >=..
Part 5: MiniScheme E; Let
expressions
The
grammar for MiniSchemeE is:
EXP
::= number parse into lit-exp
| symbol parse
into var-ref
| (if EXP EXP EXP) parse
into if-exp
| (let (LET-BINDINGS) EXP) parse
into let-exp
| (EXP EXP*) parse into app-exp
LET-BINDINGS
::= LET-BINDING*
LET-BINDING
::= (symbol EXP)
As
you can see, we have added a new clause for the let expression. To make eval-exp clearer, I suggest that
you make a let-exp datatype that contains three children:
Thus,
although we have grammar symbols for LET-BINDING and LET-BINDINGS,
we choose to build the tree slightly differently.
After
the parser is extended to handle let expressions, we extend eval-exp to handle the let-exp nodes created by
the parser. This should be straightforward -- we evaluate a let-exp node in an environment by
extending the environment with the let symbols bound to the VALUES of the
let--bindings (map a curried version of eval-exp onto the binding expressions),
and then evaluate the let body within this extended environment.
When
you are finished you should be able to evaluate expressions such as
(let ( (a 1) (b 5) ) ) (+ a b))
and
(let ( (a 23) (b 24) ) (let ( (c 2) ) (*
c (+ a b))))
There
are a lot of details for this lab, so here is a quick checklist of everything
you need to implement:
a)
Your final version of MiniScheme should be able to handle
expressions that are just numbers, variables, if-expressions, let-expressions,
and calls to primitive procedures.
b)
Your language should recognize and be able to work with primitive
procedures
+, -, *, /, add1, sub1, minus, list, build, first, rest, empty, equals?, lt?,
and gt?.
c)
In addition to numbers your language should be able to work with
primitive values True, False, and nil.
When
you hand in this lab, you should submit the files for your latest working versions of env.rkt, parse.rkt and interp.rkt. You
should include the REP.rkt and
MiniScheme.rkt file, with everything set up to run the final version of your
code.