Comp 311 Notes:
Chapter 18
letcc and error
Our goal is to produce a tail-recursive interpreter, which we will use
to understand the meaning of error and
letcc.
To recap our previous lecture, during its evaluation, every phrase will be surrounded by some computation that is waiting to be performed (and, typically, that depends on the value of this phrase). This remaining computation is known as an evaluation context. Turning this evaluation context into a function is the act of making the continuation explicit.
For instance, in
(+ (* 12 3) (- 2 23))the evaluation context of the first sub-expression (assuming it is evaluated first) is
(+ _ (- 2 23))(where we pronounce
_ as ``hole''), so the reified
version of this context is
(lambda (x) (+ x (- 2 23)))
(lambda (x) ... _ ...) be a valid
evaluation context?
errorLet us consider an interpreter for LC:
(define Eval
(lambda (M env)
(cond
((var? M)
(lookup M env))
((lam? M)
(make-closure M env))
((app? M)
(Apply (Eval (app-rator M) env) (Eval (app-rand M) env)))
((add? M)
...)
...)))
In this interpreter, we see both the creation of new continuations and
the use of the (implicit) continuation. New continuations are created
in the code for applications, at the evaluation of the sub-expressions
and again in the call to Apply. The other two clauses
shown use the current continuation by passing it a value.
We now use the standard technique for transforming Scheme code to transform the interpreter into CPS:
(define Eval/k
(lambda (M env k)
(cond
((var? M) (k (lookup M env)))
((lam? M) (k (make-closure M env)))
((app? M) (Eval/k (app-rator M) env
(lambda (rator-v)
(Eval/k (app-rand M) env
(lambda (rand-v)
(Apply/k rator-v rand-v k))))))
...)))
We need to similarly transform Apply. Whereas before we
had
(define Apply
(lambda (f a)
(cond
((closure? f)
(Eval (body-of f)
(extend (env-of f) (param-of f) a)))
(else ...))))
we now have
(define Apply/k
(lambda (f a k)
(cond
((closure? f)
(Eval/k (body-of f)
(extend (env-of f) (param-of f) a)
k))
(else ...))))
Apply procedures take
an environment as an argument; instead, they choose to use the one
stored in the closure. However, it is possible to imagine a different
semantics that passes on the current environment to
Apply, which then passes it on for the remainder of the
evaluation. Such a system is said to have dynamic binding,
as opposed to the static binding used here.
Exercise: Implement dynamic binding.
We have intentionally left the fall-through case of the
cond expressions in the Apply procedures
empty. However, we know from before that there should be a call to
error in that slot. Since we want to explain
error, we need to determine how to signal an erroneous
application.
Since the k argument always represents the entire
evaluation context, there is no additional computation awaiting the
value returned by the interpreter. Therefore, to ignore the pending
computations and return a value directly, all that error
needs to do in this interpreter is to return a value. This value is
then returned to the user. (Note that this crucially depends on the
fact that the interpreter is fully tail-recursive.) Therefore, the
code for Apply/k could be written as
(define Apply/k
(lambda (f a k)
(cond
((closure? f)
(Eval/k (body-of f)
(extend (env-of f) (param-of f) a)
k))
(else 'ouch!))))
Note that the else clause above is ``returning'' at the
meta-level, not at the level of LC. A function return in LC is
modeled by passing the result of evaluating the function call to the
appropriate continuation.
We have teased out control entirely and made it a distinct entity in the interpreter. Thus, we can use it as before, we can ignore it, or we can harness it in new ways, as we will see below.
letcc
In our earlier interpreters, we modeled letcc by
appealing to the letcc form at the meta-level:
(cond
...
((letcc? M)
(letcc k (Eval (body-of M)
(extend env (label-of M) k))))
...)
Since we want letcc to bind a program variable to the
rest of the computation, we could instead write, in
Eval/k,
(cond
...
((letcc? M)
(Eval/k (body-of M)
(extend env
(label-of M) k)
k))
...)
This rewrite makes manifest an interesting feature of control. Just
as stores were duplicated when we made them explicit, here we have
two uses of k. In the case of stores, we
hypothesized that we could have an operator that ``forgot'' the
side-effects performed in the evaluation of an expression, but we
found no use for such an operator. Now, however, we can save the
evaluation context, and return to it -- ignoring any intermediate
context -- if we wish. Furthermore, since this evaluation context is
bound in the environment, we can return to it whenever we choose to,
even outside the lexical context of the letcc
expression. One example of where this might be useful is in the
implementation of context switches during multitasking, where
we periodically store the current context and return to it later.
Hence, whereas before control was single-threaded, and could be implemented as a stack, it is now tree-shaped and cannot be implemented in that manner.
Earlier, we decided to eliminate meta-language closures from our
interpreter. Now, we have reintroduced them in the process of CPSing
our interpreter, ie, in the action for applications. As
before, we shall model this process of closure creation abstractly by
using the procedure Push. Similarly, we will abstract
over the use of the continuation -- to ``return'' values -- as the
procedure Pop.
(define Eval*/k
(lambda (M env k)
(cond
((var? M) (Pop k (lookup M env)))
((lam? M) (Pop ...))
((app? M)
(Eval (app-rator M) env
(Push 1 M env k)))
...)))
To begin with, these abstractions can map to the current
implementation method:
(define Push
(lambda (name M env k)
(lambda (x)
(Eval/k (app-rand M) env
(lambda (y)
(Apply/k x y k))))))
(define Pop
(lambda (k v)
(k v)))
Note, further, that Push has a call to
Eval/k; we rewrite this as
(Eval/k (app-rand M) env (Push 2 x k))In all, there is a finite number of locations that create continuations in the interpreter, so we can write a procedure that creates each one of these. Thus,
Push can be rewritten
as
(define Push
(lambda (name . pv)
(cond
((= name 1)
(let ((M ...pv...) (env ...pv...) (k ...pv...))
(lambda (x) (Eval/k (app-rand M) env
(Push 2 x k)))))
((= name 2)
(let ((x ...pv...) (k ...pv...))
(lambda (y) (Apply/k x y k)))))))
Finally, the remaining reliance on lambda can be removed
by replacing it with a call to list instead. In the
process, Pop has to become more elaborate: in particular,
it needs to dispatch on the ``name'' of the current continuation.
This is analogous to the change made in Apply when we
moved from a meta-level to a structure-based representation of
closures in our original interpreters.
cork@cs.rice.edu (adapted from shriram@cs.rice.edu)