Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for await with the pipeline operator #7154

Closed
wants to merge 7 commits into from

Conversation

ksmithbaylor
Copy link

Q                       A
Fixed Issues? #6889
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR
Any Dependency Changes?
License MIT

This correctly transforms AwaitExpressions which appear in pipelines, as shown here. It does not support standalone await keywords, as was the case in earlier versions of the proposal.

@babel-bot
Copy link
Collaborator

babel-bot commented Jan 4, 2018

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/6648/

@babel-bot
Copy link
Collaborator

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/6447/

@nicolo-ribaudo
Copy link
Member

Can you transform async functions in the tests? Otherwise they will fail on node <= 6

@ksmithbaylor
Copy link
Author

I updated the PR. Since I'd like to test both "pure" async functions and also those transformed to generators by transform-async-to-generator, I duplicated the test directory. I marked the original one as requiring Node 7.6.0 or above, and enabled the transform-async-to-generator plugin on the other one. This required some code changes, since this plugin should now handle yield expressions in addition to async.

It isn't clear to me that this is allowed according to the spec draft, however, and I expect that I could be doing the transformation more elegantly. This is my first time looking at the Babel source, and I'm still learning my way around. I'd appreciate feedback!

@@ -15,6 +15,8 @@ export default function() {

let optimizeArrow =
t.isArrowFunctionExpression(right) && t.isExpression(right.body);
const isAwait = t.isAwaitExpression(right);
const isYield = t.isYieldExpression(right);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foo |> yield bar shouldn't be transfrmed; it should become something like (_temp = yield bar, _temp(foo)).

If there isn't a way of transforming |> await foo before transforming the async function, you can workaround the problem by adding node.__wasAwait to yield expressions generated by the async functions plugin and then do

const isAwait = t.isAwaitExpression(right) || right.__wasAwait;

@ksmithbaylor
Copy link
Author

Alright, should be better now. I couldn't figure out a way to ensure that the pipeline operator plugin ran before the async-to-generator plugin, since it operates on a BinaryExpression which doesn't have a consistent parent to traverse down from. So I took your suggestion of adding a flag to await expressions that have been transformed to yield. I also added a test case that makes sure yield expressions aren't transformed.

@Andarist
Copy link
Member

Are special flags a common practice? I have not yet seen many of those and it seems a clunky solution (although might be necessary ofc). We really should add plugin ordering to solve such issues

@ksmithbaylor
Copy link
Author

@Andarist no, I don't really see any other examples of special flags either. I'd love to find a better way to do the check, since it does seem clunky like you said. This is my first time looking at the codebase, so I have lots of learning to do :)

As I understand it, there is some discussion around what it would look like to allow node visitors to be ordered more predictably, but a solution hasn't been decided on yet. If you know of an easier way to either force this plugin to run first or make sure yield expressions are only transformed if they were previously await expressions, I'd be open to suggestions.

@Andarist
Copy link
Member

Hm, Im also still learning and I dont know about all intricacies of babel transformations.

I thought that babel's visitor is "top down" and I would suspect that this BinaryExpression would be visited before AwaitExpression (as former is the parent). But it seems that this is not true here, right?

@ksmithbaylor
Copy link
Author

That's what I thought, but when I combined the pipeline plugin with the async-to-generator one, all the AwaitExpressions had already been transformed by the time the pipeline's BinaryExpression visitor was called.

