Bug 1660538 - Add SMDOC for async function and async generator. r=jorendorff DONTBUILD

Differential Revision: https://phabricator.services.mozilla.com/D87896
This commit is contained in:
Tooru Fujisawa 2020-08-28 16:57:38 +00:00
parent 5dfb75acf7
commit d5cbb2ceb8
2 changed files with 524 additions and 0 deletions

View File

@ -14,6 +14,266 @@
#include "vm/JSObject.h"
#include "vm/PromiseObject.h"
// [SMDOC] Async functions
//
// # Implementation
//
// Async functions are implemented based on generators, in terms of
// suspend/resume.
// Instead of returning the generator object itself, they return the async
// function's result promise to the caller.
//
// The async function's result promise is stored in the generator object
// (js::AsyncFunctionGeneratorObject) and retrieved from it whenever the
// execution needs it.
//
//
// # Start
//
// When an async function is called, it synchronously runs until the first
// `await` or `return`. This works just like a normal function.
//
// This corresponds to steps 1-3, 5-9 of AsyncFunctionStart.
//
// AsyncFunctionStart ( promiseCapability, asyncFunctionBody )
// https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start
//
// 1. Let runningContext be the running execution context.
// 2. Let asyncContext be a copy of runningContext.
// 3. NOTE: Copying the execution state is required for the step below to
// resume its execution. It is ill-defined to resume a currently executing
// context.
// ...
// 5. Push asyncContext onto the execution context stack; asyncContext is now
// the running execution context.
// 6. Resume the suspended evaluation of asyncContext. Let result be the value
// returned by the resumed computation.
// 7. Assert: When we return here, asyncContext has already been removed from
// the execution context stack and runningContext is the currently running
// execution context.
// 8. Assert: result is a normal completion with a value of undefined. The
// possible sources of completion values are Await or, if the async
// function doesn't await anything, step 4.g above.
// 9. Return.
//
// Unlike generators, async functions don't contain JSOp::InitialYield and
// don't suspend immediately when call.
//
//
// # Return
//
// Explicit/implicit `return` is implemented with the following bytecode
// sequence:
//
// ```
// GetAliasedVar ".generator" # VALUE .generator
// AsyncResolve 0 # PROMISE
// SetRval #
// GetAliasedVar ".generator" # .generator
// FinalYieldRval #
// ```
//
// JSOp::Resolve (js::AsyncFunctionResolve) resolves the current async
// function's result promise. Then this sets it as the function's return value.
// (The return value is observable if the caller is still on the stack--
// that is, the async function is returning without ever awaiting.
// Otherwise we're returning to the microtask loop, which ignores the
// return value.)
//
// This corresponds to AsyncFunctionStart steps 4.a-e. 4.g.
//
// 4. Set the code evaluation state of asyncContext such that when evaluation
// is resumed for that execution context the following steps will be
// performed:
// a. Let result be the result of evaluating asyncFunctionBody.
// b. Assert: If we return here, the async function either threw an
// exception or performed an implicit or explicit return; all awaiting
// is done.
// c. Remove asyncContext 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. If result.[[Type]] is normal, then
// i. Perform
// ! Call(promiseCapability.[[Resolve]], undefined, «undefined»).
// e. Else if result.[[Type]] is return, then
// i. Perform
// ! Call(promiseCapability.[[Resolve]], undefined,
// «result.[[Value]]»).
// ...
// g. Return.
//
//
// # Throw
//
// The body part of an async function is enclosed by an implicit try-catch
// block, to catch `throw` completion of the function body.
//
// If an exception is thrown by the function body, the catch block catches it
// and rejects the async function's result promise.
//
// If there's an expression in parameters, the entire parameters part is also
// enclosed by a separate implicit try-catch block.
//
// ```
// Try #
// (parameter expressions here) #
// Goto BODY #
//
// JumpTarget from try #
// Exception # EXCEPTION
// GetAliasedVar ".generator" # EXCEPTION .generator
// AsyncResolve 1 # PROMISE
// SetRval #
// GetAliasedVar ".generator" # .generator
// FinalYieldRval #
//
// BODY:
// JumpTarget #
// Try #
// (body here) #
//
// JumpTarget from try #
// Exception # EXCEPTION
// GetAliasedVar ".generator" # EXCEPTION .generator
// AsyncResolve 1 # PROMISE
// SetRval #
// GetAliasedVar ".generator" # .generator
// FinalYieldRval #
// ```
//
// This corresponds to AsyncFunctionStart steps 4.f-g.
//
// 4. ...
// f. Else,
// i. Assert: result.[[Type]] is throw.
// ii. Perform
// ! Call(promiseCapability.[[Reject]], undefined,
// «result.[[Value]]»).
// g. Return.
//
//
// # Await
//
// `await` is implemented with the following bytecode sequence:
// (ignoring TrySkipAwait for now, see "Optimization for await" section)
//
// ```
// (operand here) # VALUE
// GetAliasedVar ".generator" # VALUE .generator
// AsyncAwait # PROMISE
//
// GetAliasedVar ".generator" # PROMISE .generator
// Await 0 # RVAL GENERATOR RESUMEKIND
//
// AfterYield # RVAL GENERATOR RESUMEKIND
// CheckResumeKind # RVAL
// ```
//
// JSOp::AsyncAwait corresponds to Await steps 1-9, and JSOp::Await corresponds
// to Await steps 10-12 in the spec.
//
// See the next section for JSOp::CheckResumeKind.
//
// After them, the async function is suspended, and if this is the first await
// in the execution, the async function's result promise is returned to the
// caller.
//
// Await
// https://tc39.es/ecma262/#await
//
// 1. Let asyncContext be the running execution context.
// 2. Let promise be ? PromiseResolve(%Promise%, value).
// 3. Let stepsFulfilled be the algorithm steps defined in Await Fulfilled
// Functions.
// 4. Let onFulfilled be ! CreateBuiltinFunction(stepsFulfilled, «
// [[AsyncContext]] »).
// 5. Set onFulfilled.[[AsyncContext]] to asyncContext.
// 6. Let stepsRejected be the algorithm steps defined in Await Rejected
// Functions.
// 7. Let onRejected be ! CreateBuiltinFunction(stepsRejected, «
// [[AsyncContext]] »).
// 8. Set onRejected.[[AsyncContext]] to asyncContext.
// 9. Perform ! PerformPromiseThen(promise, onFulfilled, onRejected).
// 10. Remove asyncContext 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.
// 11. Set the code evaluation state of asyncContext such that when evaluation
// is resumed with a Completion completion, the following steps of the
// algorithm that invoked Await will be performed, with completion
// available.
// 12. Return.
// 13. NOTE: This returns to the evaluation of the operation that had most
// previously resumed evaluation of asyncContext.
//
// (See comments above AsyncAwait and Await in js/src/vm/Opcodes.h for more
// details)
//
//
// # Reaction jobs and resume after await
//
// When an async function performs `await` and the operand becomes settled, a
// new reaction job for the operand is enqueued to the job queue.
//
// The reaction record for the job is marked as "this is for async function"
// (see js::AsyncFunctionAwait), and handled specially in
// js::PromiseReactionJob.
//
// When the await operand resolves (either with fulfillment or rejection),
// the async function is resumed from the job queue, by calling
// js::AsyncFunctionAwaitedFulfilled or js::AsyncFunctionAwaitedRejected
// from js::AsyncFunctionPromiseReactionJob.
//
// The execution resumes from JSOp::AfterYield, with the resolved value
// and the resume kind, either normal or throw, corresponds to fulfillment or
// rejection, on the stack.
//
// The resume kind is handled by JSOp::CheckResumeKind after that.
//
// If the resume kind is normal (=fulfillment), the async function resumes
// the execution with the resolved value as the result of `await`.
//
// If the resume kind is throw (=rejection), it throws the resolved value,
// and it will be caught by the try-catch explained above.
//
//
// # Optimization for await
//
// Suspending the execution and going into the embedding's job queue is slow
// and hard to optimize.
//
// If the following conditions are met, we don't have to perform the above
// but just use the await operand as the result of await.
//
// 1. The await operand is either non-promise or already-fulfilled promise,
// so that the result value is already known
// 2. There's no jobs in the job queue,
// so that we don't have to perform other jobs before resuming from
// await
// 3. Promise constructor/prototype are not modified,
// so that the optimization isn't visible to the user code
//
// This is implemented by the following bytecode sequence:
//
// ```
// (operand here) # VALUE
//
// TrySkipAwait # VALUE_OR_RVAL, CAN_SKIP
// Not # VALUE_OR_RVAL, !CAN_SKIP
// IfEq END # VALUE
//
// JumpTarget # VALUE
// GetAliasedVar ".generator" # VALUE .generator
// Await 0 # RVAL GENERATOR RESUMEKIND
// AfterYield # RVAL GENERATOR RESUMEKIND
// CheckResumeKind # RVAL
//
// END:
// JumpTarget # RVAL
// ```
//
// JSOp::TrySkipAwait checks the above conditions. And if the await can be
// skipped, it jumps over the await code.
namespace js {
class AsyncFunctionGeneratorObject;

View File

@ -15,6 +15,270 @@
#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 TrySkipAwait; 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 TrySkipAwait 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 TrySkipAwait 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;