CS275

Lab 07

Interpreters 2 - Interpreting basic expressions

Due: Wednesday, November 2

In this assignment you will build an interpreter for serveral, increasingly powerful versions of Scheme..

 

Section 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. Since these build on each other you only need to hand in the last pair of files and your env.rkt for this lab.

parseX.rkt   Datatypes and parser definitions

interpX.rkt

eval-exp definition

env.rkt Environment datatypes and lookup function

 

Our first MiniScheme, MiniSchemeA, will be specified by the following concrete and abstract syntax:

EXP ::= number                        parse into lit-exp

The only expressions in MiniSchemeA are numbers. Admittedly, this is not very exciting. Our interpreter for Mini-Scheme-A will be very basic as well. It is contained in 2 files: parseA.rkt and interpA.rkt.

In parseA we need a datatype to hold EXP nodes that represesent 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 (LitValue). You can use any names you want; the required names are parse (for the parser), eval-exp (for the interpreter) and init-env (for the initial environment).

The parser simply creates a lit-exp when it sees a number (and throws an error otherwise). It looks like this

(define parse
        (lambda (exp)
               (cond ((number? exp) (new-lit-exp exp))
               (else (error 'parse "Invalid syntax ~s" exp)))))

 

Save this code 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.

(define eval-exp
        (lambda (tree env)
               (cond
                      [(lit-exp? tree) (LitValue tree)]
                      (else (error 'eval-exp  "Invalid tree: ~s" tree)))))

Save this as interpA.rkt

All we need to do to interpret an expression is to pass to eval-exp a parsed MiniScheme expression. Build a new Racket file MiniSchme.rkt:

#lang racket
(require "env.rkt")
(require "parseA.rkt")
(require "interpA.rkt")

(define T (parse '23))

(eval-exp T init-ent)


If you run this it 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 try the following:

#lang racket
(require "env.rkt")
(require "parseA.rkt")
(require "interpA.rkt")
(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. Remember to update the parse and interp modules (from parseA.rkt to parseB.rkt to parseC.rkt and so on as you progress through the project.

Section 2: Variables and Environments; MiniSchemeB

MiniSchemeA is somewhat lacking in utility. Our specification for MiniSchemeB will be only slightly more interesting. Change the names on your parser and interpreter files to parseB.rktand interpB.rktand include the following changes:

First, we will use the following grammar for MiniSchemeB:

                                                                 EXP ::= number              parse into lit-exp
                                 | symbol            parse into var-ref

The parser is a simple modification of our parseA.rktparser. Of course, you need a var-ref datatype including a constructor (I call it new-var-ref), recognizer (var-ref?) and getter (Symbol )


To evaluate a variable expression, MiniSchemeB needs to be able to look up references. We evaluate a var-ref node in an envionment by calling lookup on the Symbol from the node in the environment. Since we asked you to include bindings for symbols x and y in the initial environment, you should lbe 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.

Section 3: Calls to primitive functions; MiniScheme

Change the names of the parser and interpreter files to parseC.rkt and interpC.rkt. 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 several ways to do this; the way we will use will be easy to expand to non-primitive 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.

Next, we make a lists of the primitive arithmetic operators:

        (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) '(23 45) the-empty-env)))

It doesn't seem very useful to have + defined and not be able to apply it. We will now extend the grammar to include applications:

                 EXP ::= number              parse into lit-exp
                                 | symbol            parse into var-ref
                                 | ( EXP EXP*)    parse into app-exp

We now have a language that can do something In parseC.rkt implement a an app-exp datatype that can hold a procedure (which is itself a tree) and a list of argument expressions (again, these are trees). Update the parser to build an app-exp node when the expression being parsed is a pair. Remember to parse the operator and the list of operands.

To evaluate applications, we need to define a function that applies a primitive operator symbol to a list of arguments and obtains the result:

(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.

Note that in our implementation, (+ 2 3 4) equals 5, not 9. We may change this later.

Here is the first installment of a general routine that applies functions which we will update as we go along. Eventually, we will update this routine to include user-defined functions. For now, only primitive operations will be implemented.

(define apply-proc (lambda (p arg-values)
       (cond
                 [ (prim-proc? p) (apply-primitive-op (Operator p) arg-values)]    
; Operator is my name for the prim-proc getter
                 (else (error 'apply-proc "Bad procedure: ~s" p)))))

Finally, extend eval-exp to evaluate an app-exp node by calling apply-proc with the evaluated operator and the list of evaluated arguments. We can now apply our primitive operators, in expressions such as (+ 2 4) or (+ x y). Now we are getting somewhere!

Next extend MiniSchemeC to support three new primitive procedures: add1sub1, 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 and rest. The initial environment should also include a new variable, nil bound to the empty list.

Section 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.

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 enw datatype and update the parser in parseD.rkt, and upodate 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 above the more general test (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 oif expresson.

Finally, extend MiniSchemeD to implement the primitives equals?lt?, and gt?.

equals? should behave just like Scheme's eqv? while lt? and gt? are simply < and >.

Section 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 ::= ( synbol 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:

  1. A list of the symbols that are bound in the binding list
  2. A list of the expressions (i.e, trees) that the symbols are bound to
  3. The let body.

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 envirionment 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))))