CS275

Lab 08

Interpreters 3


This is due on Friday, April 18, though I will assume you have completed Sections 1 and 2
(through MiniScheme F, with lets and lambdas) by the time of our exam next week.

In the first part of our developement of Mini-Scheme, we treated our environment as a static object. This is because we have not yet implemented any language features that allow for the creation of new variable bindings. We will address this issue now.

Section 1: MiniScheme E -- Let expressions

The grammar for MiniSchemeE is:

<exp> ::= <number>                          lit-exp (datum)
        | <varref>                                       varref-exp (var)
        | (if <exp> <exp> <exp>)            if-exp (test-exp then-exp else-exp)
       | (let (<decls>) <exp>)                   let-exp (ids exps body)
        | (<exp> {<exp>}*)                       app-exp (rator rands)

<decls> ::= <empty>
        | (<id> <exp>) <decls>

As you can see, we have added a new clause for the let expression. Notice that the let-exp node for our datatype separates the local declarations into a list of identifiers and a list of values (the rands).

When you have properly extended the parser, (parse '(let ([x 3] [y 2]) (+ x 2))) should parse to

#(struct:let-exp
        (x y)
        (#(struct:lit-exp 3) #(struct:lit-exp 2))
        #(struct:app-exp #(struct:varref-exp +) (#(struct:varref-exp x) #(struct:lit-exp 2))))

Adapt your parser and interpreter to handle let expressions in the classic Scheme fashion.

> (read-eval-print)
MS> (let ((x 3)) x)
3
MS> (let ((x 3) (y 4) (z (+ 2 3))) (+ x (+ y z)))
12
MS> (let ((x 3))
                (let ((x 5))
                       (+ x 2)))
7
MS> exit
returning to Scheme proper
>

Section 2: Lambda -- MiniSchemeF

No language would be complete without the ability to create new procedures. Our new language will implement the lambda expression. A procedure object should be a package containing the formal parameters, the body, and the environment that was current when the procedure was created (i.e. when the lambda expression was evaluated. This package is known as a closure.

An easy way to represent closures is as a variant of our proc datatype in env.ss. We add a variant tagged closure to our definition of proc:

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

Now we need to add a case to apply-proc in the interpreter to handle applications of closures. A closure has a list of parameters, a body and an environment. To apply the closure to a list of argument values we extend the environement with the parameters bound to the arguments and evaluate the body within this new environment.

We are ready for MiniSchemeF. The syntax is extended once more, this time to include lambda expressions. Here is the grammar for MiniSchemeF.

<exp> ::= <number> lit-exp (datum)
                 | <varref>                                             varref-exp (var)
                 | (if <exp> <exp> <exp>)                    if-exp (test-exp then-exp else-exp)
                 | (let (<decls>) <exp>)                        let-exp (ids rands body)
                 | (lambda ({<id>}*) <exp>)                 lambda-exp (formals body)
                 | (<exp> {<exp>}*)                              app-exp (rator rands)

<decls> ::= <empty>
                 | (<id> <exp>) <decls>

Parsing (lambda (x y) (+ x y)), for example, produces

        #(struct:lambda-exp
               (x y)
        #(struct:app-exp #(struct:varref-exp +) (#(struct:varref-exp x) #(struct:varref-exp y))))

So we parse a lambda expresion into a lambda-exp node. In eval-exp, we evaluate a lambda-exp node as a closure. We have updated apply-proc so we can apply closures to arguments. When you have all of this implemented you should be able to do the following:

> (read-eval-print)
MS> ((lambda (x) x) 1)
1
MS> ((lambda (x y) (* x y)) 2 4)
8
MS> (let ((sqr (lambda (x) (* x x)))) (sqr 64))
4096
MS> (let ((sqr (lambda (x) (* x x)))) (let ((cube (lambda (x) (* x (sqr x))))) (cube 3)))
27


The let expressions you implemented in Section 1 are a good example of what is known as "syntactic sugar". That is, they could have been implemented in terms of other language features, rather than as a new feature. Think about how we might have done this. This is a good lesson. Before implementing a new language feauture, one should always be sure that one hasn't already implemented it!

Section 3: Variables, Assignments and Sequencing -- MiniSchemeG

Our next feature will be variable assignment, with set!. Unfortunately, our implementation of environments does not provide a way to change the value bound to a variable. We will modify our old implementation so that variable names are bound to a mutable datatype called a box, which is provided by Dr. Racket.

Take a moment to familiarize yourself with boxes in Scheme:

> (define abox (box 17))
> (box? abox)
#t
> (unbox abox)
17
> (set-box! abox 32)
> (unbox abox)
32
> ...

When variables are created we will bind them to boxes. When they are referenced we will unbox their bindings. We will take these tasks sequentially.

First, eval-exp currently references variables with case (varref-exp(var) (apply-env env var)). Since the variable is now bound to a box, we unbox it:

(varref-exp (var) (unbox (apply-env env var))

Secondly, whenever the environment is extended, the new bindings will be boxes that contain values. This occurs in two places. One is when we evaluate a let-expression in eval-exp, the other is when we apply a closure in apply-proc. For the latter our code used to be

(closure (params body env)
        (eval-exp body (extended-env  params args env)))
                                        

It now bocomes

(closure (params body env)
        (eval-exp body (extended-env  params (map box args( env)))

The let-expressions are handled similarly.
  

At this point your interpreter should be running exactly as it did for MiniSchemeF -- let expressions, lambda expressions and applications should all work correctly. We will now take advantage of our boxed bindings to implement set!

MiniSchemeG will implement variable assignment in the form of set! expressions. Note that we will not be implementing set! as a primitive function, but as an expression -- in (set! x 5) we don't want to evaluate varible x to its previous value, as a call would, but rather to store value 5 in its box. . The grammar for MiniSchemeH will be:

<exp> ::= <number>                                      lit-exp (datum)
                 | <varref>                                          varref-exp (var)
                 | (if <exp> <exp> <exp>)                if-exp (test-exp then-exp else-exp)
                 | (let (<decls>) <exp>)                    let-exp (ids rands body)
                 | (lambda ({<id>}*) <exp>)             lambda-exp (formals body)
                 | (set! <id> <exp>)                          varassign-exp (id rhs-exp)
                 | (<exp> {<exp>}*)                          app-exp (rator rands)

<decls> ::= <empty>
                 | (<id> <exp>) <decls>

Parsing (set! x (+ 2 3)) produces

#(struct:varassign-exp
         x
        #(struct:app-exp #(struct:varref-exp +) (#(struct:lit-exp 2) #(struct:lit-exp 3))))

In order to implement set!, we need to add the following case to eval-exp:

(varassign (id value)
        (set-box! (apply-env env id)
                        (eval-exp value env)))

Here is what we can do when this is implemented:

> (read-eval-print)
MS> (set! + -)
#
MS> (+ 2 2)
0
MS> (set! + (lambda (x y) (- x (minus y))))
#
MS> (+ 2 2)
4
MS> (+ 2 5)
7
MS> exit
returning to Scheme proper
>

Now that we have introduced side effects, it seems a natural next step to implement sequencing. We add a begin expression to the grammar::

<exp> ::= <number>                                                       lit-exp (datum)
                 | <varref>                                                         varref-exp (var)
                 | (if <exp> <exp> <exp>)                               if-exp (test-exp then-exp else-exp)
                 | (let (<decls>) <exp>)                                    let-exp (ids rands body)
                 | (lambda ({<id>}*) <exp>)                             lambda-exp (formals body)
                 | (set! <id> <exp>)                                          varassign-exp (id rhs-exp)
                 | (<exp> {<exp>}*)                                           app-exp (rator rands)
                 | (begin {<exp>}*)                                            sequence-exp (exps)

<decls> ::= <empty>
                | (<id> <exp>) <decls>

Parsing (begin (set! x 3) (+ 3 4)) produces

#(struct:sequence-exp
        (#(struct:varassign-exp x #(struct:lit-exp 3))
         #(struct:app-exp #(struct:varref-exp +) (#(struct:lit-exp 3) #(struct:lit-exp 4)))))

Evaluating (begin e1 e2 ... en) results in the evaluation of e1, e2, .. en in that order. The returned result is the last expression, en.

When this is complete you can have the following interactions:

MS> (let ((x 2)) (begin (set! x (+ x 1)) x))
3
MS> (begin (set! + *) True)
True
MS> (+ 2 2)
4
MS> (+ 2 3)
6
MS> exit
returning to Scheme proper>


Section 4: Recursion -- MiniSchemeH

It looks like we're about done; we've implemented just about everything except for global definitions (i.e. something like define). But let's take a closer look.

What happens if we try to define a recursive procedure in MiniSchemeG? Let's try the ever-familiar factorial function:

> (read-eval-print)
MS> (let
                ([fac (lambda (n)
                               (if (equals? n 0)
                                   1
                                    [ (* n (fac (- n 1)))))])
                (fac 4))
MS>

This gets an error message saying there is no binding for fac. But we bound fac using let. Why is MiniScheme reporting that fac is unbound? The problem is in the recursive call to fac in (* n (fac (sub1 n))). When we evaluated the lambda expression to create the closure, we did so in an environment in which fac was not bound. Because procedures use static environments when they are executed, the recursive call failed.

Try this

MS> (let ([fac (lambda (x) (+ x 150))])
                (let ([fac
                       (lambda (n)
                              (if (equals? n 0)
                                  1
                                   (* n (fac (- n 1)))))])
                    (fac 4)))

This time the program returns 612, which is 153*4; it is the first binding of function fac that is seen in the call to (fac (- n 1)

Recall what happens when a function is created. A closure is created that contains the environment at the time the function was created, along with the body of the function and the formal parameters. MiniScheme had no problems with this, and shouldn't have.

When a function is called, the formal parameters are bound to the actual parameters, and then all of the remaining free variables in the body are looked up in the environment that was present at the time of the creation of the function. This is where MiniScheme ran into problems. In the first example the fac in the line

(* n (fac (- n 1))))))

was not bound to anything at the time the function was created, and so we got an error. In the second example fac was bound to the earlier procedure produced by evaluating (lambda (x) (+ x 150)), which fortunately wasn't recursive. But neither case is what we want.

There is a clever way to get around this problem. Try running the following code:

MS> (let ([fac 0])
                 (let ([f (lambda (n)
                                 (if (equals? n 0)
                                           1
                                           (* n (fac (- n 1)))))])
                             (begin
                                    (set! fac f)
                                    (fac 4))))

 

This works just as well for mutually recursive procedures.

So then, it appears that recursive procedures are really syntactic sugar. Here is the grammar for our new language, Mini-SchemeH:

<exp> ::= <number>                                        lit-exp (datum)
                 | <varref>                                         varref-exp (var)
                 | (if <exp> <exp> <exp>)                if-exp (test-exp then-exp else-exp)
                 | (let (<decls>) <exp>)                    let-exp (ids rands body)
                 | (lambda ({<id>}*) <exp>)             lambda-exp (formals body)
                 | (set! <id> <exp>)                          varassign-exp (id rhs-exp)
                 | (letrec (<decls>) <exp>)
                 | (<exp> {<exp>}*)                          app-exp (rator rands)
                 | (begin {<exp>}*)                           sequence-exp (exps)

<decls> ::= <empty>
                 | (<id> <exp>) <decls>

Even though we have added a line to the BNF description. there is no addition to our expression syntax. This is because letrec will "expand" into a let-expression. This is what is known as a syntactic transformation.

Implement MiniSchemeH, which implements letrec. you should only have to modify the parser. Use a help function (make-letrec ids vals body) to do the work so that you don't clutter your parser.

You will need some fresh variables to play the role of placeholders. The procedure (gensym) always returns a fresh, unused variable.

> (gensym)
g62
In the following code sample, it is possible that you have completely correct code, but don't get the same answer as we show. It all depends on how you wrote your parser. Nevertheless, we hope you find the code sample helpful.

> (parse '(letrec ([x 3][y 2]) (+ x y)))
#(struct:let-exp
        (x y)
        (#(struct:lit-exp 0) #(struct:lit-exp 0))
        #(struct:let-exp
               (g63 g64)
               (#(struct:lit-exp 3) #(struct:lit-exp 2))
              #(struct:sequence-exp
                    (#(struct:varassign-exp x
                     #(struct:varref-exp g63))
                     #(struct:varassign-exp y #(struct:varref-exp g64)) #(struct:app-exp #(struct:varref-exp +) (#(struct:varref-exp x) #(struct:varref-exp y)))))))

> (parse '(let ([x 0][y 0]) (let ([g63 3][g64 4]) (begin (set! x g63) (set! y g64) (+ x y)))))
#(struct:let-exp
        (x y)
        (#(struct:lit-exp 0) #(struct:lit-exp 0))
        #(struct:let-exp
               (g63 g64)
               (#(struct:lit-exp 3) #(struct:lit-exp 4))
              #(struct:sequence-exp
                    (#(struct:varassign-exp x #(struct:varref-exp g63))
                   #(struct:varassign-exp y #(struct:varref-exp g64))
                  #(struct:app-exp #(struct:varref-exp +) (#(struct:varref-exp x) #(struct:varref-exp y)))))))

> (read-eval-print)
MS> (letrec ([fac (lambda (x) (if (equals? x 0) 1 (* x (fac (sub1 x)))))]) (fac 4))
24
MS> (letrec ([fac (lambda (x) (if (equals? x 0) 1 (* x (fac (sub1 x)))))]) (fac 10))
3628800
MS> (letrec
                ([even? (lambda (n)
                                     (if (equals? 0 n)
                                         True
                                         (odd? (sub1 n))))]
                 [odd? (lambda (n)
                                      (if (equals? 0 n)
                                          False
                                          (even? (sub1 n))))])
              (even? 5))
False
MS> exit
returning to Scheme proper