Lab 07

Interpreters 2 - Interpreting basic expressions

Due: Wednesday, April 9

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.ss" and "interpX.ss" respectively. Since these build on each other you only need to hand in parseD.ss and interpD.ss for this lab.

parseX.ss     Datatype, parser, and unparse definitions


eval-exp definition

env.ss Environment datatypes and apply-env function


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

<exp> ::= <number>                        lit (datum)

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.ss and interpA.ss.

In parseA.ss we have the expression datatype definition:

(define-datatype expression expression?
       (lit-exp (datum number?)))

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

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

Our unparser is just as simple:

(define unparse
       (lambda (tree)
              (cases expression tree
                     (lit-exp (datum) datum))))

Save this code as parseA.ss

We can run this:

>(parse '2)
#(struct:lit-exp 2)

or this:

> (unparse (parse 2))

As for the interpreter, you know that Scheme evaluates all integers as themselves. So our evaluation function will be very simple. It looks like this.

(define eval-exp
        (lambda (tree env)
               (cases expression tree
                      (lit-exp (datum) datum)
                      (else (error 'eval-exp  "Invalid abstract syntax: ~s" tree)))))

Save this as interpA.ss

All we need to do to interpret an expression is to pass to eval-exp a parsed MiniScheme expression. Try the following in Dr. Racket:

> (load "env.ss")
> (load "parseA.ss")
> (load "interpA.ss")
> (eval-exp (parse '2) init-env)

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.ss. Save this file to your directory and try the following:

> (load "REP.ss")
> (load "env.ss")
> (load "parseA.ss")
> (load "interpA.ss")
> (read-eval-print)
MS> 3
MS> 4
MS> howdy?
parse : Invalid concrete syntax howdy?
MS> 7
MS> exit
returning to Scheme proper

I find it is easiest to make a new file, minischeme.ss with the load statements and the call to (read-eval-print). Running this program puts you into the MiniScheme read-eval-print loop. To update from one version of MiniScheme to the next it is just necessary to update the load instructions.

Here is a tedious problem. You know you need the line

(require (lib "eopl.ss" "eopl"))

at the top if a file that uses the define-datatype structures. Unfotunately, when we load multiple files with this line into Dr. Racket an error results. One way to handle this is to have the line in your parser, interpreter and environment files for use when you are developing them, but to comment it out when you are ready to run the read-eval-print loop. You need the line just once at the top of minischeme.ss.

Section 2: Variables and Environments; MiniSchemeB

As you have undoubtedly noticed, 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.ss and interpB.ss and include the following changes:

First, we will use the following grammar for MiniSchemeB:

<exp> ::= <number>        lit (datum)
               | <varref>         varref (var)
where a variable is implemented as a symbol.

The parser is a simple modification of our parseA.ss parser. You should add a line to handle varref expressions.

You should also extend the unparser procedure to handle varref-exp tree nodes.

To evaluate a variable expression, MiniSchemeB needs to be able to look up references. We will use the environment you built in HW06. If you run this by itself you should be able to do the following:

> (define an-env
        (extended-env '(x y z) '(1 2 3) the-empty-env))
> (apply-env an-env 'x)

> (apply-env an-env 'y)
> (apply-env an-env 'z)
4> (apply-env an-env 'w)

apply-env: No binding for w

This is a good point to add primitive arithmetic operators to our environment. There is nothing to do for parsing -- '+, '* and so forth are symbols, so they will be parsed to varref-exp 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 to represent procedures in general. For now the only variant of this is for primitve procedures.

Here is the concrete and abstract syntax we will use in defining procedures.

<proc> ::= <prim-op>                          prim-proc (prim-op)
<prim-op> ::= <addition>                             +
                        | <subtraction>                       -
                        | <multiplication>                    *
                        | <division>                               /

We will represent this with the datatype

(define-datatype proc proc?
       (prim-proc (prim-op prim-op?))
       (closure (params (list-of symbol?))
                       (body expression?)
                       (env environment?)))

(define prim-op?
        (lambda (sym) (member? sym prim-op-names)))

(define prim-op-names '(+ - * /))

I find that the clearest place to put these defintions is in env.ss. We can now redefine our initial environment using the following code:

(define init-env
               (map prim-proc prim-op-names)
               (extended-env '(x y) '(1 2) the-empty-env)))

Add a line to your interpreter to evaluate variable references by looking them up in the environment. After this is all implemented in your parseB.ss and interpB.ss files, try the example shown below. Be sure to examine the code and understand how it works.

> (load "env.ss")
> (load "parseB.ss")
> (load "interpB.ss")
> (load "REP.ss")
> (read-eval-print)
MS> 3.4
MS> +
#(struct:prim-proc +)
MS> *
#(struct:prim-proc *)
MS> -
#(struct:prim-proc -)
MS> foobar
apply-env : No binding for foobar
MS> /
apply-env : No binding for /
MS> exit
returning to Scheme prope

Section 3: Calls to primitive functions; MiniSchemeC

It doesn't seem very useful to have + defined and not be able to apply it.. We will remedy this with our specification for MiniSchemeC:

<exp> ::= <number>                                lit (datum)
                | <varref>                                 varref (var)
                | (<exp> {<exp>}*)                   app (rator rands)

We now have a language that can do something! Change the names of the parser and interpreter files to parseC.ss and interpC.ss. In parseC.ss update the datatype to include an app-exp. 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. Add a case to your unparserto handle app-exp nodes.

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 args)
              [(eq? op '+) (+ (car args) (cadr args))]
              [(eq? op '-) (- (car args) (cadr args))]
              [(eq? op '*) (* (car args) (cadr args))]
              [(eq? op '/) (/ (car args) (cadr args))]))

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 args)
       (cases proc p
              (prim-proc (prim-op)
                     (apply-primitive-op prim-op args))
              (else (error 'apply-proc "Bad procedure: ~s" p)))))

We can now apply our primitive operators:

> (load "env.ss")
> (load "parseC.ss")
> (load "interpC.ss")
> (load "REP.ss")
> (read-eval-print)
MS> (* 2 3)
MS> (+ 2 4)
MS> (* 2 (+ 1 2))
MS> (- 10 foo)
apply-env: No binding for foo
MS> (* 2 (- 3 (+ 4 (* 7 (+ 2 3)))))
MS> exit
returning to Scheme proper


Next extend MiniSchemeC to support three new primitive procedures: add1sub1, and minus.

Mini-SchemeC should behave as follows:

> (read-eval-print)
MS> minus
#(struct:prim-proc minus)
MS> add1
#(struct:prim-proc add1)
MS> sub1
#(struct:prim-proc sub1)
MS> (add1 (* 2 3))
MS> (minus (+ 4 (sub1 3)))
MS> exit
returning to Scheme proper>

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. The parser will not need modification for this exercise.

MiniSchemeC should now behave as follows:

> (load "env.ss")
> (load "parseC.ss")
> (load "interpC.ss")
> (load "REP.ss")
> (read-eval-print)
MS> (build 1 nil)
MS> list
#(struct:prim-proc list)
MS> (list 1 2 3)
(1 2 3)
MS> (first (list 1 2 3))
MS> (rest (list 1 2 3))
(2 3)
MS> (minus (first (build 3 (build 4 nil))))

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 but not quite in the standard Scheme fashion. 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>                               lit (datum)
                  | <varref>                                 varref (var)
                  | (if <exp> <exp> <exp>)       if (test-exp then-exp else-exp)
                  | (<exp> {<exp>}*)                  app (rator rands)

You need to update the expression datatype, the parser, and the unparsef procedure in parseD.ss, and the eval-exp procedure in interpD.ss. 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.

MiniSchemeD should behave as follows:

> (load "env.ss")
> (load "parseD.ss")
> (load "interpD.ss")
> (load "REP.ss" )
> (read-eval-print)
MS> True
MS> False
MS> (if True (+ 1 2) 0)
MS> (if (+ 0 (* 0 (- 2 3))) True (list 1 2 3))
(1 2 3)
MS> (if False 1 2)
MS> (if #f 1 2)
parse: Invalid concrete syntax #f
MS> exit
returning to Scheme proper >
Put your solutions into parseD.ss and interpD.ss.


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

> (load "env.ss")
> (load "parseD.ss")
> (load "interpD.ss")
> (load "REP.ss" )
> (read-eval-print)
MS> (if (equals? 1 2) True (+ 1 2))
MS> (if (equals? 0 (- 2 (+ 1 1))) (list 1 2 3) 5)
(1 2 3)
MS> (gt? 3 2)
MS> (gt? 2 3)
MS> (lt? 1 1)
MS> (equals? (rest (list 1)) nil)
MS> exit
returning to Scheme proper

> (parse '(if (lt? 2 3) 1 (+ 4 5))))
       (struct:app-exp (struct:varref-exp lt?) ((struct:lit-exp 2) (struct:lit-exp 3)))
       (struct:lit-exp 1)
       (struct:app-exp (struct:varref-exp +) ((struct:lit-exp 4) (struct:lit-exp 5))))