Skip to content

Commit 5f65bd9

Browse files
committed
[JSC] Cooperative driving of async generators
https://bugs.webkit.org/show_bug.cgi?id=319133 rdar://181951971 Reviewed by Yijia Huang. This is our next step in the long journey towards native microtasks and efficient async implementation. This patch introduces cooperative driving mechanism of async generators. Let's say we have a code below. async function* gen () { yield 42; yield 42; yield 42; } async function main() { for await (let value of gen()) { ... } } In the above script, we have a great syntax-guaranteed things, 1. for-await-of completely hides `next()` invocation. This means that the result of `next()` call is not escaped anywhere user observable space. In this case, async generator's promise from next(). 2. for-await-of is awaiting for each promise result from `next()` call of async generator. And this `await` is directly invoking PerformPromiseThen, skipping `Promise.prototype.then` calls, so user cannot insert an observable function by overriding `Promise.prototype.then` when it is a native promise. 3. When usual %AsyncGeneratorPrototype%.next function is invoked, if it is normal async generator, it is guaranteed to produce a native promise. The above 3 characteristics means that we can skip promise creation if an async function is cooperatively driving async generator via for-await-of. 1. for-await-of is used in the async function. If gen()'s result is genuine async generator and `next` function is %AsyncGeneratorPrototype%.next, then the promise is not observable. Then, we do not create a promise / return a promise! Instead, we return a sentinel (instead of promise) that is "we will be driven by the fast-suspension mechanism". Then instead of enqueuing a newly created promise, we enqueue this async function's generator (driver) into the async generator's queue. 2. Then async generator will execute `yield` etc., and resolve the queued promise. Previously, there was a promise here. But instead, we now have a async function's generator. So we drive it with a special InternalMicrotask::AsyncGeneratorFastConsumerResume. 3. InternalMicrotask::AsyncGeneratorFastConsumerResume will be driven with async function generator. And we resume async function generator instead. The execution is completely aligned to the normal promise-based driving in terms of ticks. But it is driven significantly more efficiently: we do not create a promise and schedule a resolution through a promise. We capture the driver (async function generator) directly and it cooperatively registers itself to the driven async generator. And async generator cooperatively resolves this driver when it gets the resolution! As a result, we no longer directly call JS %AsyncGeneratorPrototype%.next(). So we move it to C++ too. Tests: JSTests/stress/async-iterator-completed-producer-no-double-drive.js JSTests/stress/async-iterator-fast-consumer-manual-next-drive.js JSTests/stress/async-iterator-fast-consumer-microtask-ticks.js JSTests/stress/async-iterator-fast-consumer-reentrancy.js JSTests/stress/async-iterator-fast-consumer-tampering.js JSTests/stress/async-iterator-fast-consumer-vs-generic-equivalence.js JSTests/stress/async-iterator-next-generic-inlined-osr-exit.js JSTests/stress/async-iterator-next-osr-exit-inlined-callee.js JSTests/stress/async-iterator-next-polymorphic-callee.js JSTests/stress/async-iterator-open-next-loljit.js * JSTests/stress/async-iterator-completed-producer-no-double-drive.js: Added. (assert): (makeGen): (async reconsume): (async consumeAlreadyExhausted): (async genConsumeExhausted): (async main): (async let): * JSTests/stress/async-iterator-fast-consumer-manual-next-drive.js: Added. (assert): (makeProducer): (async makeConsumer): (async driveToArray): (async main): (main.then): * JSTests/stress/async-iterator-fast-consumer-microtask-ticks.js: Added. (assert): (async scenario): (const.scenarios.async gen3.async g): (const.scenarios.async gen3): (const.scenarios.async break.async g): (const.scenarios.async break): (const.scenarios.async throw.async g): (const.scenarios.async throw): (const.scenarios.async delegate.async inner): (const.scenarios.async delegate.async g): (const.scenarios.async delegate): (const.scenarios.async internalAwait.async g): (const.scenarios.async internalAwait): (const.scenarios.async yieldPromise.async g): (const.scenarios.async yieldPromise): (const.scenarios.async nested.async inner): (const.scenarios.async nested.async outer): (const.scenarios.async nested): (const.scenarios.async empty.async g): (const.scenarios.async empty): (const.scenarios.async completedReconsume.async g): (const.scenarios.async completedReconsume): (async main): (main.then): * JSTests/stress/async-iterator-fast-consumer-reentrancy.js: Added. (assert): (async interleavedNext.async prod): (async interleavedNext.async cons): (async interleavedNext): (async nestedFast.async inner): (async nestedFast.async outer): (async nestedFast.async src): (async nestedFast): (async reentrantProducer.async prod): (async reentrantProducer.async cons): (async reentrantProducer): (async reconsume.async prod): (async reconsume.async cons): (async reconsume): (async nextDuringAwait.async prod): (async nextDuringAwait.async cons): (async nextDuringAwait): (async main): (main.then): * JSTests/stress/async-iterator-fast-consumer-tampering.js: Added. (assert): (const.AsyncGeneratorPrototype.Object.getPrototypeOf.Object.getPrototypeOf): (async tamperNext.AsyncGeneratorPrototype.next): (async tamperNext.try.async g): (async tamperNext): (async tamperPromiseThen.Promise.prototype.then): (async tamperPromiseThen.try.async g): (async tamperPromiseThen): (async tamperObjectThen.): (async tamperObjectThen.try.async g): (async tamperObjectThen): (async main): (main.then): * JSTests/stress/async-iterator-fast-consumer-vs-generic-equivalence.js: Added. (assert): (async scenario): (const.scenarios.async gen3.async g): (const.scenarios.async gen3): (const.scenarios.async breakEarly.async g): (const.scenarios.async breakEarly): (const.scenarios.async throwInBody.async g): (const.scenarios.async throwInBody): (const.scenarios.async delegate.async inner): (const.scenarios.async delegate.async g): (const.scenarios.async delegate): (const.scenarios.async internalAwait.async g): (const.scenarios.async internalAwait): (const.scenarios.async yieldPromise.async g): (const.scenarios.async yieldPromise): (const.scenarios.async nested.async inner): (const.scenarios.async nested.async outer): (const.scenarios.async nested): (const.scenarios.async errorFromProducer.async g): (const.scenarios.async errorFromProducer): (const.scenarios.async agConsumer.async producer): (const.scenarios.async agConsumer.async consumer): (const.scenarios.async agConsumer): (async runAll): (const.asyncGenProto.Object.getPrototypeOf.Object.getPrototypeOf): (async main.asyncGenProto.next): (async main): (async let): * JSTests/stress/async-iterator-next-generic-inlined-osr-exit.js: Added. (assert): (const.iteratorProto.next): (makeIterable): (async consume): (async main): (main.then): * JSTests/stress/async-iterator-next-osr-exit-inlined-callee.js: Added. (assert): (makeIterable): (async consume): (async main): (main.then): * JSTests/stress/async-iterator-next-polymorphic-callee.js: Added. (assert): (next): (else.next): (async return): (makeIterable): (expectedSum): (async consume): (async main): (main.then): * JSTests/stress/async-iterator-open-abrupt-completion-tiers.js: Added. (assert): (assertSeq): (async breakEarly.async g): (async breakEarly): (async throwInBody.async g): (async throwInBody): (async returnFromEnclosing.async g): (async returnFromEnclosing.async inner): (async continueInBody.async g): (async continueInBody): (async nested.async inner): (async nested.async outer): (async partialThenForAwait.async g): (async producerThrows.async g): (async producerThrows): (async main): * JSTests/stress/async-iterator-open-fast-path-tiers.js: Added. (assert): (const.AGP.Object.getPrototypeOf.Object.getPrototypeOf): (async consume): (async const): * JSTests/stress/async-iterator-open-generic-path.js: Added. (assert): (async assertThrowsAsync): (async consume): (customIterable): (async main.const.iterable.Symbol.asyncIterator.get return): (async main): (main.then): * JSTests/stress/async-iterator-open-next-loljit.js: Added. (assert): (async genConsumer): (customIterable): (async main): (main.then): * JSTests/stress/async-iterator-open-next-side-effect-free-probe.js: Added. (assert): (const.AsyncGeneratorPrototype.Object.getPrototypeOf.Object.getPrototypeOf): (async proxyPrototypeNext.customNext): (async proxyPrototypeNext.get const.g): (async instanceAccessorNext.const.g): (async instanceAccessorNext.async Object): (async fastEligibleFallthrough): (proxyGenerator.customNext): (proxyGenerator.get const.g): (accessorGenerator.const.g): (accessorGenerator.async Object): (async main.async const): (async main): * Source/JavaScriptCore/CMakeLists.txt: * Source/JavaScriptCore/DerivedSources-input.xcfilelist: * Source/JavaScriptCore/DerivedSources.make: * Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj: * Source/JavaScriptCore/builtins/AsyncGeneratorPrototype.js: Removed. * Source/JavaScriptCore/builtins/BuiltinNames.h: * Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.cpp: (JSC::BytecodeIntrinsicRegistry::BytecodeIntrinsicRegistry): * Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h: * Source/JavaScriptCore/bytecode/BytecodeList.rb: * Source/JavaScriptCore/bytecode/BytecodeLivenessAnalysis.cpp: (JSC::tmpLivenessForCheckpoint): * Source/JavaScriptCore/bytecode/BytecodeOperandsForCheckpoint.h: (JSC::valueProfileOffsetFor): (JSC::destinationFor): (JSC::calleeFor): (JSC::argumentCountIncludingThisFor): (JSC::stackOffsetInRegistersForCall): * Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp: (JSC::computeUsesForBytecodeIndexImpl): (JSC::computeDefsForBytecodeIndexImpl): * Source/JavaScriptCore/bytecode/CallLinkInfo.cpp: (JSC::CallLinkInfo::callTypeFor): * Source/JavaScriptCore/bytecode/CodeBlock.cpp: (JSC::CodeBlock::finishCreation): (JSC::CodeBlock::finalizeLLIntInlineCaches): (JSC::CodeBlock::tryGetValueProfileForBytecodeIndex): * Source/JavaScriptCore/bytecode/GetByStatus.cpp: (JSC::GetByStatus::computeFromLLInt): * Source/JavaScriptCore/bytecode/IterationModeMetadata.h: * Source/JavaScriptCore/bytecode/LLIntPrototypeLoadAdaptiveStructureWatchpoint.cpp: (JSC::LLIntPrototypeLoadAdaptiveStructureWatchpoint::fireInternal): * Source/JavaScriptCore/bytecode/LinkTimeConstant.h: * Source/JavaScriptCore/bytecode/Opcode.h: * Source/JavaScriptCore/bytecode/OpcodeInlines.h: (JSC::isOpcodeShape): * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp: (JSC::BytecodeGenerator::emitAsyncIteratorOpen): (JSC::BytecodeGenerator::emitAsyncIteratorNext): (JSC::BytecodeGenerator::emitEnumeration): (JSC::BytecodeGenerator::emitGenericEnumeration): Deleted. * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h: (JSC::BytecodeGenerator::emitIsIteratorHelper): (JSC::BytecodeGenerator::emitIsAsyncGenerator): Deleted. * Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp: (JSC::FunctionNode::emitBytecode): (JSC::asyncGeneratorInternalFieldIndex): Deleted. (JSC::abstractModuleRecordInternalFieldIndex): Deleted. (JSC::BytecodeIntrinsicNode::emit_intrinsic_getAsyncGeneratorInternalField): Deleted. (JSC::BytecodeIntrinsicNode::emit_intrinsic_getAbstractModuleRecordInternalField): Deleted. (JSC::BytecodeIntrinsicNode::emit_intrinsic_putAsyncGeneratorInternalField): Deleted. * Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h: (JSC::DFG::AbstractInterpreter<AbstractStateType>::executeEffects): * Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp: (JSC::DFG::ByteCodeParser::parseBlock): (JSC::DFG::ByteCodeParser::handleAsyncIteratorOpen): (JSC::DFG::ByteCodeParser::handleAsyncIteratorNext): * Source/JavaScriptCore/dfg/DFGClobberize.h: (JSC::DFG::clobberize): * Source/JavaScriptCore/dfg/DFGDoesGC.cpp: (JSC::DFG::doesGC): * Source/JavaScriptCore/dfg/DFGFixupPhase.cpp: (JSC::DFG::FixupPhase::fixupNode): * Source/JavaScriptCore/dfg/DFGNodeType.h: * Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp: (JSC::DFG::callerReturnPC): * Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp: * Source/JavaScriptCore/dfg/DFGSafeToExecute.h: (JSC::DFG::safeToExecute): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp: * Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h: * Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp: (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp: (JSC::DFG::SpeculativeJIT::compile): * Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp: * Source/JavaScriptCore/ftl/FTLCapabilities.cpp: (JSC::FTL::canCompile): * Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp: (JSC::FTL::DFG::LowerDFGToB3::compileNode): (JSC::FTL::DFG::LowerDFGToB3::compileCompareStrictEq): * Source/JavaScriptCore/jit/JIT.cpp: (JSC::JIT::privateCompileMainPass): (JSC::JIT::privateCompileSlowCases): * Source/JavaScriptCore/jit/JIT.h: * Source/JavaScriptCore/jit/JITCall.cpp: (JSC::JIT::compileOpCall): (JSC::JIT::emitIteratorOpenGeneric): (JSC::JIT::emit_op_iterator_open): (JSC::JIT::emitSlowIteratorOpenGeneric): (JSC::JIT::emitSlow_op_iterator_open): (JSC::JIT::emit_op_async_iterator_open): (JSC::JIT::emitSlow_op_async_iterator_open): (JSC::JIT::emit_op_async_iterator_next): * Source/JavaScriptCore/jit/JITOperations.cpp: (JSC::JSC_DEFINE_JIT_OPERATION): * Source/JavaScriptCore/jit/JITOperations.h: * Source/JavaScriptCore/llint/LLIntOpcode.h: * Source/JavaScriptCore/llint/LLIntSlowPaths.cpp: (JSC::LLInt::LLINT_SLOW_PATH_DECL): (JSC::LLInt::handleAsyncIteratorOpenCheckpoint): (JSC::LLInt::llint_slow_path_checkpoint_osr_exit_from_inlined_call): (JSC::LLInt::llint_slow_path_checkpoint_osr_exit): * Source/JavaScriptCore/llint/LLIntSlowPaths.h: * Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm: * Source/JavaScriptCore/llint/LowLevelInterpreter64.asm: * Source/JavaScriptCore/lol/LOLJIT.cpp: (JSC::LOL::LOLJIT::privateCompileMainPass): (JSC::LOL::LOLJIT::privateCompileSlowCases): * Source/JavaScriptCore/runtime/AsyncGeneratorPrototype.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): (JSC::AsyncGeneratorPrototype::finishCreation): * Source/JavaScriptCore/runtime/AsyncGeneratorPrototype.h: * Source/JavaScriptCore/runtime/AsyncIteratorPrototype.cpp: (JSC::AsyncIteratorPrototype::finishCreation): * Source/JavaScriptCore/runtime/AsyncIteratorPrototype.h: * Source/JavaScriptCore/runtime/CommonSlowPaths.cpp: (JSC::iteratorOpenTryFastImpl): (JSC::asyncIteratorOpenTryFastImpl): (JSC::JSC_DEFINE_COMMON_SLOW_PATH): * Source/JavaScriptCore/runtime/CommonSlowPaths.h: * Source/JavaScriptCore/runtime/Gate.h: * Source/JavaScriptCore/runtime/JSAsyncGenerator.cpp: (JSC::JSAsyncGenerator::enqueue): (JSC::JSAsyncGenerator::dequeue): (): Deleted. * Source/JavaScriptCore/runtime/JSAsyncGenerator.h: * Source/JavaScriptCore/runtime/JSGlobalObject.cpp: (JSC::JSGlobalObject::init): * Source/JavaScriptCore/runtime/JSGlobalObject.h: * Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h: (JSC::JSGlobalObject::asyncGeneratorPrototypeNextFunction const): (JSC::JSGlobalObject::asyncIteratorPrototypeSymbolAsyncIteratorFunction const): * Source/JavaScriptCore/runtime/JSMicrotask.cpp: (JSC::asyncGeneratorCompleteStep): (JSC::asyncGeneratorUnwrapYieldResumption): (JSC::enqueueAsyncGeneratorDriver): (JSC::asyncGeneratorDispatchSuspend): (JSC::asyncFunctionArrangeAwaitResume): (JSC::asyncFunctionGeneratorBodyCall): (JSC::JSC_DEFINE_HOST_FUNCTION): (JSC::resumeModeForStatus): (JSC::asyncGeneratorDriverResume): (JSC::runInternalMicrotask): * Source/JavaScriptCore/runtime/JSMicrotask.h: * Source/JavaScriptCore/runtime/Microtask.h: * Source/JavaScriptCore/runtime/OptionsList.h: * Source/JavaScriptCore/runtime/VM.cpp: (JSC::VM::VM): (JSC::VM::visitAggregateImpl): * Source/JavaScriptCore/runtime/VM.h: (JSC::VM::fastAsyncGeneratorSentinel): Canonical link: https://commits.webkit.org/317115@main
1 parent bbdc33d commit 5f65bd9

