-
Notifications
You must be signed in to change notification settings - Fork 44
Conversation
is there any reason that actually it's a bit hard to implement in that exact order, and I'm wondering if those steps can be swapped. |
@arai-a Swapping them makes sense to me; once swapped, the logic would be strictly "do the await, then do the yield", which is what I would expect. Would the semantics here allow for a little more parallelism, if Editorial suggestion: if the Async Iterator Value Unwrap Functions are only called by the AsyncFromSyncIterators, maybe the name should reflect this, or at least it could be moved into that section. Otherwise, I don't see any issues with this patch (apart from the extra microtask item issue that @caitp noted in the other thread). |
given that
hmm, well AsyncGeneratorResolve used to create an extra task anyways, so I suppose it's not too different from before, apart from producing fatter code. |
@caitp We don't prohibit tons of other things that would be arbitrarily slow. For example, in an async function or generator, you can sprinkle async roughly anywhere you want, and most of the time the only important effect of that will be that your code runs slightly slower. I'm not sure if it makes sense to make this the place where we put our foot down and start on restrictions--maybe that's better for external tools. |
There may be an issue with let asyncGenerator = gen();
asyncGenerator.return(promise);
// AsyncGeneratorResumeNext step 9.b.i:
// state === "suspendStart"
// completed === true
// return ! AsyncGeneratorResolve(generator, completion.[[Value]] (/promise/), true)
// -> returned promise resolves to an Iterator Result object with the value being the
// Promise passed to .return() rather than the unwrapped value There are a few other ways to observe this: var resolve, reject;
var p = new Promise(function(resolve_, reject_) {
resolve = resolve_;
reject = reject_;
});
async function* g() {
return p;
}
var gen = g();
gen.next().then(
x => print(x.value), // [object Promise]
x => print(x));
resolve("floof"); I think https://tc39.github.io/proposal-async-iteration/#sec-asyncgeneratorstart needs to do unwrapping in step 5.g., and https://tc39.github.io/proposal-async-iteration/#sec-asyncgeneratorresumenext needs to do unwrapping in step 9.b.i., but can skip unwrapping in step 10. |
|
Updated to swap the steps and move/rename the value unwrap functions. Preview link also updated. Agreed we should not prohibit @caitp's trickier issue at #102 (comment) I haven't been able to process yet, but will make a top priority tonight or tomorrow. |
@caitp thanks for the detailed analysis and examples. After attempting to investigate other ways of tackling the problem just to be sure, I came to the conclusion your suggestions were exactly right, and pushed another commit to fix them. PTAL! |
spec/abstract-operations.html
Outdated
1. Return ! AsyncGeneratorReject(_generator_, _resultValue_). | ||
1. Return ! AsyncGeneratorResolve(_generator_, _resultValue_, *true*). | ||
1. If _result_ is an abrupt completion, | ||
1. If _result_.[[Type]] is ~return~, then set _result_ to Await(_result_.[[Value]]). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, just to clarify:
The following code will unwrap the Promise, but unwrapping will not impact control flow at all. Thus, the finally block is evaluated immediately after the return statement is evaluated, and before any rejection handlers can fire.
That's fine, but is that the desired behaviour here? Maybe this actually needs an adjustment to https://tc39.github.io/ecma262/#sec-return-statement-runtime-semantics-evaluation instead?
async function* g() {
try {
return Promise.resolve().then(x => { throw new Error("floof"); });
} finally {
doImportantCleanupWork();
}
}
Of course, using a syntactic await
would work fix the control flow issue, so it's flexible and supports either case. Maybe that's a footgun, or maybe it's great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, no, that wasn't the intention... And yeah, adjusting the return
statement semantics was what I was thinking of as an alternative. But then, don't we also need this change, to deal with the gen.return(promise)
case? Or maybe there are less invasive fixes for that case, e.g. just unwrapping in .return() before doing the enqueuing. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I have a patch for adding an Await() in return
statements. That does feel pretty nice, basically saying that we make return
<-> return await
similar to yield
<-> yield await
.
But I'm not sure what to do about the gen.return(promise)
issue; will keep thinking on it, and I welcome suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the .return(promise) request is resumed, my understanding is:
-
If generator state is "suspendedStart", control flow cannot be affected, so just do what it already does, more or less
-
Otherwise, somehow we essentially evaluate
return <value>
when the generator is resumed anyways (I've asked @littledan which clause in the spec causes this, but I don't actually know), so I think it would just fall through to the changes to ReturnStatement evaluation, but it may need a change somewhere else instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e.g. just unwrapping in .return() before doing the enqueuing. What do you think?
I don't think you'd want to "unwrap before enqueuing", because I think that would mess with the predictable order of resumption.
gen.return(promise); // promise unwrapping begins
gen.next(1); // .next request enqueued and run immediately
resolvePromise(2); // promise is resolved
// microtasks run, generator enqueues the .return request and resumes for it.
this is confusing and likely bug-prone. Simpler and more predictable to do the await
on resumption rather than before enqueuing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about it, the more I am leaning toward not covering the .return()
case (unless somehow it magically reduces to the return
statement like you allude to, but I don't see that here...). The point of this PR is to move the unwrapping entirely to be "inside" the generator; we shouldn't mess with things inserted from the "outside".
I'll upload my tweaks in that direction.
spec/abstract-operations.html
Outdated
@@ -95,7 +94,8 @@ | |||
1. Set _generator_.[[AsyncGeneratorState]] to `"completed"`. | |||
1. Set _state_ to `"completed"`. | |||
1. If _state_ is `"completed"`, then | |||
1. If _completion_.[[Type]] is ~return~, then return ! AsyncGeneratorResolve(_generator_, _completion_.[[Value]], *true*). | |||
1. If _completion_.[[Type]] is ~return~, then set _completion_ to Await(_completion_.[[Value]]). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might suggest using the Async Iterator Value Unwrap rather than Await here, because the unwrapping at this point can never impact control flow inside the generator body. It is a bit wordier, but clearer maybe. (Same for the above change, depending on the decision made re: control flow)
This is reverting some of ca6942b, per the May 2017 TC39 meeting agreement to make `yield` automatically await. Making those changes also revealed that we needed similar changes for `return`, which were made more complicated than those for `yield` due to the special behavior of the `asyncGen.return()` method. In order to make that behave the same as a `return` statement, additional changes were necessary in a few places. This also simplifies yield* to be more simply a for-await-of that yields; the only modifications it requires from the original yield* spec text are those needed to parallel for-await-of, and those needed to call AsyncGeneratorYield instead of GeneratorYield. Fixes #93: both the original issue posted there, and the much larger issue it evolved into.
3c1d60f
to
428bd9b
Compare
Take n now ready, per some IRC discussions. People may enjoy this minor writeup I had to make to convince myself on the return-related changes: https://gist.github.com/domenic/9c743590125cb5b2fe2bcfb1e93c664a Please take a look, and thanks so much @caitp for your patience and diligence <3 |
If for example,
and
and
so, the question is, when 11.4.3.6 step 9 (Return promiseCapability.[[Promise]]. ) is executed? |
Oh, yep, that's not working as intended. Thanks... taking today off for my birthday, but will fix first thing Monday. |
Per arai-a's feedback
(I should know better than to promise things like "first thing Monday"...) New version up. This required introducing two new special-purpose closures, which is a big deal in spec-ese, although hopefully not for implementations. (I filed tc39/ecma262#933 on making the spec-ese less annoying.) PTAL! |
spec/abstract-operations.html
Outdated
1. Let _throwawayCapability_ be NewPromiseCapability(%Promise%). | ||
1. Set _throwawayCapability_.[[Promise]].[[PromiseIsHandled]] to *true*. | ||
1. Perform ! PerformPromiseThen(_promiseCapability_.[[Promise]], _onFulfilled_, _onRejected_, _throwawayCapability_). | ||
1. Return *undefined*. | ||
1. If _state_ is `"completed"`, then | ||
1. If _completion_.[[Type]] is ~return~, then return ! AsyncGeneratorResolve(_generator_, _completion_.[[Value]], *true*). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe at this point, it's still possible to resolve an unwrapped promise.
async function* g() {
yield 1;
yield 2;
}
let gen = g();
gen.next().then(x => {
gen.return(Promise.resolve(1)).then(stillAPromise => {
// stillAPromise is not unwrapped at this point
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, this comment isn't quite right. If you add a return statement between the 2 yields, though, and add an extra .next() call before the .return() call, I believe it is still not unwrapping the promise.
async function* g() {
yield 1;
return 2;
}
let gen = g();
gen.next().then(x => {
gen.next().then(x => {
gen.return(Promise.resolve(1)).then(stillAPromise => {
// stillAPromise is not unwrapped at this point
});
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose the correct fix is to just move all of this block of changes to line 108, replacing the "AsyncGeneratorResolve" call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch, that makes perfect sense.
with the patch in https://bugzilla.mozilla.org/show_bug.cgi?id=1352312 , I get the following (the expected result I think) from the first comment in #93 (comment)
anything else to verify? |
Here are some other scenarios to test:
plus the following: async function* f() {
yield Promise.resolve(1);
yield Promise.reject(2);
yield 3;
}
async function* g() {
yield 1;
throw 2;
yield 3;
} where both f() and g() should give, upon calling next():
|
#93 (comment) hits an assertion failure. Looks like, after the async generator gets closed, AsyncGeneratorResumeNext leaves the request in the queue, until the corresponding promise gets resolved.
|
so, if we await on the return value there, we should remove the request from the queue and hold the request in the different place, associating the request to the promise (await). |
@arai-a, I believe the thing that makes sense is: if an Await is in progress, abort AsyncGeneratorResumeNext(). I don't know if thats written in the proposal anywhere, but I've implemented it that way as it's the only thing that makes sense (to me). |
yeah, that change works and all testcases seem to work as expected |
yes, that's exactly same as what I did in above comment. |
This is reverting some of ca6942b, per the May 2017 TC39 meeting agreement to make
yield
automatically await.This also simplifies yield* to be more simply a for-await-of that yields; the only modifications it requires from the original yield* spec text are those needed to parallel for-await-of, and those needed to call AsyncGeneratorYield instead of GeneratorYield.
Fixes #93: both the original issue posted there, and the much larger issue it evolved into.
/cc @caitp @littledan @gskachkov @arai-a. Careful review appreciated of the whole spec after this change, to make sure I didn't miss anything... I've posted a preview of it here: https://dl.dropboxusercontent.com/u/20140634/yield-is-yield-await/index.html