Fix iterator closing around for-await-of and async-from-sync - #1649
Open
andreasrosdal wants to merge 4 commits into
Open
Fix iterator closing around for-await-of and async-from-sync#1649andreasrosdal wants to merge 4 commits into
andreasrosdal wants to merge 4 commits into
Conversation
ForIn/OfBodyEvaluation calls next(), awaits the result, and on an abrupt
completion sets iteratorRecord.[[Done]] and returns it without closing the
iterator: an iterator that failed to produce a result is not closed.
The for-await-of bytecode keeps the iterator live in the slot the loop's
catch offset unwinds to, so a next() that throws, or an awaited result that
rejects, runs the trailing OP_iterator_close and calls return():
let returnCalled = 0;
const it = { [Symbol.asyncIterator]: () => ({
next() { return Promise.reject(new Error('boom')); },
return() { returnCalled++; return Promise.resolve({done: true}); },
})};
try { for await (const v of it) {} } catch (e) {}
returnCalled // 1, V8: 0
Add two opcodes around the next()+await sequence. OP_for_await_of_dup moves
the iterator out of the slot the handler reads and parks it above the catch
offset, so an abrupt result unwinds to an undefined iterator and the close is
skipped. OP_for_await_of_restore puts it back once a result has been obtained.
They are appended at the end of the opcode list so no opcode already embedded
in a precompiled bytecode blob is renumbered.
js_iterator_get_value_done() clears the parked copy at sp[-2] instead of the
live slot at sp[-4], since the restore now decides what the live slot holds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
AsyncFromSyncIteratorContinuation takes a closeOnRejection flag, true for
%AsyncFromSyncIteratorPrototype%.next and .throw and false for .return.
When it is set and the sync iterator's result is not done, the promise the
value is wrapped in gets an onRejected handler that performs
IteratorClose(syncIteratorRecord, ThrowCompletion(error)) before the
rejection reaches the consumer:
13. Else,
a. Let closeIterator be a new Abstract Closure ... performs
IteratorClose(syncIteratorRecord, ThrowCompletion(error)).
b. Let onRejected be CreateBuiltinFunction(closeIterator, 1, "", « »).
14. Perform PerformPromiseThen(valueWrapper, onFulfilled, onRejected,
promiseCapability).
js_async_from_sync_iterator_do() always passes JS_UNDEFINED as onRejected, so
a sync iterator that yields a rejected promise is never closed. The same
applies when PromiseResolve on the value itself throws.
Install a close-then-rethrow handler for .next and .throw when the result is
not done. Together with the previous commit
test/built-ins/AsyncFromSyncIteratorPrototype goes from 6/38 failures to 0/38;
the six entries are dropped from test262_errors.txt. for-await-of (1234),
for-of (751) and async-generator (623) are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
Covers both halves of the change. ForIn/OfBodyEvaluation reaches the next result through plain '?' steps, so a throwing next(), a rejecting result, a non-object result and throwing done/value getters all propagate without closing the iterator, while an abrupt loop body still closes it. AsyncFromSyncIteratorContinuation is the opposite: a sync iterator whose yielded value is a rejecting promise is closed with a throw completion, including through yield* delegation, but an abrupt IteratorValue is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc
OP_for_await_of_dup and OP_for_await_of_restore add a slot to the loop's
stack for the duration of next(), so anything that leaves the loop from
somewhere other than its normal end, or that suspends inside it, has to
find the stack it expects. Added:
- continue, and a labelled break and continue that cross a loop boundary
- try/finally around and inside the loop, and a return out of a try with
a finally
- await in the body, and the whole loop inside an async generator that
yields, including abandoning that generator mid-iteration
- a destructuring head and a head that assigns to an existing lvalue
- five nested loops, which an understated stack effect would overrun
- a rejected next() inside a try, which must still leave the iterator
open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two halves of the same story: who closes the sync iterator when a
for awaitstep rejects. Today the loop closes it (it should not) and the async-from-sync wrapper does not (it should). Each commit alone moves a test in the wrong direction, so they are sent together.1.
for-await-ofmust not close on an abruptnext()ForIn/OfBodyEvaluation calls
next(), awaits the result, and on an abrupt completion setsiteratorRecord.[[Done]]and returns it without closing the iterator — an iterator that failed to produce a result is not closed.The for-await-of bytecode keeps the iterator live in the slot the loop's catch offset unwinds to, so a
next()that throws, or an awaited result that rejects, runs the trailingOP_iterator_closeand callsreturn():Two opcodes bracket the
next()+awaitsequence.OP_for_await_of_dupmoves the iterator out of the slot the handler reads and parks it above the catch offset, so an abrupt result unwinds to an undefined iterator and the close is skipped;OP_for_await_of_restoreputs it back once a result has been obtained. They are appended at the end of the opcode list so no opcode already embedded in a precompiled bytecode blob is renumbered.2. The async-from-sync wrapper must close on a rejected value
AsyncFromSyncIteratorContinuationtakes acloseOnRejectionflag — true for%AsyncFromSyncIteratorPrototype%.nextand.throw, false for.return. When it is set and the sync result is not done, the wrapped value promise gets anonRejectedthat performsIteratorClose(syncIteratorRecord, ThrowCompletion(error))before the rejection reaches the consumer:js_async_from_sync_iterator_do()always passesJS_UNDEFINEDasonRejected, so a sync iterator that yields a rejected promise is never closed. The same applies whenPromiseResolveon the value itself throws.Testing
built-ins/AsyncFromSyncIteratorPrototypelanguage/statements/for-await-oflanguage/statements/for-oflanguage/expressions/async-generatorThe six
AsyncFromSyncIteratorPrototypeentries are dropped fromtest262_errors.txt.🤖 Generated with Claude Code
https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
Generated by Claude Code