Llab 06

Interpreters 1-- Creating an environment

This is due Friday, October 14


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

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

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 simply list the variables that will be bound when the function is called. The function call is the point where the environment (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 expresson

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

will be ( ( (z x) (4 5) )    ((x y) (2 3)) )

When we resolve the bindings for x, y and z to evaluate (+ x (+ y z) ) we first find the binding 5 for x, 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, (+ x y) in an environment that first has x bound to 5, and then has the environment surrounding the defintion of f:

( (x) (5)   ( (x y) (2 3) ) )

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

Section 2: The environment datatype

The two most important featues 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)))

These lead to easy recognizer functions:

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

(define extended-env? (lambda (x)
            [(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)
           [(extended-env? env) (cadr env)]
           [else (error 'syms "bad environment")])))

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

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

We can define a unique empty environment (whick lets us check if we are in the empty environment:

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

We build up new environments with procedure extended-env. For example

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

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

Section 3: lookup-env

Exercise 1:

Create a Scheme file env.rkt that contains the datatype definitions above. Write function lookup-env, 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 above

(lookup-env EnvA 'x) should return 1
(lookup-env EnvB 'x) should return 5
(lookup-env EnvB 'y) should return 2
(lookup-env 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)

Section 4: The initial environment

Exercise 2:

Add to your env.rkt file definitions of the-empty-env and a new environment called init-env that contains bindings for variables x and y to numbers (such as 1 and 2); we will use this initial environment to test out features of MiniScheme before we implement expressions that allow us to extend the environment with new bindings.

Exercise 3

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 environmen? empty-env? extended-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 import env.rkt with

       (require "env.rkt")

and then run (lookup-env init-env 'x) This should give whatever value you bound to x in the initial environment.

For this lab you only need to hand in the env.rkt file.