3

I need some hits how I should go about and implement continuation for lips in JavaScript (my lisp is almost like scheme, except no continuations and TOC).

Here is my evaluate function:

function getFunctionArgs(rest, { env, dynamic_scope, error }) {
    var args = [];
    var node = rest;
    markCycles(node);
    while (true) {
        if (node instanceof Pair && !isEmptyList(node)) {
            var arg = evaluate(node.car, { env, dynamic_scope, error });
            if (dynamic_scope) {
                arg = unpromise(arg, arg => {
                    if (typeof arg === 'function' && isNativeFunction(arg)) {
                        return arg.bind(dynamic_scope);
                    }
                    return arg;
                });
            }
            args.push(arg);
            if (node.haveCycles('cdr')) {
                break;
            }
            node = node.cdr;
        } else {
            break;
        }
    }
    return resolvePromises(args);
}
// -------------------------------------------------------------------------
function evaluateMacro(macro, code, eval_args) {
    if (code instanceof Pair) {
        //code = code.clone();
    }
    var value = macro.invoke(code, eval_args);
    return unpromise(resolvePromises(value), function ret(value) {
        if (value && value.data || !value || selfEvaluated(value)) {
            return value;
        } else {
            return quote(evaluate(value, eval_args));
        }
    });
}
// -------------------------------------------------------------------------
function evaluate(code, { env, dynamic_scope, error = () => {} } = {}) {
    try {
        if (dynamic_scope === true) {
            env = dynamic_scope = env || global_env;
        } else if (env === true) {
            env = dynamic_scope = global_env;
        } else {
            env = env || global_env;
        }
        var eval_args = { env, dynamic_scope, error };
        var value;
        if (isNull(code)) {
            return code;
        }
        if (isEmptyList(code)) {
            return emptyList();
        }
        if (code instanceof Symbol) {
            return env.get(code, { weak: true });
        }
        var first = code.car;
        var rest = code.cdr;
        if (first instanceof Pair) {
            value = resolvePromises(evaluate(first, eval_args));
            if (isPromise(value)) {
                return value.then((value) => {
                    return evaluate(new Pair(value, code.cdr), eval_args);
                });
                // else is later in code
            } else if (typeof value !== 'function') {
                throw new Error(
                    type(value) + ' ' + env.get('string')(value) +
                        ' is not a function while evaluating ' + code.toString()
                );
            }
        }
        if (first instanceof Symbol) {
            value = env.get(first, { weak: true });
            if (value instanceof Macro) {
                var ret = evaluateMacro(value, rest, eval_args);
                return unpromise(ret, result => {
                    if (result instanceof Pair) {
                        return result.markCycles();
                    }
                    return result;
                });
            } else if (typeof value !== 'function') {
                if (value) {
                    var msg = `${type(value)} \`${value}' is not a function`;
                    throw new Error(msg);
                }
                throw new Error(`Unknown function \`${first.name}'`);
            }
        } else if (typeof first === 'function') {
            value = first;
        }
        if (typeof value === 'function') {
            var args = getFunctionArgs(rest, eval_args);
            return unpromise(args, function(args) {
                var scope = dynamic_scope || env;
                var result = resolvePromises(value.apply(scope, args));
                return unpromise(result, (result) => {
                    if (result instanceof Pair) {
                        return quote(result.markCycles());
                    }
                    return result;
                }, error);
            });
        } else if (code instanceof Symbol) {
            value = env.get(code);
            if (value === 'undefined') {
                throw new Error('Unbound variable `' + code.name + '\'');
            }
            return value;
        } else if (code instanceof Pair) {
            value = first && first.toString();
            throw new Error(`${type(first)} ${value} is not a function`);
        } else {
            return code;
        }
    } catch (e) {
        error && error(e, code);
    }
}

NOTES:

// unpromise and resolvePromises is just used ot unwrap any promise
// inside list and return new promise for whole expression if found
// any promise and not found it just return value as is
// markCycles is used to prevent of recursive printing of list cycles
// if you create graph cycles using `set-cdr!` or `set-car!`

Do I need to create stack when evaluating expression for continuations? How can I do this? I thought that I create somehow class Continuation that will be in two modes one in filled when it can be called like Macro and in other waiting to be filled by evaluate with code that need to be executed, I'm not sure also how should I go about and not evaluate code that is before expression that call continuation like:

(* 10 (cont 2))

(* 10 x) need to be ignored

I'm also not sure how should I go about and create call/cc as function. Should it return some intermediate data structure with it's argument stored in that data structure so it can be called by evaluate with continuation?

'call/cc': function(lambda) {
   return new CallCC(lambda);
}

and if eval find instance of CallCC it get continuation (not sure how yet) use

if (value instanceof CallCC) {
   value.call(new Continuation(stack));
}

Is this how you will do this? So generally my question is about the stack. Is it needed for continuations? If it's needed, then how it should be created?

