blob: b7a1b571e6418b3c88fb3776e6db341d204dec37 [file] [log] [blame]
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include 'src/builtins/builtins-promise.h'
#include 'src/builtins/builtins-promise-gen.h'
namespace runtime {
extern transitioning runtime
RejectPromise(implicit context: Context)(JSPromise, JSAny, Boolean): JSAny;
extern transitioning runtime
PromiseRevokeReject(implicit context: Context)(JSPromise): JSAny;
extern transitioning runtime
PromiseRejectAfterResolved(implicit context: Context)(JSPromise, JSAny): JSAny;
extern transitioning runtime
PromiseResolveAfterResolved(implicit context: Context)(JSPromise, JSAny): JSAny;
extern transitioning runtime
PromiseRejectEventFromStack(implicit context: Context)(JSPromise, JSAny): JSAny;
}
// https://tc39.es/ecma262/#sec-promise-abstract-operations
namespace promise {
extern macro PromiseForwardingHandlerSymbolConstant(): Symbol;
const kPromiseForwardingHandlerSymbol: Symbol =
PromiseForwardingHandlerSymbolConstant();
extern macro PromiseHandledBySymbolConstant(): Symbol;
const kPromiseHandledBySymbol: Symbol = PromiseHandledBySymbolConstant();
extern macro ResolveStringConstant(): String;
const kResolveString: String = ResolveStringConstant();
extern macro IsPromiseResolveProtectorCellInvalid(): bool;
extern macro AllocateFunctionWithMapAndContext(
Map, SharedFunctionInfo, FunctionContext): JSFunction;
extern macro PromiseReactionMapConstant(): Map;
extern macro PromiseFulfillReactionJobTaskMapConstant(): Map;
extern macro PromiseRejectReactionJobTaskMapConstant(): Map;
extern transitioning builtin
ResolvePromise(Context, JSPromise, JSAny): JSAny;
extern transitioning builtin
EnqueueMicrotask(Context, Microtask): Undefined;
macro
ExtractHandlerContextInternal(implicit context: Context)(
handler: Callable|Undefined): Context labels NotFound {
let iter: JSAny = handler;
while (true) {
typeswitch (iter) {
case (b: JSBoundFunction): {
iter = b.bound_target_function;
}
case (p: JSProxy): {
iter = p.target;
}
case (f: JSFunction): {
return f.context;
}
case (JSAny): {
break;
}
}
}
goto NotFound;
}
macro
ExtractHandlerContext(implicit context: Context)(handler: Callable|
Undefined): Context {
try {
return ExtractHandlerContextInternal(handler) otherwise NotFound;
} label NotFound deferred {
return context;
}
}
macro
ExtractHandlerContext(implicit context: Context)(
primary: Callable|Undefined, secondary: Callable|Undefined): Context {
try {
return ExtractHandlerContextInternal(primary) otherwise NotFound;
} label NotFound deferred {
return ExtractHandlerContextInternal(secondary) otherwise Default;
} label Default deferred {
return context;
}
}
transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(
promiseReaction: PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
let primaryHandler: Callable|Undefined;
let secondaryHandler: Callable|Undefined;
if constexpr (reactionType == kPromiseReactionFulfill) {
primaryHandler = promiseReaction.fulfill_handler;
secondaryHandler = promiseReaction.reject_handler;
} else {
static_assert(reactionType == kPromiseReactionReject);
primaryHandler = promiseReaction.reject_handler;
secondaryHandler = promiseReaction.fulfill_handler;
}
// According to HTML, we use the context of the appropriate handler as the
// context of the microtask. See step 3 of HTML's EnqueueJob:
// https://html.spec.whatwg.org/C/#enqueuejob(queuename,-job,-arguments)
const handlerContext: Context =
ExtractHandlerContext(primaryHandler, secondaryHandler);
// Morph {current} from a PromiseReaction into a PromiseReactionJobTask
// and schedule that on the microtask queue. We try to minimize the number
// of stores here to avoid screwing up the store buffer.
static_assert(
kPromiseReactionSize ==
kPromiseReactionJobTaskSizeOfAllPromiseReactionJobTasks);
if constexpr (reactionType == kPromiseReactionFulfill) {
*UnsafeConstCast(&promiseReaction.map) =
PromiseFulfillReactionJobTaskMapConstant();
const promiseReactionJobTask =
UnsafeCast<PromiseFulfillReactionJobTask>(promiseReaction);
promiseReactionJobTask.argument = argument;
promiseReactionJobTask.context = handlerContext;
EnqueueMicrotask(handlerContext, promiseReactionJobTask);
static_assert(
kPromiseReactionFulfillHandlerOffset ==
kPromiseReactionJobTaskHandlerOffset);
static_assert(
kPromiseReactionPromiseOrCapabilityOffset ==
kPromiseReactionJobTaskPromiseOrCapabilityOffset);
} else {
static_assert(reactionType == kPromiseReactionReject);
*UnsafeConstCast(&promiseReaction.map) =
PromiseRejectReactionJobTaskMapConstant();
const promiseReactionJobTask =
UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);
promiseReactionJobTask.argument = argument;
promiseReactionJobTask.context = handlerContext;
promiseReactionJobTask.handler = primaryHandler;
EnqueueMicrotask(handlerContext, promiseReactionJobTask);
static_assert(
kPromiseReactionPromiseOrCapabilityOffset ==
kPromiseReactionJobTaskPromiseOrCapabilityOffset);
}
}
// https://tc39.es/ecma262/#sec-triggerpromisereactions
transitioning macro TriggerPromiseReactions(implicit context: Context)(
reactions: Zero|PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
// We need to reverse the {reactions} here, since we record them on the
// JSPromise in the reverse order.
let current = reactions;
let reversed: Zero|PromiseReaction = kZero;
// As an additional safety net against misuse of the V8 Extras API, we
// sanity check the {reactions} to make sure that they are actually
// PromiseReaction instances and not actual JavaScript values (which
// would indicate that we're rejecting or resolving an already settled
// promise), see https://crbug.com/931640 for details on this.
while (true) {
typeswitch (current) {
case (Zero): {
break;
}
case (currentReaction: PromiseReaction): {
current = currentReaction.next;
currentReaction.next = reversed;
reversed = currentReaction;
}
}
}
// Morph the {reactions} into PromiseReactionJobTasks and push them
// onto the microtask queue.
current = reversed;
while (true) {
typeswitch (current) {
case (Zero): {
break;
}
case (currentReaction: PromiseReaction): {
current = currentReaction.next;
MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType);
}
}
}
}
// https://tc39.es/ecma262/#sec-fulfillpromise
transitioning builtin
FulfillPromise(implicit context: Context)(
promise: JSPromise, value: JSAny): Undefined {
// Assert: The value of promise.[[PromiseState]] is "pending".
assert(promise.Status() == PromiseState::kPending);
// 2. Let reactions be promise.[[PromiseFulfillReactions]].
const reactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// 3. Set promise.[[PromiseResult]] to value.
// 4. Set promise.[[PromiseFulfillReactions]] to undefined.
// 5. Set promise.[[PromiseRejectReactions]] to undefined.
promise.reactions_or_result = value;
// 6. Set promise.[[PromiseState]] to "fulfilled".
promise.SetStatus(PromiseState::kFulfilled);
// 7. Return TriggerPromiseReactions(reactions, value).
TriggerPromiseReactions(reactions, value, kPromiseReactionFulfill);
return Undefined;
}
extern macro PromiseBuiltinsAssembler::
IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool;
// https://tc39.es/ecma262/#sec-rejectpromise
transitioning builtin
RejectPromise(implicit context: Context)(
promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
// If promise hook is enabled or the debugger is active, let
// the runtime handle this operation, which greatly reduces
// the complexity here and also avoids a couple of back and
// forth between JavaScript and C++ land.
if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() ||
!promise.HasHandler()) {
// 7. If promise.[[PromiseIsHandled]] is false, perform
// HostPromiseRejectionTracker(promise, "reject").
// We don't try to handle rejecting {promise} without handler
// here, but we let the C++ code take care of this completely.
return runtime::RejectPromise(promise, reason, debugEvent);
}
// 2. Let reactions be promise.[[PromiseRejectReactions]].
const reactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// 3. Set promise.[[PromiseResult]] to reason.
// 4. Set promise.[[PromiseFulfillReactions]] to undefined.
// 5. Set promise.[[PromiseRejectReactions]] to undefined.
promise.reactions_or_result = reason;
// 6. Set promise.[[PromiseState]] to "rejected".
promise.SetStatus(PromiseState::kRejected);
// 8. Return TriggerPromiseReactions(reactions, reason).
TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);
return Undefined;
}
const kPromiseCapabilitySize:
constexpr int31 generates 'PromiseCapability::kSize';
type PromiseResolvingFunctionContext extends FunctionContext;
extern enum PromiseResolvingFunctionContextSlot extends intptr
constexpr 'PromiseBuiltins::PromiseResolvingFunctionContextSlot' {
kPromiseSlot: Slot<PromiseResolvingFunctionContext, JSPromise>,
kAlreadyResolvedSlot: Slot<PromiseResolvingFunctionContext, Boolean>,
kDebugEventSlot: Slot<PromiseResolvingFunctionContext, Boolean>,
kPromiseContextLength
}
type PromiseCapabilitiesExecutorContext extends FunctionContext;
extern enum FunctionContextSlot extends intptr
constexpr 'PromiseBuiltins::FunctionContextSlot' {
kCapabilitySlot: Slot<PromiseCapabilitiesExecutorContext, PromiseCapability>,
kCapabilitiesContextLength
}
@export
macro CreatePromiseCapabilitiesExecutorContext(
nativeContext: NativeContext, capability: PromiseCapability):
PromiseCapabilitiesExecutorContext {
const executorContext = %RawDownCast<PromiseCapabilitiesExecutorContext>(
AllocateSyntheticFunctionContext(
nativeContext, FunctionContextSlot::kCapabilitiesContextLength));
InitContextSlot(
executorContext, FunctionContextSlot::kCapabilitySlot, capability);
return executorContext;
}
@export
macro CreatePromiseCapability(
promise: JSReceiver|Undefined, resolve: JSFunction|Undefined,
reject: JSFunction|Undefined): PromiseCapability {
return new PromiseCapability{
map: kPromiseCapabilityMap,
promise: promise,
resolve: resolve,
reject: reject
};
}
@export
struct PromiseResolvingFunctions {
resolve: JSFunction;
reject: JSFunction;
}
@export
macro CreatePromiseResolvingFunctions(implicit context: Context)(
promise: JSPromise, debugEvent: Boolean, nativeContext: NativeContext):
PromiseResolvingFunctions {
const promiseContext = CreatePromiseResolvingFunctionsContext(
promise, debugEvent, nativeContext);
const map = *NativeContextSlot(
nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
const resolveInfo = PromiseCapabilityDefaultResolveSharedFunConstant();
const resolve: JSFunction =
AllocateFunctionWithMapAndContext(map, resolveInfo, promiseContext);
const rejectInfo = PromiseCapabilityDefaultRejectSharedFunConstant();
const reject: JSFunction =
AllocateFunctionWithMapAndContext(map, rejectInfo, promiseContext);
return PromiseResolvingFunctions{resolve: resolve, reject: reject};
}
transitioning macro
InnerNewPromiseCapability(implicit context: Context)(
constructor: HeapObject, debugEvent: Boolean): PromiseCapability {
const nativeContext = LoadNativeContext(context);
if (constructor ==
*NativeContextSlot(nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX)) {
const promise = NewJSPromise();
const pair =
CreatePromiseResolvingFunctions(promise, debugEvent, nativeContext);
return CreatePromiseCapability(promise, pair.resolve, pair.reject);
} else {
// We have to create the capability before the associated promise
// because the builtin PromiseConstructor uses the executor.
const capability = CreatePromiseCapability(Undefined, Undefined, Undefined);
const executorContext =
CreatePromiseCapabilitiesExecutorContext(nativeContext, capability);
const executorInfo = PromiseGetCapabilitiesExecutorSharedFunConstant();
const functionMap =
*NativeContextSlot(
nativeContext,
ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
const executor = AllocateFunctionWithMapAndContext(
functionMap, executorInfo, executorContext);
const promiseConstructor = UnsafeCast<Constructor>(constructor);
const promise = Construct(promiseConstructor, executor);
capability.promise = promise;
if (!Is<Callable>(capability.resolve) || !Is<Callable>(capability.reject)) {
ThrowTypeError(MessageTemplate::kPromiseNonCallable);
}
return capability;
}
}
// https://tc39.es/ecma262/#sec-newpromisecapability
transitioning builtin
NewPromiseCapability(implicit context: Context)(
maybeConstructor: Object, debugEvent: Boolean): PromiseCapability {
typeswitch (maybeConstructor) {
case (Smi): {
ThrowTypeError(MessageTemplate::kNotConstructor, maybeConstructor);
}
case (constructor: HeapObject): {
if (!IsConstructor(constructor)) {
ThrowTypeError(MessageTemplate::kNotConstructor, maybeConstructor);
}
return InnerNewPromiseCapability(constructor, debugEvent);
}
}
}
// https://tc39.es/ecma262/#sec-promise-reject-functions
transitioning javascript builtin
PromiseCapabilityDefaultReject(
js-implicit context: Context, receiver: JSAny)(reason: JSAny): JSAny {
const context = %RawDownCast<PromiseResolvingFunctionContext>(context);
// 2. Let promise be F.[[Promise]].
const promise =
*ContextSlot(context, PromiseResolvingFunctionContextSlot::kPromiseSlot);
// 3. Let alreadyResolved be F.[[AlreadyResolved]].
const alreadyResolved = *ContextSlot(
context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot);
// 4. If alreadyResolved.[[Value]] is true, return undefined.
if (alreadyResolved == True) {
return runtime::PromiseRejectAfterResolved(promise, reason);
}
// 5. Set alreadyResolved.[[Value]] to true.
*ContextSlot(
context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot) =
True;
// 6. Return RejectPromise(promise, reason).
const debugEvent = *ContextSlot(
context, PromiseResolvingFunctionContextSlot::kDebugEventSlot);
return RejectPromise(promise, reason, debugEvent);
}
// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning javascript builtin
PromiseCapabilityDefaultResolve(
js-implicit context: Context, receiver: JSAny)(resolution: JSAny): JSAny {
const context = %RawDownCast<PromiseResolvingFunctionContext>(context);
// 2. Let promise be F.[[Promise]].
const promise: JSPromise =
*ContextSlot(context, PromiseResolvingFunctionContextSlot::kPromiseSlot);
// 3. Let alreadyResolved be F.[[AlreadyResolved]].
const alreadyResolved: Boolean = *ContextSlot(
context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot);
// 4. If alreadyResolved.[[Value]] is true, return undefined.
if (alreadyResolved == True) {
return runtime::PromiseResolveAfterResolved(promise, resolution);
}
// 5. Set alreadyResolved.[[Value]] to true.
*ContextSlot(
context, PromiseResolvingFunctionContextSlot::kAlreadyResolvedSlot) =
True;
// The rest of the logic (and the catch prediction) is
// encapsulated in the dedicated ResolvePromise builtin.
return ResolvePromise(context, promise, resolution);
}
@export
transitioning macro PerformPromiseThenImpl(implicit context: Context)(
promise: JSPromise, onFulfilled: Callable|Undefined,
onRejected: Callable|Undefined,
resultPromiseOrCapability: JSPromise|PromiseCapability|Undefined): void {
if (promise.Status() == PromiseState::kPending) {
// The {promise} is still in "Pending" state, so we just record a new
// PromiseReaction holding both the onFulfilled and onRejected callbacks.
// Once the {promise} is resolved we decide on the concrete handler to
// push onto the microtask queue.
const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
const promiseReactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
const reaction = NewPromiseReaction(
handlerContext, promiseReactions, resultPromiseOrCapability,
onFulfilled, onRejected);
promise.reactions_or_result = reaction;
} else {
const reactionsOrResult = promise.reactions_or_result;
let microtask: PromiseReactionJobTask;
let handlerContext: Context;
if (promise.Status() == PromiseState::kFulfilled) {
handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
microtask = NewPromiseFulfillReactionJobTask(
handlerContext, reactionsOrResult, onFulfilled,
resultPromiseOrCapability);
} else
deferred {
assert(promise.Status() == PromiseState::kRejected);
handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
microtask = NewPromiseRejectReactionJobTask(
handlerContext, reactionsOrResult, onRejected,
resultPromiseOrCapability);
if (!promise.HasHandler()) {
runtime::PromiseRevokeReject(promise);
}
}
EnqueueMicrotask(handlerContext, microtask);
}
promise.SetHasHandler();
}
// https://tc39.es/ecma262/#sec-performpromisethen
transitioning builtin
PerformPromiseThen(implicit context: Context)(
promise: JSPromise, onFulfilled: Callable|Undefined,
onRejected: Callable|Undefined, resultPromise: JSPromise|Undefined): JSAny {
PerformPromiseThenImpl(promise, onFulfilled, onRejected, resultPromise);
return resultPromise;
}
// https://tc39.es/ecma262/#sec-promise-reject-functions
transitioning javascript builtin
PromiseReject(
js-implicit context: NativeContext, receiver: JSAny)(reason: JSAny): JSAny {
// 1. Let C be the this value.
// 2. If Type(C) is not Object, throw a TypeError exception.
const receiver = Cast<JSReceiver>(receiver) otherwise
ThrowTypeError(MessageTemplate::kCalledOnNonObject, 'PromiseReject');
const promiseFun = *NativeContextSlot(ContextSlot::PROMISE_FUNCTION_INDEX);
if (promiseFun == receiver) {
const promise = NewJSPromise(PromiseState::kRejected, reason);
runtime::PromiseRejectEventFromStack(promise, reason);
return promise;
} else {
// 3. Let promiseCapability be ? NewPromiseCapability(C).
const capability = NewPromiseCapability(receiver, True);
// 4. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »).
const reject = UnsafeCast<Callable>(capability.reject);
Call(context, reject, Undefined, reason);
// 5. Return promiseCapability.[[Promise]].
return capability.promise;
}
}
const kPromiseExecutorAlreadyInvoked: constexpr MessageTemplate
generates 'MessageTemplate::kPromiseExecutorAlreadyInvoked';
// https://tc39.es/ecma262/#sec-getcapabilitiesexecutor-functions
transitioning javascript builtin
PromiseGetCapabilitiesExecutor(js-implicit context: Context, receiver: JSAny)(
resolve: JSAny, reject: JSAny): JSAny {
const context = %RawDownCast<PromiseCapabilitiesExecutorContext>(context);
const capability: PromiseCapability =
*ContextSlot(context, FunctionContextSlot::kCapabilitySlot);
if (capability.resolve != Undefined || capability.reject != Undefined)
deferred {
ThrowTypeError(kPromiseExecutorAlreadyInvoked);
}
capability.resolve = resolve;
capability.reject = reject;
return Undefined;
}
macro IsPromiseResolveLookupChainIntact(implicit context: Context)(
nativeContext: NativeContext, constructor: JSReceiver): bool {
if (IsForceSlowPath()) return false;
const promiseFun =
*NativeContextSlot(nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX);
return promiseFun == constructor && !IsPromiseResolveProtectorCellInvalid();
}
// https://tc39.es/ecma262/#sec-getpromiseresolve
transitioning macro GetPromiseResolve(implicit context: Context)(
nativeContext: NativeContext, constructor: Constructor): JSAny {
// 1. Assert: IsConstructor(constructor) is true.
// We can skip the "resolve" lookup on {constructor} if it's the
// Promise constructor and the Promise.resolve protector is intact,
// as that guards the lookup path for the "resolve" property on the
// Promise constructor. In this case, promiseResolveFunction is undefined,
// and when CallResolve is called with it later, it will call Promise.resolve.
let promiseResolveFunction: JSAny = Undefined;
if (!IsPromiseResolveLookupChainIntact(nativeContext, constructor)) {
let promiseResolve: JSAny;
// 2. Let promiseResolve be ? Get(constructor, "resolve").
promiseResolve = GetProperty(constructor, kResolveString);
// 3. If IsCallable(promiseResolve) is false, throw a TypeError exception.
promiseResolveFunction =
Cast<Callable>(promiseResolve) otherwise ThrowTypeError(
MessageTemplate::kCalledNonCallable, 'resolve');
}
// 4. return promiseResolve.
return promiseResolveFunction;
}
transitioning macro CallResolve(implicit context: Context)(
constructor: Constructor, resolve: JSAny, value: JSAny): JSAny {
// Undefined can never be a valid value for the resolve function,
// instead it is used as a special marker for the fast path.
if (resolve == Undefined) {
return PromiseResolve(constructor, value);
} else
deferred {
return Call(context, UnsafeCast<Callable>(resolve), constructor, value);
}
}
transitioning javascript builtin
PromiseConstructorLazyDeoptContinuation(
js-implicit context: NativeContext, receiver: JSAny)(
promise: JSAny, reject: JSAny, exception: JSAny|TheHole,
_result: JSAny): JSAny {
typeswitch (exception) {
case (TheHole): {
}
case (e: JSAny): {
Call(context, reject, Undefined, e);
}
}
return promise;
}
extern macro PromiseCapabilityDefaultRejectSharedFunConstant():
SharedFunctionInfo;
extern macro PromiseCapabilityDefaultResolveSharedFunConstant():
SharedFunctionInfo;
extern macro PromiseGetCapabilitiesExecutorSharedFunConstant():
SharedFunctionInfo;
}