CS 2135: Programming Language Concepts
Notes on Writing a CPS Compiler


Understanding the Compiler

Having worked through many examples of converting programs into CPS by hand, we'd like to write a program to do it for us. Our program must somehow take an expression and convert it into another expression such that:

In order to implement this, we'd like to convert each expression into an expression that sends the result of the original expression to some continuation. Consider the expression 4. If we wanted to send the result of 4 to a continuation k, we would write

(k 4)

This expression can't be return value of CPS though, because the result of evaluating (eval (cps expr)) must be the same as the result of evaluating (eval expr). The new version would yield an error because k isn't the parameter of some lambda expression. Therefore, we must at least wrap our (k 4) call in a lambda expression that binds k:

(lambda (k) (k 4))

Is this sufficient? No, because evaluating 4 returns 4 while evaluating (lambda (k) (k 4)) returns a lambda. But that lambda has the 4 inside, if only we could get at it! How do we get the 4 out of the lambda? We have to send the lambda a value for k. What's the continuation (the (lambda (box) ...) expression) for an isolated 4? Simply the identity function, (lambda (box) box). This suggests the expression:

((lambda (k) (k 4))
 (lambda (box) box))

So, to recap: we want CPS to send results to a k (because it makes the compiler easier to write -- trust me on this for now). This requires us to use (lambda (k) ...) so that the k doesn't yield an error. The application is required to actually get at the expression which is inside the (lambda (k) ...).

In class, we have been working on the (k 4) part -- the part where we take a k, send an expression to it, then "push" the k inward until all of the function calls are in tail-position. This is exactly what we've done every time we've added a k parameter to a function and worked the k through the body.

So, what's the big picture here? Our CPS converter will take an expression (exp) and a continuation (cont), and produce an expression of the form:

((lambda (k) (push-k expr k))
 cont)

Before we write the compiler, let's look at some examples. I am going to write these initially with lambdas, and then with make-proc, etc, to help bridge the conversions we've done by hand with the compiler versions (which need the make-proc representation).


Writing the Compiler

Our discussion above suggested three things that the converter must be able to do, and thus three functions that must be in the compiler:

Let's write these functions. We'll start with make-accept-k and cps, since they're a bit easier.

;; make-accept-k : symbol expr -> proc
;; creates a proc with the symbol as parameter and body expr
(define (make-accept-k kname expr)
  (make-proc kname expr))

;; cps : expr proc -> apply
;; converts expr to cps by giving it a parameter k, pushing that k
;; through the expression, and sending k the given proc as a continuation 
(define (cps expr cont)
  (make-apply (make-accept-k 'k (push-k expr (make-var 'k)))
              cont))

Now, it's time to write push-k, which is what we've been doing in class. Where do we start? We know that push-k will behave differently for each kind of expr, so we'll start with a cond clause to determine what kind of expr we're in.

;; push-k : expr var -> apply
;; rewrites expr to have no tail calls, sending the result to the
;; given (named) continuation
(define (push-k expr kvar)
  (cond [(number? expr) ...]
	[(var? expr) ...]
	[(proc? expr) ...]
	[(plus? expr) ...]
	[(apply? expr) ...]))

Now, let's fill in the cases based on our examples. We'll start with the number? case. Recall that the example showed the output of cps, not the output of push-k.

(cps 4 (lambda (box) box)) 
==>
(make-apply (make-proc 'k (make-apply (make-var 'k) 4))
            (make-proc 'box (make-var 'box)))

If we expand out the definitions of cps (until we hit the push-k call) and make-accept-k, we get:

(make-apply (make-proc 'k (push-k 4 (make-var 'k)))
            (make-proc 'box (make-var 'box)))

Looking at where these two differ tells us what push-k needs to do when the expr is a number.