I've found this article Writing a Lisp: Continuations that show how to implement continuations, but it's hard to understand because it's in Haskell.

jcubic
  • 61,973
  • 54
  • 229
  • 402
  • Found this short implementation of Scheme like lisp in JavaScript called [Nconc](https://github.com/patrickdlogan/nconc/blob/develop/public/scripts/nconc.js). – jcubic Jun 06 '19 at 15:37
  • [Matt Might to the rescue](http://matt.might.net/articles/by-example-continuation-passing-style/)? I don't think you can so it without changing the surface code in an interpreter. Then you'll need `call/cc` also on JS. Generators are pretty close though. – Sylwester Jun 06 '19 at 20:34
  • The link to `Writing a Lisp: Continuations` is dead, here's an archive: https://web.archive.org/web/20170719132012/http://www.reinvanderwoerd.nl/blog/2017/05/16/writing-a-lisp-continuations/ – n0099 Mar 10 '22 at 20:07

3 Answers3

4

One way to implement continuations in an interpreter is to make that interpreter use its own explicit stack for function calling/returning and parameter passing. If you use the stack of the host language, and that language doesn't have continuations, then things are difficult.

If you use your own explicit stack, you can turn it into a "spaghetti stack" for continuations. A "spaghetti stack" is very similar to common representations of lexical environments. It contains frames, which point to parent frames. Capturing a continuation amounts to retaining a pointer to such a frame, and some code. Resuming a continuation means, more or less, restoring the stack to that frame, and the execution to that point in the code. The interpreter for the language doesn't recurse. When the interpreted language nests or recurses, the interpreter iterates and just pushes and pops the explicit stack to keep track of state.

An alternative approach is to use a linear stack, but copy it when a continuation is made. To resume a continuation, you can restore the entire stack from the copied snapshot. This is useful for delimited continuations, which can avoid copying the entire stack, but only that part of it that is delimited (and restore it on top of the existing stack, rather than by replacement). I have implemented delimited continuations in a language that uses the underlying C stack. A segment of the C stack is memcpy-d out into an object that lives on the heap. When a continuation is restored, that saved stack segment is blasted on top of the current stack. Pointers have to be adjusted, of course, because the segment is now at a different address, and the "dangling cabling" has to be hooked up to integrate that stack segment into the stack properly.

If a language is treated by compilation to CPS (continuation passing style), then continuations pop out for free. Every function has an implicit hidden argument: the continuation it has received. Function returning is compiled into a call to this continuation. If a block of code in the function needs to compute the current continuation, it just has to compose a small local computational future (representable as a lambda) with that incoming continuation (the future computation that happens when the local part returns). Henry Baker wrote a paper based on the observation that under CPS, since no function ever returns (returns compile to tail calls to the continuation), old stack frames are never revisited. The stack can just be allowed to grow, and when it reaches a limit, it can be "rewound" back to the top. Chicken Scheme implements the concept; it's worth investigating if you're interested in continuations. Chicken Scheme compiles to C code that uses the regular C stack. However, the generated functions never return: they simulate returning by calling continuations, and so the stack grows. What is even more fascinating is that objects which we normally understand as dynamic material are allocated from the stack also. Since nothing returns, those objects are safe. Whenever a certain stack limit is reached, all of the objects on the stack which are still live are moved to the heap, and the stack pointer is rewound back to the top.

Kaz
  • 55,781
  • 9
  • 100
  • 149
2

First off. All languages has continuations. When you do 7 + n * 5 JavaScript reorders that to mul_k(n, 5, (_v) => (add _v 7 k) where k is a function that does everything that happnes after that expression.

Now the first argument to mul_k is a continuation. Nothing scary about it except for one little think. You never actually return anything any longer. Every "return" is just passed to it's continuation which always is a tail call.

A recursive function that itself isn't tail recursive will produce a new closure at each step and nest the next. These are are stored on the heap so non tail recursive functions becomes tail calls with lots of nested closures created.

Here is a small example:

(define (get-c) (call/cc (lambda (cont) cont)))

(let ((example (get-c)))
  (displayln example)
  (if (procedure? example)
      (example 10)
      "done"))

Lets imagine this is the whole program. Lets just write this to JavaScript.

// simple CPS version of displayln
const displayln = (arg, k) k(console.log(arg));

// simple CPS version of procedure?
const isProcedure = (arg, k) k(arg instanceOf Function);

// Simple CPS version of call/cc
const callCC = (userFun, callCCK) 
  => userFun((result, actualK) => callCCK(result), callCCK);

// simple top level continutation. Not a CPS function.
const kTopLevel = console.log;

// the user function get-c transformed into CPS
const getC = getCK => callCC((cont, k) => k(cont), getCK);

// the let code transformed into CPS
getC((example) => // c1
  displayln(example, (_undefined) => // c2 
    isProcedure(example, (result) => // c3
      result ? example(10, kTopLevel) : kTopLevel("done"))

Here is what's happening:

  • getC gets the whole continuation (c1) and it calls callCC
  • callCC calls c1 with gets (result, _) => c1(result) as example
  • display prints example, the continuation, passes its undefinded return value to it's continuation c2
  • isPorcedure on example and it being a continuation it passes true as result to continuation c3
  • being true it calls example(10, kTopLevel) which becomes c1(10), thus 10 as example
  • display prints èxample, the number10, passes its underfined return value to its continuationc2`
  • isProcedure on example passes false as result to continuation c3
  • Being false it calls TopLevel("dome")
  • TopLevel prints "done"

As you can see. call/cc is easy peasy as long as the code is transformed to CPS before execution. JavaScript supports CPS very well.

To help with how to convert code to CPS Matt Might has done it countless times in his essays. Look for continuations in that list. He has even done it in JavaScript.

Now if your implementation needs to beinterpreted you could do the same conversion in Scheme and interpret that instead of user code. If you have continuation barriers you just need CPS from call/cc until the barrier.

Sylwester
  • 47,942
  • 4
  • 47
  • 79
0

synchronous call/cc

callcc can be implemented in direct style using try-catch. The important bit is that the continuation must "box" the return value and "unbox" it in catch to avoid swallowing real errors -

const callcc = f => {
  class Box { constructor(v) { this.unbox = v } }
  try { return f(value => { throw new Box(value) }) }
  catch (e) { if (e instanceof Box) return e.unbox; throw e  }
}

console.log(5 + callcc(exit => 10 * 3))
console.log(5 + callcc(exit => 10 * exit(3)))
console.log(5 + callcc(exit => { exit(10); return 3 }))
console.log(5 + callcc(exit => { exit(10); throw Error("test failed") }))

try {
  console.log(5 + callcc(exit => { throw Error("test passed!") }))
}
catch (e) {
  console.error(e)
}
.as-console-wrapper { min-height: 100%; top: 0; }
35                     ✅ 5 + 10 * 3
8                      ✅ 5 + 3
15                     ✅ 5 + 10
15                     ✅ 5 + 10
Error: test passed!    ✅ Errors are not swallowed

callcc was originally implemented in this Q&A. Read on for additional info and examples.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Interesting but I don't think that it will be full Scheme call/cc that can be called multiple times and assigned to variable scope outside and call later to re-rerun the code. I don't think it will be of any use to me, but maybe someone finds it usefull. – jcubic May 01 '22 at 07:55
  • that detail is discussed in the linked Q&A. I have another implementation of first-class continuations in [this Q&A](https://stackoverflow.com/a/57813405/633183) that allows the continuation to be called multiple times. – Mulan May 01 '22 at 16:15
  • Your answer is not really an answer because it's not even close to real Scheme call/cc in JS, even Kawa call/cc that doesn't allow multiple calls are 100% better than this, sorry but you can't do much with your code, it's basically an implementation of return as function, nothing more. But the last link looks promising. – jcubic May 01 '22 at 18:21
  • I have really hard time to implement factorial function. I'm keep getting infinite loop detected (NodeJS freezes so I guess it's infinite loop). I just want to test if something like this scheme function can be implemented `(define k 0) (define (! x acc) (call/cc (lambda (c) (set! k c) (if (< x 1) acc (! (- x 1) (* x acc))))))` https://codepen.io/jcubic/pen/qBxBydr?editors=0011 – jcubic May 01 '22 at 20:22
  • @jcubic mechanics for full-stack `callcc` is slightly different than delimited-stack continuations, `shift` and `reset`. `callcc` can easily be implemented in the linked Q&A but maybe you can fix your example Scheme program first? `k` is assigned to a number then later assigned to a continuation. Neither `k` nor `!` are applied. What does execution look like and what is the expected output? See also [delimited continuations](https://en.wikipedia.org/wiki/Delimited_continuation) and O. Kiselyov's [crticism of call/cc](https://en.wikipedia.org/wiki/Call-with-current-continuation#Criticism). – Mulan May 02 '22 at 15:42
  • Sure I've just expected that is trivial `(define k 0) (define (! x acc) (call/cc (lambda (c) (set! k c) (if (< x 1) acc (! (- x 1) (* x acc)))))) (display (! 10 1)) (newline) (display (k 5)) (newline)` it doesn't matter what the variable contain before assigning the continuation in dynamic language. It's the exactly the same as `(define k '())` but you can use that if you're type purist. – jcubic May 02 '22 at 16:09
  • I think that I've read about criticism of call/cc but it doesn't matter, it's part of the Scheme, any implementation that says to be Scheme needs to have continuations, it's required by the standard. This question is old by now I aim to implement the full Scheme standard. – jcubic May 02 '22 at 16:13
  • sorry, i only meant to differentiate stack-limited continuations from full-stack `callcc` and provide a bit of surrounding info. i'll try to make time over the next few days and add a `callcc` demo. – Mulan May 02 '22 at 18:35