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

Improve output of for await #15943

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

liuxingbaoyu
Copy link
Member

@liuxingbaoyu liuxingbaoyu commented Sep 8, 2023

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

@liuxingbaoyu liuxingbaoyu added the PR: Output optimization 🔬 A type of pull request used for our changelog categories label Sep 8, 2023
@babel-bot
Copy link
Collaborator

babel-bot commented Sep 8, 2023

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

} finally {
try {
if (ITERATOR_ABRUPT_COMPLETION && ITERATOR_KEY.return != null) {
if (!STEP_KEY.done && ITERATOR_KEY.return != null) {
Copy link
Member

Choose a reason for hiding this comment

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

This accesses .done on the object twice, which is observable if it is a getter.

Copy link
Member

Choose a reason for hiding this comment

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

Can we do something like this?

  async function wrapper() {
    try {
      for (
        var ITERATOR_KEY = GET_ITERATOR(OBJECT), STEP_KEY, NOT_DONE_KEY;
        NOT_DONE_KEY = !(STEP_KEY = await ITERATOR_KEY.next()).done;
      ) {
      }
    } finally {
      try {
        if (NOT_DONE_KEY && ITERATOR_KEY.return != null) {
          await ITERATOR_KEY.return();
        }
      } catch (e) {}
    }
  }

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to set NOT_DONE_KEY to false after the loop ends to avoid executing return() when it throws in next().

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

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

Can you check if this test behaves properly:

function* gen() {
  try {
    yield 1;
  } finally {
    throw 2;
  }
}

let err;
try {
  for await (const _ of gen()) break;
} catch (e) {
  err = e;
}

expect(err).toBe(2);

@liuxingbaoyu liuxingbaoyu added PR: Bug Fix 🐛 A type of pull request used for our changelog categories PR: Output optimization 🔬 A type of pull request used for our changelog categories and removed PR: Output optimization 🔬 A type of pull request used for our changelog categories PR: Bug Fix 🐛 A type of pull request used for our changelog categories labels Sep 9, 2023
@liuxingbaoyu
Copy link
Member Author

liuxingbaoyu commented Sep 9, 2023

Yes, this PR breaks the test.
After fixing it it only saved two variables. 🤦‍♂️
But luckily our testing is now better. :)

@nicolo-ribaudo
Copy link
Member

We should submit that test to test262, since it didn't catch the bug!

var ITERATOR_HAD_ERROR_KEY = false;
var ITERATOR_ERROR_KEY;
const buildForAwait = template.statements(`
var ITERATOR_KEY = GET_ITERATOR(OBJECT), STEP_KEY = {}, NOT_DONE_KEY;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an upside to initializing STEP_KEY here? It'll be overwritten before use.

Copy link
Member Author

Choose a reason for hiding this comment

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

Now it's no longer needed, thank you!

}
} catch (e) {
if (STEP_KEY) throw e;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (STEP_KEY) throw e;
if (STEP_KEY != null) throw e;

.next() might set Boolean.prototype.done to true and return false :)

Could you also rename _notDone to _iteratorAbruptCompletion, to reduce the diff and make it easier to follow the changes?

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: .next() must return an Object; otherwise agree explicit null-check looks clearer.

ForIn/OfBodyEvaluation

6.c. If nextResult is not an Object, throw a TypeError exception.

Copy link
Member Author

Choose a reason for hiding this comment

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

.next() might set Boolean.prototype.done to true and return false :)

I'm not sure if it's worth doing, it's really weird usage. :)

Could you also rename _notDone to _iteratorAbruptCompletion, to reduce the diff and make it easier to follow the changes?

Since I completely changed the position of variable declarations and the logic of throwing exceptions, the changes may not be less, _notDone is slightly shorter and easier to read in the output.

var _iteratorAbruptCompletion = false;
var _didIteratorError = false;
var _iteratorError;
var _iterator = babelHelpers.asyncIterator(y),
Copy link
Member

Choose a reason for hiding this comment

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

If calling y[Symbol.asyncIterator]() throws, does this still behave the same?

} finally {
try {
if (_iteratorAbruptCompletion && _iterator.return != null) {
if (_notDone && _iterator.return) {
Copy link
Member

Choose a reason for hiding this comment

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

If _iterator.return is falsy but not nullish, we should still call it to get the proper TypeError.

Copy link
Contributor

Choose a reason for hiding this comment

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

Only undefined should skip the call.

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

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

I have been thinking about this more, and I think we can avoid the catch block. Does this code lead to smaller minified size, and still passes all the tests?

const buildForAwait = template.statements(`
  var ITERATOR_KEY = GET_ITERATOR(OBJECT), STEP_KEY, NOT_DONE_KEY, ITERATOR_HAD_NO_ERROR_KEY;
  try {
    for (
      ;
      NOT_DONE_KEY = !(STEP_KEY = await ITERATOR_KEY.next()).done;
      NOT_DONE_KEY = false
    ) {
    }
    ITERATOR_HAD_NO_ERROR_KEY = true;
  } finally {
    try {
      if (NOT_DONE_KEY && ITERATOR_KEY.return) {
        await ITERATOR_KEY.return();
      }
    } catch (e) {
      if (!ITERATOR_HAD_NO_ERROR_KEY) throw e;
    }
  }
`);

@nicolo-ribaudo
Copy link
Member

It might have problems if iterator.return() throws and the for-await-of body uses return 🤔

@lightmare
Copy link
Contributor

It might have problems if iterator.return() throws and the for-await-of body uses return 🤔

Yes, to follow the spec, you need to know whether the loop body completed with

  • a) throw => that exception propagates, and exception from iterator.return() is ignored
  • b) return / break outer / continue outer => exception from iterator.return() propagates

And the only way to tell the difference, is to catch the exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: Output optimization 🔬 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants