CSCI 275

Lab 05B
Interpreters 1 – Creating an Environment
Due Friday April 3 at 11:59 PM

This is the first portion of your Scheme interpreter. The initial portion of this lab is material we discussed in class on Tuesday, March 3. If you followed this you can skip down to the 3 exercises at the end of this document.

Part 1: Environments

We need to create an environment to hold the data for the expressions we will interpret. The scoping rules for Scheme determine the structure of this environment. Consider the following three examples. First

(let ([x 2] [y 3])
       (+ x y))

This has an environment with bindings for x and y created in a let-block. These bindings are used in the body of the let.

Next, consider the following. This has a let-block that creates a lamba-expression, which is called in the body of the let:

(let ([f (lambda (x) (+ x 4))])
        (f 5))

When this is evaluated we want to bind x to the value of the argument, 5, and then evaluate the body of f using that binding.

Finally, we combine these. At the outer level in the following expression we have a let-block with bindings for x and y. The body is another, nested, let, which binds a lembda expression with a parameter x. The body of the interior let is a call to the function.

(let ([x 2] [y 3])
        (let ( [f (lambda(x) (+ x y))])
                 (f 5)))

When we evaluate this we first make an environment with bindings of x and y to 2 and 3 respectively, then we use this to evaluate the inner let-expression.  In that expression we make a binding of f to the value of the lambda expression (a closure, of course), and then we call f with argument 5.  This requires us to evaluate the body of f with x bound to 5.  The body of f does not have a binding for y, so we look it up in the outer environment and see that its value is 3.  Finally, we evaluates (+ x y) with x bound to 5 and y bound to 3, yielding 8 for the value of the full expression.

 

Environments are extended in two ways. Let-expressions have bindings that extend the current environment; the body of the let is evaluated in the extended environment. Lambda expressions do not extend the environment; they evaluate to closures that store the environment in place when the lambda is evaluated.  The function call is the point where the closure's environment is extended, with the parameters from the lambda expression being bound to the values of the arguments.

We will define the environment as an association list, where symbols are associated with values. There are two ways we might do this. In the first example above, where x is bound to 2 and y to 3, we might use the list ( (x 2) (y 3) ), or we might use ( (x y) ( 2 3) ). The former structure is closer to the way the bindings appear in let-expressions; the latter is closer to the components of a call. The former structure might appear simpler, but thanks to procedure map, the latter is actually easier to code and we will go with that.

Scheme, and most other languages you are likely to use, employs lexical scoping. When we want to resolve the binding for a free variable, we look first in the current scope, then in the surrounding scope, then in the scope that surrounds that, until the variable is found or we reach the outermost scope. To implement this our environments will be structured as linked lists, with the head of the list the current scope and the tail the previous environment. Thus, the environment for the expression

(let ([x 2] [y 3])
      (let ([z 4]  [x 5])
               (+ x (+ y z))))

will be something like   ( ‘env (z x) (4 5) ( ‘env (x y) (2 3) (‘empty-env)))

When we resolve the bindings for x, y and z to evaluate (+ x (+ y z) ) we find the binding 5 for x (there are two bindings for x, but the one we want is closer to the head of the list so it is the first one we come to), and of course we find 4 for z and 3 for y. This leads to the correct value, 12 for the expression.


 

Similarly, in the expression

(let ([x 2] [y 3])
      (let ([f (lambda(x) (+ x y))])
               (f 5)))

we evaluate the call (f 5) by evaluating the body of f in an environment that first has x bound to 5, and then has the environment surrounding the definition of f.

You will see in the next two labs how this environment is created. At present we need to create the tools that will allow this.

 

Part 2: The environment datatype

The two most important features of an environment are that we need to be able to look up symbols to get the values they are bound to, and we need to be able to extend an environment with new bindings to get a new environment. We'll define an environment as either the empty environment, with no bindings, or an extended environment with a list of symbols, a corresponding list of the values those symbols are bound to, and a previous environment that is being extended. Here are constructor functions for the two types of environment:

(define empty-env (lambda () (list 'empty-env)))
(define extended-env (lambda (syms vals old-env)
                                                       (list 'extended-env syms vals old-env)))

Note that extended-env is the function you will use every time you need to extend an environment when evaluating a let-expression or a function call. For example, when evaluating the expression (let ([x 1]  [y 2]) …) we might use

(define EnvA (extended-env '(x y) '(1 2) the-empty-env))


We could further extend this environment:

(define EnvB (extended-env '(x z) '(5 7) EnvA))

 

This datatype has easy recognizer functions:

(define empty-env? (lambda (x)
      (cond
            [(not (pair? x)) #f]
            [else (eq? (car x) 'empty-env)])))

(define extended-env? (lambda (x)
      (cond
            [(not (pair? x)) #f]
            [else (eq? (car x) 'extended-env)])))

(define environment? (lambda (x)
          (or (empty-env? x) (extended-env? x))))

The accessor functions for the different fields of an extended environment are also easy:

(define syms (lambda (env)
     (cond
           [(extended-env? env) (cadr env)]
           [else (error 'syms "bad environment")])))


(define vals (lambda (env)
      (cond
            [(extended-env? env) (caddr env)]
            [else (error 'vals "bad environment")])))


(define old-env (lambda (env)
     (cond
           [(extended-env? env) (cadddr env)]
           [else (error 'old-env "bad environment")])))

We can define a unique empty environment::

(define the-empty-env (empty-env))

All that remains is to build some helper functions to look up bindings in an environment.

Part 3: Exercises

Exercise 1: lookup

File env.rkt contains the code given above for the Environment datatype. Add to this file function (lookup  environment symbol), which takes an environment and a symbol and returns the first binding for that symbol in the environment. For example, with environments EnvA and EnvB defined as

(define EnvA (extended-env '(x y) '(1 2) the-empty-env))
(define EnvB (extended-env '(x z) '(5 7) EnvA))

we should have the following behavior:

(lookup  EnvA 'x) should return 1
(lookup  EnvB 'x) should return 5
(lookup  EnvB 'y) should return 2
(lookup  EnvB 'bob) should cause an error

If lookup-env does not find a binding for the symbol you should invoke the error handler (error sym string ), as in

(error 'apply-env "No binding for ~s" sym)

 

Exercise 2: init-env

Add to your env.rkt file the definition of a new environment called init-env that contains bindings for variables x and y to numbers 10 and 23; we will use this initial environment to test out features of the MiniScheme interpreter before we implement expressions that allow us to extend the environment with new bindings.

Exercise 3: provide and testing

Make your env.rkt file into a module by givng provide directives for all of the objects you want to make availablel to other modules. For example,

(provide environment? empty-env? extended-env? ...init-env)

You can supply any number of these at the top of the program; I find it convenient to group the functions being exported this way into related clusters.

To test your module, open a new Scheme file, have it require env.rkt with

(require "env.rkt")

and then run 

(lookup  init-env 'x)

This should give the value 10 you bound to x in the initial environment.

 

For this lab you only need to hand in the env.rkt file. Be sure you hand in the one you extended with solutions to Exercises 1-3, not the vanilla one I posted.