COS 441 - Dynamic Scope and Parameter Passing - Feb 27, 1996

Dynamic Scope

Suppose we "simplify" our first order interpreter for Micro-Scheme by removing the highlighted pieces below. The "simplified" interpreter will use the current environment to evaluate the body of a procedure.
(define eval
  (lambda (e env)
    (variant-case e
      (Const (value)
        value)
      (Var (name)
        (or (lookup env name) (make-Prim name)))
      (Lam (formal body)
        (make-Closure formal body env))
      (Ap (fun arg)
        (let ((vfun (eval fun env))
              (varg (eval arg env)))
          (variant-case vfun
            (Prim (name)
              (cond ((eq? name 'not) (not varg)) ...))
            (Closure (formal body env)
              (eval body (extend env formal varg)))))))))
Environments are now manipulated in a stack-like manner: extended before evaluating the body of a procedure and retracted after. Furthermore, there's only ever one stack. So let's eliminate the env parameter to eval, make it a global definition, and put in explicit stack operations.
 
(define eval
  (lambda (e)
    (variant-case e
      (Const (value)
        value)
      (Var (name)
        (or (lookup env name) (make-Prim name)))
      (Lam (formal body)
        e)
      (Ap (fun arg)
        (let ((vfun (eval fun))
              (varg (eval arg)))
          (variant-case vfun
            (Prim (name)
              (cond ((eq? name 'not) (not varg)) ...))
            (Lam (formal body)
              (push! formal arg)
              (let ((v (eval body)))
                (pop!)
                v))))))))
Notice that we have also gotten rid of Closure sine they are now the same as Lam. Using the first-order implementation of environments, the stack operations are defined as follows:
(define env (make-empty))

(define push!
  (lambda (x v)
    (set! env (make-extend-ff env x v))))

(define pop!
  (lambda ()
    (variant-case env
      (extend-ff (fun dom ran) (set! env fun)))))
This interpreter is more efficient: it does not have to pass env around as an argument, and closures are smaller. (More importantly, we can explicitly free the extend-ff records when we pop!, where we can't do this in the original interpreter because environments don't behave like stacks. But we haven't yet discussed memory allocation and reclamation.)

But what is the cost of this "simplification" we have performed? Consider:

(let* ((x 0)
       (f (lambda (y) (add1 x)))
       (x 2))
  (f 0))
In the original interpreter (or Scheme) this expression yields the value 1. Let's run it in our new interpreter. First we have to expand let*.
A:   ((lambda (x)
B:      ((lambda (f)
C:        ((lambda (x)
D:           (f 0))
           2))
         (lambda (y) (add1 x)))
        0))
The environment stacks at each marked point in the above program are (top of stack on left):
A: [x = 0]
B: [f = (lambda (y)...)] [x = 0]
C: [x = 2] [f = (lambda (y)...)] [x = 0]
D: [y = 0] [x = 2] [f = (lambda (y)...)] [x = 0]
The evaluation of (f 0) occurs in environment D. Here (lookup env x) yields 2. Thus the entire expression evaluates to 3. Clearly this is not equivalent to the original interpreter. This new interpreter implements dynamic scoping, while the original uses lexical scoping. With dynamic scoping a variable's binding may be superseded by another binding of the same name. With lexical scoping, a variable's binding remains in effect forever.

Recall that earlier we said that names don't matter in Scheme. This is because Scheme is lexically scoped. In Scheme, or the original interpreter, the expression

(let* ((z 0)
       (f (lambda (y) (add1 z)))
       (x 2))
  (f 0))
has the same value as the expression above (all we've done is to rename the first bound variable x). But evaluating this expression in the interpreter with dynamic scoping we get 1, not 3. With dynamic scoping, names DO matter. Changing bound variables is not something we can freely do.

If you think that this makes it harder to reason about programs in languages with dynamic scope, you're right. Nevertheless, for many years, Lisp systems were dynamically scoped. Emacs Lisp still is, as are many other programming languages whose designers did not understand this issue.

Implementing dynamic scoping using one global stack as above is called deep binding. An alternative way to implement dynamic scoping is to use a separate stack for each variable. This is called shallow binding. Lookup is cheaper because each current binding is at the top of the stack. But calling a procedure may be more expensive because it involves pushing and popping several stacks (and figuring out which stacks to push and pop).

Dynamic Assignment

The dynamic-extent nature of the bindings introduced by dynamic scoping is sometimes useful. Suppose you want to temporarily change the current output port during the evaluation of some procedure P. With just lexical scoping, you would have to make the output port a parameter of all the display routines within P and of all the routines P calls. But with dynamic scoping, you could make the output port be a variable output-port, and the display routines write to that port. You could then establish a new binding of output-port just before calling P, which would be discarded when P returns.

Dynamic assignment is a way to accommodate bindings with dynamic extent into a lexically scoped language. It is quite simple: make a copy of the current value, change the variable to the new value, call P, restore the old value. Read: EOPL 5.7.2.

Reading

Parameter Passing

To begin an investigation of parameter passing, let us return to the first-order interpreter which supports set! with environments that map names to boxes. In addition, we will put the result of eval in a box. The new boxes (and unboxing operations) are highlighted below:
(define eval
  (lambda (e env)
    (variant-case e
      (Const (value)
        (box value))
      (Var (name)
        (box (unbox (lookup env name))))
      (Lam (formal body)
        (box (make-Closure formal body env)))
      (Ap (fun arg)
        (let* ((vfun (unbox (eval fun env)))
               (varg (unbox (eval arg env))))
          (variant-case vfun
            (Closure (formal body arg)
               (eval body (extend env formal (box varg)))))))
      (Set! (name body)
        (set-box! (lookup env name) (unbox (eval body env)))
	(box #f))
      (Let (name binding body)
	(box (eval body (extend env name (box (unbox (eval body env))))))))))
This new interpreter behaves the same as without the new boxes. Furthermore, we can eliminate the (box (unbox ...)) around (lookup env name) in Var without changing anything. We'll call this interpreter B.

Exercise: Verify that interpreter B behaves the same as the original interpreter without boxed results.

Call-by-reference

There are other places where unnecessary boxing and unboxing seems to happen. For instance, in Ap the result of (eval arg env) is unboxed, only to be immediately boxed again when extending the environment for the call. Suppose we "simplify" this:
(define eval
  (lambda (e env)
    (variant-case e
      (Const (value)
        (box value))
      (Var (name)
        (lookup env name))
      (Lam (formal body)
        (box (make-Closure formal body env)))
      (Ap (fun arg)
        (let* ((vfun (unbox (eval fun env)))
               (varg (eval arg env)))
          (variant-case vfun
            (Closure (formal body arg)
               (eval body (extend env formal varg))))))
      (Set! (name body)
        (set-box! (lookup env name) (unbox (eval body env)))
	(box #f))
      (Let (name binding body)
	(box (eval body (extend env name (box (unbox (eval body env))))))))))
Now consider the following expression:
(let ((x 1)
      (f (lambda (a) (begin (set! a 2) a))))
  (begin 
    (f x)
    (display x)))
In the original interpreter (or Scheme), this expression displays 1. But in the interpreter above, it displays 2. What's going on?

This new interpreter uses a different parameter passing convention known as call-by-reference. In contrast, Scheme uses call-by-value. Under call-by-reference, every value "lives" in some location. A procedure call passes the location of the argument value (or a reference to the value), rather than the value itself. When the argument is a variable, this variable's value can be modified by changing the value of the corresponding formal parameter, because both the argument value and the formal parameter are bound to the same location. In the example above, the argument x and the formal parameter a are bound to the same location, so the assignment to a changes the value of x.

Fortran is an example of a language that uses call-by-reference. Call-by-reference makes it much more difficult to reason about programs. Let us look at a few more examples:

(define f 
  (lambda (a b)
    (set! a (+ a b))
    (set! a (+ a b))
    a))

(let ((x 1) (y 1)) (f x y))
(let ((z 1)) (f z z))
With call-by-value, f returns a + 2b for any arguments. Hence the two let-expressions yield 3 and 3 respectively. But with call-by-reference, the first let-expression yields 3, and the second yields 4. The difference arises because in the second call the two formal parameters of f share the same location. When two formal parameters share the same location they are said to alias. Under call-by-value, aliasing cannot occur because values are not locations. (Caution: this doesn't mean aliasing cannot occur in Scheme, see below.)

Another example:

(let ((f (lambda (a) (set! (add1 a)))))
  (f 1))
A call to f increments the formal parameter a. But here the argument is a constant, which lives in some location. So the value in the constant's location is changed! Since our interpreter constructs a new box each time it evaluates a constant, this doesn't matter. But most compilers "optimize" evaluation of constants by constructing a single box for a particular constant at compile time, and reusing the box every time the constant is encountered. Then, with the code above in a loop, the next time the constant 1 is encountered its box will have the value 2. You can actually do this in many Fortran compilers.

A final example:

(define swap 
  (lambda (a b)
    (let ((temp a))
      (set! a b)
      (set! b temp))))

(let ((x 1) (y 2)) (swap x y))
With call-by-value, calling swap has no effect. But with call-by-reference, swap does what its name suggests: swaps the values of x and y.

Call-by-value-result

Ada uses a somewhat different parameter passing method called call-by-value-result or copy-in-copy-out. It would be implemented in the interpreter with boxed results (interpreter B) as follows:
      (Ap (fun arg)
          (let* ((vfun (unbox (eval fun env)))
                 (varg (eval arg env)))
            (variant-case vfun
              (Closure (formal body env)
                (let* ((b (box (unbox varg)))     ; copy in
                       (v (eval body (extend env formal b))))
                  (set-box! varg (unbox b))       ; copy out
                  v)))))
Exercise: Construct an expression that yields different results under call-by-value and call-by-value-result.

Parameter Passing in Real Languages

Are call-by-reference and call-by-value-result completely useless? No, real languages typically include some combination of parameter passing mechanisms, using different mechanisms for different kinds of data. Scheme passes mutable data structures by reference. Such as

Typically languages use a mix of these conventions, based on the type of each arg. Let's add boxes to the original call-by-value interpreter represented as pairs with cdr #f.

(variant-case vfun
  (Prim (name)
    (case name
      (make-box (cons varg) #f)
      (unbox    (car varg))
      (set-box! (set! ...))))
  (Closure (formal body env)
    (eval body (extend env formal (box varg)))))
Boxes are passed by reference, but other values by value. So where is the code to select the parameter passing based on type? It is done for us by Scheme, since since we are representing boxes as pairs, and Scheme passes pairs by reference.

Let's add pass-by-value pairs. Their operations are:

    (variant-case vfun
      (Closure (formal body env)
        (eval body (extend env formal
                     (box (if (vpair? varg) 
                              (vcons (vcar varg) (vcdr varg))
                              (varg)))))))
Observe that when a vpair is passed to a function, vset-car! and vset-cdr! operations on that pair will not affect the argument pair. Doing this correctly for lists constructed from vpairs requires a recursive procedure that copies the entire list.

Exercise

Reading