Single frame continuations using yield keyword with generators compatibility proposal

Kris Zyp kris at sitepen.com
Thu Apr 1 07:15:37 PDT 2010


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
 
David Herman's comment about JS1.7 generators spurred me to consider
if we could achieve the goals stated for single-frame continuations as
a broadly useful mechanism for replacing CPS code with more natural
flow for constructs like promises and one-time event handlers *and*
preserve the syntax of and compatibility with JS1.7 generators. At the
risk of creating confusion for a somewhat already complicated concept,
I wanted to put forth alternate proposal for single-frame
continuations. The tenants here are:

* Use of the "yield" keyword as the syntax addition for capturing
continuations
* Can default to behaving like JS1.7 generators
* Still maintain the principles of avoiding multi-frame stack
splitting with interleaving hazards and VM implementation burden
* Alternate continuation handlers can be used that control the
continuation of a function can be employed for other purposes. Unlike
JS1.7 generators, code would could have the ability to:
** Begin execution of the body of the function when it is called
** Recreate call stacks
** Yielding functions can exit through a normal return (using the
return operator to return a value).

The key idea of this approach is that when a function is called that
contains a yield operator, rather than following a hard-coded
prescription to return an generator/iterator object, this triggers a
call to the startCoroutine variable (from the current lexical scope)
that can implement various different behaviors, including immediately
executing the function and handling various values differently, or
returning an iterator. The startCoroutine function would be called
with a controller object that provides a "resume" function and a
"suspended" boolean property, that can be used by the startCoroutine
to resume execution, get the result of the function exiting through
yield and return operators and determine when the function is complete
(if it exited with a return). In addition the engine could define a
global startCoroutine variable such that returning iterators would be
the default behavior of a yielding functions, but this could easily be
overriden (globally or in the local scope).

This ultimately could be used a similar fashion as the other proposal,
but with slightly different syntax (use of "yield" instead "->()") like:

startCoroutine = ... some promise style library's handler ...
showData = function(){
  var data = yield xhrGet({url:"my-data"}); // async
  if(data.additionalInformation){
    // there is additional data we need to get, load it too
    data.additionalData = yield
xhrGet({url:data.additionalInformation}); // async
  }
  someElement.innerHTML = template.process(data);
};


Here is the definition, adapted from some of David Herman's
corrections of my previous algorithm (hopefully I did a little better
this time):

semantics of a function that contains the yield keyword:
    1. Define a *controller* object with a "suspended" property set to
true.
    2. Set the "resume" property to be a function defined as *resume*.
    3. Let *k* be the current function continuation (including code,
stack frame, and exception handlers).
    4. Let *startCoroutine* be the result of looking up
"startCoroutine" variable starting in the current scope of the function
    5. Call *startCoroutine* passing the *controller* object
    6. Let *returned* be the value returned from the call to
startCoroutine.
    7. Return *returned* from this function

semantics of calling *resume* with argument *v*:
    1. Create a new function activation using the stack chain from the
point of capture.
    2. Reinstall the function activation's exception handlers from the
point of capture (on top of the exception handlers of the rest of the
stack that has created *k*).
    3. Call *v* with no arguments at the completion of the point of
capture.
    4. Continue executing from the point of capture using the result
of the call to *v*.
    5. If a yield operator is encountered proceed to the semantics of
"yield" <expr>.
    6. If a return operator is encountered, set the *controller*
object's "suspended" property set to false.
    7. Return the value of evaluated expression provided to the return
operator from the initiating call to *resume*.

semantics of "yield" <expr>:

    1. Evaluate the expression and store the result as *result*.
    2. Let *k* be the current function continuation (including code,
stack frame, and exception handlers).
    3. Exit the current activation.
    4. Return *result* from the initiating call to *resume*

An example of an equivalent translation, first a function that uses a
yield operator:
function foo(){
  return (yield bar()) + 2;
}

Would effectively act like the following ES5 code:
function foo(){
  var $pointer = 0; // used to keep track of where we are
  // create the controller
  var $controller = {
    suspended: true,
    resume: function(getValue){
      var nextValue;
      if(getValue){
        nextValue = getValue();
      }
      if($pointer == 0){
        // start of function
        $pointer++;
        // execute until the yield operator and return the value
passed to yield
        return bar();
      }
      if($pointer == 1){
        // continuation from the yield
        $pointer++;
        // continue execution until the return operator
        $controller.suspended = false; // no more yield, function is done
        return nextValue + 2;
      }
      if($pointer > 1){
        throw new Error("Can not resume a function that has returned");
      }
    }
  };
  // call the startCoroutine function
  return startCoroutine($controller);
}

And we can define the global variable startCoroutine such that
functions with a yield operator act like JS1.7 generators by default:

// Default implementation of startCoroutine, returns JS1.7 generators
startCoroutine = function(controller){
  // return a JS1.7 iterator
  return {
    send: function(value){
      var nextValue = controller.resume(function(){
        return value;
      });
      if(!controller.suspended){
        throw StopIteration;
      }
      return nextValue;
    },
    throws: function(error){
      controller.resume(function(){
        throw error;
      });
    },
    next: function(){
      var nextValue = controller.resume();
      if(!controller.suspended){
        throw StopIteration;
      }
      return nextValue;
    }
  };
};

A JavaScript module that wanted the behavior of the previous proposal
could easily define startCoroutine in local (or global) variable and
implement it oneself.

One potential drawback to this proposal is that it creates some
contention for the startCoroutine variable (at least at the global
level). Originally, I was thinking of having startCoroutine be a
property of the calling function (default behavior defined at
Function.prototype.startCoroutine), but having the behavior defined
through lexical scope seems like the most appropriate level of
specificity and ease of use.

Of course the advantage of this proposal is that it is compatible with
JS1.7 generators, and utilizes an existing ES5 strict-mode future
reserved keyword (presumably for what it is actually intended for).

Anyway, I am curious if this would be a more desirable approach for an
evolutionary addition to EcmaScript.

Thanks,
Kris
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
 
iEYEARECAAYFAku0qogACgkQ9VpNnHc4zAwczgCgpZaF0xMT8Sy5EUY9Pdc/Xqd9
CtYAoIOUNcwjybbr6Pve9+ZmIsbQe5LH
=I7y7
-----END PGP SIGNATURE-----



More information about the es-discuss mailing list