// 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 write barrier overhead. static_assert( kPromiseReactionSize == kPromiseReactionJobTaskSizeOfAllPromiseReactionJobTasks); if constexpr (reactionType == kPromiseReactionFulfill) { *UnsafeConstCast(&promiseReaction.map) = PromiseFulfillReactionJobTaskMapConstant(); const promiseReactionJobTask = UnsafeCast(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(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". dcheck(promise.Status() == PromiseState::kPending); RunContextPromiseHookResolve(promise); // 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:: IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool; extern macro PromiseBuiltinsAssembler:: IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(uint32): bool; // https://tc39.es/ecma262/#sec-rejectpromise transitioning builtin RejectPromise(implicit context: Context)( promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny { const promiseHookFlags = PromiseHookFlags(); // 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 (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( promiseHookFlags) || !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); } RunContextPromiseHookResolve(promise, promiseHookFlags); // 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, kAlreadyResolvedSlot: Slot, kDebugEventSlot: Slot, kPromiseContextLength } type PromiseCapabilitiesExecutorContext extends FunctionContext; extern enum FunctionContextSlot extends intptr constexpr 'PromiseBuiltins::FunctionContextSlot' { kCapabilitySlot: Slot, kCapabilitiesContextLength } @export macro CreatePromiseCapabilitiesExecutorContext( nativeContext: NativeContext, capability: PromiseCapability): PromiseCapabilitiesExecutorContext { const executorContext = %RawDownCast( 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); const promise = Construct(promiseConstructor, executor); capability.promise = promise; if (!Is(capability.resolve) || !Is(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(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(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 { dcheck(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(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(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(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(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(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; }