COS 441 - Store Passing - Mar 14, 1996

Store Passing

The clause for set! in our CPS interpreter looks like

     (Set! (name exp)
	(eval exp env (lambda (v)
                        (set-box! (lookup env name) v)
                        (k #f))))
This is meta-circular, so it is not a very good explanation of assignment, unless you already know what assignment is.

Let env : name -> loc and store : loc -> value where store is a new parameter to the interpreter. The store is an explicit map representing the contents of the boxes in the earlier interpreter. Adding the store parameter, our interpreter looks like this:


(define eval
  (lambda (e env s k)
    (variant-case e
      (Const (value)
        (k value))
      (Var (name)
        (k (lookup s (lookup env name))))
      (Lam (formal body)
        (k (lambda (varg s2 k2)
             (let ((new-cell (length s2)))
               (eval body
                     (extend env formal new-cell)
                     (extend s2 new-cell varg)
                     k2)))
           s))
      (Ap (fun arg)
        (eval fun env s
              (lambda (vfun s1) 
                (eval arg env s1 (lambda (varg s2) 
                                   (vfun varg s2 k))))))
      (Set! (name exp)
        (eval exp env s
              (lambda (vexp s1) 
                (k #f (extend s1 (lookup env name) vexp)))))
      (Letcc (name body)
        (let ((new-cell (length s)))
	  (eval body (extend env name new-cell)
		(extend s new-cell (lambda (v s2 k2) (k v s2))) k))))))
Notice that the store is passed explicitly from each clause to the continuation. This is how we mimic assignment in languages like Haskell that are purely functional.

The store is single threaded: only one store is ever in use at one time. To see this, observe that eval and all continuation lambdas are always called in tail position. Look at each call, and verify that whenever a new store is constructed, the old one is not used again. Hence we can pull out the store argument and make it a global variable.

(define s ...)
...
      (Set! (name exp)
        (eval exp env s
              (lambda (v)
                (set! s (extend s (lookup env name) v))
                (k #f))))
      (Lam (formal body)
        (k (lambda (arg k2)
             (let ((new-cell (length s)))
               (set! s (extend s new-cell arg))
               (eval body (extend env formal new-cell) k2)))))
But this interpreter does not capture all allocation explicitly--the continuation lambdas allocate. To fix this, let's use the first-order transformation to make continuations into records.
(define eval
  (lambda (e env s k)
    (variant-case e
      (Const (value)
        (apply-k k value))
      (Var (name)
        (apply-k k (lookup s (lookup env name))))
      (Lam (formal body)
        (apply-k k (make-Closure body env formal)))
      (Ap (fun arg)
        (eval fun env (make-After-fun arg env k))))))

(define apply-k
  (lambda (k v)
    (variant-case k
      (After-fun (arg env next) 
        (eval arg env (make-After-arg v next)))
      (After-arg (vfun next)
        (variant-case vfun
          (Closure (body env formal)
            (let (new-cell (length s))
              (set! s (extend s new-cell v))
              (eval body (extend env formal new-cell) next))))))))
Now we can see the remaining allocation: make-After-fun, make-Closure, make-After-arg. Let's put them in the store.
(define alloc 
  (lambda () 
    (length s)))

(define store-set!
  (lambda (loc val)
    (set! s (extend s loc val))))

(define store-ref
  (lambda (loc)
    (lookup s loc)))

...
      (Ap (fun arg)
        (let ((new-cell (alloc)))
	  (store-set! new-cell (make-After-fun arg env k))
	  (eval fun env new-cell)))
This still doesn't quite look like the allocations are going in the store. Let's try again. Rather than use make-After-fun, we will explicitly allocate 4 cells to hold the three fields of an After-fun plus a ``tag''.
(define p 0)

(define alloc
  (lambda (n)
    (let ((cell p))
      (set! p (+ p n))
      cell)))

...
      (Ap (fun arg)
        (let ((cell (alloc 4)))
	  (store-set! cell 'After-fun)
	  (store-set! (+ 1 cell) arg)
	  (store-set! (+ 2 cell) env)
	  (store-set! (+ 3 cell) k)
	  (eval fun env cell)))
One thing is still wrong. The extend calls also allocate. Making these allocations explicit is similar to making continuation allocations explicit.

Now we have a CPS interpreter in which all allocation is explicit. All eval calls are tail calls, which are essentially jumps with arguments. But what is a jump with arguments?

Registerization

Recall the tail-recursive implementation of Pi. Since it never uses its arguments after making a call (as it is tail-recursive), we can modify the arguments as follows:
(define Pi
  (lambda (l acc)
    (if (null? l) acc
	(let ((arg1 (cdr l))
	      (arg2 (* (car l) acc)))
	  (set! l arg1)
	  (set! acc arg2)
	  (Pi l acc)))))
Notice that Pi passes its own arguments to itself. The arguments are in different boxes (check the evaluator - a new box is constructed for each argument at each call). But the old boxes are never needed after the next call to Pi because the function is tail-recursive and never returns. So we could use the same boxes. We do this by using global variables.
(define l #f)
(define acc #f)
(define Pi
  (lambda ()
    (if (null? l) acc
	(let ((arg1 (cdr l))
	      (arg2 (* (car l) acc)))
	  (set! l arg1)
	  (set! acc arg2)
	  (Pi)))))
Pi no longer takes an arguments. We can think of top level arguments as registers. Let's do the same thing to our evaluator.
(define e #f)
(define env #f)
(define k #f)
(define eval
  (lambda ()
    (variant-case e
      (Ap (fun arg)
        (let ((new-cell (alloc)))
	  (set-store! new-cell (make-After-fun arg env k))
	  (set! e fun)
	  (set! k new-cell)
	  (eval))))))
This final interpreter Hence it is a C program!

Exercises

Reading