Transform async functions and await expressions #101

Merged
merged 4 commits into from Sep 11, 2014

Conversation

Projects
None yet
9 participants
@benjamn
Collaborator

benjamn commented Apr 3, 2014

This functionality depends on my async-await branch of the Esprima parser (pull request: https://github.com/ariya/esprima/pull/234).

The wrapGenerator.async function is heavily inspired by @lukehoban's spawn function: https://github.com/lukehoban/ecmascript-asyncawait#spawning

Note that Promise is left as a free variable, so you have to bring your own Promise polyfill.

Adding a bunch of tests for this functionality is my next order of business.

cc @amasad @thomasboyt @subtleGradient @bklimt @lukehoban @arv

@benjamn

This comment has been minimized.

Show comment
Hide comment
@benjamn

benjamn Apr 3, 2014

Collaborator

And here's an example of the transform in action (borrowed from here):

async function chainAnimationsAsync(elem, animations) {
  var ret = null;
  try {
    for (var anim in animations) {
      ret = await anim(elem);
    }
  } catch(e) { /* ignore and keep going */ }
  return ret;
}

becomes

function chainAnimationsAsync(elem, animations) {
  var ret, anim;

  return wrapGenerator.async(function chainAnimationsAsync$($ctx0) {
    while (1) switch ($ctx0.prev = $ctx0.next) {
    case 0:
      ret = null;
      $ctx0.prev = 1;
      $ctx0.t0 = $ctx0.keys(animations);
    case 3:
      if (($ctx0.t1 = $ctx0.t0()).done) {
        $ctx0.next = 10;
        break;
      }

      anim = $ctx0.t1.value;
      $ctx0.next = 7;
      return anim(elem);
    case 7:
      ret = $ctx0.sent;
      $ctx0.next = 3;
      break;
    case 10:
      $ctx0.next = 14;
      break;
    case 12:
      $ctx0.prev = 12;
      $ctx0.t2 = $ctx0.catch(1);
    case 14:
      return $ctx0.abrupt("return", ret);
    case 15:
    case "end":
      return $ctx0.stop();
    }
  }, this, [[1, 12]]);
}
Collaborator

benjamn commented Apr 3, 2014

And here's an example of the transform in action (borrowed from here):

async function chainAnimationsAsync(elem, animations) {
  var ret = null;
  try {
    for (var anim in animations) {
      ret = await anim(elem);
    }
  } catch(e) { /* ignore and keep going */ }
  return ret;
}

becomes

function chainAnimationsAsync(elem, animations) {
  var ret, anim;

  return wrapGenerator.async(function chainAnimationsAsync$($ctx0) {
    while (1) switch ($ctx0.prev = $ctx0.next) {
    case 0:
      ret = null;
      $ctx0.prev = 1;
      $ctx0.t0 = $ctx0.keys(animations);
    case 3:
      if (($ctx0.t1 = $ctx0.t0()).done) {
        $ctx0.next = 10;
        break;
      }

      anim = $ctx0.t1.value;
      $ctx0.next = 7;
      return anim(elem);
    case 7:
      ret = $ctx0.sent;
      $ctx0.next = 3;
      break;
    case 10:
      $ctx0.next = 14;
      break;
    case 12:
      $ctx0.prev = 12;
      $ctx0.t2 = $ctx0.catch(1);
    case 14:
      return $ctx0.abrupt("return", ret);
    case 15:
    case "end":
      return $ctx0.stop();
    }
  }, this, [[1, 12]]);
}
@benjamn

This comment has been minimized.

Show comment
Hide comment
@benjamn

benjamn Apr 3, 2014

Collaborator

Regenerator doesn't support arrow generator functions yet, because there's no agreed-upon syntax for them, but I definitely intend to add support for async arrow functions.

Collaborator

benjamn commented Apr 3, 2014

Regenerator doesn't support arrow generator functions yet, because there's no agreed-upon syntax for them, but I definitely intend to add support for async arrow functions.

runtime/dev.js
+
+ function step(arg) {
+ try {
+ var info = this(arg);

This comment has been minimized.

@amasad

amasad Apr 3, 2014

Contributor

nit: calling this looks awkward, why not just bind to the first arg instead?

@amasad

amasad Apr 3, 2014

Contributor

nit: calling this looks awkward, why not just bind to the first arg instead?

This comment has been minimized.

@benjamn

benjamn Apr 3, 2014

Collaborator

Just trying to shave bytes, that's all.

@benjamn

benjamn Apr 3, 2014

Collaborator

Just trying to shave bytes, that's all.

runtime/dev.js
+ var info = this(arg);
+ var value = info.value;
+ } catch (error) {
+ return reject(error);

This comment has been minimized.

@amasad

amasad Apr 3, 2014

Contributor

Is this actually returning a value? or just early return

@amasad

amasad Apr 3, 2014

Contributor

Is this actually returning a value? or just early return

This comment has been minimized.

@benjamn

benjamn Apr 3, 2014

Collaborator

Just an early return.

@benjamn

benjamn Apr 3, 2014

Collaborator

Just an early return.

@amasad

This comment has been minimized.

Show comment
Hide comment
@amasad

amasad Apr 3, 2014

Contributor

nice! I thought it's going to take a lot more code to implement.
Excited about this! 👍

Contributor

amasad commented Apr 3, 2014

nice! I thought it's going to take a lot more code to implement.
Excited about this! 👍

@@ -15,7 +15,7 @@ var transform = require("./lib/visit").transform;
var utils = require("./lib/util");
var recast = require("recast");
var types = recast.types;
-var genFunExp = /\bfunction\s*\*/;
+var genOrAsyncFunExp = /\bfunction\s*\*|\basync\b/;

This comment has been minimized.

@amasad

amasad Apr 3, 2014

Contributor

"async" is more likely to occur in JS codebases, e.g. the popular library. Maybe 'await' is better to check for?

@amasad

amasad Apr 3, 2014

Contributor

"async" is more likely to occur in JS codebases, e.g. the popular library. Maybe 'await' is better to check for?

This comment has been minimized.

@jeffmo

jeffmo Apr 3, 2014

Member

You can have an async function that has no awaits in it -- but you probably could get away with being more specific a la /\bfunction\s*\*|\basync\s+function\b/

@jeffmo

jeffmo Apr 3, 2014

Member

You can have an async function that has no awaits in it -- but you probably could get away with being more specific a la /\bfunction\s*\*|\basync\s+function\b/

runtime/dev.js
+ if (info.done) {
+ resolve(value);
+ } else {
+ Promise.cast(value).then(callNext, callThrow);

This comment has been minimized.

@jeffmo

jeffmo Apr 3, 2014

Member

Promise.cast is no more -- only Promise.resolve

Quick summary on the latest-ness: http://esdiscuss.org/topic/the-promise-resolve-cast-tldr

@jeffmo

jeffmo Apr 3, 2014

Member

Promise.cast is no more -- only Promise.resolve

Quick summary on the latest-ness: http://esdiscuss.org/topic/the-promise-resolve-cast-tldr

lib/visit.js
+
+ if (n.AwaitExpression.check(node)) {
+ this.replace(b.yieldExpression(
+ node.all ? b.callExpression(

This comment has been minimized.

@jeffmo

jeffmo Apr 3, 2014

Member

Interesting -- I assume node.all is experimental for something like await* ?

@jeffmo

jeffmo Apr 3, 2014

Member

Interesting -- I assume node.all is experimental for something like await* ?

This comment has been minimized.

lib/visit.js
+ if (n.AwaitExpression.check(node)) {
+ this.replace(b.yieldExpression(
+ node.all ? b.callExpression(
+ b.memberExpression(

This comment has been minimized.

@jeffmo

jeffmo Apr 3, 2014

Member

If you have the following:

var result = await someAsyncFunction();

Shouldn't the value of result be the value of the promise (and not the promise itself)?

If I'm reading this correctly, this desugars the above to:

var result = yield someAsyncFunction();

Which means I'd still have to do result.then(), right?

@jeffmo

jeffmo Apr 3, 2014

Member

If you have the following:

var result = await someAsyncFunction();

Shouldn't the value of result be the value of the promise (and not the promise itself)?

If I'm reading this correctly, this desugars the above to:

var result = yield someAsyncFunction();

Which means I'd still have to do result.then(), right?

This comment has been minimized.

@jeffmo

jeffmo Apr 3, 2014

Member

Oh, derp -- nevermind. You're wrapping the whole body of the generator and next()ing the result in the runtime

@jeffmo

jeffmo Apr 3, 2014

Member

Oh, derp -- nevermind. You're wrapping the whole body of the generator and next()ing the result in the runtime

lib/visit.js
@@ -144,3 +160,25 @@ function renameArguments(funcPath, argsId) {
// function's arguments object to the variable named by argsId.
return didReplaceArguments && hasImplicitArguments;
}
+
+function renameAwaitToYield(bodyPath) {

This comment has been minimized.

@arv

arv Apr 7, 2014

You need to add some parentheses here since the precedence for yield expression is different than await expressions:

await E1 + await E2

needs to be wrapped in parens like this:

(yield E1) + (yield E2)

or the semantics becomes:

yield (E1 + (yield E2))
@arv

arv Apr 7, 2014

You need to add some parentheses here since the precedence for yield expression is different than await expressions:

await E1 + await E2

needs to be wrapped in parens like this:

(yield E1) + (yield E2)

or the semantics becomes:

yield (E1 + (yield E2))

This comment has been minimized.

@benjamn

benjamn Apr 7, 2014

Collaborator

This is an excellent point, but I'm happy to say that recast.print knows to add parentheses when necessary, at least according to this experiment:

var r = require("recast");
var b = r.types.builders;
r.print(
  b.binaryExpression(
    "+",
    b.yieldExpression(b.literal(1), false),
    b.yieldExpression(b.literal(2), false)
  )
);

which prints (yield 1) + (yield 2).

@benjamn

benjamn Apr 7, 2014

Collaborator

This is an excellent point, but I'm happy to say that recast.print knows to add parentheses when necessary, at least according to this experiment:

var r = require("recast");
var b = r.types.builders;
r.print(
  b.binaryExpression(
    "+",
    b.yieldExpression(b.literal(1), false),
    b.yieldExpression(b.literal(2), false)
  )
);

which prints (yield 1) + (yield 2).

@arv

This comment has been minimized.

Show comment
Hide comment
@arv

arv Apr 7, 2014

Very nice

arv commented Apr 7, 2014

Very nice

@sophiebits

This comment has been minimized.

Show comment
Hide comment
@sophiebits

sophiebits Apr 8, 2014

Member

This looks so awesome.

Member

sophiebits commented Apr 8, 2014

This looks so awesome.

@lukehoban

This comment has been minimized.

Show comment
Hide comment
@lukehoban

lukehoban Apr 8, 2014

This is awesome.

I tried a more complex example, and ran into a problem with the handling of this:

var headers;
async function foo() {
}

This compiles to the following:

var headers;
(function foo() {
  return wrapGenerator.async(function foo$($ctx0) {
    while (1) switch ($ctx0.prev = $ctx0.next) {
    case 0:
    case "end":
      return $ctx0.stop();
    }
  }, this);
})

Note the extra parentheses added around the function declaration, turning it into a function expression and not binding a global variable to the name foo.

This only happens when a statement like var headers appears before the function declaration.

I expect this is an issue in the esprima additions for await. I tried a similar example with generator functions and do not see this issue.

This is awesome.

I tried a more complex example, and ran into a problem with the handling of this:

var headers;
async function foo() {
}

This compiles to the following:

var headers;
(function foo() {
  return wrapGenerator.async(function foo$($ctx0) {
    while (1) switch ($ctx0.prev = $ctx0.next) {
    case 0:
    case "end":
      return $ctx0.stop();
    }
  }, this);
})

Note the extra parentheses added around the function declaration, turning it into a function expression and not binding a global variable to the name foo.

This only happens when a statement like var headers appears before the function declaration.

I expect this is an issue in the esprima additions for await. I tried a similar example with generator functions and do not see this issue.

@lukehoban lukehoban referenced this pull request in tc39/ecmascript-asyncawait Apr 8, 2014

Merged

Use regenerator #11

@lukehoban

This comment has been minimized.

Show comment
Hide comment
@lukehoban

lukehoban Apr 8, 2014

A larger sample working with this is here: https://github.com/lukehoban/ecmascript-asyncawait/blob/regenerator/server.asyncawait.js as part of a branch of the async/await proposal to try to switch to using this regenerator branch for the sample instead of sweet.js macros.

A larger sample working with this is here: https://github.com/lukehoban/ecmascript-asyncawait/blob/regenerator/server.asyncawait.js as part of a branch of the async/await proposal to try to switch to using this regenerator branch for the sample instead of sweet.js macros.

@benjamn

This comment has been minimized.

Show comment
Hide comment
@benjamn

benjamn Apr 8, 2014

Collaborator

Yep, I can confirm that this is related to my Esprima changes, and Recast is just trying to respect the wrong FunctionExpression type by adding the parentheses.

Will investigate.

Collaborator

benjamn commented Apr 8, 2014

Yep, I can confirm that this is related to my Esprima changes, and Recast is just trying to respect the wrong FunctionExpression type by adding the parentheses.

Will investigate.

@benjamn

This comment has been minimized.

Show comment
Hide comment
@benjamn

benjamn Apr 8, 2014

Collaborator

@lukehoban Fixed this bug by adding benjamn/esprima@b714306 to my https://github.com/ariya/esprima/pull/234 pull request. In short, I was misusing peekLineTerminator() in matchAsync.

I also switched getCollaboratorImages back to a function declaration in your server.asyncawait.js file, and the transformation did not add parentheses to it.

Collaborator

benjamn commented Apr 8, 2014

@lukehoban Fixed this bug by adding benjamn/esprima@b714306 to my https://github.com/ariya/esprima/pull/234 pull request. In short, I was misusing peekLineTerminator() in matchAsync.

I also switched getCollaboratorImages back to a function declaration in your server.asyncawait.js file, and the transformation did not add parentheses to it.

@benjamn

This comment has been minimized.

Show comment
Hide comment
@benjamn

benjamn Apr 12, 2014

Collaborator

@lukehoban where did TC39 come down on await*? The notes seem to indicate they don't want it if there's no analogy to yield*, which is fine with me.

Collaborator

benjamn commented Apr 12, 2014

@lukehoban where did TC39 come down on await*? The notes seem to indicate they don't want it if there's no analogy to yield*, which is fine with me.

@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Aug 13, 2014

Hey everybody, great work @benjamn . I see no movement since April, how's the progress on this?
This is such a nice feature to have!

niieani commented Aug 13, 2014

Hey everybody, great work @benjamn . I see no movement since April, how's the progress on this?
This is such a nice feature to have!

@benjamn benjamn referenced this pull request in facebookarchive/esprima Sep 8, 2014

Closed

Support parsing async functions and await expressions #45

benjamn added a commit that referenced this pull request Sep 11, 2014

Merge pull request #101 from facebook/async-await
Transform async functions and await expressions.

@benjamn benjamn merged commit b130d87 into master Sep 11, 2014

1 check passed

continuous-integration/travis-ci The Travis CI build passed
Details
@sophiebits

This comment has been minimized.

Show comment
Hide comment
@sophiebits

sophiebits Sep 11, 2014

Member

Nice!

Member

sophiebits commented Sep 11, 2014

Nice!

@jayphelps

This comment has been minimized.

Show comment
Hide comment

matthewrobb referenced this pull request in esnext/esnext Sep 27, 2014

@chrisabrams

This comment has been minimized.

Show comment
Hide comment

👍

@Globegitter Globegitter referenced this pull request in esnext/esnext Nov 7, 2014

Closed

async await support #53

@dashed dashed referenced this pull request in pjeby/regenerator-loader Nov 14, 2014

Closed

Update peer dependencies #1

@benjamn benjamn referenced this pull request Jan 29, 2015

Closed

Support await* delegation #171

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment