From 9744b0c2bcac6d39ccc9f0d3306af1be3a896cc6 Mon Sep 17 00:00:00 2001 From: Martok Date: Sun, 22 Jan 2023 15:14:08 +0100 Subject: [PATCH] Issue #2089 - Optimize Promise.then and Promise.all/race with default resolving functions Based-on: m-c 1401508,1412200,1473970/{1-4} --- js/src/builtin/Promise.cpp | 672 ++++++++++++++---- .../debug/Promise-race-dependent-promises.js | 46 ++ .../debugger-reaction-does-not-resolve.js | 71 ++ .../resolve-promise-scripted-and-api.js | 15 + .../settle-now-breaks-all-invariants-1.js | 21 + .../settle-now-breaks-all-invariants-10.js | 18 + .../settle-now-breaks-all-invariants-11.js | 18 + .../settle-now-breaks-all-invariants-2.js | 27 + .../settle-now-breaks-all-invariants-3.js | 27 + .../settle-now-breaks-all-invariants-4.js | 18 + .../settle-now-breaks-all-invariants-5.js | 18 + .../settle-now-breaks-all-invariants-6.js | 20 + .../settle-now-breaks-all-invariants-7.js | 23 + .../settle-now-breaks-all-invariants-8.js | 7 + .../settle-now-breaks-all-invariants-9.js | 15 + js/src/jit-test/tests/realms/promise-then.js | 30 + js/src/vm/Runtime.cpp | 4 +- 17 files changed, 907 insertions(+), 143 deletions(-) create mode 100644 js/src/jit-test/tests/debug/Promise-race-dependent-promises.js create mode 100644 js/src/jit-test/tests/promise/debugger-reaction-does-not-resolve.js create mode 100644 js/src/jit-test/tests/promise/resolve-promise-scripted-and-api.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-1.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-10.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-11.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-2.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-3.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-4.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-5.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-6.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-7.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-8.js create mode 100644 js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-9.js create mode 100644 js/src/jit-test/tests/realms/promise-then.js diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 881eb6e774..a6f7a29d10 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -7,6 +7,7 @@ #include "builtin/Promise.h" #include "mozilla/Atomics.h" +#include "mozilla/Maybe.h" #include "mozilla/TimeStamp.h" #include "jscntxt.h" @@ -59,6 +60,9 @@ enum PromiseHandler { // enumerate both true and false cases here. PromiseHandlerAsyncFromSyncIteratorValueUnwrapDone, PromiseHandlerAsyncFromSyncIteratorValueUnwrapNotDone, + + // One past the maximum allowed PromiseHandler value. + PromiseHandlerLimit }; enum ResolutionMode { @@ -86,16 +90,35 @@ enum ReactionJobSlots { }; 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, +}; + enum PromiseAllDataHolderSlots { PromiseAllDataHolderSlot_Promise = 0, PromiseAllDataHolderSlot_RemainingElements, @@ -359,27 +382,74 @@ AbruptRejectPromise(JSContext *cx, CallArgs& args, HandleObject promiseObj, Hand } enum ReactionRecordSlots { + // The promise for which this record provides a reaction handler. + // Matches the [[Capability]].[[Promise]] field from the spec. + // + // The slot value is either an object, but not necessarily a built-in + // Promise object, or null. The latter case is only possible for async + // generator functions, in which case the REACTION_FLAG_ASYNC_GENERATOR + // flag must be set. 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. ReactionRecordSlot_OnFulfilled, 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, + + // Argument when calling the function stored in OnFulfilled or OnRejected. ReactionRecordSlot_HandlerArg, - ReactionRecordSlot_Generator, + + // Additional slot to store extra data for specific reaction record types. + // + // - When the REACTION_FLAG_ASYNC_GENERATOR flag is set, this slot store + // the async generator function 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, }; -#define REACTION_FLAG_RESOLVED 0x1 -#define REACTION_FLAG_FULFILLED 0x2 -#define REACTION_FLAG_IGNORE_DEFAULT_RESOLUTION 0x4 -#define REACTION_FLAG_ASYNC_FUNCTION 0x8 -#define REACTION_FLAG_ASYNC_GENERATOR 0x10 - // ES2016, 25.4.1.2. class PromiseReactionRecord : public NativeObject { + static constexpr size_t REACTION_FLAG_RESOLVED = 0x1; + static constexpr size_t REACTION_FLAG_FULFILLED = 0x2; + static constexpr size_t REACTION_FLAG_DEFAULT_RESOLVING_HANDLER = 0x4; + static constexpr size_t REACTION_FLAG_ASYNC_FUNCTION = 0x8; + static constexpr size_t REACTION_FLAG_ASYNC_GENERATOR = 0x10; + static constexpr size_t REACTION_FLAG_DEBUGGER_DUMMY = 0x20; + + void setFlagOnInitialState(size_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)); + } + public: static const Class class_; @@ -402,21 +472,29 @@ class PromiseReactionRecord : public NativeObject flags |= REACTION_FLAG_FULFILLED; setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags)); } - void setIsAsyncFunction() { + void setIsDefaultResolvingHandler(PromiseObject* promiseToResolve) { + setFlagOnInitialState(REACTION_FLAG_DEFAULT_RESOLVING_HANDLER); + setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve, ObjectValue(*promiseToResolve)); + } + bool isDefaultResolvingHandler() { int32_t flags = this->flags(); - flags |= REACTION_FLAG_ASYNC_FUNCTION; - setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags)); + return flags & REACTION_FLAG_DEFAULT_RESOLVING_HANDLER; + } + PromiseObject* defaultResolvingPromise() { + MOZ_ASSERT(isDefaultResolvingHandler()); + const Value& promiseToResolve = getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve); + return &promiseToResolve.toObject().as(); + } + void setIsAsyncFunction() { + setFlagOnInitialState(REACTION_FLAG_ASYNC_FUNCTION); } bool isAsyncFunction() { int32_t flags = this->flags(); return flags & REACTION_FLAG_ASYNC_FUNCTION; } - void setIsAsyncGenerator(Handle asyncGenObj) { - int32_t flags = this->flags(); - flags |= REACTION_FLAG_ASYNC_GENERATOR; - setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags)); - - setFixedSlot(ReactionRecordSlot_Generator, ObjectValue(*asyncGenObj)); + void setIsAsyncGenerator(AsyncGeneratorObject* asyncGenObj) { + setFlagOnInitialState(REACTION_FLAG_ASYNC_GENERATOR); + setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve, ObjectValue(*asyncGenObj)); } bool isAsyncGenerator() { int32_t flags = this->flags(); @@ -424,8 +502,15 @@ class PromiseReactionRecord : public NativeObject } AsyncGeneratorObject* asyncGenerator() { MOZ_ASSERT(isAsyncGenerator()); - return &getFixedSlot(ReactionRecordSlot_Generator).toObject() - .as(); + const Value& generator = getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve); + return &generator.toObject().as(); + } + 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); @@ -470,9 +555,9 @@ static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp); // ES2016, 25.4.1.3. static MOZ_MUST_USE bool -CreateResolvingFunctions(JSContext* cx, HandleValue promise, - MutableHandleValue resolveVal, - MutableHandleValue rejectVal) +CreateResolvingFunctions(JSContext* cx, HandleObject promise, + MutableHandleObject resolveFn, + MutableHandleObject rejectFn) { RootedAtom funName(cx, cx->names().empty); RootedFunction resolve(cx, NewNativeFunction(cx, ResolvePromiseFunction, 1, funName, @@ -485,19 +570,34 @@ CreateResolvingFunctions(JSContext* cx, HandleValue promise, if (!reject) return false; - resolve->setExtendedSlot(ResolveFunctionSlot_Promise, promise); + resolve->setExtendedSlot(ResolveFunctionSlot_Promise, ObjectValue(*promise)); resolve->setExtendedSlot(ResolveFunctionSlot_RejectFunction, ObjectValue(*reject)); - reject->setExtendedSlot(RejectFunctionSlot_Promise, promise); + reject->setExtendedSlot(RejectFunctionSlot_Promise, ObjectValue(*promise)); reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, ObjectValue(*resolve)); - resolveVal.setObject(*resolve); - rejectVal.setObject(*reject); + resolveFn.set(resolve); + rejectFn.set(reject); return true; } static void ClearResolutionFunctionSlots(JSFunction* resolutionFun); + +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().state() != JS::PromiseState::Pending; +} + static MOZ_MUST_USE bool RejectMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue reason); @@ -531,9 +631,8 @@ RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp) // 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 (promise->is() && - promise->as().state() != JS::PromiseState::Pending) - { + if (IsSettledMaybeWrappedPromise(promise)) { + args.rval().setUndefined(); return true; } @@ -552,10 +651,20 @@ static MOZ_MUST_USE bool EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue thenable, HandleValue thenVal); +static MOZ_MUST_USE bool EnqueuePromiseResolveThenableBuiltinJob(JSContext* cx, + HandleObject promiseToResolve, + HandleObject thenable); + +static bool Promise_then(JSContext* cx, unsigned argc, Value* vp); +static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal, HandleValue onFulfilled, + HandleValue onRejected, MutableHandleValue rval, bool rvalUsed); + // ES2016, 25.4.1.3.2, steps 6-13. static MOZ_MUST_USE bool ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resolutionVal) { + MOZ_ASSERT(!IsSettledMaybeWrappedPromise(promise)); + // Step 7 (reordered). if (!resolutionVal.isObject()) return FulfillMaybeWrappedPromise(cx, promise, resolutionVal); @@ -578,25 +687,57 @@ ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resoluti RootedValue thenVal(cx); bool status = GetProperty(cx, resolution, resolution, cx->names().then, &thenVal); - // Step 9. + RootedValue error(cx); if (!status) { - RootedValue error(cx); if (!MaybeGetAndClearException(cx, &error)) return false; - - return RejectMaybeWrappedPromise(cx, promise, error); } + // 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 9. + if (!status) + return RejectMaybeWrappedPromise(cx, promise, error); + // Step 10 (implicit). // Step 11. if (!IsCallable(thenVal)) return FulfillMaybeWrappedPromise(cx, promise, resolutionVal); + // If the resolution object is a built-in Promise object, possibly from a + // different realm in the same compartment, and the `then` property is the + // original Promise.prototype.then function from the current realm, we + // skip storing/calling it. + // And additionally require that |promise| itself is also a built-in + // Promise object from the same compartment, so the fast path doesn't need + // to cope with wrappers. + bool isBuiltinThen = false; + if (resolution->is() && + resolution->as().compartment() == cx->compartment() && + IsNativeFunction(thenVal, Promise_then) && + promise->is() && + promise->as().compartment() == cx->compartment()) + { + thenVal = UndefinedValue(); + isBuiltinThen = true; + } + // Step 12. - RootedValue promiseVal(cx, ObjectValue(*promise)); - if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal, thenVal)) - return false; + if (!isBuiltinThen) { + RootedValue promiseVal(cx, ObjectValue(*promise)); + if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal, thenVal)) + return false; + } else { + if (!EnqueuePromiseResolveThenableBuiltinJob(cx, promise, resolution)) + return false; + } // Step 13. return true; @@ -631,9 +772,8 @@ ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp) // 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 (promise->is() && - promise->as().state() != JS::PromiseState::Pending) - { + if (IsSettledMaybeWrappedPromise(promise)) { + args.rval().setUndefined(); return true; } @@ -659,6 +799,9 @@ MOZ_MUST_USE 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 @@ -715,7 +858,7 @@ EnqueuePromiseReactionJob(JSContext* cx, HandleObject reactionObj, } // Create the JS function to call when the job is triggered. - RootedAtom funName(cx, cx->names().empty); + HandlePropertyName funName = cx->names().empty; RootedFunction job(cx, NewNativeFunction(cx, PromiseReactionJob, 0, funName, gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); if (!job) @@ -738,8 +881,8 @@ EnqueuePromiseReactionJob(JSContext* cx, HandleObject reactionObj, // single compartment to the enqueuePromiseJob callback. RootedObject promise(cx, reaction->promise()); if (promise && promise->is()) { - if (!cx->compartment()->wrap(cx, &promise)) - return false; + if (!cx->compartment()->wrap(cx, &promise)) + return false; } // Using objectFromIncumbentGlobal, we can derive the incumbent global by @@ -829,8 +972,6 @@ FulfillMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue v return false; } - MOZ_ASSERT(promise->state() == JS::PromiseState::Pending); - return ResolvePromise(cx, promise, value, JS::PromiseState::Fulfilled); } @@ -858,6 +999,32 @@ CreatePromiseObjectWithoutResolutionFunctions(JSContext* cx) return promise; } +static MOZ_MUST_USE PromiseObject* +CreatePromiseWithDefaultResolutionFunctions(JSContext* cx, MutableHandleObject resolve, + MutableHandleObject reject) +{ + // ES2016, 25.4.3.1., as if called with GetCapabilitiesExecutor as the + // executor argument. + + // Steps 1-2 (Not applicable). + + // Steps 3-7. + Rooted promise(cx, CreatePromiseObjectInternal(cx)); + if (!promise) + return nullptr; + + // Step 8. + if (!CreateResolvingFunctions(cx, promise, resolve, reject)) + return nullptr; + + promise->setFixedSlot(PromiseSlot_RejectFunction, ObjectValue(*reject)); + + // Steps 9-10 (Not applicable). + + // Step 11. + return promise; +} + // ES2016, 25.4.1.5. static MOZ_MUST_USE bool NewPromiseCapability(JSContext* cx, HandleObject C, MutableHandleObject promise, @@ -881,8 +1048,15 @@ NewPromiseCapability(JSContext* cx, HandleObject C, MutableHandleObject promise, // pass the reject (and resolve, in the race case) function to thenables // in the list passed to all/race, which (potentially) means exposing them // to content. - if (canOmitResolutionFunctions && IsNativeFunction(cVal, PromiseConstructor)) { - promise.set(CreatePromiseObjectWithoutResolutionFunctions(cx)); + // + // For Promise.all and Promise.race we can only optimize away the creation + // of the GetCapabilitiesExecutor function, and directly allocate the + // result promise instead of invoking the Promise constructor. + if (IsNativeFunction(cVal, PromiseConstructor)) { + if (canOmitResolutionFunctions) + promise.set(CreatePromiseObjectWithoutResolutionFunctions(cx)); + else + promise.set(CreatePromiseWithDefaultResolutionFunctions(cx, resolve, reject)); if (!promise) return false; return true; @@ -891,7 +1065,7 @@ NewPromiseCapability(JSContext* cx, HandleObject C, MutableHandleObject promise, // Step 3 (omitted). // Step 4. - RootedAtom funName(cx, cx->names().empty); + HandlePropertyName funName = cx->names().empty; RootedFunction executor(cx, NewNativeFunction(cx, GetCapabilitiesExecutor, 2, funName, gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); if (!executor) @@ -995,8 +1169,6 @@ RejectMaybeWrappedPromise(JSContext *cx, HandleObject promiseObj, HandleValue re } } - MOZ_ASSERT(promise->state() == JS::PromiseState::Pending); - return ResolvePromise(cx, promise, reason, JS::PromiseState::Rejected); } @@ -1005,6 +1177,8 @@ static MOZ_MUST_USE bool TriggerPromiseReactions(JSContext* cx, HandleValue reactionsVal, JS::PromiseState state, HandleValue valueOrReason) { + MOZ_ASSERT(state == JS::PromiseState::Fulfilled || state == JS::PromiseState::Rejected); + RootedObject reactions(cx, &reactionsVal.toObject()); RootedObject reaction(cx); @@ -1024,6 +1198,54 @@ TriggerPromiseReactions(JSContext* cx, HandleValue reactionsVal, JS::PromiseStat return true; } +// Implements PromiseReactionJob optimized for the case when the reaction +// handler is one of the default resolving functions as created by the +// CreateResolvingFunctions abstract operation. +static MOZ_MUST_USE bool +DefaultResolvingPromiseReactionJob(JSContext* cx, Handle reaction, + MutableHandleValue rval) +{ + MOZ_ASSERT(reaction->targetState() != JS::PromiseState::Pending); + + Rooted promiseToResolve(cx, reaction->defaultResolvingPromise()); + + // 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. We still call + // RunResolutionFunction for consistency with PromiseReactionJob. + ResolutionMode resolutionMode = ResolveMode; + RootedValue handlerResult(cx, UndefinedValue()); + if (promiseToResolve->state() == JS::PromiseState::Pending) { + RootedValue argument(cx, reaction->handlerArg()); + + // Step 6. + bool ok; + if (reaction->targetState() == JS::PromiseState::Fulfilled) + ok = ResolvePromiseInternal(cx, promiseToResolve, argument); + else + ok = RejectMaybeWrappedPromise(cx, promiseToResolve, argument); + + if (!ok) { + resolutionMode = RejectMode; + if (!MaybeGetAndClearException(cx, &handlerResult)) + return false; + } + } + + // Steps 7-9. + size_t hookSlot = resolutionMode == RejectMode + ? ReactionRecordSlot_Reject + : ReactionRecordSlot_Resolve; + RootedObject callee(cx, reaction->getFixedSlot(hookSlot).toObjectOrNull()); + RootedObject promiseObj(cx, reaction->promise()); + if (!RunResolutionFunction(cx, callee, handlerResult, resolutionMode, promiseObj)) + return false; + + rval.setUndefined(); + return true; +} + static MOZ_MUST_USE bool AsyncFunctionPromiseReactionJob(JSContext* cx, Handle reaction, MutableHandleValue rval) @@ -1035,7 +1257,7 @@ AsyncFunctionPromiseReactionJob(JSContext* cx, Handle re Rooted resultPromise(cx, &reaction->promise()->as()); RootedValue generatorVal(cx, resultPromise->getFixedSlot(PromiseSlot_AwaitGenerator)); - int32_t handlerNum = int32_t(handlerVal.toNumber()); + int32_t handlerNum = handlerVal.toInt32(); // Await's handlers don't return a value, nor throw exception. // They fail only on OOM. @@ -1062,7 +1284,7 @@ AsyncGeneratorPromiseReactionJob(JSContext* cx, Handle r RootedValue argument(cx, reaction->handlerArg()); Rooted asyncGenObj(cx, reaction->asyncGenerator()); - int32_t handlerNum = int32_t(handlerVal.toNumber()); + int32_t handlerNum = handlerVal.toInt32(); // Await's handlers don't return a value, nor throw exception. // They fail only on OOM. @@ -1144,10 +1366,14 @@ PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) // Steps 1-2. Rooted reaction(cx, &reactionObj->as()); + if (reaction->isDefaultResolvingHandler()) + return DefaultResolvingPromiseReactionJob(cx, reaction, args.rval()); if (reaction->isAsyncFunction()) return AsyncFunctionPromiseReactionJob(cx, reaction, args.rval()); if (reaction->isAsyncGenerator()) return AsyncGeneratorPromiseReactionJob(cx, reaction, args.rval()); + if (reaction->isDebuggerDummy()) + return true; // Step 3. RootedValue handlerVal(cx, reaction->handler()); @@ -1158,8 +1384,8 @@ PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) ResolutionMode resolutionMode = ResolveMode; // Steps 4-6. - if (handlerVal.isNumber()) { - int32_t handlerNum = int32_t(handlerVal.toNumber()); + if (handlerVal.isInt32()) { + int32_t handlerNum = handlerVal.toInt32(); // Step 4. if (handlerNum == PromiseHandlerIdentity) { @@ -1181,6 +1407,9 @@ PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) handlerResult = ObjectValue(*resultObj); } } else { + MOZ_ASSERT(handlerVal.isObject()); + MOZ_ASSERT(IsCallable(handlerVal)); + // Step 6. FixedInvokeArgs<1> args2(cx); args2[0].set(argument); @@ -1209,28 +1438,14 @@ PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) * Callback for resolving a thenable, to be invoked by the embedding during * its processing of the Promise job queue. * - * See http://www.ecma-international.org/ecma-262/7.0/index.html#sec-jobs-and-job-queues + * See https://tc39.github.io/ecma262/#sec-jobs-and-job-queues * * A PromiseResolveThenableJob is set as the native function of an extended * JSFunction object, with all information required for the job's * execution stored in the function's extended slots. * - * Usage of the function's extended slots is as follows: - * ThenableJobSlot_Handler: The handler to use as the Promise reaction. - * This can be PromiseHandlerIdentity, - * PromiseHandlerThrower, or a callable. In the - * latter case, it's guaranteed to be an object - * from the same compartment as the - * PromiseReactionJob. - * ThenableJobSlot_JobData: JobData - a, potentially CCW-wrapped, dense list - * containing data required for proper execution of - * the reaction. - * - * The JobData list has the following entries: - * ThenableJobDataSlot_Promise: The Promise to resolve using the given - * thenable. - * ThenableJobDataSlot_Thenable: The thenable to use as the receiver when - * calling the `then` function. + * Usage of the function's extended slots is described in the ThenableJobSlots + * enum. */ static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) @@ -1239,39 +1454,96 @@ PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) RootedFunction job(cx, &args.callee().as()); RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler)); + MOZ_ASSERT(then.isObject()); MOZ_ASSERT(!IsWrapper(&then.toObject())); RootedNativeObject jobArgs(cx, &job->getExtendedSlot(ThenableJobSlot_JobData) .toObject().as()); - RootedValue promise(cx, jobArgs->getDenseElement(ThenableJobDataIndex_Promise)); + RootedObject promise(cx, &jobArgs->getDenseElement(ThenableJobDataIndex_Promise).toObject()); RootedValue thenable(cx, jobArgs->getDenseElement(ThenableJobDataIndex_Thenable)); // Step 1. - RootedValue resolveVal(cx); - RootedValue rejectVal(cx); - if (!CreateResolvingFunctions(cx, promise, &resolveVal, &rejectVal)) + RootedObject resolveFn(cx); + RootedObject rejectFn(cx); + if (!CreateResolvingFunctions(cx, promise, &resolveFn, &rejectFn)) return false; // Step 2. FixedInvokeArgs<2> args2(cx); - args2[0].set(resolveVal); - args2[1].set(rejectVal); - - RootedValue rval(cx); + args2[0].setObject(*resolveFn); + args2[1].setObject(*rejectFn); // In difference to the usual pattern, we return immediately on success. + RootedValue rval(cx); if (Call(cx, then, thenable, args2, &rval)) return true; + // Steps 3-4. if (!MaybeGetAndClearException(cx, &rval)) return false; FixedInvokeArgs<1> rejectArgs(cx); rejectArgs[0].set(rval); + RootedValue rejectVal(cx, ObjectValue(*rejectFn)); return Call(cx, rejectVal, UndefinedHandleValue, rejectArgs, &rval); } +static MOZ_MUST_USE bool +OriginalPromiseThenWithoutSettleHandlers(JSContext* cx, Handle promise, + Handle promiseToResolve); + +/** + * Specialization of PromiseResolveThenableJob when the `thenable` is a + * built-in Promise object and the `then` property is the built-in + * `Promise.prototype.then` function. + * + * A PromiseResolveBuiltinThenableJob is set as the native function of an + * extended JSFunction object, with all information required for the job's + * execution stored in the function's extended slots. + * + * Usage of the function's extended slots is described in the + * BuiltinThenableJobSlots enum. + */ +static bool +PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + RootedFunction job(cx, &args.callee().as()); + RootedObject promise(cx, &job->getExtendedSlot(BuiltinThenableJobSlot_Promise).toObject()); + RootedObject thenable(cx, &job->getExtendedSlot(BuiltinThenableJobSlot_Thenable).toObject()); + + assertSameCompartment(cx, promise, thenable); + MOZ_ASSERT(promise->is()); + MOZ_ASSERT(thenable->is()); + + // Step 1 (Skipped). + + // Step 2. + // In difference to the usual pattern, we return immediately on success. + if (OriginalPromiseThenWithoutSettleHandlers(cx, thenable.as(), + promise.as())) + { + return true; + } + + // Steps 3-4. + RootedValue exception(cx); + if (!MaybeGetAndClearException(cx, &exception)) + 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 (promise->as().state() != JS::PromiseState::Pending) + return true; + + return RejectMaybeWrappedPromise(cx, promise, exception); +} + /** * Tells the embedding to enqueue a Promise resolve thenable job, based on * three parameters: @@ -1295,7 +1567,7 @@ EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve_, RootedObject then(cx, CheckedUnwrap(&thenVal.toObject())); AutoCompartment ac(cx, then); - RootedAtom funName(cx, cx->names().empty); + HandlePropertyName funName = cx->names().empty; RootedFunction job(cx, NewNativeFunction(cx, PromiseResolveThenableJob, 0, funName, gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); if (!job) @@ -1306,7 +1578,7 @@ EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve_, // Create a dense array to hold the data needed for the reaction job to // work. - // See the doc comment for PromiseResolveThenableJob for the layout. + // The layout is described in the ThenableJobDataIndices enum. RootedArrayObject data(cx, NewDenseFullyAllocatedArray(cx, ThenableJobDataLength)); if (!data || data->ensureDenseElements(cx, 0, ThenableJobDataLength) != DenseElementResult::Success) @@ -1335,10 +1607,37 @@ EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve_, return cx->runtime()->enqueuePromiseJob(cx, job, promise, incumbentGlobal); } +/** + * Tells the embedding to enqueue a Promise resolve thenable built-in job, + * based on two parameters: + * promiseToResolve - The promise to resolve, obviously. + * thenable - The thenable to resolve the Promise with. + */ static MOZ_MUST_USE bool -AddPromiseReaction(JSContext* cx, Handle promise, HandleValue onFulfilled, - HandleValue onRejected, HandleObject dependentPromise, - HandleObject resolve, HandleObject reject, HandleObject incumbentGlobal); +EnqueuePromiseResolveThenableBuiltinJob(JSContext* cx, HandleObject promiseToResolve, + HandleObject thenable) +{ + assertSameCompartment(cx, promiseToResolve, thenable); + MOZ_ASSERT(promiseToResolve->is()); + MOZ_ASSERT(thenable->is()); + + HandlePropertyName funName = cx->names().empty; + RootedFunction job(cx, NewNativeFunction(cx, PromiseResolveBuiltinThenableJob, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!job) + return false; + + // Store the promise and the thenable on the reaction job. + job->setExtendedSlot(BuiltinThenableJobSlot_Promise, ObjectValue(*promiseToResolve)); + job->setExtendedSlot(BuiltinThenableJobSlot_Thenable, ObjectValue(*thenable)); + + RootedObject incumbentGlobal(cx, cx->runtime()->getIncumbentGlobal(cx)); + return cx->runtime()->enqueuePromiseJob(cx, job, promiseToResolve, incumbentGlobal); +} + +static MOZ_MUST_USE bool +AddDummyPromiseReactionForDebugger(JSContext* cx, Handle promise, + HandleObject dependentPromise); static MOZ_MUST_USE bool AddPromiseReaction(JSContext* cx, Handle promise, @@ -1389,6 +1688,11 @@ GetResolveFunctionFromPromise(PromiseObject* promise) if (rejectFun->maybeNative() != &RejectPromiseFunction) return nullptr; + // The reject function was already called and cleared its resolve-function + // extended slot. + if (rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction).isUndefined()) + return nullptr; + return GetResolveFunctionFromReject(rejectFun); } @@ -1565,28 +1869,28 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto / if (!promise) return nullptr; - RootedValue promiseVal(cx, ObjectValue(*promise)); - if (needsWrapping && !cx->compartment()->wrap(cx, &promiseVal)) + RootedObject promiseObj(cx, promise); + if (needsWrapping && !cx->compartment()->wrap(cx, &promiseObj)) return nullptr; // Step 8. // The resolving functions are created in the compartment active when the // (maybe wrapped) Promise constructor was called. They contain checks and // can unwrap the Promise if required. - RootedValue resolveVal(cx); - RootedValue rejectVal(cx); - if (!CreateResolvingFunctions(cx, promiseVal, &resolveVal, &rejectVal)) + RootedObject resolveFn(cx); + RootedObject rejectFn(cx); + if (!CreateResolvingFunctions(cx, promiseObj, &resolveFn, &rejectFn)) return nullptr; // Need to wrap the resolution functions before storing them on the Promise. if (needsWrapping) { AutoCompartment ac(cx, promise); - RootedValue wrappedRejectVal(cx, rejectVal); - if (!cx->compartment()->wrap(cx, &wrappedRejectVal)) + RootedObject wrappedRejectFn(cx, rejectFn); + if (!cx->compartment()->wrap(cx, &wrappedRejectFn)) return nullptr; - promise->setFixedSlot(PromiseSlot_RejectFunction, wrappedRejectVal); + promise->setFixedSlot(PromiseSlot_RejectFunction, ObjectValue(*wrappedRejectFn)); } else { - promise->setFixedSlot(PromiseSlot_RejectFunction, rejectVal); + promise->setFixedSlot(PromiseSlot_RejectFunction, ObjectValue(*rejectFn)); } // Step 9. @@ -1594,8 +1898,8 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto / { FixedInvokeArgs<2> args(cx); - args[0].set(resolveVal); - args[1].set(rejectVal); + args[0].setObject(*resolveFn); + args[1].setObject(*rejectFn); RootedValue calleeOrRval(cx, ObjectValue(*executor)); success = Call(cx, calleeOrRval, UndefinedHandleValue, args, &calleeOrRval); @@ -1611,8 +1915,8 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto / args[0].set(exceptionVal); - // |rejectVal| is unused after this, so we can safely write to it. - if (!Call(cx, rejectVal, UndefinedHandleValue, args, &rejectVal)) + RootedValue calleeOrRval(cx, ObjectValue(*rejectFn)); + if (!Call(cx, calleeOrRval, UndefinedHandleValue, args, &calleeOrRval)) return nullptr; } @@ -1747,6 +2051,12 @@ static MOZ_MUST_USE bool PerformPromiseThen(JSContext* cx, Handle promise, + Handle promiseToResolve, + HandleObject resultPromise, HandleObject resolve, + HandleObject reject); + static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp); static bool PromiseAllSettledResolveElementFunction(JSContext* cx, unsigned argc, Value* vp); static bool PromiseAllSettledRejectElementFunction(JSContext* cx, unsigned argc, Value* vp); @@ -2026,7 +2336,7 @@ PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, // "resolve" on the canonical Promise constructor. RootedValue nextPromise(cx); RootedValue staticResolve(cx); - if (!GetProperty(cx, CVal, cx->names().resolve, &staticResolve)) + if (!GetProperty(cx, C, CVal, cx->names().resolve, &staticResolve)) return false; FixedInvokeArgs<1> resolveArgs(cx); @@ -2502,7 +2812,7 @@ PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, // "resolve" on the canonical Promise constructor. RootedValue nextPromise(cx); RootedValue staticResolve(cx); - if (!GetProperty(cx, CVal, cx->names().resolve, &staticResolve)) + if (!GetProperty(cx, C, CVal, cx->names().resolve, &staticResolve)) return false; FixedInvokeArgs<1> resolveArgs(cx); @@ -2658,10 +2968,14 @@ Promise_static_species(JSContext* cx, unsigned argc, Value* vp) // ES2016, 25.4.5.1, implemented in Promise.js. +enum class IncumbentGlobalObject { + Yes, No +}; + static PromiseReactionRecord* NewReactionRecord(JSContext* cx, HandleObject resultPromise, HandleValue onFulfilled, HandleValue onRejected, HandleObject resolve, HandleObject reject, - HandleObject incumbentGlobalObject) + IncumbentGlobalObject incumbentGlobalObjectOption) { // Either of the following conditions must be met: // * resultPromise is a PromiseObject @@ -2672,6 +2986,27 @@ NewReactionRecord(JSContext* cx, HandleObject resultPromise, HandleValue onFulfi MOZ_ASSERT_IF(resultPromise && !resultPromise->is(), reject); MOZ_ASSERT_IF(resultPromise && !resultPromise->is(), IsCallable(reject)); + // Ensure the onFulfilled handler has the expected type. + MOZ_ASSERT(onFulfilled.isInt32() || onFulfilled.isObjectOrNull()); + MOZ_ASSERT_IF(onFulfilled.isObject(), IsCallable(onFulfilled)); + MOZ_ASSERT_IF(onFulfilled.isInt32(), + 0 <= onFulfilled.toInt32() && onFulfilled.toInt32() < PromiseHandlerLimit); + + // Ensure the onRejected handler has the expected type. + MOZ_ASSERT(onRejected.isInt32() || onRejected.isObjectOrNull()); + MOZ_ASSERT_IF(onRejected.isObject(), IsCallable(onRejected)); + MOZ_ASSERT_IF(onRejected.isInt32(), + 0 <= onRejected.toInt32() && onRejected.toInt32() < PromiseHandlerLimit); + + // Handlers must either both be present or both be absent. + MOZ_ASSERT(onFulfilled.isNull() == onRejected.isNull()); + + RootedObject incumbentGlobalObject(cx); + if (incumbentGlobalObjectOption == IncumbentGlobalObject::Yes) { + if (!GetObjectFromIncumbentGlobal(cx, &incumbentGlobalObject)) + return nullptr; + } + Rooted reaction(cx, NewObjectWithClassProto(cx)); if (!reaction) return nullptr; @@ -2701,6 +3036,30 @@ IsPromiseSpecies(JSContext* cx, JSFunction* species) return species->maybeNative() == Promise_static_species; } +static bool +PromiseThenNewPromiseCapability(JSContext* cx, HandleObject promiseObj, + CreateDependentPromise createDependent, + MutableHandleObject resultPromise, + MutableHandleObject resolve, MutableHandleObject reject) +{ + if (createDependent != CreateDependentPromise::Never) { + // Step 3. + RootedObject C(cx, SpeciesConstructor(cx, promiseObj, JSProto_Promise, IsPromiseSpecies)); + if (!C) + return false; + + if (createDependent == CreateDependentPromise::Always || + !IsNativeFunction(C, PromiseConstructor)) + { + // Step 4. + if (!NewPromiseCapability(cx, C, resultPromise, resolve, reject, true)) + return false; + } + } + + return true; +} + // ES2016, 25.4.5.3., steps 3-5. MOZ_MUST_USE bool js::OriginalPromiseThen(JSContext* cx, Handle promise, @@ -2717,20 +3076,10 @@ js::OriginalPromiseThen(JSContext* cx, Handle promise, RootedObject resolve(cx); RootedObject reject(cx); - if (createDependent != CreateDependentPromise::Never) { - // Step 3. - RootedObject C(cx, SpeciesConstructor(cx, promiseObj, JSProto_Promise, IsPromiseSpecies)); - if (!C) - return false; - - // Step 4. - if (createDependent == CreateDependentPromise::Always || - !IsNativeFunction(C, PromiseConstructor)) - { - // Step 4. - if (!NewPromiseCapability(cx, C, &resultPromise, &resolve, &reject, true)) - return false; - } + if (!PromiseThenNewPromiseCapability(cx, promiseObj, createDependent, &resultPromise, + &resolve, &reject)) + { + return false; } // Step 5. @@ -2741,6 +3090,27 @@ js::OriginalPromiseThen(JSContext* cx, Handle promise, return true; } +static MOZ_MUST_USE bool +OriginalPromiseThenWithoutSettleHandlers(JSContext* cx, Handle promise, + Handle promiseToResolve) +{ + assertSameCompartment(cx, promise); + + // Steps 3-4. + RootedObject resultPromise(cx); + RootedObject resolve(cx); + RootedObject reject(cx); + if (!PromiseThenNewPromiseCapability(cx, promise, CreateDependentPromise::SkipIfCtorUnobservable, + &resultPromise, &resolve, &reject)) + { + return false; + } + + // Step 5. + return PerformPromiseThenWithoutSettleHandlers(cx, promise, promiseToResolve, resultPromise, + resolve, reject); +} + static MOZ_MUST_USE bool PerformPromiseThenWithReaction(JSContext* cx, Handle promise, Handle reaction); @@ -2804,8 +3174,8 @@ static MOZ_MUST_USE bool InternalAwait(JSContext* cx, HandleValue value, HandleObject resultPromise, HandleValue onFulfilled, HandleValue onRejected, T extraStep) { - MOZ_ASSERT(onFulfilled.isNumber() || onFulfilled.isObject()); - MOZ_ASSERT(onRejected.isNumber() || onRejected.isObject()); + MOZ_ASSERT(onFulfilled.isInt32()); + MOZ_ASSERT(onRejected.isInt32()); // Step 2. Rooted promise(cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); @@ -2816,15 +3186,11 @@ InternalAwait(JSContext* cx, HandleValue value, HandleObject resultPromise, if (!ResolvePromiseInternal(cx, promise, value)) return false; - RootedObject incumbentGlobal(cx); - if (!GetObjectFromIncumbentGlobal(cx, &incumbentGlobal)) - return false; - // Steps 7-8. Rooted reaction(cx, NewReactionRecord(cx, resultPromise, onFulfilled, onRejected, nullptr, nullptr, - incumbentGlobal)); + IncumbentGlobalObject::Yes)); if (!reaction) return false; @@ -3269,10 +3635,6 @@ js::AsyncGeneratorEnqueue(JSContext* cx, HandleValue asyncGenVal, return true; } -static bool Promise_then(JSContext* cx, unsigned argc, Value* vp); -static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal, HandleValue onFulfilled, - HandleValue onRejected, MutableHandleValue rval, bool rvalUsed); - static bool Promise_catch_impl(JSContext* cx, unsigned argc, Value* vp, bool rvalUsed) { @@ -3420,18 +3782,42 @@ PerformPromiseThen(JSContext* cx, Handle promise, HandleValue on if (!IsCallable(onRejected)) onRejected = Int32Value(PromiseHandlerThrower); - RootedObject incumbentGlobal(cx); - if (!GetObjectFromIncumbentGlobal(cx, &incumbentGlobal)) + // Step 7. + Rooted reaction(cx, NewReactionRecord(cx, resultPromise, + onFulfilled, onRejected, + resolve, reject, + IncumbentGlobalObject::Yes)); + if (!reaction) return false; + return PerformPromiseThenWithReaction(cx, promise, reaction); +} + +static MOZ_MUST_USE bool +PerformPromiseThenWithoutSettleHandlers(JSContext* cx, Handle promise, + Handle promiseToResolve, + HandleObject resultPromise, HandleObject resolve, + HandleObject reject) +{ + // Step 1 (implicit). + // Step 2 (implicit). + + // Step 3. + HandleValue onFulfilled = NullHandleValue; + + // Step 4. + HandleValue onRejected = NullHandleValue; + // Step 7. Rooted reaction(cx, NewReactionRecord(cx, resultPromise, onFulfilled, onRejected, resolve, reject, - incumbentGlobal)); + IncumbentGlobalObject::Yes)); if (!reaction) return false; + reaction->setIsDefaultResolvingHandler(promiseToResolve); + return PerformPromiseThenWithReaction(cx, promise, reaction); } @@ -3492,15 +3878,15 @@ static MOZ_MUST_USE bool BlockOnPromise(JSContext* cx, HandleValue promiseVal, HandleObject blockedPromise_, HandleValue onFulfilled, HandleValue onRejected) { - RootedValue thenVal(cx); - if (!GetProperty(cx, promiseVal, cx->names().then, &thenVal)) + RootedObject promiseObj(cx, ToObject(cx, promiseVal)); + if (!promiseObj) return false; - RootedObject promiseObj(cx); - if (promiseVal.isObject()) - promiseObj = &promiseVal.toObject(); + RootedValue thenVal(cx); + if (!GetProperty(cx, promiseObj, promiseVal, cx->names().then, &thenVal)) + return false; - if (promiseObj && promiseObj->is() && IsNativeFunction(thenVal, Promise_then)) { + if (promiseObj->is() && IsNativeFunction(thenVal, Promise_then)) { // |promise| is an unwrapped Promise, and |then| is the original // |Promise.prototype.then|, inline it here. // 25.4.5.3., step 3. @@ -3550,7 +3936,7 @@ BlockOnPromise(JSContext* cx, HandleValue promiseVal, HandleObject blockedPromis // (potentially after unwrapping them), and non-object values can't be // Promise objects. This can happen if Promise.all is called on an object // with a `resolve` method that returns primitives. - if (!promiseObj) + if (!promiseVal.isObject()) return true; // The object created by the |promise.then| call or the inlined version @@ -3589,8 +3975,7 @@ BlockOnPromise(JSContext* cx, HandleValue promiseVal, HandleObject blockedPromis return true; Rooted promise(cx, &unwrappedPromiseObj->as()); - return AddPromiseReaction(cx, promise, UndefinedHandleValue, UndefinedHandleValue, - blockedPromise, nullptr, nullptr, nullptr); + return AddDummyPromiseReactionForDebugger(cx, promise, blockedPromise); } static MOZ_MUST_USE bool @@ -3661,19 +4046,21 @@ AddPromiseReaction(JSContext* cx, Handle promise, } static MOZ_MUST_USE bool -AddPromiseReaction(JSContext* cx, Handle promise, HandleValue onFulfilled, - HandleValue onRejected, HandleObject dependentPromise, - HandleObject resolve, HandleObject reject, HandleObject incumbentGlobal) +AddDummyPromiseReactionForDebugger(JSContext* cx, Handle promise, + HandleObject dependentPromise) { if (promise->state() != JS::PromiseState::Pending) return true; Rooted reaction(cx, NewReactionRecord(cx, dependentPromise, - onFulfilled, onRejected, - resolve, reject, - incumbentGlobal)); + NullHandleValue, NullHandleValue, + nullptr, nullptr, + IncumbentGlobalObject::No)); if (!reaction) return false; + + reaction->setIsDebuggerDummy(); + return AddPromiseReaction(cx, promise, reaction); } @@ -3760,6 +4147,9 @@ PromiseObject::resolve(JSContext* cx, Handle promise, HandleValu return ResolvePromiseInternal(cx, promise, resolutionValue); RootedObject resolveFun(cx, GetResolveFunctionFromPromise(promise)); + if (!resolveFun) + return true; + RootedValue funVal(cx, ObjectValue(*resolveFun)); // For xray'd Promises, the resolve fun may have been created in another diff --git a/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js b/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js new file mode 100644 index 0000000000..a2437c3e2d --- /dev/null +++ b/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js @@ -0,0 +1,46 @@ +// Promise.race(...) may add a dummy PromiseReaction which is only used for the +// debugger. +// +// See BlockOnPromise when called from PerformPromiseRace for when this dummy +// reaction is created. + +var g = newGlobal(); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +function test(withFastPath) { + g.eval(` + function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; + } + + var {promise: alwaysPending} = newPromiseCapability(); + + if (!${withFastPath}) { + // Disable the BlockOnPromise fast path by giving |alwaysPending| a + // non-default "then" function property. This will ensure the dummy + // reaction is created. + alwaysPending.then = function() {}; + } + + var result = Promise.race([alwaysPending]); + `); + + var alwaysPending = gw.makeDebuggeeValue(g.alwaysPending); + var result = gw.makeDebuggeeValue(g.result); + + assertEq(alwaysPending.promiseDependentPromises.length, 1); + assertEq(alwaysPending.promiseDependentPromises[0], result); + + assertEq(result.promiseDependentPromises.length, 0); +} + +// No dummy reaction created when the fast path is taken. +test(true); + +// Dummy reaction is created when we can't take the fast path. +test(false); diff --git a/js/src/jit-test/tests/promise/debugger-reaction-does-not-resolve.js b/js/src/jit-test/tests/promise/debugger-reaction-does-not-resolve.js new file mode 100644 index 0000000000..48aec08e26 --- /dev/null +++ b/js/src/jit-test/tests/promise/debugger-reaction-does-not-resolve.js @@ -0,0 +1,71 @@ +// Promise.race(...) may add a dummy PromiseReaction which is only used for the +// debugger. Ensure that this dummy reaction can't influence the normal Promise +// resolution behaviour. +// +// See BlockOnPromise when called from PerformPromiseRace for when this dummy +// reaction is created. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + +function neverCalled() { + // Quit with non-zero exit code to ensure a test suite error is shown, + // even when this function is called within promise handlers which normally + // swallow any exceptions. + quit(1); +} + +var c = 0; +var g_resolve; + +var resolvedValues = []; + +function resolveCapability(v) { + resolvedValues.push(v); +} + +class P extends Promise { + constructor(executor) { + // Only the very first object created through this constructor gets + // special treatment, all other invocations create built-in Promise + // objects. + if (c++ > 1) { + return new Promise(executor); + } + + executor(resolveCapability, neverCalled); + + var {promise, resolve} = newPromiseCapability(); + g_resolve = resolve; + + // Use an async function to create a Promise without resolving functions. + var p = async function(){ await promise; return 456; }(); + + // Ensure the species constructor is not the built-in Promise constructor + // to avoid falling into the fast path. + p.constructor = { + [Symbol.species]: P + }; + + return p; + } +} + +var {promise: alwaysPending} = newPromiseCapability(); + +// The promise returned from race() should never be resolved. +P.race([alwaysPending]).then(neverCalled, neverCalled); + +g_resolve(123); + +drainJobQueue(); + +// Check |resolvedValues| to ensure resolving functions were properly called. +assertEq(resolvedValues.length, 2); +assertEq(resolvedValues[0], alwaysPending); +assertEq(resolvedValues[1], 456); diff --git a/js/src/jit-test/tests/promise/resolve-promise-scripted-and-api.js b/js/src/jit-test/tests/promise/resolve-promise-scripted-and-api.js new file mode 100644 index 0000000000..d69a6d9167 --- /dev/null +++ b/js/src/jit-test/tests/promise/resolve-promise-scripted-and-api.js @@ -0,0 +1,15 @@ +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +resolve(Promise.resolve(0)); + +// Don't assert when the Promise was already resolved. +resolvePromise(promise, 123); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-1.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-1.js new file mode 100644 index 0000000000..940adf660d --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-1.js @@ -0,0 +1,21 @@ +// Test we don't assert when the promise is settled after enqueuing a PromiseReactionJob. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +var p = Promise.resolve(0); + +// Enqueue a PromiseResolveThenableJob followed by a PromiseReactionJob. +resolve(p); + +// The PromiseReactionJob expects a pending promise, but this settlePromiseNow +// call will already have settled the promise. +settlePromiseNow(promise); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-10.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-10.js new file mode 100644 index 0000000000..4d19966cba --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-10.js @@ -0,0 +1,18 @@ +// Don't assert when the promise in the resolving functions is wrapped in a CCW. + +function newPromiseCapability(newTarget) { + var resolve, reject, promise = Reflect.construct(Promise, [function(r1, r2) { + resolve = r1; + reject = r2; + }], newTarget); + return {promise, resolve, reject}; +} + +var g = newGlobal(); + +var {promise, resolve} = newPromiseCapability(g.Promise); + +g.settlePromiseNow(promise); + +// Don't assert when resolving the promise. +resolve(0); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-11.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-11.js new file mode 100644 index 0000000000..e4887b7267 --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-11.js @@ -0,0 +1,18 @@ +// Don't assert when the promise in the resolving functions is wrapped in a CCW. + +function newPromiseCapability(newTarget) { + var resolve, reject, promise = Reflect.construct(Promise, [function(r1, r2) { + resolve = r1; + reject = r2; + }], newTarget); + return {promise, resolve, reject}; +} + +var g = newGlobal(); + +var {promise, reject} = newPromiseCapability(g.Promise); + +g.settlePromiseNow(promise); + +// Don't assert when rejecting the promise. +reject(0); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-2.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-2.js new file mode 100644 index 0000000000..79129003e3 --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-2.js @@ -0,0 +1,27 @@ +// Test we don't assert when the promise is settled and the SpeciesConstructor +// call in Promise.prototype.then throws an exception. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +var p = Promise.resolve(0); + +p.constructor = { + [Symbol.species]: function() { + throw new Error(); + } +}; + +// Enqueue a PromiseResolveThenableJob. +resolve(p); + +// Settle the promise after the resolve call. +settlePromiseNow(promise); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-3.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-3.js new file mode 100644 index 0000000000..591eba456f --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-3.js @@ -0,0 +1,27 @@ +// Test we don't assert when the promise is settled and the SpeciesConstructor +// call in Promise.prototype.then throws an exception. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +var p = Promise.resolve(0); + +p.constructor = { + [Symbol.species]: function() { + // Settle the promise in the SpeciesConstructor call. + settlePromiseNow(promise); + + throw new Error(); + } +}; + +// Enqueue a PromiseResolveThenableJob. +resolve(p); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-4.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-4.js new file mode 100644 index 0000000000..a9d147ccaa --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-4.js @@ -0,0 +1,18 @@ +// Test we don't assert when the promise is settled and we then try to call the +// resolving function. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +settlePromiseNow(promise); + +// Don't assert when the promise is already settled. +resolve(0); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-5.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-5.js new file mode 100644 index 0000000000..e5d2ceaafe --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-5.js @@ -0,0 +1,18 @@ +// Test we don't assert when the promise is settled and we then try to call the +// rejecting function. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, reject} = newPromiseCapability(); + +settlePromiseNow(promise); + +// Don't assert when the promise is already settled. +reject(0); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-6.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-6.js new file mode 100644 index 0000000000..d694f849a6 --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-6.js @@ -0,0 +1,20 @@ +// Don't assert when a side-effect when getting the "then" property settled the promise. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +var thenable = { + get then() { + settlePromiseNow(promise); + } +}; + +resolve(thenable); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-7.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-7.js new file mode 100644 index 0000000000..d6920f9b79 --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-7.js @@ -0,0 +1,23 @@ +// Don't assert when a side-effect when getting the "then" property settled the promise. + +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve} = newPromiseCapability(); + +var thenable = { + get then() { + settlePromiseNow(promise); + + // Throw an error to reject the promise. + throw new Error(); + } +}; + +resolve(thenable); diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-8.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-8.js new file mode 100644 index 0000000000..8b581b7725 --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-8.js @@ -0,0 +1,7 @@ +// Don't assert when settlePromiseNow() is called on an async-function promise. + +var promise = async function(){ await 0; }(); + +try { + settlePromiseNow(promise); +} catch {} diff --git a/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-9.js b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-9.js new file mode 100644 index 0000000000..45413d768c --- /dev/null +++ b/js/src/jit-test/tests/promise/settle-now-breaks-all-invariants-9.js @@ -0,0 +1,15 @@ +function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; +} + + +var {promise, resolve, reject} = newPromiseCapability(); + +settlePromiseNow(promise); + +assertEq(resolve(0), undefined); +assertEq(reject(0), undefined); diff --git a/js/src/jit-test/tests/realms/promise-then.js b/js/src/jit-test/tests/realms/promise-then.js new file mode 100644 index 0000000000..4242239a03 --- /dev/null +++ b/js/src/jit-test/tests/realms/promise-then.js @@ -0,0 +1,30 @@ +load(libdir + "asserts.js"); + +const g = newGlobal({sameCompartmentAs: this}); + +let resolve, reject; +let promise = new Promise((resolveFn, rejectFn) => { + resolve = resolveFn; + reject = rejectFn; +}); + +// Set to a built-in Promise.prototype.then function, but from a different realm. +promise.then = g.Promise.prototype.then; + +// Make SpeciesConstructor throw a TypeError exception. +promise.constructor = { + [Symbol.species]: "not a constructor" +}; + +async function f(p) { + await p; +} + +let error; +f(promise).catch(e => { error = e; }); + +resolve(promise); + +drainJobQueue(); + +assertEq(error.constructor === g.TypeError, true); diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index a12255c636..2430c72846 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -721,8 +721,8 @@ JSRuntime::enqueuePromiseJob(JSContext* cx, HandleFunction job, HandleObject pro if (promise) { RootedObject unwrappedPromise(cx, promise); // While the job object is guaranteed to be unwrapped, the promise - // might be wrapped. See the comments in - // intrinsic_EnqueuePromiseReactionJob for details. + // might be wrapped. See the comments in EnqueuePromiseReactionJob in + // builtin/Promise.cpp for details. if (IsWrapper(promise)) unwrappedPromise = UncheckedUnwrap(promise); if (unwrappedPromise->is())