diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index 8447a97881..57f7cf63a8 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -757,6 +757,7 @@ static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal, HandleValue static MOZ_MUST_USE bool ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resolutionVal) { + assertSameCompartment(cx, promise, resolutionVal); MOZ_ASSERT(!IsSettledMaybeWrappedPromise(promise)); // Step 7 (reordered). @@ -805,19 +806,16 @@ ResolvePromiseInternal(JSContext* cx, HandleObject promise, HandleValue resoluti 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. + // 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() && - resolution->as().compartment() == cx->compartment() && - IsNativeFunction(thenVal, Promise_then) && promise->is() && - promise->as().compartment() == cx->compartment()) + IsNativeFunction(thenVal, Promise_then) && + thenVal.toObject().as().compartment() == cx->compartment()) { thenVal = UndefinedValue(); isBuiltinThen = true; @@ -2030,16 +2028,26 @@ PromiseObject::createSkippingExecutor(JSContext* cx) return CreatePromiseObjectWithoutResolutionFunctions(cx); } +class MOZ_STACK_CLASS PromiseForOfIterator : public JS::ForOfIterator { + public: + using JS::ForOfIterator::ForOfIterator; + + bool isOptimizedDenseArrayIteration() { + MOZ_ASSERT(valueIsIterable()); + return index != NOT_ARRAY && IsPackedArray(iterator); + } +}; + static MOZ_MUST_USE bool -PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, +PerformPromiseAll(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done); static MOZ_MUST_USE bool -PerformPromiseAllSettled(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, +PerformPromiseAllSettled(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done); static MOZ_MUST_USE bool PerformPromiseRace( - JSContext* cx, JS::ForOfIterator& iterator, HandleObject C, + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done); enum class IterationMode { All, AllSettled, Race }; @@ -2087,7 +2095,7 @@ static MOZ_MUST_USE bool CommonStaticAllRace(JSContext* cx, CallArgs& args, return false; // Steps 4-5. - JS::ForOfIterator iter(cx); + PromiseForOfIterator iter(cx); if (!iter.init(iterable, JS::ForOfIterator::AllowNonIterable)) return AbruptRejectPromise(cx, args, promiseCapability); @@ -2332,6 +2340,9 @@ RunResolutionFunction(JSContext *cx, HandleObject resolutionFun, HandleValue res return RejectPromiseInternal(cx, promise, result); } +static MOZ_MUST_USE JSObject* +CommonStaticResolveRejectImpl(JSContext* cx, HandleValue thisVal, HandleValue argVal, + ResolutionMode mode); static bool IsPromiseSpecies(JSContext* cx, JSFunction* species); @@ -2345,7 +2356,7 @@ IsPromiseSpecies(JSContext* cx, JSFunction* species); // Runtime Semantics: PerformPromiseAllSettled, step 6. template static MOZ_MUST_USE bool -CommonPerformPromiseAllRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, +CommonPerformPromiseAllRace(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C, HandleObject resultPromise, bool* done, bool resolveReturnsUndefined, T getResolveAndReject) { @@ -2353,6 +2364,18 @@ CommonPerformPromiseAllRace(JSContext *cx, JS::ForOfIterator& iterator, HandleOb if (!promiseCtor) return false; + // Optimized dense array iteration ensures no side-effects take place + // during the iteration. + bool iterationMayHaveSideEffects = !iterator.isOptimizedDenseArrayIteration(); + + // Try to optimize when the Promise object is in its default state, seeded + // with |C == promiseCtor| because we can only perform this optimization + // for the builtin Promise constructor. + bool isDefaultPromiseState = C == promiseCtor; + bool validatePromiseState = true; + + PromiseLookup& promiseLookup = cx->compartment()->promiseLookup; + RootedValue CVal(cx, ObjectValue(*C)); RootedValue resolveFunVal(cx); RootedValue rejectFunVal(cx); @@ -2381,17 +2404,61 @@ CommonPerformPromiseAllRace(JSContext *cx, JS::ForOfIterator& iterator, HandleOb if (*done) return true; - // 25.6.4.1.1, step 6.i. - // 25.6.4.3.1, step 3.h. - // Sadly, because someone could have overridden - // "resolve" on the canonical Promise constructor. - RootedValue& staticResolve = resolveOrThen; - if (!GetProperty(cx, C, CVal, cx->names().resolve, &staticResolve)) - return false; + // Set to false when we can skip the [[Get]] for "then" and instead + // use the built-in Promise.prototype.then function. + bool getThen = true; + + if (isDefaultPromiseState && validatePromiseState) + isDefaultPromiseState = promiseLookup.isDefaultPromiseState(cx); RootedValue& nextPromise = nextValueOrNextPromise; - if (!Call(cx, staticResolve, CVal, nextValue, &nextPromise)) - return false; + if (isDefaultPromiseState) { + PromiseObject* nextValuePromise = nullptr; + if (nextValue.isObject() && nextValue.toObject().is()) + nextValuePromise = &nextValue.toObject().as(); + + if (nextValuePromise && + promiseLookup.isDefaultInstanceWhenPromiseStateIsSane(cx, nextValuePromise)) + { + // The below steps don't produce any side-effects, so we can + // skip the Promise state revalidation in the next iteration + // when the iterator itself also doesn't produce any + // side-effects. + validatePromiseState = iterationMayHaveSideEffects; + + // 25.6.4.1.1, step 6.i. + // 25.6.4.3.1, step 3.h. + // Promise.resolve is a no-op for the default case. + MOZ_ASSERT(&nextPromise.toObject() == nextValuePromise); + + // `nextPromise` uses the built-in `then` function. + getThen = false; + } else { + // Need to revalidate the Promise state in the next iteration, + // because CommonStaticResolveRejectImpl may have modified it. + validatePromiseState = true; + + // 25.6.4.1.1, step 6.i. + // 25.6.4.3.1, step 3.h. + // Inline the call to Promise.resolve. + JSObject* res = CommonStaticResolveRejectImpl(cx, CVal, nextValue, ResolveMode); + if (!res) + return false; + + nextPromise.setObject(*res); + } + } else { + // 25.6.4.1.1, step 6.i. + // 25.6.4.3.1, step 3.h. + // Sadly, because someone could have overridden + // "resolve" on the canonical Promise constructor. + RootedValue& staticResolve = resolveOrThen; + if (!GetProperty(cx, C, CVal, cx->names().resolve, &staticResolve)) + return false; + + if (!Call(cx, staticResolve, CVal, nextValue, &nextPromise)) + return false; + } // Get the resolving functions for this iteration. // 25.6.4.1.1, steps 6.j-q. @@ -2414,27 +2481,44 @@ CommonPerformPromiseAllRace(JSContext *cx, JS::ForOfIterator& iterator, HandleOb return false; RootedValue& thenVal = resolveOrThen; - if (!GetProperty(cx, nextPromiseObj, nextPromise, cx->names().then, &thenVal)) - return false; + bool isBuiltinThen; + if (getThen) { + // We don't use the Promise lookup cache here, because this code + // is only called when we had a lookup cache miss, so it's likely + // we'd get another cache miss when trying to use the cache here. + if (!GetProperty(cx, nextPromiseObj, nextPromise, cx->names().then, &thenVal)) + return false; + + // |nextPromise| is an unwrapped Promise, and |then| is the + // original |Promise.prototype.then|, inline it here. + isBuiltinThen = nextPromiseObj->is() && + IsNativeFunction(thenVal, Promise_then); + } else { + isBuiltinThen = true; + } // By default, the blocked promise is added as an extra entry to the // rejected promises list. bool addToDependent = true; - if (nextPromiseObj->is() && IsNativeFunction(thenVal, Promise_then)) { - // |nextPromise| is an unwrapped Promise, and |then| is the - // original |Promise.prototype.then|, inline it here. + if (isBuiltinThen) { + MOZ_ASSERT(nextPromise.isObject()); + MOZ_ASSERT(&nextPromise.toObject() == nextPromiseObj); // 25.6.5.4, step 3. RootedObject& thenSpecies = thenSpeciesOrBlockedPromise; - thenSpecies = SpeciesConstructor(cx, nextPromiseObj, JSProto_Promise, - IsPromiseSpecies); - if (!thenSpecies) - return false; + if (getThen) { + thenSpecies = SpeciesConstructor(cx, nextPromiseObj, JSProto_Promise, + IsPromiseSpecies); + if (!thenSpecies) + return false; + } else { + thenSpecies = promiseCtor; + } - // The fast path here and in NewPromiseCapability may not set - // the resolve and reject handlers, so we need to clear the fields - // in case they were set in the previous iteration. + // The fast path here and the one in NewPromiseCapability may not + // set the resolve and reject handlers, so we need to clear the + // fields in case they were set in the previous iteration. thenCapability.resolve().set(nullptr); thenCapability.reject().set(nullptr); @@ -2525,7 +2609,7 @@ CommonPerformPromiseAllRace(JSContext *cx, JS::ForOfIterator& iterator, HandleOb // ES2020 draft rev a09fc232c137800dbf51b6204f37fdede4ba1646 // 25.6.4.1.1 PerformPromiseAll (iteratorRecord, constructor, resultCapability) static MOZ_MUST_USE bool -PerformPromiseAll(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, +PerformPromiseAll(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done) { *done = false; @@ -2738,7 +2822,7 @@ Promise_static_race(JSContext* cx, unsigned argc, Value* vp) // ES2020 draft rev a09fc232c137800dbf51b6204f37fdede4ba1646 // 25.6.4.3.1 PerformPromiseRace (iteratorRecord, constructor, resultCapability) static MOZ_MUST_USE bool -PerformPromiseRace(JSContext *cx, JS::ForOfIterator& iterator, HandleObject C, +PerformPromiseRace(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done) { *done = false; @@ -2793,7 +2877,7 @@ static bool Promise_static_allSettled(JSContext* cx, unsigned argc, Value* vp) { // // PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability ) static MOZ_MUST_USE bool PerformPromiseAllSettled( - JSContext* cx, JS::ForOfIterator& iterator, HandleObject C, + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, Handle resultCapability, bool* done) { *done = false; @@ -3328,6 +3412,45 @@ OriginalPromiseThenWithoutSettleHandlers(JSContext* cx, Handle p return PerformPromiseThenWithoutSettleHandlers(cx, promise, promiseToResolve, resultCapability); } +static bool +CanCallOriginalPromiseThenBuiltin(JSContext* cx, HandleValue promise) +{ + return promise.isObject() && + promise.toObject().is() && + cx->compartment()->promiseLookup.isDefaultInstance(cx, &promise.toObject().as()); +} + +// ES2016, 25.4.5.3., steps 3-5. +static bool +OriginalPromiseThenBuiltin(JSContext* cx, HandleValue promiseVal, HandleValue onFulfilled, + HandleValue onRejected, MutableHandleValue rval, bool rvalUsed) +{ + assertSameCompartment(cx, promiseVal, onFulfilled, onRejected); + MOZ_ASSERT(CanCallOriginalPromiseThenBuiltin(cx, promiseVal)); + + Rooted promise(cx, &promiseVal.toObject().as()); + + // Steps 3-4. + Rooted resultCapability(cx); + if (rvalUsed) { + PromiseObject* resultPromise = CreatePromiseObjectWithoutResolutionFunctions(cx); + if (!resultPromise) + return false; + + resultCapability.promise().set(resultPromise); + } + + // Step 5. + if (!PerformPromiseThen(cx, promise, onFulfilled, onRejected, resultCapability)) + return false; + + if (rvalUsed) + rval.setObject(*resultCapability.promise()); + else + rval.setUndefined(); + return true; +} + static MOZ_MUST_USE bool PerformPromiseThenWithReaction(JSContext* cx, Handle promise, Handle reaction); @@ -3856,21 +3979,25 @@ Promise_catch_impl(JSContext* cx, unsigned argc, Value* vp, bool rvalUsed) { CallArgs args = CallArgsFromVp(argc, vp); - // Step 1. - RootedValue thenVal(cx); - if (!GetProperty(cx, args.thisv(), cx->names().then, &thenVal)) - return false; + HandleValue thisVal = args.thisv(); + HandleValue onFulfilled = UndefinedHandleValue; + HandleValue onRejected = args.get(0); - if (IsNativeFunction(thenVal, &Promise_then)) { - return Promise_then_impl(cx, args.thisv(), UndefinedHandleValue, args.get(0), - args.rval(), rvalUsed); + // Fast path when the default Promise state is intact. + if (CanCallOriginalPromiseThenBuiltin(cx, thisVal)) { + return OriginalPromiseThenBuiltin(cx, thisVal, onFulfilled, onRejected, args.rval(), + rvalUsed); } - FixedInvokeArgs<2> iargs(cx); - iargs[0].setUndefined(); - iargs[1].set(args.get(0)); + // Step 1. + RootedValue thenVal(cx); + if (!GetProperty(cx, thisVal, cx->names().then, &thenVal)) + return false; - return Call(cx, thenVal, args.thisv(), iargs, args.rval()); + if (IsNativeFunction(thenVal, &Promise_then)) + return Promise_then_impl(cx, thisVal, onFulfilled, onRejected, args.rval(), rvalUsed); + + return Call(cx, thenVal, thisVal, UndefinedHandleValue, onRejected, args.rval()); } static MOZ_ALWAYS_INLINE bool @@ -3924,6 +4051,11 @@ Promise_then_impl(JSContext* cx, HandleValue promiseVal, HandleValue onFulfilled "Receiver of Promise.prototype.then call"); return false; } + + // Fast path when the default Promise state is intact. + if (CanCallOriginalPromiseThenBuiltin(cx, promiseVal)) + return OriginalPromiseThenBuiltin(cx, promiseVal, onFulfilled, onRejected, rval, rvalUsed); + RootedObject promiseObj(cx, &promiseVal.toObject()); Rooted promise(cx); @@ -4292,6 +4424,239 @@ PromiseObject::onSettled(JSContext* cx, Handle promise) JS::dbg::onPromiseSettled(cx, promise); } +JSFunction* +js::PromiseLookup::getPromiseConstructor(JSContext* cx) +{ + const Value& val = cx->global()->getConstructor(JSProto_Promise); + return val.isObject() ? &val.toObject().as() : nullptr; +} + +NativeObject* +js::PromiseLookup::getPromisePrototype(JSContext* cx) +{ + const Value& val = cx->global()->getPrototype(JSProto_Promise); + return val.isObject() ? &val.toObject().as() : nullptr; +} + +bool +js::PromiseLookup::isDataPropertyNative(JSContext* cx, NativeObject* obj, uint32_t slot, + JSNative native) +{ + JSFunction* fun; + if (!IsFunctionObject(obj->getSlot(slot), &fun)) + return false; + return fun->maybeNative() == native && fun->compartment() == cx->compartment(); +} + +bool +js::PromiseLookup::isAccessorPropertyNative(JSContext* cx, Shape* shape, JSNative native) +{ + JSObject* getter = shape->getterObject(); + return getter && IsNativeFunction(getter, native) && + getter->as().compartment() == cx->compartment(); +} + +void +js::PromiseLookup::initialize(JSContext* cx) +{ + MOZ_ASSERT(state_ == State::Uninitialized); + + // Get the canonical Promise.prototype. + NativeObject* promiseProto = getPromisePrototype(cx); + + // Check condition 1: + // Leave the cache uninitialized if the Promise class itself is not yet + // initialized. + if (!promiseProto) + return; + + // Get the canonical Promise constructor. + JSFunction* promiseCtor = getPromiseConstructor(cx); + MOZ_ASSERT(promiseCtor, + "The Promise constructor is initialized iff Promise.prototype is initialized"); + + // Shortcut returns below means Promise[@@species] will never be + // optimizable, set to disabled now, and clear it later when we succeed. + state_ = State::Disabled; + + // Check condition 2: + // Look up Promise.prototype.constructor and ensure it's a data property. + Shape* ctorShape = promiseProto->lookup(cx, cx->names().constructor); + if (!ctorShape || !ctorShape->hasSlot()) + return; + + // Get the referred value, and ensure it holds the canonical Promise + // constructor. + JSFunction* ctorFun; + if (!IsFunctionObject(promiseProto->getSlot(ctorShape->slot()), &ctorFun)) + return; + if (ctorFun != promiseCtor) + return; + + // Check condition 3: + // Look up Promise.prototype.then and ensure it's a data property. + Shape* thenShape = promiseProto->lookup(cx, cx->names().then); + if (!thenShape || !thenShape->hasSlot()) + return; + + // Get the referred value, and ensure it holds the canonical "then" + // function. + if (!isDataPropertyNative(cx, promiseProto, thenShape->slot(), Promise_then)) + return; + + // Check condition 4: + // Look up the '@@species' value on Promise. + Shape* speciesShape = promiseCtor->lookup(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols().species)); + if (!speciesShape || !speciesShape->hasGetterObject()) + return; + + // Get the referred value, ensure it holds the canonical Promise[@@species] + // function. + if (!isAccessorPropertyNative(cx, speciesShape, Promise_static_species)) + return; + + // Check condition 5: + // Look up Promise.resolve and ensure it's a data property. + Shape* resolveShape = promiseCtor->lookup(cx, cx->names().resolve); + if (!resolveShape || !resolveShape->hasSlot()) + return; + + // Get the referred value, and ensure it holds the canonical "resolve" + // function. + if (!isDataPropertyNative(cx, promiseCtor, resolveShape->slot(), Promise_static_resolve)) + return; + + // Store raw pointers below. This is okay to do here, because all objects + // are in the tenured heap. + MOZ_ASSERT(!IsInsideNursery(promiseCtor->lastProperty())); + MOZ_ASSERT(!IsInsideNursery(speciesShape)); + MOZ_ASSERT(!IsInsideNursery(promiseProto->lastProperty())); + + state_ = State::Initialized; + promiseConstructorShape_ = promiseCtor->lastProperty(); +#ifdef DEBUG + promiseSpeciesShape_ = speciesShape; +#endif + promiseProtoShape_ = promiseProto->lastProperty(); + promiseResolveSlot_ = resolveShape->slot(); + promiseProtoConstructorSlot_ = ctorShape->slot(); + promiseProtoThenSlot_ = thenShape->slot(); +} + +void +js::PromiseLookup::reset() +{ + JS_POISON(this, 0xBB, sizeof(this)); + state_ = State::Uninitialized; +} + +bool +js::PromiseLookup::isPromiseStateStillSane(JSContext* cx) +{ + MOZ_ASSERT(state_ == State::Initialized); + + NativeObject* promiseProto = getPromisePrototype(cx); + MOZ_ASSERT(promiseProto); + + NativeObject* promiseCtor = getPromiseConstructor(cx); + MOZ_ASSERT(promiseCtor); + + // Ensure that Promise.prototype still has the expected shape. + if (promiseProto->lastProperty() != promiseProtoShape_) + return false; + + // Ensure that Promise still has the expected shape. + if (promiseCtor->lastProperty() != promiseConstructorShape_) + return false; + + // Ensure that Promise.prototype.constructor is the canonical constructor. + if (promiseProto->getSlot(promiseProtoConstructorSlot_) != ObjectValue(*promiseCtor)) + return false; + + // Ensure that Promise.prototype.then is the canonical "then" function. + if (!isDataPropertyNative(cx, promiseProto, promiseProtoThenSlot_, Promise_then)) + return false; + + // Ensure the species getter contains the canonical @@species function. + // Note: This is currently guaranteed to be always true, because modifying + // the getter property implies a new shape is generated. If this ever + // changes, convert this assertion into an if-statement. +#ifdef DEBUG + MOZ_ASSERT(isAccessorPropertyNative(cx, promiseSpeciesShape_, Promise_static_species)); +#endif + + // Ensure that Promise.resolve is the canonical "resolve" function. + if (!isDataPropertyNative(cx, promiseCtor, promiseResolveSlot_, Promise_static_resolve)) + return false; + + return true; +} + +bool +js::PromiseLookup::ensureInitialized(JSContext* cx, Reinitialize reinitialize) +{ + if (state_ == State::Uninitialized) { + // If the cache is not initialized, initialize it. + initialize(cx); + } else if (state_ == State::Initialized) { + if (reinitialize == Reinitialize::Allowed) { + if (!isPromiseStateStillSane(cx)) { + // If the promise state is no longer sane, reinitialize. + reset(); + initialize(cx); + } + } else { + // When we're not allowed to reinitialize, the promise state must + // still be sane if the cache is already initialized. + MOZ_ASSERT(isPromiseStateStillSane(cx)); + } + } + + // If the cache is disabled or still uninitialized, don't bother trying to + // optimize. + if (state_ != State::Initialized) + return false; + + // By the time we get here, we should have a sane promise state. + MOZ_ASSERT(isPromiseStateStillSane(cx)); + + return true; +} + +bool +js::PromiseLookup::isDefaultPromiseState(JSContext* cx) +{ + // Promise and Promise.prototype are in their default states iff the + // lookup cache was successfully initialized. + return ensureInitialized(cx, Reinitialize::Allowed); +} + +bool +js::PromiseLookup::hasDefaultProtoAndNoShadowedProperties(JSContext* cx, PromiseObject* promise) +{ + // Ensure |promise|'s prototype is the actual Promise.prototype. + if (promise->staticPrototype() != getPromisePrototype(cx)) + return false; + + // Ensure |promise| doesn't define any own properties. This serves as a + // quick check to make sure |promise| doesn't define an own "constructor" + // or "then" property which may shadow Promise.prototype.constructor or + // Promise.prototype.then. + return promise->lastProperty()->isEmptyShape(); +} + +bool +js::PromiseLookup::isDefaultInstance(JSContext* cx, PromiseObject* promise, + Reinitialize reinitialize) +{ + // Promise and Promise.prototype must be in their default states. + if (!ensureInitialized(cx, reinitialize)) + return false; + + // The object uses the default properties from Promise.prototype. + return hasDefaultProtoAndNoShadowedProperties(cx, promise); +} + PromiseTask::PromiseTask(JSContext* cx, Handle promise) : runtime_(cx), promise_(cx, promise) diff --git a/js/src/builtin/Promise.h b/js/src/builtin/Promise.h index 1fe9b060ad..3d6dbe0e62 100644 --- a/js/src/builtin/Promise.h +++ b/js/src/builtin/Promise.h @@ -181,6 +181,142 @@ AsyncGeneratorEnqueue(JSContext* cx, HandleValue asyncGenVal, CompletionKind com bool AsyncFromSyncIteratorMethod(JSContext* cx, CallArgs& args, CompletionKind completionKind); +class MOZ_NON_TEMPORARY_CLASS PromiseLookup final +{ + /* + * A PromiseLookup holds the following: + * + * Promise's shape (promiseConstructorShape_) + * To ensure that Promise has not been modified. + * + * Promise.prototype's shape (promiseProtoShape_) + * To ensure that Promise.prototype has not been modified. + * + * Promise's shape for the @@species getter. (promiseSpeciesShape_) + * To quickly retrieve the @@species getter for Promise. + * + * Promise's slot number for resolve (promiseResolveSlot_) + * To quickly retrieve the Promise.resolve function. + * + * Promise.prototype's slot number for constructor (promiseProtoConstructorSlot_) + * To quickly retrieve the Promise.prototype.constructor property. + * + * Promise.prototype's slot number for then (promiseProtoThenSlot_) + * To quickly retrieve the Promise.prototype.then function. + * + * MOZ_INIT_OUTSIDE_CTOR fields below are set in |initialize()|. The + * constructor only initializes a |state_| field, that defines whether the + * other fields are accessible. + */ + + // Shape of matching Promise object. + MOZ_INIT_OUTSIDE_CTOR Shape* promiseConstructorShape_; + +#ifdef DEBUG + // Accessor Shape containing the @@species property. + // See isPromiseStateStillSane() for why this field is debug-only. + MOZ_INIT_OUTSIDE_CTOR Shape* promiseSpeciesShape_; +#endif + + // Shape of matching Promise.prototype object. + MOZ_INIT_OUTSIDE_CTOR Shape* promiseProtoShape_; + + // Slots Promise.resolve, Promise.prototype.constructor, and + // Promise.prototype.then. + MOZ_INIT_OUTSIDE_CTOR uint32_t promiseResolveSlot_; + MOZ_INIT_OUTSIDE_CTOR uint32_t promiseProtoConstructorSlot_; + MOZ_INIT_OUTSIDE_CTOR uint32_t promiseProtoThenSlot_; + + enum class State : uint8_t { + // Flags marking the lazy initialization of the above fields. + Uninitialized, + Initialized, + + // The disabled flag is set when we don't want to try optimizing + // anymore because core objects were changed. + Disabled + }; + + State state_ = State::Uninitialized; + + // Initialize the internal fields. + // + // The cache is successfully initialized iff + // 1. Promise and Promise.prototype classes are initialized. + // 2. Promise.prototype.constructor is equal to Promise. + // 3. Promise.prototype.then is the original `then` function. + // 4. Promise[@@species] is the original @@species getter. + // 5. Promise.resolve is the original `resolve` function. + void initialize(JSContext* cx); + + // Reset the cache. + void reset(); + + // Check if the global promise-related objects have not been messed with + // in a way that would disable this cache. + bool isPromiseStateStillSane(JSContext* cx); + + // Flags to control whether or not ensureInitialized() is allowed to + // reinitialize the cache when the Promise state is no longer sane. + enum class Reinitialize : bool { + Allowed, + Disallowed + }; + + // Return true if the lookup cache is properly initialized for usage. + bool ensureInitialized(JSContext* cx, Reinitialize reinitialize); + + // Return true if the prototype of the given Promise object is + // Promise.prototype and the object doesn't shadow properties from + // Promise.prototype. + bool hasDefaultProtoAndNoShadowedProperties(JSContext* cx, PromiseObject* promise); + + // Return true if the given Promise object uses the default @@species, + // "constructor", and "then" properties. + bool isDefaultInstance(JSContext* cx, PromiseObject* promise, Reinitialize reinitialize); + + // Return the built-in Promise constructor or null if not yet initialized. + static JSFunction* getPromiseConstructor(JSContext* cx); + + // Return the built-in Promise prototype or null if not yet initialized. + static NativeObject* getPromisePrototype(JSContext* cx); + + // Return true if the slot contains the given native. + static bool isDataPropertyNative(JSContext* cx, NativeObject* obj, uint32_t slot, + JSNative native); + + // Return true if the accessor shape contains the given native. + static bool isAccessorPropertyNative(JSContext* cx, Shape* shape, JSNative native); + + public: + /** Construct a |PromiseSpeciesLookup| in the uninitialized state. */ + PromiseLookup() { + reset(); + } + + // Return true if the Promise constructor and Promise.prototype still use + // the default built-in functions. + bool isDefaultPromiseState(JSContext* cx); + + // Return true if the given Promise object uses the default @@species, + // "constructor", and "then" properties. + bool isDefaultInstance(JSContext* cx, PromiseObject* promise) { + return isDefaultInstance(cx, promise, Reinitialize::Allowed); + } + + // Return true if the given Promise object uses the default @@species, + // "constructor", and "then" properties. + bool isDefaultInstanceWhenPromiseStateIsSane(JSContext* cx, PromiseObject* promise) { + return isDefaultInstance(cx, promise, Reinitialize::Disallowed); + } + + // Purge the cache and all info associated with it. + void purge() { + if (state_ == State::Initialized) + reset(); + } +}; + /** * A PromiseTask represents a task that can be dispatched to a helper thread * (via StartPromiseTask), executed (by implementing PromiseTask::execute()), diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index 7cccd25ad6..c93dee510b 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -947,6 +947,7 @@ void JSCompartment::purge() { dtoaCache.purge(); + promiseLookup.purge(); lastCachedNativeIterator = nullptr; } diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h index becf9a39ff..dbada58439 100644 --- a/js/src/jscompartment.h +++ b/js/src/jscompartment.h @@ -881,6 +881,10 @@ struct JSCompartment // Aggregated output used to collect JSScript hit counts when code coverage // is enabled. js::coverage::LCovCompartment lcovOutput; + + public: + // Property lookup table for promises + js::PromiseLookup promiseLookup; }; inline bool