With tail call optimization implemented, I converted the set!-rich version of the evaluator back to a more function-style version. I decided not to give the details for that, even though it produced a version that was not quite the same as where I started from. With tail call optimization implemented, we know that we can limit stack growth by shoving as much computation as possible into tail calls.
Continuations are a bit difficult to understand at first. The notion of “the rest of the computation” is easy enough. The difficulty is in seeing how the continuation “fits” into the evaluation process.
I have looked at Reynolds’s tutorial paper, Definitional Interpreters for Higher-Order Programming Languages (Proceedings of 25th ACM National Conference, 1972), that shows one evolution of Scheme-like continuations. There are actually two versions of the continuation system presented there. There’s a continuation, which is the directly executable “rest of the computation” procedure, and a cont function which handles continuations in the form of “micro-ops”.
With the help of the Reynolds paper, I have adapted the evaluator into a set of procedures using continuation passing style. It will take a few days to describe the changes.
As in the Reynolds paper, we start with the directly executable version. In order to handle environments, I started with the apply procedure. But to handle continuations, it seemed more natural to start with the eval procedure.
We create a new ev-c procedure that evaluates an expression and then uses thse result as an argument to a continuation. The continuation is explicitly exposed as an argument to ev-c. A continuation that returns its argument value is supplied. The purpose of this continuation is to stop the chain of continuations that we will create, and in the process, yield a value to the caller of eval. I named this continuation yield.
(yield (lambda (x) x) )
(eval
(lambda (x env)
(ev-c x env yield)
)
)
(ev-c
(lambda (exp env c)
(c (ev-x exp env))
)
)
As may be guessed, ev-x is the reconstituted function-style evaluator. We can inline the ev-x procedure, and then instead of keeping the continuation as an outer level call, we can distribute the continuation call, as inner level calls.
(ev-c
(lambda (exp env c)
(cond
( (constant? exp) (c (evconst-x exp)) )
( (symbol? exp) (c (evvar-x exp env)) )
( (symbol? (car exp)) (c (evsyntax-x exp env)) )
( #t (c (evcall-x exp env)) )
)
)
)
At this point, we want to make a distinction between two kinds of procedures. The Reynolds paper calls them trivial and serious. Trivial procedures are ones that are known to always terminate and yield a result. The serious procedures are all the others.
We know that evconst-x is a trivial call. It always returns the exp argument. The evvar-x is also a trivial call. It always returns with a result (as long as we don’t create or supply an invalid environment). Basically, neither of these procedures will call an evaluator procedure to (recursively) handle subexpressions.
On the other hand, the expressions that are supplied to our evaluator may be (recursively) nonterminating by design. To handle these expressions, we want our evaluator to recurse via tail calls, to take advantage of tail call optimization. To do this, we pass along the continuation, rather than waiting for the evaluator procedure to return.
(ev-c
(lambda (exp env c)
(cond
( (constant? exp) (c (evconst-x exp)) )
( (symbol? exp) (c (evvar-x exp env)) )
( (symbol? (car exp)) (evsyntax-c exp env c) )
( #t (evcall-c exp env c) )
)
)
)
(evsyntax-c
(lambda (exp env c)
(evsyntaxc-c (bound-value (lookup (car exp) env)) exp env c)
)
)
(evsyntaxc-c
(lambda (cmddef exp env c)
(cond
(
(eq? (first cmddef) prim-syntax-mark)
(cond
( (eq? (second cmddef) 'quote) (c (evquote-x exp)) )
( (eq? (second cmddef) 'cond) (evcond-c exp env c) )
( (eq? (second cmddef) 'set!) (evset-c exp env c) )
( (eq? (second cmddef) 'letrec) (evletrec-c exp env c) )
( (eq? (second cmddef) 'lambda) (c (evlambda-x exp env)) )
( #t (c (undefined-x)) )
)
)
( #t (evcall-c exp env c) )
)
)
)
(evcall-c
(lambda (exp env c)
(c (evcallv-x (evlist-x exp env)) )
)
)
Three of the syntax expressions (quote, lambda, undefined) are recognized as trivial. It might also be noted that we could distribute the continuation to the cond branches because all of the cond tests had trivial procedure calls only.
Next, we will look at how the procedure call might be handled.