Double-barrelled Continuation Passing Style Interpreter

Posted on January 10, 2017 by Kwang Yul Seo

In the Continuation Passing Style Interpreter, we wrote a continuation-passing style interpreter for a small functional language and implemented the escape expression which is the binder form of Scheme’s call/cc.

Though call/cc is a powerful control operator, it is generally considered as a bad abstraction as a core language feature. So, in this post, we will drop escape expressions and add ML-style exceptions.

Exceptions can be used to effect non-local transfers of control. By using an exception handler we may “catch” a raised exception and continue evaluation. For example,

1 + (raise 2)
handle \x -> x + 3

evaluates to 5 because 2 raised by raise 2 is passed to the exception handler \x -> x + 3.

To support exceptions in our interpreter, eval function is modified to take two continuations: an exception-handler continuation, and a return continuation. This is the so-called double-barrelled continuation-passing style introduced in Comparing Control Constructs by Double-barrelled CPS.

eval :: Env -> Expr -> Cont -> Cont -> Value
eval env term h k = case term of
  Var n -> k $ env !! n
  Lam a -> k $ VClosure (\v k' -> eval (v : env) a h k')
  App a b ->
    eval env a h $ \(VClosure c) ->
    eval env b h $ \v ->
    c v k

  Lit n -> k $ VInt n
  Prim p a b -> eval env a h $ \v1 ->
                eval env b h $ \v2 ->
                k $ evalPrim p v1 v2

evalExpr :: Expr -> Value
evalExpr e = eval emptyEnv e (\x -> error "uncaught exception") id

h is the exception-handler continuation and it is simply passed along the application of eval. evalExpr is also modified to handle an uncaught exception.

Once our interpreter is transformed into a double-barrelled continuation-passing style, it is easy to add handle and raise expressions. First, let’s extend Expr with Handle and Raise nodes.

data Expr
  = ...
  | Handle Expr Expr
  | Raise Expr

Then extend eval function with two additional AST nodes.

eval :: Env -> Expr -> Cont -> Cont -> Value
eval env term h k = case term of
  Raise a -> eval env a h h
  Handle a b ->
    let h' x = eval (x : env) b h k
    in eval env a h' k

Raise evaluates a with both continuations set to the error-handler continuation h. So the value is passed to the current error-handler.

Handle sets up a new error-handler h' which evaluates b with the environment extended with the raised value x. Note that a is evaluated with the error-handler set to h' so that any exception raised while evaluating a is passed to h'.

Let’s run the example above!

λ> evalExpr $ (Prim Add (Lit 1) (Raise (Lit 2))) `Handle` (Prim Add (Var 0) (Lit 3))

Yay, it works again!

If you would like to know why we can’t implement exceptions using call/cc alone, please read Oleg Kiselyov’s article Vast difference between delimited and undelimited continuations.