;; push-k : expr var -> apply
;; rewrites expr to have no tail calls, sending the result to the
;; given (named) continuation
(define (push-k expr kvar)
  (cond [(number? expr) (make-apply kvar expr)]
	[(var? expr) ...]
	[(proc? expr) ...]
	[(plus? expr) ...]
	[(apply? expr) ...]))

The var? case is similar to the number? case.

;; push-k : expr var -> apply
;; rewrites expr to have no tail calls, sending the result to the
;; given (named) continuation
(define (push-k expr kvar)
  (cond [(number? expr) (make-apply kvar expr)]
	[(var? expr) (make-apply kvar expr)]
	[(proc? expr) ...]
	[(plus? expr) ...]
	[(apply? expr) ...]))

Now consider the plus case, for which we also had an example above:

(cps (make-plus 4 5) (lambda (box) (box)))
==>
(make-apply 
  (make-proc 'k (make-apply 
                  (make-proc 'k1 (make-apply (make-var 'k1) 4))
                  (make-proc 'leftval
                             (make-apply 
                               (make-proc 'k2 (make-apply (make-var 'k2) 5))
                               (make-proc 'rightval
                                          (make-apply 
                                            (make-var 'k)
                                            (make-plus (make-var 'leftval) 
                                                       (make-var 'rightval))))))))
  (make-proc 'box (make-var 'box)))

Taking the same step of expanding out the call to cps until the push-k gives:

(make-apply (make-proc 'k (push-k (make-plus 4 5) (make-var 'k)))
            (make-proc 'box (make-var 'box)))
==>
(make-apply 
  (make-proc 'k (make-apply 
                  (make-proc 'k1 (make-apply (make-var 'k1) 4))
                  (make-proc 'leftval
                             (make-apply 
                               (make-proc 'k2 (make-apply (make-var 'k2) 5))
                               (make-proc 'rightval
                                          (make-apply 
                                            (make-var 'k)
                                            (make-plus (make-var 'leftval) 
                                                       (make-var 'rightval))))))))
  (make-proc 'box (make-var 'box)))

So, stripping off the common parts shows that (push-k (make-plus 4 5) (make-var 'k)) must return

(make-apply 
 (make-proc 'k1 (make-apply (make-var 'k1) 4))
 (make-proc 'leftval
	    (make-apply 
	     (make-proc 'k2 (make-apply (make-var 'k2) 5))
	     (make-proc 'rightval
			(make-apply 
			 (make-var 'k)
			 (make-plus (make-var 'leftval) 
				    (make-var 'rightval)))))))

Notice how this expression looks similar to what we got by running cps on 4, though the cont argument is a little different. Matter of fact, this expression looks like the result of

(cps 4 (make-proc 'leftval
		  (make-apply 
		   (make-proc 'k2 (make-apply (make-var 'k2) 5))
		   (make-proc 'rightval
			      (make-apply 
			       (make-var 'k)
			       (make-plus (make-var 'leftval) 
					  (make-var 'rightval)))))))

Where did the 4 come from? That was the left argument to make-plus. This suggests that in the plus? case push-k should return:

(cps (plus-left expr)
     (make-proc 'leftval
		(make-apply 
		 (make-proc 'k2 (make-apply (make-var 'k2) 5))
		 (make-proc 'rightval
			    (make-apply 
			     (make-var 'k)
			     (make-plus (make-var 'leftval) 
					(make-var 'rightval)))))))

Notice we haven't used (plus-right expr) though. That must go in place of the 5. But shouldn't we call cps on (plus-right expr), as we did for (plus-left expr)? Again looking for the pattern of what cps produces, this suggests:

(cps (plus-left expr)
     (make-proc 'leftval
		(cps (plus-right expr)
		     (make-proc 'rightval
				(make-apply 
				 (make-var 'k)
				 (make-plus (make-var 'leftval) 
					    (make-var 'rightval)))))))

We've put in recursive calls on the left and right expressions (as the template suggests we should, and nothing else in the body of the rightval-proc seems to match one of our existing definitions. These suggest that we're done with the plus? case:

At this point, our compiler looks like:

(define (push-k expr k)
  (cond [(number? expr) (make-apply k expr)]
	[(var? expr) (make-apply k expr)]
	[(plus? expr)
	 (cps (plus-left expr)
	      (make-proc 'leftval
			 (cps (plus-right expr)
			      (make-proc 'rightval
					 (make-apply 
                                            k (make-plus (make-var 'leftval)
						         (make-var 'rightval)))))))]))

We are left with the proc? and apply? cases to finish. Let's look at proc? first. Procs correspond to lambdas, so let's remind ourselves how we CPS lambda expressions:

(lambda (x) (+ x 3)) becomes (lambda (x k) (k (+ x 3)))

We can't express the converted lambda with our make-procs though, because make-procs allow only one parameter. So, we have to turn (lambda (x k) (k (+ x 3))) into an expression where all lambdas have exactly one parameter. We can do this by having the function take its arguments "in turn", as follows:

  (lambda (x)
    (lambda (k)
      (k (+ x 3))))

This version meets our requirements in that all of the lambdas have one argument, but the final expression passes its result to k.

Based on this example worked by hand, how do we fill in the proc? case of the compiler?

(define (push-k expr k)
  (cond [(number? expr) (make-apply k expr)]
	[(var? expr) (make-apply k expr)]
	[(proc? expr) 
         (make-apply k (make-proc (proc-param expr)
                                  (make-proc 'prock
                                             (cps (proc-body expr)
                                                  (make-var 'prock)))))]
	[(plus? expr)
	 (cps (plus-left expr)
	      (make-proc 'leftval
			 (cps (plus-right expr)
			      (make-proc 'rightval
					 (make-apply 
                                            k (make-plus (make-var 'leftval)
						         (make-var 'rightval)))))))]

Where does the proc? answer come from?

Now we need to figure out the apply? case. Again, let's look at a conversion done by hand, and use that to guide the answer. If we wanted to send the result of ((f 6) (g 4)) to a continuation k, we would get:

  (f/k 6 (lambda (procval)
           (g/k 4 (lambda (argval)
                    (procval argval k)))))

Remember, though, that our functions (namely procval) can only take one argument. So, we need to give procval its arguments only one at a time (since it takes them only one at a time). So the converted code in which procval takes arguments one at a time looks like:

  (f/k 6 (lambda (procval)
           (g/k 4 (lambda (argval)
                    ((procval argval) k)))))

So, our answer in the apply? case simply has to turn this answer into code. The conversions of the calls to f and g are handled with calls to cps (note how similar the apply? case looks to the plus? case in this regard) -- the part of apply? that differs from plus? lies in calling procval with argval and k.

(define (push-k expr k)
  (cond [(number? expr) (make-apply k expr)]
	[(var? expr) (make-apply k expr)]
	[(proc? expr) 
         (make-apply k (make-proc (proc-param expr)
                                  (make-proc 'prock
                                             (cps (proc-body expr)
                                                  (make-var 'prock)))))]
	[(plus? expr)
	 (cps (plus-left expr)
	      (make-proc 'leftval
			 (cps (plus-right expr)
			      (make-proc 'rightval
					 (make-apply 
                                            k (make-plus (make-var 'leftval)
						         (make-var 'rightval)))))))]
	[(apply? expr)
         (cps (apply-func expr)
              (make-proc 'procval
                         (cps (apply-arg expr)
                              (make-proc 'argval
                                         (make-apply (make-apply 
                                                       (make-var 'procval)
                                                       (make-var 'argval))
                                                     k)))))]))
))

Again, note how similar this compiler looks to what you've done when converting expressions by hand. The compiler simply turns that manual process into code.

This finishes our compiler for converting to CPS. This shows you the structure of the compiler (accepts a program, produces a program). It also shows you how a fairly short program can do some pretty powerful transformations.