Continuation Passing Style Continuation passing style (CPS) is a technique that converts subroutine calls (and returns) into goto's. Instead of saving the state of the calling routine (program counter, previous stack pointer, saved registers, etc.) on a stack, it calls a new function that will execute the remainder of the code in the calling routine. If all functions are converted to CPS, then there will never be a return from subroutine, until we are ready to return to top level. Hence, we can remove the stack, at the cost of computing an extra continuation argument, described below, for each function call. There are circumstances where the overhead of the function calls is high, and so this can result in code that is faster than traditionally compiled code (for example by a C compiler). A function is converted to continuation passing style by adding an additional continuation argument (by convention called K). The continuation function is a function of one argument. The body of the continuation is then modified so that the function, K, is called on the return value of the original (non-CPS) function. We must examine this principle both from the viewpoint of the calling function and from th viewpoint of the called function. ==================================================== We must first talk about continuations from the viewpoint of the calling function. Assume that we have generic code: foo(...) A_1 A_2 A_3 bar(...) A_4 A_5 return Then a compiler can create a second function (the continuation of foo after the call to bar). It would define a function: K_{foo}(...) A_4 A_5 return Further, bar is then called with the second argument: bar(... K_{foo}) It is clear that any compiler can create the new function, K_{foo}. In Scheme, they allow users to create this function, by use of the scheme function, call/cc. We will not discuss the details of call/cc, and you will not be responsible for it on the final. ==================================================== We next give an example for the called function. We assume that a continuation, K, has been passed down and that K encapsulates all of the code remaining to be executed at higher levels. (define factorial (lambda (x) (if (= x 0) 1 (* x (factorial (- x 1)))))) (define factorial-CPS (lambda (x K) (K (if (= x 0) 1 (* x (factorial (- x 1))))))) The problem with this example is that the recursive call to factorial is not in continuation passing style. We must replace the call to factorial by a call to factorial-CPS. We do this by a sequence of two transformations. The first transformation pushes lower into the body the call to K. (define factorial-CPS (lambda (x K) (if (= x 0) (K 1) (K (* x (factorial (- x 1))))))) The correctness of the last transformation is clear. There are similar transformations possible for other control structures. Such transformations involving control structures should be clear to the reader and are not discussed further here. (define factorial-CPS (lambda (x K) (if (= x 0) (K 1) (factorial-CPS (- x 1) (lambda (y) (K (* x y))))))) To understand correctness of the last transformation, it helps to recall the order in which the elements of the "else" condition of the "if" are executed. Examination of "(K (* x (factorial (- x 1))))" makes it clear that the order of the steps is: 1. (- x 1) 2. (factorial ...) 3. (* x ...) 4. (K ...) We should now examine the order of the steps for "(factorial-CPS (- x 1) (lambda (y) (K (* x y))))". It is important to recall that "factorial-CPS" will first evaluate its argument, then the body of factorial-CPS and then it will execute its continuation argument. 1. (- x 1) 2. (factorial-CPS ... ...) ; evaluation of body only, not of continuation 3. (* x ...) 4. (K ...) Since the order and operations are preserved, the transformation is correct. ==================================================== Finally, a little retrospection is useful here. It's clear that what has been gained is that we no longer need the traditional stack. In exchange, we must now create new continuation arguments each time a new function is called, and such continuations might be stored on a heap (in the data segment). Somtimes a compiler can pre-compile such continuation arguments. In such cases, there is a potential speedup. Further, the nature of a program is quite different. Since subroutine calls are replaced by goto's, a set of routines is converted into one large program. This makes it easier for the compiler to use optimizations such as global register allocation. Just as tail recursion makes a single recursive function more efficient, this technique can give rise to more efficient compilations of mutually recursive functions. ====================================================