diff options
Diffstat (limited to 'js/src/vm/AsyncIteration.h')
-rw-r--r-- | js/src/vm/AsyncIteration.h | 571 |
1 files changed, 571 insertions, 0 deletions
diff --git a/js/src/vm/AsyncIteration.h b/js/src/vm/AsyncIteration.h new file mode 100644 index 0000000000..4629329cc8 --- /dev/null +++ b/js/src/vm/AsyncIteration.h @@ -0,0 +1,571 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef vm_AsyncIteration_h +#define vm_AsyncIteration_h + +#include "builtin/Promise.h" // js::PromiseHandler +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/GeneratorObject.h" +#include "vm/JSObject.h" +#include "vm/List.h" +#include "vm/PromiseObject.h" + +// [SMDOC] Async generators +// +// # Start +// +// When an async generator is called, it synchronously runs until the +// JSOp::InitialYield and then suspends, just like a sync generator, and returns +// an async generator object (js::AsyncGeneratorObject). +// +// +// # Request queue +// +// When next/return/throw is called on the async generator object, +// js::AsyncGeneratorEnqueue performs the following: +// * Create a new AsyncGeneratorRequest and enqueue it in the generator +// object's request queue. +// * Resume the generator with the oldest request, if the generator is +// suspended (see "Resume" section below) +// * Return the promise for the request +// +// This is done in js::AsyncGeneratorEnqueue, which corresponds to +// AsyncGeneratorEnqueue in the spec, +// and js::AsyncGeneratorResumeNext corresponds to the following: +// * AsyncGeneratorResolve +// * AsyncGeneratorReject +// * AsyncGeneratorResumeNext +// +// The returned promise is resolved when the resumption for the request +// completes with yield/throw/return, in js::AsyncGeneratorResolve and +// js::AsyncGeneratorReject. +// They correspond to AsyncGeneratorResolve and AsyncGeneratorReject in the +// spec. +// +// +// # Await +// +// Async generator's `await` is implemented differently than async function's +// `await`. +// +// The bytecode is the following: +// (ignoring CanSkipAwait; see the comment in AsyncFunction.h for more details) +// +// ``` +// (operand here) # VALUE +// GetAliasedVar ".generator" # VALUE .generator +// Await 0 # RVAL GENERATOR RESUMEKIND +// +// AfterYield # RVAL GENERATOR RESUMEKIND +// CheckResumeKind # RVAL +// ``` +// +// Async generators don't use JSOp::AsyncAwait, and that part is handled +// in js::AsyncGeneratorResume, and js::AsyncGeneratorAwait called there. +// +// Both JSOp::Await and JSOp::Yield behave in the exactly same way, +// and js::AsyncGeneratorResume checks the last opcode and branches for +// await/yield/return cases. +// +// +// # Reaction jobs and resume after await +// +// This is almost same as for async functions (see AsyncFunction.h). +// +// The reaction record for the job is marked as "this is for async generator" +// (see js::AsyncGeneratorAwait), and handled specially in +// js::PromiseReactionJob, which calls js::AsyncGeneratorPromiseReactionJob. +// +// +// # Yield +// +// `yield` is implemented with the following bytecode sequence: +// (Ignoring CanSkipAwait for simplicity) +// +// ``` +// (operand here) # VALUE +// GetAliasedVar ".generator" # VALUE .generator +// Await 1 # RVAL GENERATOR RESUMEKIND +// AfterYield # RVAL GENERATOR RESUMEKIND +// CheckResumeKind # RVAL +// +// GetAliasedVar ".generator" # RVAL .generator +// Yield 2 # RVAL2 GENERATOR RESUMEKIND +// +// AfterYield # RVAL2 GENERATOR RESUMEKIND +// CheckResumeKind # RVAL2 +// ``` +// +// The 1st part (JSOp::Await + JSOp::CheckResumeKind) performs an implicit +// `await`, as specified in AsyncGeneratorYield step 5. +// +// AsyncGeneratorYield ( value ) +// https://tc39.es/ecma262/#sec-asyncgeneratoryield +// +// 5. Set value to ? Await(value). +// +// The 2nd part (JSOp::Yield) suspends execution and yields the result of +// `await`, as specified in AsyncGeneratorYield steps 1-4, 6-7, 9-10. +// +// AsyncGeneratorYield ( value ) +// https://tc39.es/ecma262/#sec-asyncgeneratoryield +// +// 1. Let genContext be the running execution context. +// 2. Assert: genContext is the execution context of a generator. +// 3. Let generator be the value of the Generator component of genContext. +// 4. Assert: GetGeneratorKind() is async. +// .. +// 6. Set generator.[[AsyncGeneratorState]] to suspendedYield. +// 7. Remove genContext from the execution context stack and restore the +// execution context that is at the top of the execution context stack as +// the running execution context. +// 8. ... +// 9. Return ! AsyncGeneratorResolve(generator, value, false). +// 10. NOTE: This returns to the evaluation of the operation that had most +// previously resumed evaluation of genContext. +// +// The last part (JSOp::CheckResumeKind) checks the resumption type and +// resumes/throws/returns the execution, as specified in AsyncGeneratorYield +// step 8. +// +// 8. Set the code evaluation state of genContext such that when evaluation is +// resumed with a Completion resumptionValue the following steps will be +// performed: +// a. If resumptionValue.[[Type]] is not return, return +// Completion(resumptionValue). +// b. Let awaited be Await(resumptionValue.[[Value]]). +// c. If awaited.[[Type]] is throw, return Completion(awaited). +// d. Assert: awaited.[[Type]] is normal. +// e. Return Completion { [[Type]]: return, [[Value]]: awaited.[[Value]], +// [[Target]]: empty }. +// f. NOTE: When one of the above steps returns, it returns to the +// evaluation of the YieldExpression production that originally called +// this abstract operation. +// +// Resumption with `AsyncGenerator.prototype.return` is handled differently. +// See "Resumption with return" section below. +// +// +// # Return +// +// `return` with operand is implemented with the following bytecode sequence: +// (Ignoring CanSkipAwait for simplicity) +// +// ``` +// (operand here) # VALUE +// GetAliasedVar ".generator" # VALUE .generator +// Await 0 # RVAL GENERATOR RESUMEKIND +// AfterYield # RVAL GENERATOR RESUMEKIND +// CheckResumeKind # RVAL +// +// SetRval # +// GetAliasedVar ".generator" # .generator +// FinalYieldRval # +// ``` +// +// The 1st part (JSOp::Await + JSOp::CheckResumeKind) performs implicit +// `await`, as specified in ReturnStatement's Evaluation step 3. +// +// ReturnStatement: return Expression; +// https://tc39.es/ecma262/#sec-return-statement-runtime-semantics-evaluation +// +// 3. If ! GetGeneratorKind() is async, set exprValue to ? Await(exprValue). +// +// And the 2nd part corresponds to AsyncGeneratorStart steps 5.a-e, 5.g. +// +// AsyncGeneratorStart ( generator, generatorBody ) +// https://tc39.es/ecma262/#sec-asyncgeneratorstart +// +// 5. Set the code evaluation state of genContext such that when evaluation +// is resumed for that execution context the following steps will be +// performed: +// a. Let result be the result of evaluating generatorBody. +// b. Assert: If we return here, the async generator either threw an +// exception or performed either an implicit or explicit return. +// c. Remove genContext from the execution context stack and restore the +// execution context that is at the top of the execution context stack +// as the running execution context. +// d. Set generator.[[AsyncGeneratorState]] to completed. +// e. If result is a normal completion, let resultValue be undefined. +// ... +// g. Return ! AsyncGeneratorResolve(generator, resultValue, true). +// +// `return` without operand or implicit return is implicit with the following +// bytecode sequence: +// +// ``` +// Undefined # undefined +// SetRval # +// GetAliasedVar ".generator" # .generator +// FinalYieldRval # +// ``` +// +// This is also AsyncGeneratorStart steps 5.a-e, 5.g. +// +// +// # Throw +// +// Unlike async function, async generator doesn't use implicit try-catch, +// but the throw completion is handled by js::AsyncGeneratorResume, +// and js::AsyncGeneratorThrown is called there. +// +// 5. ... +// f. Else, +// i. Let resultValue be result.[[Value]]. +// ii. If result.[[Type]] is not return, then +// 1. Return ! AsyncGeneratorReject(generator, resultValue). +// +// +// # Resumption with return +// +// Resumption with return completion is handled in js::AsyncGeneratorResumeNext. +// +// If the generator is suspended, it doesn't immediately resume the generator +// script itself, but handles implicit `await` it in +// js::AsyncGeneratorResumeNext. +// (See PromiseHandlerAsyncGeneratorYieldReturnAwaitedFulfilled and +// PromiseHandlerAsyncGeneratorYieldReturnAwaitedRejected), and resumes the +// generator with the result of await. +// And the return completion is finally handled in JSOp::CheckResumeKind +// after JSOp::Yield. +// +// This corresponds to AsyncGeneratorYield step 8. +// +// AsyncGeneratorYield ( value ) +// https://tc39.es/ecma262/#sec-asyncgeneratoryield +// +// 8. Set the code evaluation state of genContext such that when evaluation +// is resumed with a Completion resumptionValue the following steps will +// be performed: +// .. +// b. Let awaited be Await(resumptionValue.[[Value]]). +// c. If awaited.[[Type]] is throw, return Completion(awaited). +// d. Assert: awaited.[[Type]] is normal. +// e. Return Completion { [[Type]]: return, [[Value]]: awaited.[[Value]], +// [[Target]]: empty }. +// +// If the generator is already completed, it awaits on the return value, +// (See PromiseHandlerAsyncGeneratorResumeNextReturnFulfilled and +// PromiseHandlerAsyncGeneratorResumeNextReturnRejected), and resolves the +// request's promise with the value. +// +// It corresponds to AsyncGeneratorResumeNext step 10.b.i. +// +// AsyncGeneratorResumeNext ( generator ) +// https://tc39.es/ecma262/#sec-asyncgeneratorresumenext +// +// 10. If completion is an abrupt completion, then +// .. +// b. If state is completed, then +// i. If completion.[[Type]] is return, then +// 1. Set generator.[[AsyncGeneratorState]] to awaiting-return. +// 2. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). +// 3. Let stepsFulfilled be the algorithm steps defined in +// AsyncGeneratorResumeNext Return Processor Fulfilled Functions. +// 4. Let onFulfilled be ! CreateBuiltinFunction(stepsFulfilled, « +// [[Generator]] »). +// 5. Set onFulfilled.[[Generator]] to generator. +// 6. Let stepsRejected be the algorithm steps defined in +// AsyncGeneratorResumeNext Return Processor Rejected Functions. +// 7. Let onRejected be ! CreateBuiltinFunction(stepsRejected, « +// [[Generator]] »). +// 8. Set onRejected.[[Generator]] to generator. +// 9. Perform ! PerformPromiseThen(promise, onFulfilled, onRejected). +// 10. Return undefined. +// + +namespace js { + +class AsyncGeneratorObject; +enum class CompletionKind : uint8_t; + +extern const JSClass AsyncGeneratorFunctionClass; + +[[nodiscard]] bool AsyncGeneratorPromiseReactionJob( + JSContext* cx, PromiseHandler handler, + Handle<AsyncGeneratorObject*> generator, HandleValue argument); + +bool AsyncGeneratorNext(JSContext* cx, unsigned argc, Value* vp); +bool AsyncGeneratorReturn(JSContext* cx, unsigned argc, Value* vp); +bool AsyncGeneratorThrow(JSContext* cx, unsigned argc, Value* vp); + +// AsyncGeneratorRequest record in the spec. +// Stores the info from AsyncGenerator#{next,return,throw}. +// +// This object is reused across multiple requests as an optimization, and +// stored in the Slot_CachedRequest slot. +class AsyncGeneratorRequest : public NativeObject { + private: + enum AsyncGeneratorRequestSlots { + // Int32 value with CompletionKind. + // Normal: next + // Return: return + // Throw: throw + Slot_CompletionKind = 0, + + // The value passed to AsyncGenerator#{next,return,throw}. + Slot_CompletionValue, + + // The promise returned by AsyncGenerator#{next,return,throw}. + Slot_Promise, + + Slots, + }; + + void init(CompletionKind completionKind, const Value& completionValue, + PromiseObject* promise) { + setFixedSlot(Slot_CompletionKind, + Int32Value(static_cast<int32_t>(completionKind))); + setFixedSlot(Slot_CompletionValue, completionValue); + setFixedSlot(Slot_Promise, ObjectValue(*promise)); + } + + // Clear the request data for reuse. + void clearData() { + setFixedSlot(Slot_CompletionValue, NullValue()); + setFixedSlot(Slot_Promise, NullValue()); + } + + friend AsyncGeneratorObject; + + public: + static const JSClass class_; + + static AsyncGeneratorRequest* create(JSContext* cx, + CompletionKind completionKind, + HandleValue completionValue, + Handle<PromiseObject*> promise); + + CompletionKind completionKind() const { + return static_cast<CompletionKind>( + getFixedSlot(Slot_CompletionKind).toInt32()); + } + JS::Value completionValue() const { + return getFixedSlot(Slot_CompletionValue); + } + PromiseObject* promise() const { + return &getFixedSlot(Slot_Promise).toObject().as<PromiseObject>(); + } +}; + +class AsyncGeneratorObject : public AbstractGeneratorObject { + private: + enum AsyncGeneratorObjectSlots { + // Int32 value containing one of the |State| fields from below. + Slot_State = AbstractGeneratorObject::RESERVED_SLOTS, + + // * null value if this async generator has no requests + // * AsyncGeneratorRequest if this async generator has only one request + // * list object if this async generator has 2 or more requests + Slot_QueueOrRequest, + + // Cached AsyncGeneratorRequest for later use. + // undefined if there's no cache. + Slot_CachedRequest, + + Slots + }; + + public: + enum State { + // "suspendedStart" in the spec. + // Suspended after invocation. + State_SuspendedStart, + + // "suspendedYield" in the spec + // Suspended with `yield` expression. + State_SuspendedYield, + + // "executing" in the spec. + // Resumed from initial suspend or yield, and either running the script + // or awaiting for `await` expression. + State_Executing, + + // Part of "executing" in the spec. + // Awaiting on the value passed by AsyncGenerator#return which is called + // while executing. + State_AwaitingYieldReturn, + + // "awaiting-return" in the spec. + // Awaiting on the value passed by AsyncGenerator#return which is called + // after completed. + State_AwaitingReturn, + + // "completed" in the spec. + // The generator is completed. + State_Completed + }; + + State state() const { + return static_cast<State>(getFixedSlot(Slot_State).toInt32()); + } + void setState(State state_) { setFixedSlot(Slot_State, Int32Value(state_)); } + + private: + // Queue is implemented in 2 ways. If only one request is queued ever, + // request is stored directly to the slot. Once 2 requests are queued, a + // list is created and requests are appended into it, and the list is + // stored to the slot. + + bool isSingleQueue() const { + return getFixedSlot(Slot_QueueOrRequest).isNull() || + getFixedSlot(Slot_QueueOrRequest) + .toObject() + .is<AsyncGeneratorRequest>(); + } + bool isSingleQueueEmpty() const { + return getFixedSlot(Slot_QueueOrRequest).isNull(); + } + void setSingleQueueRequest(AsyncGeneratorRequest* request) { + setFixedSlot(Slot_QueueOrRequest, ObjectValue(*request)); + } + void clearSingleQueueRequest() { + setFixedSlot(Slot_QueueOrRequest, NullValue()); + } + AsyncGeneratorRequest* singleQueueRequest() const { + return &getFixedSlot(Slot_QueueOrRequest) + .toObject() + .as<AsyncGeneratorRequest>(); + } + + ListObject* queue() const { + return &getFixedSlot(Slot_QueueOrRequest).toObject().as<ListObject>(); + } + void setQueue(ListObject* queue_) { + setFixedSlot(Slot_QueueOrRequest, ObjectValue(*queue_)); + } + + public: + static const JSClass class_; + static const JSClassOps classOps_; + + static AsyncGeneratorObject* create(JSContext* cx, HandleFunction asyncGen); + + bool isSuspendedStart() const { return state() == State_SuspendedStart; } + bool isSuspendedYield() const { return state() == State_SuspendedYield; } + bool isExecuting() const { return state() == State_Executing; } + bool isAwaitingYieldReturn() const { + return state() == State_AwaitingYieldReturn; + } + bool isAwaitingReturn() const { return state() == State_AwaitingReturn; } + bool isCompleted() const { return state() == State_Completed; } + + void setSuspendedStart() { setState(State_SuspendedStart); } + void setSuspendedYield() { setState(State_SuspendedYield); } + void setExecuting() { setState(State_Executing); } + void setAwaitingYieldReturn() { setState(State_AwaitingYieldReturn); } + void setAwaitingReturn() { setState(State_AwaitingReturn); } + void setCompleted() { setState(State_Completed); } + + [[nodiscard]] static bool enqueueRequest( + JSContext* cx, Handle<AsyncGeneratorObject*> generator, + Handle<AsyncGeneratorRequest*> request); + + static AsyncGeneratorRequest* dequeueRequest( + JSContext* cx, Handle<AsyncGeneratorObject*> generator); + + static AsyncGeneratorRequest* peekRequest( + Handle<AsyncGeneratorObject*> generator); + + bool isQueueEmpty() const { + if (isSingleQueue()) { + return isSingleQueueEmpty(); + } + return queue()->getDenseInitializedLength() == 0; + } + + // This function does either of the following: + // * return a cached request object with the slots updated + // * create a new request object with the slots set + static AsyncGeneratorRequest* createRequest( + JSContext* cx, Handle<AsyncGeneratorObject*> generator, + CompletionKind completionKind, HandleValue completionValue, + Handle<PromiseObject*> promise); + + // Stores the given request to the generator's cache after clearing its data + // slots. The cached request will be reused in the subsequent createRequest + // call. + void cacheRequest(AsyncGeneratorRequest* request) { + if (hasCachedRequest()) { + return; + } + + request->clearData(); + setFixedSlot(Slot_CachedRequest, ObjectValue(*request)); + } + + private: + bool hasCachedRequest() const { + return getFixedSlot(Slot_CachedRequest).isObject(); + } + + AsyncGeneratorRequest* takeCachedRequest() { + auto request = &getFixedSlot(Slot_CachedRequest) + .toObject() + .as<AsyncGeneratorRequest>(); + clearCachedRequest(); + return request; + } + + void clearCachedRequest() { setFixedSlot(Slot_CachedRequest, NullValue()); } +}; + +JSObject* CreateAsyncFromSyncIterator(JSContext* cx, HandleObject iter, + HandleValue nextMethod); + +class AsyncFromSyncIteratorObject : public NativeObject { + private: + enum AsyncFromSyncIteratorObjectSlots { + // Object that implements the sync iterator protocol. + Slot_Iterator = 0, + + // The `next` property of the iterator object. + Slot_NextMethod = 1, + + Slots + }; + + void init(JSObject* iterator, const Value& nextMethod) { + setFixedSlot(Slot_Iterator, ObjectValue(*iterator)); + setFixedSlot(Slot_NextMethod, nextMethod); + } + + public: + static const JSClass class_; + + static JSObject* create(JSContext* cx, HandleObject iter, + HandleValue nextMethod); + + JSObject* iterator() const { return &getFixedSlot(Slot_Iterator).toObject(); } + + const Value& nextMethod() const { return getFixedSlot(Slot_NextMethod); } +}; + +class AsyncIteratorObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass protoClass_; +}; + +// Iterator Helpers proposal +class AsyncIteratorHelperObject : public NativeObject { + public: + static const JSClass class_; + + enum { GeneratorSlot, SlotCount }; + + static_assert(GeneratorSlot == ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + "GeneratorSlot must match self-hosting define for generator " + "object slot."); +}; + +AsyncIteratorHelperObject* NewAsyncIteratorHelper(JSContext* cx); + +} // namespace js + +#endif /* vm_AsyncIteration_h */ |