82 files changed

Lines changed: 2490 additions & 489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Regression test for the completed-producer fast path in enqueueAsyncGeneratorFastConsumer
2+
// (runtime/JSMicrotask.cpp). When a `for await` consumes a genuine async generator that is ALREADY in
3+
// the Completed state, op_async_iterator_next's fast branch settles { value: undefined, done: true }
4+
// immediately AND schedules an AsyncGeneratorFastConsumerResume microtask for the consumer frame. That
5+
// completed branch must still set the consumer's SuppressFastResume flag; otherwise the consumer is
6+
// driven twice -- once by that microtask, once by the normal await-Promise machinery its own driver
7+
// (driveAsyncFunction / asyncFunctionGeneratorBodyCall / asyncGeneratorBodyCall) attaches when it
8+
// suspends at the following op_yield -- re-entering an already-resolving frame and double-advancing it.
9+
//
10+
// We exercise every driver: an async-function consumer's initial run (driveAsyncFunction) and its
11+
// subsequent resumes (asyncFunctionGeneratorBodyCall), plus an async-generator consumer
12+
// (asyncGeneratorBodyCall), each re-iterating an exhausted genuine async generator. Warmed hot enough
13+
// to reach the upper JIT tiers. With the bug, the stray extra resume inflates counters / sums, throws,
14+
// or hangs (main never completes); with the fix every count is exact.
15+
16+
function assert(cond, message) {
17+
if (!cond)
18+
throw new Error("Assertion failed: " + message);
19+
}
20+
21+
function makeGen() {
22+
return (async function* () {
23+
yield 1;
24+
yield 2;
25+
})();
26+
}
27+
28+
// Counts how many times each consumer's post-loop tail actually executes. A double-drive would run a
29+
// tail more than once (or corrupt the frame), so these must exactly match the number of calls.
30+
let tailRuns = 0;
31+
32+
// Async-function consumer. First loop exhausts g; second loop re-iterates the now-Completed g (body
33+
// must never run); then a real await forces a second suspension so any stray resume is observable.
34+
async function reconsume() {
35+
const g = makeGen();
36+
let sum = 0;
37+
for await (const x of g)
38+
sum += x; // 1 + 2
39+
for await (const x of g)
40+
sum += 1000; // g Completed -> must NOT run
41+
await Promise.resolve(); // suspend again; a stray resume would double-advance past here
42+
tailRuns++;
43+
return sum;
44+
}
45+
46+
// Async-function consumer whose very first suspension is the completed-producer next (initial-run path,
47+
// driven by driveAsyncFunction rather than asyncFunctionGeneratorBodyCall).
48+
async function consumeAlreadyExhausted(g) {
49+
let count = 0;
50+
for await (const x of g)
51+
count += 1; // g Completed -> must NOT run
52+
tailRuns++;
53+
return count;
54+
}
55+
56+
// Async-generator consumer over a completed producer (asyncGeneratorBodyCall driver).
57+
async function* genConsumeExhausted(g) {
58+
for await (const x of g)
59+
yield x; // g Completed -> must NOT yield
60+
yield -1; // sentinel, must be produced exactly once
61+
tailRuns++;
62+
}
63+
64+
let done = false;
65+
let error = null;
66+
67+
async function main() {
68+
const N = testLoopCount;
69+
let expectedTailRuns = 0;
70+
71+
for (let k = 0; k < N; k++) {
72+
const r = await reconsume();
73+
assert(r === 3, "reconsume expected 3, got " + r);
74+
expectedTailRuns++;
75+
76+
const g = makeGen();
77+
for await (const _ of g) { } // exhaust
78+
const c = await consumeAlreadyExhausted(g);
79+
assert(c === 0, "consumeAlreadyExhausted expected 0, got " + c);
80+
expectedTailRuns++;
81+
82+
const g2 = makeGen();
83+
for await (const _ of g2) { } // exhaust
84+
const collected = [];
85+
for await (const y of genConsumeExhausted(g2))
86+
collected.push(y);
87+
assert(collected.length === 1 && collected[0] === -1,
88+
"genConsumeExhausted produced " + JSON.stringify(collected));
89+
expectedTailRuns++;
90+
}
91+
92+
assert(tailRuns === expectedTailRuns, "tailRuns expected " + expectedTailRuns + ", got " + tailRuns);
93+
}
94+
95+
main().then(() => { done = true; }, (e) => { error = e; });
96+
97+
drainMicrotasks();
98+
99+
if (error)
100+
throw error;
101+
assert(done, "async main() did not complete");
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Regression test for the double-drive segfault fixed in asyncGeneratorSuspend (runtime/JSMicrotask.cpp).
2+
// An async generator that is itself a `for await` consumer of another async generator takes
3+
// op_async_iterator_next's fast-enqueue branch, which sets the consumer's SuppressFastResume flag and
4+
// arranges its resume via a JSSlimPromiseReaction on the producer. When such a consumer is driven
5+
// DIRECTLY by .next() (the inline AsyncGeneratorPrototype.next path -> asyncGeneratorSuspend host fn),
6+
// rather than by an outer for-await, asyncGeneratorSuspend must honor SuppressFastResume too; otherwise
7+
// the consumer is resumed twice (once by the producer's reaction, once by the normal await machinery),
8+
// corrupting the state machine and crashing. Every other driver already had this guard.
9+
10+
function assert(cond, message) {
11+
if (!cond)
12+
throw new Error("Assertion failed: " + message);
13+
}
14+
15+
function makeProducer() {
16+
return (async function* () { yield 1; yield 2; yield 3; })();
17+
}
18+
19+
// Async-generator consumer, driven below by manual .next() (not by a for-await).
20+
async function* makeConsumer(producer) {
21+
let hits = 0;
22+
for await (const x of producer) {
23+
hits++;
24+
yield x * 10 + hits; // 11, 22, 33
25+
}
26+
yield 999; // sentinel: emitted exactly once, after the loop
27+
}
28+
29+
// Manually drive an async generator to exhaustion via awaited .next() calls.
30+
async function driveToArray(gen) {
31+
const out = [];
32+
let r;
33+
while (!(r = await gen.next()).done)
34+
out.push(r.value);
35+
return out;
36+
}
37+
38+
let done = false;
39+
let error = null;
40+
41+
async function main() {
42+
const N = testLoopCount; // warm hot enough to reach the upper JIT tiers.
43+
for (let k = 0; k < N; k++) {
44+
const collected = await driveToArray(makeConsumer(makeProducer()));
45+
assert(collected.length === 4, "expected 4 values, got " + collected.length);
46+
assert(collected[0] === 11 && collected[1] === 22 && collected[2] === 33 && collected[3] === 999,
47+
"unexpected values: " + collected.join(","));
48+
49+
// Also drive one where the consumer is closed early via .return() mid-stream.
50+
const g = makeConsumer(makeProducer());
51+
const first = await g.next();
52+
assert(first.value === 11 && !first.done, "first: " + JSON.stringify(first));
53+
const ret = await g.return("bye");
54+
assert(ret.done === true && ret.value === "bye", "return: " + JSON.stringify(ret));
55+
const after = await g.next();
56+
assert(after.done === true && after.value === undefined, "after-return: " + JSON.stringify(after));
57+
}
58+
}
59+
60+
main().then(() => { done = true; }, (e) => { error = e; });
61+
62+
drainMicrotasks();
63+
64+
if (error)
65+
throw error;
66+
assert(done, "async main() did not complete");
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Verifies the for-await fast consumer consumes the EXACT number of microtask turns per step that the
2+
// spec (and V8/Node) require -- no more, no fewer. Each scenario launches a fixed 8-turn background
3+
// microtask "ruler" before the consumer; the interleaving of ruler ticks (Rn) with consumer/producer
4+
// events makes the per-step tick cadence observable. The expected sequences below were captured from
5+
// V8 (Node) and are the spec-correct ground truth; the fast consumer must reproduce them byte-for-byte.
6+
// A drifted tick count (e.g. an extra/missing await turn) reorders these and fails.
7+
8+
function assert(cond, message) {
9+
if (!cond)
10+
throw new Error("Assertion failed: " + message);
11+
}
12+
13+
async function scenario(body) {
14+
const log = [];
15+
const E = s => log.push(s);
16+
let p = Promise.resolve();
17+
for (let i = 0; i < 8; i++) {
18+
const j = i;
19+
p = p.then(() => E("R" + j));
20+
}
21+
await body(E);
22+
return log.join(",");
23+
}
24+
25+
const scenarios = {
26+
async gen3(E) {
27+
async function* g() { yield 1; yield 2; yield 3; }
28+
for await (const x of g()) E("v" + x);
29+
E("done");
30+
},
31+
async break(E) {
32+
async function* g() { try { yield 1; yield 2; yield 3; } finally { E("fin"); } }
33+
for await (const x of g()) { E("v" + x); if (x === 2) break; }
34+
E("after");
35+
},
36+
async throw(E) {
37+
async function* g() { try { yield 1; yield 2; } finally { E("fin"); } }
38+
try { for await (const x of g()) { E("v" + x); if (x === 1) throw new Error("b"); } } catch (e) { E("caught"); }
39+
},
40+
async delegate(E) {
41+
async function* inner() { yield "a"; yield "b"; }
42+
async function* g() { yield 0; yield* inner(); yield 9; }
43+
for await (const x of g()) E("v" + x);
44+
},
45+
async internalAwait(E) {
46+
async function* g() { await 0; yield 1; await 0; yield 2; }
47+
for await (const x of g()) E("v" + x);
48+
},
49+
async yieldPromise(E) {
50+
async function* g() { yield Promise.resolve("p"); yield "q"; }
51+
for await (const x of g()) E("v" + x);
52+
},
53+
async nested(E) {
54+
async function* inner(k) { yield k + "a"; yield k + "b"; }
55+
async function* outer() { yield 1; yield 2; }
56+
for await (const o of outer()) for await (const i of inner(o)) E("v" + i);
57+
},
58+
async empty(E) {
59+
async function* g() { }
60+
for await (const x of g()) E("v" + x);
61+
E("done");
62+
},
63+
async completedReconsume(E) {
64+
async function* g() { yield 1; }
65+
const it = g();
66+
for await (const x of it) E("a" + x);
67+
for await (const x of it) E("b" + x); // completed
68+
E("done");
69+
},
70+
};
71+
72+
// Spec-correct interleavings (captured from V8 / Node).
73+
const expected = {
74+
gen3: "R0,R1,v1,R2,R3,v2,R4,R5,v3,R6,done,R7",
75+
break: "R0,R1,v1,R2,R3,v2,R4,fin,R5,after,R6",
76+
throw: "R0,R1,v1,R2,fin,R3,caught,R4",
77+
delegate: "R0,R1,v0,R2,R3,R4,va,R5,R6,R7,vb,v9",
78+
internalAwait: "R0,R1,R2,v1,R3,R4,R5,v2,R6,R7",
79+
yieldPromise: "R0,R1,vp,R2,R3,vq,R4,R5",
80+
nested: "R0,R1,R2,R3,v1a,R4,R5,v1b,R6,R7,v2a,v2b",
81+
empty: "R0,done,R1",
82+
completedReconsume: "R0,R1,a1,R2,R3,done,R4",
83+
};
84+
85+
let done = false;
86+
let error = null;
87+
88+
async function main() {
89+
for (const name of Object.keys(expected)) {
90+
const got = await scenario(scenarios[name]);
91+
assert(got === expected[name], name + "\n expected: " + expected[name] + "\n got: " + got);
92+
}
93+
}
94+
95+
main().then(() => { done = true; }, (e) => { error = e; });
96+
97+
drainMicrotasks();
98+
99+
if (error)
100+
throw error;
101+
assert(done, "async main() did not complete");
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Regression tests for the SuppressFastResume flag (JSAsyncGenerator / JSAsyncFunctionGenerator per-cell
2+
// bit) under recursion and re-entrancy. The flag is set on a for-await consumer just before it suspends
3+
// at op_yield, and consumed at that suspend, so it must survive: interleaved/queued .next() requests on a
4+
// fast consumer, nested fast for-await in one generator, a producer that re-enters the consumer's .next()
5+
// while the consumer is Executing (must enqueue, not double-drive), reconsuming a completed generator, and
6+
// a manual .next() racing the fast resume. Expected values are the spec-correct results (verified against
7+
// V8); a double-drive or a lost/stale flag would reorder or duplicate them. Warmed to reach the upper JIT
8+
// tiers so the DFG/FTL fast path is exercised too.
9+
10+
function assert(cond, message) {
11+
if (!cond)
12+
throw new Error("Assertion failed: " + message);
13+
}
14+
15+
// Four queued .next() requests on an async-generator that itself for-awaits a producer.
16+
async function interleavedNext() {
17+
async function* prod() { yield 1; yield 2; yield 3; }
18+
async function* cons(p) { for await (const x of p) yield x * 10; }
19+
const c = cons(prod());
20+
const r = await Promise.all([c.next(), c.next(), c.next(), c.next()]);
21+
return r.map(v => v.value + "/" + v.done).join(" ");
22+
}
23+
24+
// Nested fast for-await inside one consumer generator (each op_async_iterator_next sets the flag, each
25+
// following op_yield consumes it -- the inner and outer loops must not clobber each other's flag).
26+
async function nestedFast() {
27+
async function* inner(k) { yield k + "a"; yield k + "b"; }
28+
async function* outer(p) { for await (const x of p) for await (const y of inner(x)) yield y; }
29+
async function* src() { yield 1; yield 2; }
30+
let s = "";
31+
for await (const v of outer(src()))
32+
s += v + ",";
33+
return s;
34+
}
35+
36+
// The producer, resumed synchronously by the consumer's fast next, calls the consumer's own .next() while
37+
// the consumer is Executing. That must only enqueue (not re-enter/double-drive) the consumer.
38+
async function reentrantProducer() {
39+
let cRef;
40+
async function* prod() { cRef.next(); yield 1; cRef.next(); yield 2; }
41+
async function* cons(p) { for await (const x of p) yield x * 100; }
42+
cRef = cons(prod());
43+
const collected = [];
44+
for await (const v of cRef) {
45+
collected.push(v);
46+
if (collected.length >= 4)
47+
break;
48+
}
49+
return collected.join(",");
50+
}
51+
52+
// Re-consume the same consumer instance with a second loop after it has completed.
53+
async function reconsume() {
54+
async function* prod() { yield 1; yield 2; }
55+
async function* cons(p) { for await (const x of p) yield x; yield -1; }
56+
const c = cons(prod());
57+
let s = "";
58+
for await (const v of c) s += "a" + v + ",";
59+
for await (const v of c) s += "b" + v + ","; // completed -> yields nothing
60+
return s;
61+
}
62+
63+
// A manual .next() issued while the consumer is mid-await of the producer (races the fast resume).
64+
async function nextDuringAwait() {
65+
async function* prod() { yield 10; yield 20; yield 30; }
66+
async function* cons(p) { for await (const x of p) yield x; }
67+
const c = cons(prod());
68+
const a = await c.next();
69+
const pending = c.next();
70+
const b = await pending;
71+
const rest = await c.next();
72+
return [a, b, rest].map(v => v.value + "/" + v.done).join(" ");
73+
}
74+
75+
const cases = [
76+
[interleavedNext, "10/false 20/false 30/false undefined/true"],
77+
[nestedFast, "1a,1b,2a,2b,"],
78+
[reentrantProducer, "100"],
79+
[reconsume, "a1,a2,a-1,"],
80+
[nextDuringAwait, "10/false 20/false 30/false"],
81+
];
82+
83+
let done = false;
84+
let error = null;
85+
86+
async function main() {
87+
const N = testLoopCount; // warm hot enough to reach the upper JIT tiers.
88+
for (let i = 0; i < N; i++) {
89+
for (const [fn, expected] of cases) {
90+
const got = await fn();
91+
assert(got === expected, fn.name + " iter " + i + ": expected [" + expected + "] got [" + got + "]");
92+
}
93+
}
94+
}
95+
96+
main().then(() => { done = true; }, (e) => { error = e; });
97+
98+
drainMicrotasks();
99+
100+
if (error)
101+
throw error;
102+
assert(done, "async main() did not complete");

0 commit comments

Comments
 (0)