CSCI 275

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

                         | (EXP  EXP*)                              parse into app-exp

     

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: 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, firstrest, and empty?. (for list, cons, car, cdr, and null?). The initial environment should also include a new variable, nil bound to the empty list. When all of this is complete save your files as parseC.rkt and interpC.rkt.

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:

  1. A list of the symbols that are bound in the binding list
  2. A list of the parsed 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 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.