@@ -13,6 +13,7 @@ const awaitVisitor = {

AwaitExpression(path, { wrapAwait }) {
const argument = path.get("argument");
argument.node.__wasAwait = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we wanted to stay away from using a hidden properties? Or we need a more formalized way to handle this kind of thing (like knowing if it was transformed or original source, and what it used to be)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Do you have any suggestions that could help either act as an indicator or force these two plugins to run in order that wouldn't require blocking until those issues are solved at a higher level?

Copy link
Member

@xtuc xtuc Mar 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately no, it's related to #5854

Inside the same plugin you can use a WeakMap/WeakSet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin ordering definitely makes this harder. Could we make this throw an error if the async function tries to transform an await that is inside of a pipeline expression, and then have the pipeline transform have a visitor for async functions that does an explicit sub-traversal for pipeline expressions? Then we could just make sure that the pipeline plugin runs before the async function transform and I think we'd be okay?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me, we don't need to order the plugins if they are both idenpendent. Each will transform what it needs.

return 42 |> (yield 10);
}

var fooGen;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to have this outside of the promise?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, nope. That was left-over from moving something around.

@@ -0,0 +1,10 @@
var incPromise = (x) => Promise.resolve(x + 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd prefer to have the same code in actual / exec if they are in the same directory, exec differs slightly right now and it makes it hard to compare those files (assert ofc should stay just in exec)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made them match now. I think I may have saved the file with Prettier on at some point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didnt mean that they do not match in sense of formatting - i think there was some differenc in how incFuncPromise was used

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, got it. I also changed the usage of that variable down below. Either way, they're the same now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

function* foo() {
var _3;

return _3 = 42, (yield incFuncPromise)(_3);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this intentional? looks weird, but i guess it could test something, but then exec should reflect that test

@Andarist
Copy link
Member

@ksmithbaylor regarding why AwaitExpression gets visited first - I think its because actually its not visited from a "top level" traversal - there is a traverse call from some other visitor and that's why whole subtree is visited first there (i guess traverse is called in async/generator function, which is a parent of that BinaryExpression). I have not tested this, but I believe that's the case.

@nicolo-ribaudo
Copy link
Member

@Andarist You are right: @babel/helper-remap-async-to-generator transforms await expressions by spawning a new traversal:

path.traverse(awaitVisitor, {
file,
wrapAwait: helpers.wrapAwait,
});

I think that if we transform async functions on exit this problem would go away, but probably it would make it be traversed one more time.

@Andarist
Copy link
Member

Andarist commented Jan 18, 2018

I think that if we transform async functions on exit this problem would go away, but probably it would make it be traversed one more time.

Why would it be traversed twice?

EDIT: oh yeah, content of the async function would get traversed twice, because after remapping it would get requeued, right?

@nicolo-ribaudo
Copy link
Member

Actually it is already traversed twice (the main traversal and the one spawned by the helper); it would be traversed three times.

oh yeah, content of the async function would get traversed twice, because after remapping it would get requeued, right?

Yes 👍

@@ -15,6 +15,8 @@ export default function() {

let optimizeArrow =
t.isArrowFunctionExpression(right) && t.isExpression(right.body);
const wasAwait = right.argument && right.argument.__wasAwait;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

const wasAwait = t.isYieldExpression(right) && right.argument && right.argument.__wasAwait;

?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea.

@@ -0,0 +1,21 @@
var incPromise = x => Promise.resolve(x + 1);
var incFuncPromise = Promise.resolve(x => x + 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed!

@ksmithbaylor
Copy link
Author

Does anyone have ideas on how to improve this without relying on special flags?

@mAAdhaTTah
Copy link
Contributor

mAAdhaTTah commented Mar 7, 2018

Wondering if it would be acceptable to land this w/o supporting transformed await and revist that issue in the future?

Also, I thought this had labels for Spec: Pipeline previously, but I don't see them, could they be added?

@mAAdhaTTah
Copy link
Contributor

So this PR may not actually be needed. We're going to ban await in the basic, "minimal proposal" spec here: tc39/proposal-pipeline-operator#108 and solve await in each of the proposals we're working on currently working on. This is not going to be the final semantics of pipeline in any of the proposals, given the ambiguity between whether x |> await f becomes await f(x) or (await f)(x), so I think we can just close this.

@ksmithbaylor
Copy link
Author

@mAAdhaTTah agreed!

@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 5, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 5, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue Spec: Pipeline Operator
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants