From 8454ac767afc87b7c690ccbaab6b42a82209df2e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:40:03 +0000 Subject: [PATCH 1/4] Do not close the iterator when for-await-of's next() is abrupt 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 Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn --- quickjs-opcode.h | 5 +++++ quickjs.c | 57 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/quickjs-opcode.h b/quickjs-opcode.h index ec2a5ad91..07e1a0974 100644 --- a/quickjs-opcode.h +++ b/quickjs-opcode.h @@ -372,6 +372,11 @@ DEF( is_null, 1, 1, 1, none) DEF(typeof_is_undefined, 1, 1, 1, none) DEF( typeof_is_function, 1, 1, 1, none) +/* appended at the end so as not to renumber any opcode embedded in the + precompiled bytecode blobs (builtin-array-fromasync.h and friends) */ +DEF(for_await_of_dup, 1, 5, 6, none) /* iter_obj next catch iter_obj_c next_c -> undefined next catch iter_obj iter_obj_c next_c */ +DEF(for_await_of_restore, 1, 6, 5, none) /* undefined next catch iter_obj value done -> iter_obj next catch value done */ + #undef DEF #undef def #endif /* DEF */ diff --git a/quickjs.c b/quickjs.c index bbac00c33..862201366 100644 --- a/quickjs.c +++ b/quickjs.c @@ -17258,14 +17258,13 @@ static __exception int js_iterator_get_value_done(JSContext *ctx, JSValue *sp) sp[-1] = value; sp[0] = js_bool(done); if (done) { - /* Iterator exhausted via {done:true}: drop the iterator object (stack - layout at the for-await `next` call is iter_obj,next,catch_offset,result - so iter_obj is sp[-4]) so the trailing OP_iterator_close skips calling - return(). Mirrors js_for_of_next, which nulls the iterator on done. - Per spec AsyncIteratorClose must NOT run on normal completion. This op - is emitted only by the for-await-of loop, so the layout is fixed. */ - JS_FreeValue(ctx, sp[-4]); - sp[-4] = JS_UNDEFINED; + /* Iterator exhausted via {done:true}: drop the saved iterator object + so that OP_for_await_of_restore makes the trailing + OP_iterator_close skip return(). Mirrors js_for_of_next, which + nulls the iterator on done. Per spec AsyncIteratorClose must NOT + run on normal completion. */ + JS_FreeValue(ctx, sp[-2]); + sp[-2] = JS_UNDEFINED; } return 0; } @@ -19344,6 +19343,43 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, goto exception; sp += 1; BREAK; + CASE(OP_for_await_of_dup): + { + /* stack: iter_obj next catch iter_obj_c next_c -> + undefined next catch iter_obj iter_obj_c next_c + Clearing the live iter_obj slot for the duration of the + next()+await means that if it throws (the awaited value + rejects, say), unwinding to the loop's catch offset sees an + undefined iterator and skips the close: per + ForIn/OfBodyEvaluation an abrupt next() result must not + close the iterator. */ + JSValue iter_obj = sp[-5]; + JSValue iter_obj_c = sp[-2]; + JSValue next_c = sp[-1]; + sp[-5] = JS_UNDEFINED; + sp[-2] = iter_obj; + sp[-1] = iter_obj_c; + sp[0] = next_c; + sp += 1; + } + BREAK; + CASE(OP_for_await_of_restore): + { + /* stack: undefined next catch iter_obj value done -> + iter_obj next catch value done + Reached only once next()'s result has been obtained without + throwing: restore the live iterator so the loop body and any + later close see it again. */ + JSValue iter_obj = sp[-3]; + JSValue value = sp[-2]; + JSValue done = sp[-1]; + JS_FreeValue(ctx, sp[-6]); + sp[-6] = iter_obj; + sp[-3] = value; + sp[-2] = done; + sp -= 1; + } + BREAK; CASE(OP_check_object): if (unlikely(!JS_IsObject(sp[-1]))) { JS_ThrowTypeErrorNotAnObject(ctx); @@ -29340,12 +29376,17 @@ static __exception int js_parse_for_in_of(JSParseState *s, int label_name, /* stack: iter_obj next catch_offset */ emit_op(s, OP_dup3); emit_op(s, OP_drop); + /* clear the live iter_obj while next() is pending: an abrupt + next()/await must not close the iterator */ + emit_op(s, OP_for_await_of_dup); emit_op(s, OP_call_method); emit_u16(s, 0); /* get the result of the promise */ emit_op(s, OP_await); /* unwrap the value and done values */ emit_op(s, OP_iterator_get_value_done); + /* next() succeeded: restore the live iter_obj */ + emit_op(s, OP_for_await_of_restore); } else { emit_op(s, OP_for_of_next); emit_u8(s, 0); From 25894bf8018dbccd01b669c5ea097da7cb76cc06 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:41:02 +0000 Subject: [PATCH 2/4] Close the sync iterator when an async-from-sync value rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn --- quickjs.c | 41 ++++++++++++++++++++++++++++++++++++++++- test262_errors.txt | 12 ------------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/quickjs.c b/quickjs.c index 862201366..49789d6d6 100644 --- a/quickjs.c +++ b/quickjs.c @@ -56659,6 +56659,27 @@ static JSValue js_async_from_sync_iterator_unwrap_func_create(JSContext *ctx, 1, 0, 1, func_data); } +static JSValue js_async_from_sync_iterator_close_on_reject( + JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, + int magic, JSValueConst *func_data) +{ + /* IteratorClose(syncIterator, ThrowCompletion(reason)): close the sync + iterator, then reject with the original reason whatever return() does */ + JS_Throw(ctx, js_dup(argv[0])); + JS_IteratorClose(ctx, func_data[0], true); + return JS_EXCEPTION; +} + +static JSValue js_async_from_sync_iterator_close_func_create(JSContext *ctx, + JSValueConst sync_iter) +{ + JSValueConst func_data[1]; + + func_data[0] = sync_iter; + return JS_NewCFunctionData(ctx, js_async_from_sync_iterator_close_on_reject, + 1, 0, 1, func_data); +} + /* AsyncIteratorPrototype */ static const JSCFunctionListEntry js_async_iterator_proto_funcs[] = { @@ -56798,6 +56819,10 @@ static JSValue js_async_from_sync_iterator_next(JSContext *ctx, JSValueConst thi 1, vc(&value), 0); if (JS_IsException(value_wrapper_promise)) { JS_FreeValue(ctx, value); + /* an abrupt PromiseResolve closes the sync iterator too when + closeOnRejection is true and the result is not done */ + if (magic != GEN_MAGIC_RETURN && !done) + JS_IteratorClose(ctx, s->sync_iter, true); goto reject; } @@ -56807,13 +56832,27 @@ static JSValue js_async_from_sync_iterator_next(JSContext *ctx, JSValueConst thi JS_FreeValue(ctx, value_wrapper_promise); goto fail; } + /* closeOnRejection is true for .next and .throw and false for + .return: when the result is not done, a rejected value promise + has to close the sync iterator */ + if (magic != GEN_MAGIC_RETURN && !done) { + resolve_reject[1] = + js_async_from_sync_iterator_close_func_create(ctx, s->sync_iter); + if (JS_IsException(resolve_reject[1])) { + JS_FreeValue(ctx, resolve_reject[0]); + JS_FreeValue(ctx, value_wrapper_promise); + goto fail; + } + } else { + resolve_reject[1] = JS_UNDEFINED; + } JS_FreeValue(ctx, value); - resolve_reject[1] = JS_UNDEFINED; res = perform_promise_then(ctx, value_wrapper_promise, vc(resolve_reject), vc(resolving_funcs)); JS_FreeValue(ctx, resolve_reject[0]); + JS_FreeValue(ctx, resolve_reject[1]); JS_FreeValue(ctx, value_wrapper_promise); JS_FreeValue(ctx, resolving_funcs[0]); JS_FreeValue(ctx, resolving_funcs[1]); diff --git a/test262_errors.txt b/test262_errors.txt index 082fb016a..d95d9c72e 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -5,18 +5,6 @@ test262/test/annexB/language/expressions/assignmenttargettype/callexpression-in- test262/test/annexB/language/expressions/assignmenttargettype/callexpression-in-prefix-update.js:27: SyntaxError: invalid increment/decrement operand test262/test/annexB/language/expressions/assignmenttargettype/callexpression.js:33: SyntaxError: invalid assignment left-hand side test262/test/annexB/language/expressions/assignmenttargettype/cover-callexpression-and-asyncarrowhead.js:20: SyntaxError: invalid assignment left-hand side -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/iterator-result-poisoned-wrapper.js:64: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/iterator-result-poisoned-wrapper.js:64: strict mode: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/next-result-poisoned-wrapper.js:69: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/next-result-poisoned-wrapper.js:69: strict mode: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-iterator-next-rejected-promise-close.js:59: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-iterator-next-rejected-promise-close.js:59: strict mode: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-next-rejected-promise-close.js:64: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-next-rejected-promise-close.js:64: strict mode: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/throw/iterator-result-rejected-promise-close.js:74: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/throw/iterator-result-rejected-promise-close.js:74: strict mode: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/throw/throw-result-poisoned-wrapper.js:81: TypeError: $DONE() not called -test262/test/built-ins/AsyncFromSyncIteratorPrototype/throw/throw-result-poisoned-wrapper.js:81: strict mode: TypeError: $DONE() not called test262/test/language/destructuring/binding/keyed-destructuring-property-reference-target-evaluation-order-with-bindings.js:73: Test262Error: Actual [binding::source, binding::sourceKey, sourceKey, get source, binding::defaultValue, binding::varTarget] and expected [binding::source, binding::sourceKey, sourceKey, binding::varTarget, get source, binding::defaultValue] should have the same contents. test262/test/language/expressions/assignment/destructuring/iterator-destructuring-property-reference-target-evaluation-order.js:42: Test262Error: Actual [source, iterator, target, target-key, target-key-tostring, iterator-step, iterator-done, set] and expected [source, iterator, target, target-key, iterator-step, iterator-done, target-key-tostring, set] should have the same contents. test262/test/language/expressions/assignment/destructuring/iterator-destructuring-property-reference-target-evaluation-order.js:42: strict mode: Test262Error: Actual [source, iterator, target, target-key, target-key-tostring, iterator-step, iterator-done, set] and expected [source, iterator, target, target-key, iterator-step, iterator-done, target-key-tostring, set] should have the same contents. From e3102eefaac667220f61959306a6f92f460d2db9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 09:27:41 +0000 Subject: [PATCH 3/4] Add a test for iterator closing around for-await-of 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 Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc --- tests/for-await-of-iterator-close.js | 252 +++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 tests/for-await-of-iterator-close.js diff --git a/tests/for-await-of-iterator-close.js b/tests/for-await-of-iterator-close.js new file mode 100644 index 000000000..5d32c812e --- /dev/null +++ b/tests/for-await-of-iterator-close.js @@ -0,0 +1,252 @@ +import { assert } from "./assert.js"; + +/* Two rules meet in for-await-of. + + ForIn/OfBodyEvaluation gets the next result with plain `?` steps: if + next() throws, if the awaited result rejects, if it is not an object, or + if the `done`/`value` getters throw, the loop propagates the failure + *without* closing the iterator. Only an abrupt completion of the loop body + (throw, break, return) closes it. + + AsyncFromSyncIteratorContinuation is the other way around: when the value + a *sync* iterator yields is a promise that rejects, the sync iterator is + closed with a throw completion before the rejection is handed on. */ + +function trace() { + const log = []; + return { + log, + push(x) { log.push(x); return x; }, + get text() { return log.join(","); }, + }; +} + +async function collect(t, body) { + try { + await body(); + t.push("no-throw"); + } catch (e) { + t.push("caught:" + e.message); + } + return t.text; +} + +/* an async iterator that fails in a configurable way */ +function asyncIter(t, fail) { + return { + [Symbol.asyncIterator]() { + return { + next() { + t.push("next"); + return fail(); + }, + return() { + t.push("return"); + return Promise.resolve({ done: true }); + }, + }; + }, + }; +} + +/* --- an abrupt next() must not close the iterator --------------------- */ +{ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of asyncIter(t, () => { throw new Error("nx"); })) + t.push("body"); + }), "next,caught:nx"); +} +{ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of asyncIter(t, () => Promise.reject(new Error("nr")))) + t.push("body"); + }), "next,caught:nr"); +} +{ + /* the result is not an object */ + const t = trace(); + const got = await collect(t, async () => { + for await (const x of asyncIter(t, () => 1)) + t.push("body"); + }); + assert(got.startsWith("next,caught:"), true, got); + assert(t.log.indexOf("return"), -1, got); + assert(t.log.length, 2, got); +} +{ + /* the awaited result resolves, but its getters throw */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of asyncIter(t, () => Promise.resolve({ + get done() { t.push("done"); throw new Error("dn"); }, + value: 1, + }))) t.push("body"); + }), "next,done,caught:dn"); +} +{ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of asyncIter(t, () => Promise.resolve({ + done: false, + get value() { t.push("value"); throw new Error("vl"); }, + }))) t.push("body"); + }), "next,value,caught:vl"); +} +/* --- an abrupt body must close it ------------------------------------- */ +{ + const ok = () => Promise.resolve({ done: false, value: 1 }); + + const t1 = trace(); + assert(await collect(t1, async () => { + for await (const x of asyncIter(t1, ok)) { + t1.push("body"); + throw new Error("bd"); + } + }), "next,body,return,caught:bd"); + + const t2 = trace(); + assert(await collect(t2, async () => { + for await (const x of asyncIter(t2, ok)) { + t2.push("body"); + break; + } + }), "next,body,return,no-throw"); + + const t3 = trace(); + assert(await collect(t3, async () => { + await (async () => { + for await (const x of asyncIter(t3, ok)) { + t3.push("body"); + return; + } + })(); + }), "next,body,return,no-throw"); + + /* a normal finish never closes */ + const t4 = trace(); + let i = 0; + assert(await collect(t4, async () => { + for await (const x of asyncIter(t4, () => + Promise.resolve(i++ < 1 ? { done: false, value: 1 } : { done: true }))) + t4.push("body"); + }), "next,body,next,no-throw"); +} + +/* --- the sync iterator behind an async-from-sync wrapper -------------- */ +function syncIter(t, next) { + return { + [Symbol.iterator]() { + return { + next() { t.push("next"); return next(); }, + return() { t.push("return"); return { done: true }; }, + }; + }, + }; +} + +{ + /* a yielded promise that rejects closes the sync iterator */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => ({ + done: false, + value: Promise.reject(new Error("rp")), + }))) t.push("body"); + }), "next,return,caught:rp"); +} +{ + /* ... including through yield* delegation in an async generator */ + const t = trace(); + const src = syncIter(t, () => ({ + done: false, + value: Promise.reject(new Error("yp")), + })); + async function* g() { yield* src; } + assert(await collect(t, async () => { + for await (const x of g()) t.push("body"); + }), "next,return,caught:yp"); +} +{ + /* a value that is a thenable whose then() throws is the same shape */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => ({ + done: false, + value: { then(res, rej) { t.push("then"); rej(new Error("th")); } }, + }))) t.push("body"); + }), "next,then,return,caught:th"); +} +{ + /* a done result still has its value awaited, so a rejection propagates, + but there is nothing left to close */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => ({ + done: true, + value: Promise.reject(new Error("dr")), + }))) t.push("body"); + }), "next,caught:dr"); + assert(t.log.indexOf("return"), -1); +} +{ + /* but an abrupt IteratorValue rejects without closing */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => ({ + done: false, + get value() { t.push("value"); throw new Error("sv"); }, + }))) t.push("body"); + }), "next,value,caught:sv"); +} +{ + /* and so does an abrupt next() on the sync iterator */ + const t = trace(); + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => { throw new Error("sn"); })) + t.push("body"); + }), "next,caught:sn"); +} +{ + /* a plain sync iteration still runs to completion */ + const t = trace(); + let i = 0; + assert(await collect(t, async () => { + for await (const x of syncIter(t, () => + i++ < 2 ? { done: false, value: i } : { done: true })) + t.push("body" + x); + }), "next,body1,next,body2,next,no-throw"); +} + +/* --- the loop still yields the right values -------------------------- */ +{ + const seen = []; + for await (const x of [1, Promise.resolve(2), 3]) + seen.push(x); + assert(seen.join(","), "1,2,3"); + + async function* gen() { yield 1; yield 2; } + const seen2 = []; + for await (const x of gen()) + seen2.push(x); + assert(seen2.join(","), "1,2"); + + /* nested loops keep their own iterators straight */ + const pairs = []; + for await (const a of [1, 2]) + for await (const b of ["x", "y"]) + pairs.push(a + b); + assert(pairs.join(","), "1x,1y,2x,2y"); + + /* a labelled break closes only the inner iterator */ + const t = trace(); + const inner = asyncIter(t, () => Promise.resolve({ done: false, value: 1 })); + for await (const a of [1]) { + for await (const b of inner) { + t.push("body"); + break; + } + } + assert(t.text, "next,body,return"); +} From 9866c830646d2ab286e058c28462e4538ff83e14 Mon Sep 17 00:00:00 2001 From: Andreas Rosdal Date: Fri, 7 Aug 2026 12:47:02 +0000 Subject: [PATCH 4/4] Cover the control flow that moves the for-await-of loop's stack 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 --- tests/for-await-of-iterator-close.js | 159 +++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/tests/for-await-of-iterator-close.js b/tests/for-await-of-iterator-close.js index 5d32c812e..351bb7335 100644 --- a/tests/for-await-of-iterator-close.js +++ b/tests/for-await-of-iterator-close.js @@ -250,3 +250,162 @@ function syncIter(t, next) { } assert(t.text, "next,body,return"); } + +/* --- the control flow that moves the loop's stack around -------------- */ +{ + const upto = (t, n) => { + let i = 0; + return asyncIter(t, () => Promise.resolve( + i++ < n ? { done: false, value: i } : { done: true })); + }; + + /* continue takes the back edge, which has to find the same stack it + started with */ + { + const t = trace(); + const seen = []; + for await (const x of upto(t, 4)) { + if (x % 2) continue; + seen.push(x); + } + assert(seen.join(","), "2,4"); + assert(t.text, "next,next,next,next,next"); + } + + /* a labelled break and a labelled continue that cross a loop boundary + close every iterator they leave, innermost first */ + { + const t = trace(); + outer: + for await (const a of upto(t, 3)) { + for await (const b of upto(t, 3)) { + if (a === 2) break outer; + continue outer; + } + } + assert(t.text, "next,next,return,next,next,return,return"); + } + + /* try/finally around and inside the loop: the finally blocks run in + order and the iterator is still closed exactly once */ + { + const t = trace(); + try { + for await (const x of upto(t, 3)) { + try { + throw new Error("boom"); + } finally { + t.push("inner-finally"); + } + } + } catch (e) { + t.push("caught:" + e.message); + } finally { + t.push("outer-finally"); + } + assert(t.text, "next,inner-finally,return,caught:boom,outer-finally"); + } + + /* a return out of a try with a finally, from inside the loop */ + { + const t = trace(); + const r = await (async () => { + for await (const x of upto(t, 3)) { + try { + return "returned"; + } finally { + t.push("finally"); + } + } + })(); + assert(r, "returned"); + assert(t.text, "next,finally,return"); + } + + /* awaiting in the body suspends the function with the loop's stack live */ + { + const t = trace(); + const seen = []; + for await (const x of upto(t, 3)) { + await null; + seen.push(await Promise.resolve(x * 10)); + await null; + } + assert(seen.join(","), "10,20,30"); + } + + /* the same, in an async generator, where yield suspends it as well */ + { + async function* relay(t) { + for await (const x of upto(t, 3)) + yield x * 2; + } + const t = trace(); + const seen = []; + for await (const x of relay(t)) + seen.push(x); + assert(seen.join(","), "2,4,6"); + + /* abandoning the outer loop closes the generator, which closes the + inner iterator it was driving */ + const t2 = trace(); + for await (const x of relay(t2)) + break; + assert(t2.text, "next,return"); + } + + /* a destructuring head, and a head that assigns to an existing lvalue, + both run after the iterator has been restored */ + { + const t = trace(); + let i = 0; + const pairs = { + [Symbol.asyncIterator]: () => ({ + next: () => Promise.resolve( + i++ < 2 ? { done: false, value: { a: i, b: [i, i + 1] } } + : { done: true }), + }), + }; + const seen = []; + for await (const { a, b: [b0, b1] } of pairs) + seen.push(a + ":" + b0 + ":" + b1); + assert(seen.join(","), "1:1:2,2:2:3"); + + const obj = {}; + let last; + i = 0; + for await (obj.k of pairs) last = obj.k.a; + assert(last, 2); + i = 0; + for await (last of pairs) ; + assert(last.a, 2); + } + + /* nesting deeply enough that an understated stack effect would run off + the end of the frame */ + { + const t = trace(); + let n = 0; + for await (const a of upto(t, 2)) + for await (const b of upto(t, 2)) + for await (const c of upto(t, 2)) + for await (const d of upto(t, 2)) + for await (const e of upto(t, 2)) + n += a + b + c + d + e; + assert(n, 240); + } + + /* a rejected next() leaves the iterator open even from inside a try */ + { + const t = trace(); + let caught = null; + try { + for await (const x of asyncIter(t, () => Promise.reject(new Error("nx")))) + t.push("body"); + } catch (e) { + caught = e.message; + } + assert(caught, "nx"); + assert(t.text, "next"); + } +}