Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "builtin/Promise.h"
#include "mozilla/Atomics.h"
#include "mozilla/Maybe.h"
#include "mozilla/TimeStamp.h"
#include "jsapi.h"
#include "jsexn.h"
#include "jsfriendapi.h"
#include "js/CallAndConstruct.h" // JS::Construct, JS::IsCallable
#include "js/experimental/JitInfo.h" // JSJitGetterOp, JSJitInfo
#include "js/ForOfIterator.h" // JS::ForOfIterator
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/PropertySpec.h"
#include "js/Stack.h"
#include "vm/ArrayObject.h"
#include "vm/AsyncFunction.h"
#include "vm/AsyncIteration.h"
#include "vm/CompletionKind.h"
#include "vm/ErrorObject.h"
#include "vm/ErrorReporting.h"
#include "vm/Iteration.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/PromiseLookup.h" // js::PromiseLookup
#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseSlot_*
#include "vm/SelfHosting.h"
#include "vm/Warnings.h" // js::WarnNumberASCII
#include "debugger/DebugAPI-inl.h"
#include "vm/Compartment-inl.h"
#include "vm/ErrorObject-inl.h"
#include "vm/JSContext-inl.h" // JSContext::check
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
using namespace js;
static double MillisecondsSinceStartup() {
auto now = mozilla::TimeStamp::Now();
return (now - mozilla::TimeStamp::FirstTimeStamp()).ToMilliseconds();
}
enum ResolutionMode { ResolveMode, RejectMode };
/**
* ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5
*
* Promise Resolve Functions
*/
enum ResolveFunctionSlots {
// NOTE: All slot represent [[AlreadyResolved]].[[Value]].
//
// The spec creates single record for [[AlreadyResolved]] and shares it
// between Promise Resolve Function and Promise Reject Function.
//
// Step 1. Let alreadyResolved be the Record { [[Value]]: false }.
// ...
// Step 6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
// ...
// Step 11. Set reject.[[AlreadyResolved]] to alreadyResolved.
//
// We implement it by clearing all slots, both in
// Promise Resolve Function and Promise Reject Function at the same time.
//
// If none of slots are undefined, [[AlreadyResolved]].[[Value]] is false.
// If all slot are undefined, [[AlreadyResolved]].[[Value]] is true.
// [[Promise]] slot.
// A possibly-wrapped promise.
ResolveFunctionSlot_Promise = 0,
// The corresponding Promise Reject Function.
ResolveFunctionSlot_RejectFunction,
};
/**
* ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5
*
* Promise Reject Functions
*/
enum RejectFunctionSlots {
// [[Promise]] slot.
// A possibly-wrapped promise.
RejectFunctionSlot_Promise = 0,
// The corresponding Promise Resolve Function.
RejectFunctionSlot_ResolveFunction,
};
enum PromiseCombinatorElementFunctionSlots {
PromiseCombinatorElementFunctionSlot_Data = 0,
PromiseCombinatorElementFunctionSlot_ElementIndex,
};
enum ReactionJobSlots {
ReactionJobSlot_ReactionRecord = 0,
};
enum ThenableJobSlots {
// The handler to use as the Promise reaction. It is a callable object
// that's guaranteed to be from the same compartment as the
// PromiseReactionJob.
ThenableJobSlot_Handler = 0,
// JobData - a, potentially CCW-wrapped, dense list containing data
// required for proper execution of the reaction.
ThenableJobSlot_JobData,
};
enum ThenableJobDataIndices {
// The Promise to resolve using the given thenable.
ThenableJobDataIndex_Promise = 0,
// The thenable to use as the receiver when calling the `then` function.
ThenableJobDataIndex_Thenable,
ThenableJobDataLength,
};
enum BuiltinThenableJobSlots {
// The Promise to resolve using the given thenable.
BuiltinThenableJobSlot_Promise = 0,
// The thenable to use as the receiver when calling the built-in `then`
// function.
BuiltinThenableJobSlot_Thenable,
};
struct PromiseCapability {
JSObject* promise = nullptr;
JSObject* resolve = nullptr;
JSObject* reject = nullptr;
PromiseCapability() = default;
void trace(JSTracer* trc);
};
void PromiseCapability::trace(JSTracer* trc) {
if (promise) {
TraceRoot(trc, &promise, "PromiseCapability::promise");
}
if (resolve) {
TraceRoot(trc, &resolve, "PromiseCapability::resolve");
}
if (reject) {
TraceRoot(trc, &reject, "PromiseCapability::reject");
}
}
namespace js {
template <typename Wrapper>
class WrappedPtrOperations<PromiseCapability, Wrapper> {
const PromiseCapability& capability() const {
return static_cast<const Wrapper*>(this)->get();
}
public:
HandleObject promise() const {
return HandleObject::fromMarkedLocation(&capability().promise);
}
HandleObject resolve() const {
return HandleObject::fromMarkedLocation(&capability().resolve);
}
HandleObject reject() const {
return HandleObject::fromMarkedLocation(&capability().reject);
}
};
template <typename Wrapper>
class MutableWrappedPtrOperations<PromiseCapability, Wrapper>
: public WrappedPtrOperations<PromiseCapability, Wrapper> {
PromiseCapability& capability() { return static_cast<Wrapper*>(this)->get(); }
public:
MutableHandleObject promise() {
return MutableHandleObject::fromMarkedLocation(&capability().promise);
}
MutableHandleObject resolve() {
return MutableHandleObject::fromMarkedLocation(&capability().resolve);
}
MutableHandleObject reject() {
return MutableHandleObject::fromMarkedLocation(&capability().reject);
}
};
} // namespace js
struct PromiseCombinatorElements;
class PromiseCombinatorDataHolder : public NativeObject {
enum {
Slot_Promise = 0,
Slot_RemainingElements,
Slot_ValuesArray,
Slot_ResolveOrRejectFunction,
SlotsCount,
};
public:
static const JSClass class_;
JSObject* promiseObj() { return &getFixedSlot(Slot_Promise).toObject(); }
JSObject* resolveOrRejectObj() {
return &getFixedSlot(Slot_ResolveOrRejectFunction).toObject();
}
Value valuesArray() { return getFixedSlot(Slot_ValuesArray); }
int32_t remainingCount() {
return getFixedSlot(Slot_RemainingElements).toInt32();
}
int32_t increaseRemainingCount() {
int32_t remainingCount = getFixedSlot(Slot_RemainingElements).toInt32();
remainingCount++;
setFixedSlot(Slot_RemainingElements, Int32Value(remainingCount));
return remainingCount;
}
int32_t decreaseRemainingCount() {
int32_t remainingCount = getFixedSlot(Slot_RemainingElements).toInt32();
remainingCount--;
MOZ_ASSERT(remainingCount >= 0, "unpaired calls to decreaseRemainingCount");
setFixedSlot(Slot_RemainingElements, Int32Value(remainingCount));
return remainingCount;
}
static PromiseCombinatorDataHolder* New(
JSContext* cx, HandleObject resultPromise,
Handle<PromiseCombinatorElements> elements, HandleObject resolveOrReject);
};
const JSClass PromiseCombinatorDataHolder::class_ = {
"PromiseCombinatorDataHolder", JSCLASS_HAS_RESERVED_SLOTS(SlotsCount)};
// Smart pointer to the "F.[[Values]]" part of the state of a Promise.all or
// Promise.allSettled invocation, or the "F.[[Errors]]" part of the state of a
// Promise.any invocation. Copes with compartment issues when setting an
// element.
struct MOZ_STACK_CLASS PromiseCombinatorElements final {
// Object value holding the elements array. The object can be a wrapper.
Value value;
// Unwrapped elements array. May not belong to the current compartment!
ArrayObject* unwrappedArray = nullptr;
// Set to true if the |setElement| method needs to wrap its input value.
bool setElementNeedsWrapping = false;
PromiseCombinatorElements() = default;
void trace(JSTracer* trc);
};
void PromiseCombinatorElements::trace(JSTracer* trc) {
TraceRoot(trc, &value, "PromiseCombinatorElements::value");
if (unwrappedArray) {
TraceRoot(trc, &unwrappedArray,
"PromiseCombinatorElements::unwrappedArray");
}
}
namespace js {
template <typename Wrapper>
class WrappedPtrOperations<PromiseCombinatorElements, Wrapper> {
const PromiseCombinatorElements& elements() const {
return static_cast<const Wrapper*>(this)->get();
}
public:
HandleValue value() const {
return HandleValue::fromMarkedLocation(&elements().value);
}
Handle<ArrayObject*> unwrappedArray() const {
return Handle<ArrayObject*>::fromMarkedLocation(&elements().unwrappedArray);
}
};
template <typename Wrapper>
class MutableWrappedPtrOperations<PromiseCombinatorElements, Wrapper>
: public WrappedPtrOperations<PromiseCombinatorElements, Wrapper> {
PromiseCombinatorElements& elements() {
return static_cast<Wrapper*>(this)->get();
}
public:
MutableHandleValue value() {
return MutableHandleValue::fromMarkedLocation(&elements().value);
}
MutableHandle<ArrayObject*> unwrappedArray() {
return MutableHandle<ArrayObject*>::fromMarkedLocation(
&elements().unwrappedArray);
}
void initialize(ArrayObject* arrayObj) {
unwrappedArray().set(arrayObj);
value().setObject(*arrayObj);
// |needsWrapping| isn't tracked here, because all modifications on the
// initial elements don't require any wrapping.
}
void initialize(PromiseCombinatorDataHolder* data, ArrayObject* arrayObj,
bool needsWrapping) {
unwrappedArray().set(arrayObj);
value().set(data->valuesArray());
elements().setElementNeedsWrapping = needsWrapping;
}
[[nodiscard]] bool pushUndefined(JSContext* cx) {
// Helper for the AutoRealm we need to work with |array|. We mostly do this
// for performance; we could go ahead and do the define via a cross-
// compartment proxy instead...
AutoRealm ar(cx, unwrappedArray());
Handle<ArrayObject*> arrayObj = unwrappedArray();
return js::NewbornArrayPush(cx, arrayObj, UndefinedValue());
}
// `Promise.all` Resolve Element Functions
// Step 9. Set values[index] to x.
//
// `Promise.allSettled` Resolve Element Functions
// `Promise.allSettled` Reject Element Functions
// Step 12. Set values[index] to obj.
//
// `Promise.any` Reject Element Functions
// Step 9. Set errors[index] to x.
//
// These handler functions are always created in the compartment of the
// Promise.all/allSettled/any function, which isn't necessarily the same
// compartment as unwrappedArray as explained in NewPromiseCombinatorElements.
// So before storing |val| we may need to enter unwrappedArray's compartment.
[[nodiscard]] bool setElement(JSContext* cx, uint32_t index,
HandleValue val) {
// The index is guaranteed to be initialized to `undefined`.
MOZ_ASSERT(unwrappedArray()->getDenseElement(index).isUndefined());
if (elements().setElementNeedsWrapping) {
AutoRealm ar(cx, unwrappedArray());
RootedValue rootedVal(cx, val);
if (!cx->compartment()->wrap(cx, &rootedVal)) {
return false;
}
unwrappedArray()->setDenseElement(index, rootedVal);
} else {
unwrappedArray()->setDenseElement(index, val);
}
return true;
}
};
} // namespace js
PromiseCombinatorDataHolder* PromiseCombinatorDataHolder::New(
JSContext* cx, HandleObject resultPromise,
Handle<PromiseCombinatorElements> elements, HandleObject resolveOrReject) {
auto* dataHolder = NewBuiltinClassInstance<PromiseCombinatorDataHolder>(cx);
if (!dataHolder) {
return nullptr;
}
cx->check(resultPromise);
cx->check(elements.value());
cx->check(resolveOrReject);
dataHolder->setFixedSlot(Slot_Promise, ObjectValue(*resultPromise));
dataHolder->setFixedSlot(Slot_RemainingElements, Int32Value(1));
dataHolder->setFixedSlot(Slot_ValuesArray, elements.value());
dataHolder->setFixedSlot(Slot_ResolveOrRejectFunction,
ObjectValue(*resolveOrReject));
return dataHolder;
}
namespace {
// Generator used by PromiseObject::getID.
mozilla::Atomic<uint64_t> gIDGenerator(0);
} // namespace
class PromiseDebugInfo : public NativeObject {
private:
enum Slots {
Slot_AllocationSite,
Slot_ResolutionSite,
Slot_AllocationTime,
Slot_ResolutionTime,
Slot_Id,
SlotCount
};
public:
static const JSClass class_;
static PromiseDebugInfo* create(JSContext* cx,
Handle<PromiseObject*> promise) {
Rooted<PromiseDebugInfo*> debugInfo(
cx, NewBuiltinClassInstance<PromiseDebugInfo>(cx));
if (!debugInfo) {
return nullptr;
}
RootedObject stack(cx);
if (!JS::CaptureCurrentStack(cx, &stack,
JS::StackCapture(JS::AllFrames()))) {
return nullptr;
}
debugInfo->setFixedSlot(Slot_AllocationSite, ObjectOrNullValue(stack));
debugInfo->setFixedSlot(Slot_ResolutionSite, NullValue());
debugInfo->setFixedSlot(Slot_AllocationTime,
DoubleValue(MillisecondsSinceStartup()));
debugInfo->setFixedSlot(Slot_ResolutionTime, NumberValue(0));
promise->setFixedSlot(PromiseSlot_DebugInfo, ObjectValue(*debugInfo));
return debugInfo;
}
static PromiseDebugInfo* FromPromise(PromiseObject* promise) {
Value val = promise->getFixedSlot(PromiseSlot_DebugInfo);
if (val.isObject()) {
return &val.toObject().as<PromiseDebugInfo>();
}
return nullptr;
}
/**
* Returns the given PromiseObject's process-unique ID.
* The ID is lazily assigned when first queried, and then either stored
* in the DebugInfo slot if no debug info was recorded for this Promise,
* or in the Id slot of the DebugInfo object.
*/
static uint64_t id(PromiseObject* promise) {
Value idVal(promise->getFixedSlot(PromiseSlot_DebugInfo));
if (idVal.isUndefined()) {
idVal.setDouble(++gIDGenerator);
promise->setFixedSlot(PromiseSlot_DebugInfo, idVal);
} else if (idVal.isObject()) {
PromiseDebugInfo* debugInfo = FromPromise(promise);
idVal = debugInfo->getFixedSlot(Slot_Id);
if (idVal.isUndefined()) {
idVal.setDouble(++gIDGenerator);
debugInfo->setFixedSlot(Slot_Id, idVal);
}
}
return uint64_t(idVal.toNumber());
}
double allocationTime() {
return getFixedSlot(Slot_AllocationTime).toNumber();
}
double resolutionTime() {
return getFixedSlot(Slot_ResolutionTime).toNumber();
}
JSObject* allocationSite() {
return getFixedSlot(Slot_AllocationSite).toObjectOrNull();
}
JSObject* resolutionSite() {
return getFixedSlot(Slot_ResolutionSite).toObjectOrNull();
}
// The |unwrappedRejectionStack| parameter should only be set on promise
// rejections and should be the stack of the exception that caused the promise
// to be rejected. If the |unwrappedRejectionStack| is null, the current stack
// will be used instead. This is also the default behavior for fulfilled
// promises.
static void setResolutionInfo(JSContext* cx, Handle<PromiseObject*> promise,
Handle<SavedFrame*> unwrappedRejectionStack) {
MOZ_ASSERT_IF(unwrappedRejectionStack,
promise->state() == JS::PromiseState::Rejected);
if (!JS::IsAsyncStackCaptureEnabledForRealm(cx)) {
return;
}
// If async stacks weren't enabled and the Promise's global wasn't a
// debuggee when the Promise was created, we won't have a debugInfo
// object. We still want to capture the resolution stack, so we
// create the object now and change it's slots' values around a bit.
Rooted<PromiseDebugInfo*> debugInfo(cx, FromPromise(promise));
if (!debugInfo) {
RootedValue idVal(cx, promise->getFixedSlot(PromiseSlot_DebugInfo));
debugInfo = create(cx, promise);
if (!debugInfo) {
cx->clearPendingException();
return;
}
// The current stack was stored in the AllocationSite slot, move
// it to ResolutionSite as that's what it really is.
debugInfo->setFixedSlot(Slot_ResolutionSite,
debugInfo->getFixedSlot(Slot_AllocationSite));
debugInfo->setFixedSlot(Slot_AllocationSite, NullValue());
// There's no good default for a missing AllocationTime, so
// instead of resetting that, ensure that it's the same as
// ResolutionTime, so that the diff shows as 0, which isn't great,
// but bearable.
debugInfo->setFixedSlot(Slot_ResolutionTime,
debugInfo->getFixedSlot(Slot_AllocationTime));
// The Promise's ID might've been queried earlier, in which case
// it's stored in the DebugInfo slot. We saved that earlier, so
// now we can store it in the right place (or leave it as
// undefined if it wasn't ever initialized.)
debugInfo->setFixedSlot(Slot_Id, idVal);
return;
}
RootedObject stack(cx, unwrappedRejectionStack);
if (stack) {
// The exception stack is always unwrapped so it might be in
// a different compartment.
if (!cx->compartment()->wrap(cx, &stack)) {
cx->clearPendingException();
return;
}
} else {
if (!JS::CaptureCurrentStack(cx, &stack,
JS::StackCapture(JS::AllFrames()))) {
cx->clearPendingException();
return;
}
}
debugInfo->setFixedSlot(Slot_ResolutionSite, ObjectOrNullValue(stack));
debugInfo->setFixedSlot(Slot_ResolutionTime,
DoubleValue(MillisecondsSinceStartup()));
}
};
const JSClass PromiseDebugInfo::class_ = {
"PromiseDebugInfo", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)};
double PromiseObject::allocationTime() {
auto debugInfo = PromiseDebugInfo::FromPromise(this);
if (debugInfo) {
return debugInfo->allocationTime();
}
return 0;
}
double PromiseObject::resolutionTime() {
auto debugInfo = PromiseDebugInfo::FromPromise(this);
if (debugInfo) {
return debugInfo->resolutionTime();
}
return 0;
}
JSObject* PromiseObject::allocationSite() {
auto debugInfo = PromiseDebugInfo::FromPromise(this);
if (debugInfo) {
return debugInfo->allocationSite();
}
return nullptr;
}
JSObject* PromiseObject::resolutionSite() {
auto debugInfo = PromiseDebugInfo::FromPromise(this);
if (debugInfo) {
return debugInfo->resolutionSite();
}
return nullptr;
}
/**
* Wrapper for GetAndClearExceptionAndStack that handles cases where
* no exception is pending, but an error occurred.
* This can be the case if an OOM was encountered while throwing the error.
*/
static bool MaybeGetAndClearExceptionAndStack(
JSContext* cx, MutableHandleValue rval, MutableHandle<SavedFrame*> stack) {
if (!cx->isExceptionPending()) {
return false;
}
return GetAndClearExceptionAndStack(cx, rval, stack);
}
[[nodiscard]] static bool CallPromiseRejectFunction(
JSContext* cx, HandleObject rejectFun, HandleValue reason,
HandleObject promiseObj, Handle<SavedFrame*> unwrappedRejectionStack,
UnhandledRejectionBehavior behavior);
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* IfAbruptRejectPromise ( value, capability )
*
* Steps 1.a-b.
*
* Extracting all of this internal spec algorithm into a helper function would
* be tedious, so the check in step 1 and the entirety of step 2 aren't
* included.
*/
static bool AbruptRejectPromise(JSContext* cx, CallArgs& args,
HandleObject promiseObj, HandleObject reject) {
// Step 1.a. Perform
// ? Call(capability.[[Reject]], undefined, « value.[[Value]] »).
RootedValue reason(cx);
Rooted<SavedFrame*> stack(cx);
if (!MaybeGetAndClearExceptionAndStack(cx, &reason, &stack)) {
return false;
}
if (!CallPromiseRejectFunction(cx, reject, reason, promiseObj, stack,
UnhandledRejectionBehavior::Report)) {
return false;
}
// Step 1.b. Return capability.[[Promise]].
args.rval().setObject(*promiseObj);
return true;
}
static bool AbruptRejectPromise(JSContext* cx, CallArgs& args,
Handle<PromiseCapability> capability) {
return AbruptRejectPromise(cx, args, capability.promise(),
capability.reject());
}
enum ReactionRecordSlots {
// This is the promise-like object that gets resolved with the result of this
// reaction, if any. If this reaction record was created with .then or .catch,
// this is the promise that .then or .catch returned.
//
// The spec says that a PromiseReaction record has a [[Capability]] field
// whose value is either undefined or a PromiseCapability record, but we just
// store the PromiseCapability's fields directly in this object. This is the
// capability's [[Promise]] field; its [[Resolve]] and [[Reject]] fields are
// stored in ReactionRecordSlot_Resolve and ReactionRecordSlot_Reject.
//
// This can be 'null' in reaction records created for a few situations:
//
// - When you resolve one promise to another. When you pass a promise P1 to
// the 'fulfill' function of a promise P2, so that resolving P1 resolves P2
// in the same way, P1 gets a reaction record with the
// REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag set and whose
// ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds P2.
//
// - When you await a promise. When an async function or generator awaits a
// value V, then the await expression generates an internal promise P,
// resolves it to V, and then gives P a reaction record with the
// REACTION_FLAG_ASYNC_FUNCTION or REACTION_FLAG_ASYNC_GENERATOR flag set
// and whose ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds the
// generator object. (Typically V is a promise, so resolving P to V gives V
// a REACTION_FLAGS_DEFAULT_RESOLVING_HANDLER reaction record as described
// above.)
//
// - When JS::AddPromiseReactions{,IgnoringUnhandledRejection} cause the
// reaction to be created. (These functions act as if they had created a
// promise to invoke the appropriate provided reaction function, without
// actually allocating a promise for them.)
ReactionRecordSlot_Promise = 0,
// The [[Handler]] field(s) of a PromiseReaction record. We create a
// single reaction record for fulfillment and rejection, therefore our
// PromiseReaction implementation needs two [[Handler]] fields.
//
// The slot value is either a callable object, an integer constant from
// the |PromiseHandler| enum, or null. If the value is null, either the
// REACTION_FLAG_DEBUGGER_DUMMY or the
// REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag must be set.
//
// After setting the target state for a PromiseReaction, the slot of the
// no longer used handler gets reused to store the argument of the active
// handler.
ReactionRecordSlot_OnFulfilled,
ReactionRecordSlot_OnRejectedArg = ReactionRecordSlot_OnFulfilled,
ReactionRecordSlot_OnRejected,
ReactionRecordSlot_OnFulfilledArg = ReactionRecordSlot_OnRejected,
// The functions to resolve or reject the promise. Matches the
// [[Capability]].[[Resolve]] and [[Capability]].[[Reject]] fields from
// the spec.
//
// The slot values are either callable objects or null, but the latter
// case is only allowed if the promise is either a built-in Promise object
// or null.
ReactionRecordSlot_Resolve,
ReactionRecordSlot_Reject,
// The incumbent global for this reaction record. Can be null.
ReactionRecordSlot_IncumbentGlobalObject,
// Bitmask of the REACTION_FLAG values.
ReactionRecordSlot_Flags,
// Additional slot to store extra data for specific reaction record types.
//
// - When the REACTION_FLAG_ASYNC_FUNCTION flag is set, this slot stores
// the (internal) generator object for this promise reaction.
// - When the REACTION_FLAG_ASYNC_GENERATOR flag is set, this slot stores
// the async generator object for this promise reaction.
// - When the REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag is set, this
// slot stores the promise to resolve when conceptually "calling" the
// OnFulfilled or OnRejected handlers.
ReactionRecordSlot_GeneratorOrPromiseToResolve,
ReactionRecordSlots,
};
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* PromiseReaction Records
*/
class PromiseReactionRecord : public NativeObject {
// If this flag is set, this reaction record is already enqueued to the
// job queue, and the spec's [[Type]] field is represented by
// REACTION_FLAG_FULFILLED flag.
//
// If this flag isn't yet set, [[Type]] field is undefined.
static constexpr uint32_t REACTION_FLAG_RESOLVED = 0x1;
// This bit is valid only when REACTION_FLAG_RESOLVED flag is set.
//
// If this flag is set, [[Type]] field is Fulfill.
// If this flag isn't set, [[Type]] field is Reject.
static constexpr uint32_t REACTION_FLAG_FULFILLED = 0x2;
// If this flag is set, this reaction record is created for resolving
// one promise P1 to another promise P2, and
// ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds P2.
static constexpr uint32_t REACTION_FLAG_DEFAULT_RESOLVING_HANDLER = 0x4;
// If this flag is set, this reaction record is created for async function
// and ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds
// internal generator object of the async function.
static constexpr uint32_t REACTION_FLAG_ASYNC_FUNCTION = 0x8;
// If this flag is set, this reaction record is created for async generator
// and ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds
// the async generator object of the async generator.
static constexpr uint32_t REACTION_FLAG_ASYNC_GENERATOR = 0x10;
// If this flag is set, this reaction record is created only for providing
// information to debugger.
static constexpr uint32_t REACTION_FLAG_DEBUGGER_DUMMY = 0x20;
// This bit is valid only when the promise object is optimized out
// for the reaction.
//
// If this flag is set, unhandled rejection should be ignored.
// Otherwise, promise object should be created on-demand for unhandled
// rejection.
static constexpr uint32_t REACTION_FLAG_IGNORE_UNHANDLED_REJECTION = 0x40;
void setFlagOnInitialState(uint32_t flag) {
int32_t flags = this->flags();
MOZ_ASSERT(flags == 0, "Can't modify with non-default flags");
flags |= flag;
setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags));
}
uint32_t handlerSlot() {
MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
return targetState() == JS::PromiseState::Fulfilled
? ReactionRecordSlot_OnFulfilled
: ReactionRecordSlot_OnRejected;
}
uint32_t handlerArgSlot() {
MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
return targetState() == JS::PromiseState::Fulfilled
? ReactionRecordSlot_OnFulfilledArg
: ReactionRecordSlot_OnRejectedArg;
}
public:
static const JSClass class_;
JSObject* promise() {
return getFixedSlot(ReactionRecordSlot_Promise).toObjectOrNull();
}
int32_t flags() const {
return getFixedSlot(ReactionRecordSlot_Flags).toInt32();
}
JS::PromiseState targetState() {
int32_t flags = this->flags();
if (!(flags & REACTION_FLAG_RESOLVED)) {
return JS::PromiseState::Pending;
}
return flags & REACTION_FLAG_FULFILLED ? JS::PromiseState::Fulfilled
: JS::PromiseState::Rejected;
}
void setTargetStateAndHandlerArg(JS::PromiseState state, const Value& arg) {
MOZ_ASSERT(targetState() == JS::PromiseState::Pending);
MOZ_ASSERT(state != JS::PromiseState::Pending,
"Can't revert a reaction to pending.");
int32_t flags = this->flags();
flags |= REACTION_FLAG_RESOLVED;
if (state == JS::PromiseState::Fulfilled) {
flags |= REACTION_FLAG_FULFILLED;
}
setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags));
setFixedSlot(handlerArgSlot(), arg);
}
void setShouldIgnoreUnhandledRejection() {
setFlagOnInitialState(REACTION_FLAG_IGNORE_UNHANDLED_REJECTION);
}
UnhandledRejectionBehavior unhandledRejectionBehavior() const {
int32_t flags = this->flags();
return (flags & REACTION_FLAG_IGNORE_UNHANDLED_REJECTION)
? UnhandledRejectionBehavior::Ignore
: UnhandledRejectionBehavior::Report;
}
void setIsDefaultResolvingHandler(PromiseObject* promiseToResolve) {
setFlagOnInitialState(REACTION_FLAG_DEFAULT_RESOLVING_HANDLER);
setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve,
ObjectValue(*promiseToResolve));
}
bool isDefaultResolvingHandler() {
int32_t flags = this->flags();
return flags & REACTION_FLAG_DEFAULT_RESOLVING_HANDLER;
}
PromiseObject* defaultResolvingPromise() {
MOZ_ASSERT(isDefaultResolvingHandler());
const Value& promiseToResolve =
getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve);
return &promiseToResolve.toObject().as<PromiseObject>();
}
void setIsAsyncFunction(AsyncFunctionGeneratorObject* genObj) {
setFlagOnInitialState(REACTION_FLAG_ASYNC_FUNCTION);
setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve,
ObjectValue(*genObj));
}
bool isAsyncFunction() {
int32_t flags = this->flags();
return flags & REACTION_FLAG_ASYNC_FUNCTION;
}
AsyncFunctionGeneratorObject* asyncFunctionGenerator() {
MOZ_ASSERT(isAsyncFunction());
const Value& generator =
getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve);
return &generator.toObject().as<AsyncFunctionGeneratorObject>();
}
void setIsAsyncGenerator(AsyncGeneratorObject* generator) {
setFlagOnInitialState(REACTION_FLAG_ASYNC_GENERATOR);
setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve,
ObjectValue(*generator));
}
bool isAsyncGenerator() {
int32_t flags = this->flags();
return flags & REACTION_FLAG_ASYNC_GENERATOR;
}
AsyncGeneratorObject* asyncGenerator() {
MOZ_ASSERT(isAsyncGenerator());
const Value& generator =
getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve);
return &generator.toObject().as<AsyncGeneratorObject>();
}
void setIsDebuggerDummy() {
setFlagOnInitialState(REACTION_FLAG_DEBUGGER_DUMMY);
}
bool isDebuggerDummy() {
int32_t flags = this->flags();
return flags & REACTION_FLAG_DEBUGGER_DUMMY;
}
Value handler() {
MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
return getFixedSlot(handlerSlot());
}
Value handlerArg() {
MOZ_ASSERT(targetState() != JS::PromiseState::Pending);
return getFixedSlot(handlerArgSlot());
}
JSObject* getAndClearIncumbentGlobalObject() {
JSObject* obj =
getFixedSlot(ReactionRecordSlot_IncumbentGlobalObject).toObjectOrNull();
setFixedSlot(ReactionRecordSlot_IncumbentGlobalObject, UndefinedValue());
return obj;
}
};
const JSClass PromiseReactionRecord::class_ = {
"PromiseReactionRecord", JSCLASS_HAS_RESERVED_SLOTS(ReactionRecordSlots)};
static void AddPromiseFlags(PromiseObject& promise, int32_t flag) {
int32_t flags = promise.flags();
promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags | flag));
}
static void RemovePromiseFlags(PromiseObject& promise, int32_t flag) {
int32_t flags = promise.flags();
promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags & ~flag));
}
static bool PromiseHasAnyFlag(PromiseObject& promise, int32_t flag) {
return promise.flags() & flag;
}
static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp);
static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp);
static JSFunction* GetResolveFunctionFromReject(JSFunction* reject);
static JSFunction* GetRejectFunctionFromResolve(JSFunction* resolve);
#ifdef DEBUG
/**
* Returns Promise Resolve Function's [[AlreadyResolved]].[[Value]].
*/
static bool IsAlreadyResolvedMaybeWrappedResolveFunction(
JSObject* resolveFunObj) {
if (IsWrapper(resolveFunObj)) {
resolveFunObj = UncheckedUnwrap(resolveFunObj);
}
JSFunction* resolveFun = &resolveFunObj->as<JSFunction>();
MOZ_ASSERT(resolveFun->maybeNative() == ResolvePromiseFunction);
bool alreadyResolved =
resolveFun->getExtendedSlot(ResolveFunctionSlot_Promise).isUndefined();
// Other slots should agree.
if (alreadyResolved) {
MOZ_ASSERT(resolveFun->getExtendedSlot(ResolveFunctionSlot_RejectFunction)
.isUndefined());
} else {
JSFunction* rejectFun = GetRejectFunctionFromResolve(resolveFun);
MOZ_ASSERT(
!rejectFun->getExtendedSlot(RejectFunctionSlot_Promise).isUndefined());
MOZ_ASSERT(!rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction)
.isUndefined());
}
return alreadyResolved;
}
/**
* Returns Promise Reject Function's [[AlreadyResolved]].[[Value]].
*/
static bool IsAlreadyResolvedMaybeWrappedRejectFunction(
JSObject* rejectFunObj) {
if (IsWrapper(rejectFunObj)) {
rejectFunObj = UncheckedUnwrap(rejectFunObj);
}
JSFunction* rejectFun = &rejectFunObj->as<JSFunction>();
MOZ_ASSERT(rejectFun->maybeNative() == RejectPromiseFunction);
bool alreadyResolved =
rejectFun->getExtendedSlot(RejectFunctionSlot_Promise).isUndefined();
// Other slots should agree.
if (alreadyResolved) {
MOZ_ASSERT(rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction)
.isUndefined());
} else {
JSFunction* resolveFun = GetResolveFunctionFromReject(rejectFun);
MOZ_ASSERT(!resolveFun->getExtendedSlot(ResolveFunctionSlot_Promise)
.isUndefined());
MOZ_ASSERT(!resolveFun->getExtendedSlot(ResolveFunctionSlot_RejectFunction)
.isUndefined());
}
return alreadyResolved;
}
#endif // DEBUG
/**
* Set Promise Resolve Function's and Promise Reject Function's
* [[AlreadyResolved]].[[Value]] to true.
*
* `resolutionFun` can be either of them.
*/
static void SetAlreadyResolvedResolutionFunction(JSFunction* resolutionFun) {
JSFunction* resolve;
JSFunction* reject;
if (resolutionFun->maybeNative() == ResolvePromiseFunction) {
resolve = resolutionFun;
reject = GetRejectFunctionFromResolve(resolutionFun);
} else {
resolve = GetResolveFunctionFromReject(resolutionFun);
reject = resolutionFun;
}
resolve->setExtendedSlot(ResolveFunctionSlot_Promise, UndefinedValue());
resolve->setExtendedSlot(ResolveFunctionSlot_RejectFunction,
UndefinedValue());
reject->setExtendedSlot(RejectFunctionSlot_Promise, UndefinedValue());
reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, UndefinedValue());
MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve));
MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject));
}
/**
* Returns true if given promise is created by
* CreatePromiseObjectWithoutResolutionFunctions.
*/
bool js::IsPromiseWithDefaultResolvingFunction(PromiseObject* promise) {
return PromiseHasAnyFlag(*promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS);
}
/**
* Returns Promise Resolve Function's [[AlreadyResolved]].[[Value]] for
* a promise created by CreatePromiseObjectWithoutResolutionFunctions.
*/
static bool IsAlreadyResolvedPromiseWithDefaultResolvingFunction(
PromiseObject* promise) {
MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise));
if (promise->as<PromiseObject>().state() != JS::PromiseState::Pending) {
MOZ_ASSERT(PromiseHasAnyFlag(
*promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED));
return true;
}
return PromiseHasAnyFlag(
*promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED);
}
/**
* Set Promise Resolve Function's [[AlreadyResolved]].[[Value]] to true for
* a promise created by CreatePromiseObjectWithoutResolutionFunctions.
*/
void js::SetAlreadyResolvedPromiseWithDefaultResolvingFunction(
PromiseObject* promise) {
MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise));
promise->setFixedSlot(
PromiseSlot_Flags,
JS::Int32Value(
promise->flags() |
PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED));
}
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* CreateResolvingFunctions ( promise )
*/
[[nodiscard]] static MOZ_ALWAYS_INLINE bool CreateResolvingFunctions(
JSContext* cx, HandleObject promise, MutableHandleObject resolveFn,
MutableHandleObject rejectFn) {
// Step 1. Let alreadyResolved be the Record { [[Value]]: false }.
// (implicit, see steps 5-6, 10-11 below)
// Step 2. Let stepsResolve be the algorithm steps defined in Promise Resolve
// Functions.
// Step 3. Let lengthResolve be the number of non-optional parameters of the
// function definition in Promise Resolve Functions.
// Step 4. Let resolve be
// ! CreateBuiltinFunction(stepsResolve, lengthResolve, "",
// « [[Promise]], [[AlreadyResolved]] »).
Handle<PropertyName*> funName = cx->names().empty;
resolveFn.set(NewNativeFunction(cx, ResolvePromiseFunction, 1, funName,
gc::AllocKind::FUNCTION_EXTENDED,
GenericObject));
if (!resolveFn) {
return false;
}
// Step 7. Let stepsReject be the algorithm steps defined in Promise Reject
// Functions.
// Step 8. Let lengthReject be the number of non-optional parameters of the
// function definition in Promise Reject Functions.
// Step 9. Let reject be
// ! CreateBuiltinFunction(stepsReject, lengthReject, "",
// « [[Promise]], [[AlreadyResolved]] »).
rejectFn.set(NewNativeFunction(cx, RejectPromiseFunction, 1, funName,
gc::AllocKind::FUNCTION_EXTENDED,
GenericObject));
if (!rejectFn) {
return false;
}
JSFunction* resolveFun = &resolveFn->as<JSFunction>();
JSFunction* rejectFun = &rejectFn->as<JSFunction>();
// Step 5. Set resolve.[[Promise]] to promise.
// Step 6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
//
// NOTE: We use these references as [[AlreadyResolved]].[[Value]].
// See the comment in ResolveFunctionSlots for more details.
resolveFun->initExtendedSlot(ResolveFunctionSlot_Promise,
ObjectValue(*promise));
resolveFun->initExtendedSlot(ResolveFunctionSlot_RejectFunction,
ObjectValue(*rejectFun));
// Step 10. Set reject.[[Promise]] to promise.
// Step 11. Set reject.[[AlreadyResolved]] to alreadyResolved.
//
// NOTE: We use these references as [[AlreadyResolved]].[[Value]].
// See the comment in ResolveFunctionSlots for more details.
rejectFun->initExtendedSlot(RejectFunctionSlot_Promise,
ObjectValue(*promise));
rejectFun->initExtendedSlot(RejectFunctionSlot_ResolveFunction,
ObjectValue(*resolveFun));
MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedResolveFunction(resolveFun));
MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedRejectFunction(rejectFun));
// Step 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }.
return true;
}
static bool IsSettledMaybeWrappedPromise(JSObject* promise) {
if (IsProxy(promise)) {
promise = UncheckedUnwrap(promise);
// Caller needs to handle dead wrappers.
if (JS_IsDeadWrapper(promise)) {
return false;
}
}
return promise->as<PromiseObject>().state() != JS::PromiseState::Pending;
}
[[nodiscard]] static bool RejectMaybeWrappedPromise(
JSContext* cx, HandleObject promiseObj, HandleValue reason,
Handle<SavedFrame*> unwrappedRejectionStack);
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* Promise Reject Functions
*/
static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JSFunction* reject = &args.callee().as<JSFunction>();
HandleValue reasonVal = args.get(0);
// Step 1. Let F be the active function object.
// Step 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// (implicit)
// Step 3. Let promise be F.[[Promise]].
const Value& promiseVal = reject->getExtendedSlot(RejectFunctionSlot_Promise);
// Step 4. Let alreadyResolved be F.[[AlreadyResolved]].
// Step 5. If alreadyResolved.[[Value]] is true, return undefined.
//
// If the Promise isn't available anymore, it has been resolved and the
// reference to it removed to make it eligible for collection.
bool alreadyResolved = promiseVal.isUndefined();
MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject) ==
alreadyResolved);
if (alreadyResolved) {
args.rval().setUndefined();
return true;
}
RootedObject promise(cx, &promiseVal.toObject());
// Step 6. Set alreadyResolved.[[Value]] to true.
SetAlreadyResolvedResolutionFunction(reject);
// In some cases the Promise reference on the resolution function won't
// have been removed during resolution, so we need to check that here,
// too.
if (IsSettledMaybeWrappedPromise(promise)) {
args.rval().setUndefined();
return true;
}
// Step 7. Return RejectPromise(promise, reason).
if (!RejectMaybeWrappedPromise(cx, promise, reasonVal, nullptr)) {
return false;
}
args.rval().setUndefined();
return true;
}
[[nodiscard]] static bool FulfillMaybeWrappedPromise(JSContext* cx,
HandleObject promiseObj,
HandleValue value_);
[[nodiscard]] static bool EnqueuePromiseResolveThenableJob(
JSContext* cx, HandleValue promiseToResolve, HandleValue thenable,
HandleValue thenVal);
[[nodiscard]] static bool EnqueuePromiseResolveThenableBuiltinJob(
JSContext* cx, HandleObject promiseToResolve, HandleObject thenable);
static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal,
HandleValue onFulfilled, HandleValue onRejected,
MutableHandleValue rval, bool rvalExplicitlyUsed);
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* Promise Resolve Functions
*
* Steps 7-15.
*/
[[nodiscard]] bool js::ResolvePromiseInternal(
JSContext* cx, JS::Handle<JSObject*> promise,
JS::Handle<JS::Value> resolutionVal) {
cx->check(promise, resolutionVal);
MOZ_ASSERT(!IsSettledMaybeWrappedPromise(promise));
// (reordered)
// Step 8. If Type(resolution) is not Object, then
if (!resolutionVal.isObject()) {
// Step 8.a. Return FulfillPromise(promise, resolution).
return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
}
RootedObject resolution(cx, &resolutionVal.toObject());
// Step 7. If SameValue(resolution, promise) is true, then
if (resolution == promise) {
// Step 7.a. Let selfResolutionError be a newly created TypeError object.
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_CANNOT_RESOLVE_PROMISE_WITH_ITSELF);
RootedValue selfResolutionError(cx);
Rooted<SavedFrame*> stack(cx);
if (!MaybeGetAndClearExceptionAndStack(cx, &selfResolutionError, &stack)) {
return false;
}
// Step 7.b. Return RejectPromise(promise, selfResolutionError).
return RejectMaybeWrappedPromise(cx, promise, selfResolutionError, stack);
}
// Step 9. Let then be Get(resolution, "then").
RootedValue thenVal(cx);
bool status =
GetProperty(cx, resolution, resolution, cx->names().then, &thenVal);
RootedValue error(cx);
Rooted<SavedFrame*> errorStack(cx);
// Step 10. If then is an abrupt completion, then
if (!status) {
// Get the `then.[[Value]]` value used in the step 10.a.
if (!MaybeGetAndClearExceptionAndStack(cx, &error, &errorStack)) {
return false;
}
}
// Testing functions allow to directly settle a promise without going
// through the resolving functions. In that case the normal bookkeeping to
// ensure only pending promises can be resolved doesn't apply and we need
// to manually check for already settled promises. The exception is simply
// dropped when this case happens.
if (IsSettledMaybeWrappedPromise(promise)) {
return true;
}
// Step 10. If then is an abrupt completion, then
if (!status) {
// Step 10.a. Return RejectPromise(promise, then.[[Value]]).
return RejectMaybeWrappedPromise(cx, promise, error, errorStack);
}
// Step 11. Let thenAction be then.[[Value]].
// (implicit)
// Step 12. If IsCallable(thenAction) is false, then
if (!IsCallable(thenVal)) {
// Step 12.a. Return FulfillPromise(promise, resolution).
return FulfillMaybeWrappedPromise(cx, promise, resolutionVal);
}
// Step 13. Let thenJobCallback be HostMakeJobCallback(thenAction).
// (implicit)
// Step 14. Let job be
// NewPromiseResolveThenableJob(promise, resolution,
// thenJobCallback).
// Step 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
// If the resolution object is a built-in Promise object and the
// `then` property is the original Promise.prototype.then function
// from the current realm, we skip storing/calling it.
// Additionally we require that |promise| itself is also a built-in
// Promise object, so the fast path doesn't need to cope with wrappers.
bool isBuiltinThen = false;
if (resolution->is<PromiseObject>() && promise->is<PromiseObject>() &&
IsNativeFunction(thenVal, Promise_then) &&
thenVal.toObject().as<JSFunction>().realm() == cx->realm()) {
isBuiltinThen = true;
}
if (!isBuiltinThen) {
RootedValue promiseVal(cx, ObjectValue(*promise));
if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal,
thenVal)) {
return false;
}
} else {
if (!EnqueuePromiseResolveThenableBuiltinJob(cx, promise, resolution)) {
return false;
}
}
return true;
}
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* Promise Resolve Functions
*/
static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1. Let F be the active function object.
// Step 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// (implicit)
JSFunction* resolve = &args.callee().as<JSFunction>();
HandleValue resolutionVal = args.get(0);
// Step 3. Let promise be F.[[Promise]].
const Value& promiseVal =
resolve->getExtendedSlot(ResolveFunctionSlot_Promise);
// Step 4. Let alreadyResolved be F.[[AlreadyResolved]].
// Step 5. If alreadyResolved.[[Value]] is true, return undefined.
//
// NOTE: We use the reference to the reject function as [[AlreadyResolved]].
bool alreadyResolved = promiseVal.isUndefined();
MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve) ==
alreadyResolved);
if (alreadyResolved) {
args.rval().setUndefined();
return true;
}
RootedObject promise(cx, &promiseVal.toObject());
// Step 6. Set alreadyResolved.[[Value]] to true.
SetAlreadyResolvedResolutionFunction(resolve);
// In some cases the Promise reference on the resolution function won't
// have been removed during resolution, so we need to check that here,
// too.
if (IsSettledMaybeWrappedPromise(promise)) {
args.rval().setUndefined();
return true;
}
// Steps 7-15.
if (!ResolvePromiseInternal(cx, promise, resolutionVal)) {
return false;
}
// Step 16. Return undefined.
args.rval().setUndefined();
return true;
}
static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* NewPromiseReactionJob ( reaction, argument )
* HostEnqueuePromiseJob ( job, realm )
*
* Tells the embedding to enqueue a Promise reaction job, based on
* three parameters:
* reactionObj - The reaction record.
* handlerArg_ - The first and only argument to pass to the handler invoked by
* the job. This will be stored on the reaction record.
* targetState - The PromiseState this reaction job targets. This decides
* whether the onFulfilled or onRejected handler is called.
*/
[[nodiscard]] static bool EnqueuePromiseReactionJob(
JSContext* cx, HandleObject reactionObj, HandleValue handlerArg_,
JS::PromiseState targetState) {
MOZ_ASSERT(targetState == JS::PromiseState::Fulfilled ||
targetState == JS::PromiseState::Rejected);
// The reaction might have been stored on a Promise from another
// compartment, which means it would've been wrapped in a CCW.
// To properly handle that case here, unwrap it and enter its
// compartment, where the job creation should take place anyway.
Rooted<PromiseReactionRecord*> reaction(cx);
RootedValue handlerArg(cx, handlerArg_);
mozilla::Maybe<AutoRealm> ar;
if (!IsProxy(reactionObj)) {
MOZ_RELEASE_ASSERT(reactionObj->is<PromiseReactionRecord>());
reaction = &reactionObj->as<PromiseReactionRecord>();
if (cx->realm() != reaction->realm()) {
// If the compartment has multiple realms, create the job in the
// reaction's realm. This is consistent with the code in the else-branch
// and avoids problems with running jobs against a dying global (Gecko
// drops such jobs).
ar.emplace(cx, reaction);
}
} else {
JSObject* unwrappedReactionObj = UncheckedUnwrap(reactionObj);
if (JS_IsDeadWrapper(unwrappedReactionObj)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEAD_OBJECT);
return false;
}
reaction = &unwrappedReactionObj->as<PromiseReactionRecord>();
MOZ_RELEASE_ASSERT(reaction->is<PromiseReactionRecord>());
ar.emplace(cx, reaction);
if (!cx->compartment()->wrap(cx, &handlerArg)) {
return false;
}
}
// Must not enqueue a reaction job more than once.
MOZ_ASSERT(reaction->targetState() == JS::PromiseState::Pending);
// NOTE: Instead of capturing reaction and arguments separately in the
// Job Abstract Closure below, store arguments (= handlerArg) in
// reaction object and capture it.
// Also, set reaction.[[Type]] is represented by targetState here.
cx->check(handlerArg);
reaction->setTargetStateAndHandlerArg(targetState, handlerArg);
RootedValue reactionVal(cx, ObjectValue(*reaction));
RootedValue handler(cx, reaction->handler());
// NewPromiseReactionJob
// Step 2. Let handlerRealm be null.
// NOTE: Instead of passing job and realm separately, we use the job's
// JSFunction object's realm as the job's realm.
// So we should enter the handlerRealm before creating the job function.
//
// GetFunctionRealm performed inside AutoFunctionOrCurrentRealm uses checked
// unwrap and it can hit permission error if there's a security wrapper, and
// in that case the reaction job is created in the current realm, instead of
// the target function's realm.
//
// If this reaction crosses chrome/content boundary, and the security
// wrapper would allow "call" operation, it still works inside the
// reaction job.
//
// This behavior is observable only when the job belonging to the content
// realm stops working (*1, *2), and it won't matter in practice.
//
// *1: "we can run script" performed inside HostEnqueuePromiseJob
// in HTML spec
// *2: nsIGlobalObject::IsDying performed inside PromiseJobRunnable::Run
// in our implementation
mozilla::Maybe<AutoFunctionOrCurrentRealm> ar2;
// NewPromiseReactionJob
// Step 3. If reaction.[[Handler]] is not empty, then
if (handler.isObject()) {
// Step 3.a. Let getHandlerRealmResult be
// GetFunctionRealm(reaction.[[Handler]].[[Callback]]).
// Step 3.b. If getHandlerRealmResult is a normal completion,
// set handlerRealm to getHandlerRealmResult.[[Value]].
// Step 3.c. Else, set handlerRealm to the current Realm Record.
// Step 3.d. NOTE: handlerRealm is never null unless the handler is
// undefined. When the handler is a revoked Proxy and no
// ECMAScript code runs, handlerRealm is used to create error
// objects.
RootedObject handlerObj(cx, &handler.toObject());
ar2.emplace(cx, handlerObj);
// We need to wrap the reaction to store it on the job function.
if (!cx->compartment()->wrap(cx, &reactionVal)) {
return false;
}
}
// NewPromiseReactionJob
// Step 1. Let job be a new Job Abstract Closure with no parameters that
// captures reaction and argument and performs the following steps
// when called:
Handle<PropertyName*> funName = cx->names().empty;
RootedFunction job(
cx, NewNativeFunction(cx, PromiseReactionJob, 0, funName,
gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
if (!job) {
return false;
}
job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal);
// When using JS::AddPromiseReactions{,IgnoringUnHandledRejection}, no actual
// promise is created, so we might not have one here.
// Additionally, we might have an object here that isn't an instance of
// Promise. This can happen if content overrides the value of
// Promise[@@species] (or invokes Promise#then on a Promise subclass
// instance with a non-default @@species value on the constructor) with a
// function that returns objects that're not Promise (subclass) instances.
// In that case, we just pretend we didn't have an object in the first
// place.
// If after all this we do have an object, wrap it in case we entered the
// handler's compartment above, because we should pass objects from a
// single compartment to the enqueuePromiseJob callback.
RootedObject promise(cx, reaction->promise());
if (promise) {
if (promise->is<PromiseObject>()) {
if (!cx->compartment()->wrap(cx, &promise)) {
return false;
}
} else if (IsWrapper(promise)) {
// `promise` can be already-wrapped promise object at this point.
JSObject* unwrappedPromise = UncheckedUnwrap(promise);
if (unwrappedPromise->is<PromiseObject>()) {
if (!cx->compartment()->wrap(cx, &promise)) {
return false;
}
} else {
promise = nullptr;
}
} else {
promise = nullptr;
}
}
// Using objectFromIncumbentGlobal, we can derive the incumbent global by
// unwrapping and then getting the global. This is very convoluted, but
// much better than having to store the original global as a private value
// because we couldn't wrap it to store it as a normal JS value.
Rooted<GlobalObject*> global(cx);
if (JSObject* objectFromIncumbentGlobal =
reaction->getAndClearIncumbentGlobalObject()) {
objectFromIncumbentGlobal = CheckedUnwrapStatic(objectFromIncumbentGlobal);
MOZ_ASSERT(objectFromIncumbentGlobal);
global = &objectFromIncumbentGlobal->nonCCWGlobal();
}
// HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
//
// Note: the global we pass here might be from a different compartment
// than job and promise. While it's somewhat unusual to pass objects
// from multiple compartments, in this case we specifically need the
// global to be unwrapped because wrapping and unwrapping aren't
// necessarily symmetric for globals.
return cx->runtime()->enqueuePromiseJob(cx, job, promise, global);
}
[[nodiscard]] static bool TriggerPromiseReactions(JSContext* cx,
HandleValue reactionsVal,
JS::PromiseState state,
HandleValue valueOrReason);
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* FulfillPromise ( promise, value )
* RejectPromise ( promise, reason )
*
* This method takes an additional optional |unwrappedRejectionStack| parameter,
* which is only used for debugging purposes.
* It allows callers to to pass in the stack of some exception which
* triggered the rejection of the promise.
*/
[[nodiscard]] static bool ResolvePromise(
JSContext* cx, Handle<PromiseObject*> promise, HandleValue valueOrReason,
JS::PromiseState state,
Handle<SavedFrame*> unwrappedRejectionStack = nullptr) {
// Step 1. Assert: The value of promise.[[PromiseState]] is pending.
MOZ_ASSERT(promise->state() == JS::PromiseState::Pending);
MOZ_ASSERT(state == JS::PromiseState::Fulfilled ||
state == JS::PromiseState::Rejected);
MOZ_ASSERT_IF(unwrappedRejectionStack, state == JS::PromiseState::Rejected);
// FulfillPromise
// Step 2. Let reactions be promise.[[PromiseFulfillReactions]].
// RejectPromise
// Step 2. Let reactions be promise.[[PromiseRejectReactions]].
//
// We only have one list of reactions for both resolution types. So
// instead of getting the right list of reactions, we determine the
// resolution type to retrieve the right information from the
// reaction records.
RootedValue reactionsVal(cx, promise->reactions());
// FulfillPromise
// Step 3. Set promise.[[PromiseResult]] to value.
// RejectPromise
// Step 3. Set promise.[[PromiseResult]] to reason.
//
// Step 4. Set promise.[[PromiseFulfillReactions]] to undefined.
// Step 5. Set promise.[[PromiseRejectReactions]] to undefined.
//
// The same slot is used for the reactions list and the result, so setting
// the result also removes the reactions list.
promise->setFixedSlot(PromiseSlot_ReactionsOrResult, valueOrReason);
// FulfillPromise
// Step 6. Set promise.[[PromiseState]] to fulfilled.
// RejectPromise
// Step 6. Set promise.[[PromiseState]] to rejected.
int32_t flags = promise->flags();
flags |= PROMISE_FLAG_RESOLVED;