diff --git a/js/public/ExternalCompilerHooks.h b/js/public/ExternalCompilerHooks.h index 741ff49a52f11..795f7a3969ec2 100644 --- a/js/public/ExternalCompilerHooks.h +++ b/js/public/ExternalCompilerHooks.h @@ -37,6 +37,8 @@ namespace js { class AbstractGeneratorObject; class NativeObject; +class ObjectFuse; +class SharedShape; class RegExpShared; class RunState; class VectorMatchPairs; @@ -87,6 +89,17 @@ struct ExternalCompilerHooks { void (*propertyAdded)(JSContext* cx, js::NativeObject* obj, JS::PropertyKey id, uint32_t slot, uint32_t numFixedSlots); + // Slot placement. Before the engine adds a property to a shared-shape + // object whose word is nonzero, it asks here where to put it: set *result + // to a shape from js::ExternalShapeWithPropertyAtSlot, made from the + // object's current shape with this key and flags (the raw PropertyFlags + // byte), to place the property in that shape's slot, or leave it null to + // let the engine append the property at the slot span. Return false only + // with an exception pending. Engine-side caches keyed by (shape, key) are + // not filled with such shapes; the tier memoizes its own. + bool (*shapeForAdd)(JSContext* cx, JS::Handle obj, + JS::HandleId id, uint8_t flags, + js::SharedShape** result); // Global object. A property was defined or deleted on a global, a data // property of a global was written, or a global lexical binding now @@ -96,6 +109,15 @@ struct ExternalCompilerHooks { uint64_t valueBits); void (*globalLexicalShadowAdded)(JSContext* cx, uint64_t idBits); + // Object fuses (vm/ObjectFuse.h). Wherever the engine invalidates the Ion + // code depending on a constant property of an object fuse's object, it + // reports the fuse and the property's slot here, or UINT32_MAX for every + // property (a proto mutation or swap). A tier that marks properties + // constant (ObjectFuse::tryOptimizeConstantProperty) and relies on them + // drops what it assumed. Must not GC. + void (*objectFuseInvalidated)(JSContext* cx, js::ObjectFuse* fuse, + uint32_t propSlot); + // Script entry. Every script carries a pointer-sized external word, zero // at birth; the engine consults these only for scripts whose word is // nonzero. @@ -153,6 +175,26 @@ extern JS_PUBLIC_API void ExternalPropertyAdded(JSContext* cx, NativeObject* obj, JS::PropertyKey id, uint32_t slot); +extern JS_PUBLIC_API bool ExternalShapeForAdd(JSContext* cx, + JS::Handle obj, + JS::HandleId id, uint8_t flags, + SharedShape** result); + +// Custom slot placement (SharedShape::getShapeWithPropertyAtSlot): the shape +// that adds `id` with raw PropertyFlags `flags` to `shape` in slot `slot`, or +// null in *result (no exception) if that placement is not possible. A slot +// other than the span gives the shape ObjectFlag::PermutedSlots, which turns +// off the engine's fast paths that assume slot order is insertion order. +extern JS_PUBLIC_API bool ExternalShapeWithPropertyAtSlot( + JSContext* cx, JS::Handle shape, JS::HandleId id, + uint8_t flags, uint32_t slot, SharedShape** result); + +// Add the property `newShape` adds to `obj`, whose shape it was made from +// (see NativeObject::addPropertyWithShape). The slot, holding undefined, is +// returned in *slot for the caller to initialize. +extern JS_PUBLIC_API bool ExternalAddPropertyWithShape( + JSContext* cx, JS::Handle obj, SharedShape* newShape, + uint32_t* slot); } // namespace js diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index d845de248ae8b..c402dec9823e8 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -878,6 +878,12 @@ void PlainObjectAssignCache::assertValid() const { if (fromPlain->getDenseInitializedLength() > 0 || fromPlain->isIndexed()) { return true; } + + // The fast path copies slot i to slot i and assumes property i is in slot + // i, which permuted slots do not. + if (fromPlain->shape()->hasPermutedSlots()) { + return true; + } MOZ_ASSERT(!fromPlain->getClass()->getNewEnumerate()); MOZ_ASSERT(!fromPlain->getClass()->getEnumerate()); @@ -1030,7 +1036,7 @@ void PlainObjectAssignCache::assertValid() const { // more complicated slot layout (the slot numbers may not match the property // definition order and the slots may contain holes). if (toWasEmpty && hasOnlyEnumerableProps && !fromPlain->inDictionaryMode() && - !toPlain->inDictionaryMode()) { + !toPlain->inDictionaryMode() && !toPlain->shape()->hasPermutedSlots()) { PlainObjectAssignCache& cache = cx->realm()->plainObjectAssignCache; cache.fill(&origToShape->asShared(), fromPlain->sharedShape(), toPlain->sharedShape()); diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index eb04d37ce7ef7..01b96a76d4410 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -8545,6 +8545,75 @@ static bool NumAllocSitesPretenured(JSContext* cx, unsigned argc, Value* vp) { return true; } +#ifdef JS_EXTERNAL_COMPILER_HOOKS +// addPropertyAtSlot(obj, key, slot): add a writable, enumerable, configurable +// data property `key` (value undefined) to plain object `obj` in slot `slot` +// (SharedShape::getShapeWithPropertyAtSlot). Returns whether it was placed. +static bool AddPropertyAtSlot(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 3 || !args[0].isObject() || + !args[0].toObject().is() || !args[2].isInt32() || + args[2].toInt32() < 0) { + JS_ReportErrorASCII(cx, + "addPropertyAtSlot: expected (plain object, key, " + "slot)"); + return false; + } + Rooted obj(cx, &args[0].toObject().as()); + RootedId id(cx); + if (!ToPropertyKey(cx, args[1], &id)) { + return false; + } + if (id.isInt() || obj->inDictionaryMode() || !obj->isExtensible() || + obj->containsPure(id)) { + args.rval().setBoolean(false); + return true; + } + Rooted shape(cx, obj->sharedShape()); + SharedShape* newShape = nullptr; + if (!SharedShape::getShapeWithPropertyAtSlot( + cx, shape, id, PropertyFlags::defaultDataPropFlags, + uint32_t(args[2].toInt32()), &newShape)) { + return false; + } + if (!newShape) { + args.rval().setBoolean(false); + return true; + } + uint32_t slot; + if (!NativeObject::addPropertyWithShape(cx, obj, newShape, &slot)) { + return false; + } + args.rval().setBoolean(true); + return true; +} + +// hasPermutedSlots(obj): whether obj's shape has ObjectFlag::PermutedSlots. +static bool HasPermutedSlots(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "hasPermutedSlots: expected an object"); + return false; + } + args.rval().setBoolean(args[0].toObject().shape()->hasPermutedSlots()); + return true; +} + +// objectSlotSpan(obj): obj's slot span, or -1 for a non-native object. +static bool ObjectSlotSpan(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "objectSlotSpan: expected an object"); + return false; + } + JSObject* obj = &args[0].toObject(); + args.rval().setInt32(obj->is() + ? int32_t(obj->as().slotSpan()) + : -1); + return true; +} +#endif + static bool GetLcovInfo(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); @@ -10986,6 +11055,21 @@ JS_FOR_WASM_FEATURES(WASM_FEATURE) " Return the number of allocation sites that were pretenured for the current\n" " global\n"), +#ifdef JS_EXTERNAL_COMPILER_HOOKS + JS_FN_HELP("addPropertyAtSlot", AddPropertyAtSlot, 3, 0, +"addPropertyAtSlot(obj, key, slot)", +" Add data property key (value undefined) to plain object obj in the given\n" +" slot, not at the slot span. Returns whether it was placed.\n"), + + JS_FN_HELP("hasPermutedSlots", HasPermutedSlots, 1, 0, +"hasPermutedSlots(obj)", +" Return whether obj's slots may not follow property insertion order.\n"), + + JS_FN_HELP("objectSlotSpan", ObjectSlotSpan, 1, 0, +"objectSlotSpan(obj)", +" Return obj's slot span (-1 for a non-native object).\n"), +#endif + JS_FN_HELP("getLcovInfo", GetLcovInfo, 1, 0, "getLcovInfo(global)", " Generate LCOV tracefile for the given compartment. If no global are provided then\n" diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp index 0b81276753995..70a50e6e95f87 100644 --- a/js/src/jit/CacheIR.cpp +++ b/js/src/jit/CacheIR.cpp @@ -5609,6 +5609,11 @@ AttachDecision SetPropIRGenerator::tryAttachAddSlotStub( return AttachDecision::NoAction; } + // Add stubs store only the new property's slot, which is the old span. + if (holder->shape()->hasPermutedSlots()) { + return AttachDecision::NoAction; + } + SharedShape* oldSharedShape = &oldShape->asShared(); // The property must be the last added property of the object. diff --git a/js/src/jit/VMFunctions.cpp b/js/src/jit/VMFunctions.cpp index 4e0563f9c3fff..a3e23d939c016 100644 --- a/js/src/jit/VMFunctions.cpp +++ b/js/src/jit/VMFunctions.cpp @@ -2345,7 +2345,10 @@ static bool TryAddOrSetPlainObjectProperty(JSContext* cx, bool res = AddDataPropertyToPlainObject(cx, obj, keyRoot, value, &resultSlot); if constexpr (UseCache) { + // A transition to permuted slots may skip slots (initialized by the add) + // that a cached replay would leave alone. if (res && obj->shape()->isShared() && + !obj->shape()->hasPermutedSlots() && resultSlot < SharedPropMap::MaxPropsForNonDictionary && !Watchtower::watchesPropertyAdd(obj)) { TaggedSlotOffset offset = obj->getTaggedSlotOffset(resultSlot); diff --git a/js/src/vm/ExternalCompilerHooks.cpp b/js/src/vm/ExternalCompilerHooks.cpp index 6c79aa7df56ce..c20ce8313fdba 100644 --- a/js/src/vm/ExternalCompilerHooks.cpp +++ b/js/src/vm/ExternalCompilerHooks.cpp @@ -10,6 +10,7 @@ #include "vm/JSObject.h" #include "vm/NativeObject.h" #include "vm/Runtime.h" +#include "vm/Shape.h" using namespace js; @@ -90,3 +91,38 @@ JS_PUBLIC_API void js::ExternalPropertyAdded(JSContext* cx, NativeObject* obj, hooks->propertyAdded(cx, obj, id, slot, obj->numFixedSlots()); } } + +JS_PUBLIC_API bool js::ExternalShapeForAdd(JSContext* cx, + JS::Handle obj, + JS::HandleId id, uint8_t flags, + SharedShape** result) { + *result = nullptr; + JS::ExternalCompilerHooks* hooks = cx->externalCompilerHooks(); + if (!hooks || !hooks->shapeForAdd) { + return true; + } + if (!hooks->shapeForAdd(cx, obj, id, flags, result)) { + return false; + } + if (SharedShape* shape = *result) { + // Cheap validation of the tier's answer; addPropertyWithShape checks + // that the shape extends the object's by one property. + PropertyInfoWithKey prop = shape->lastProperty(); + MOZ_RELEASE_ASSERT(prop.key() == id.get()); + MOZ_RELEASE_ASSERT(prop.flags().toRaw() == flags); + } + return true; +} + +JS_PUBLIC_API bool js::ExternalShapeWithPropertyAtSlot( + JSContext* cx, JS::Handle shape, JS::HandleId id, + uint8_t flags, uint32_t slot, SharedShape** result) { + return SharedShape::getShapeWithPropertyAtSlot( + cx, shape, id, PropertyFlags::fromRaw(flags), slot, result); +} + +JS_PUBLIC_API bool js::ExternalAddPropertyWithShape( + JSContext* cx, JS::Handle obj, SharedShape* newShape, + uint32_t* slot) { + return NativeObject::addPropertyWithShape(cx, obj, newShape, slot); +} diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h index 7b1aad337156b..d9207b97bcd52 100644 --- a/js/src/vm/JSContext.h +++ b/js/src/vm/JSContext.h @@ -482,6 +482,11 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, return externalCompilerHooks_; } void* getExternalCompilerState() const { return externalCompilerState_; } + // For a tier's compiled code, which reaches its per-context state from + // the context it is passed. + static constexpr size_t offsetOfExternalCompilerState() { + return offsetof(JSContext, externalCompilerState_); + } void setExternalCompilerState(void* state) { externalCompilerState_ = state; } diff --git a/js/src/vm/NativeObject.h b/js/src/vm/NativeObject.h index 12dbdc35eb22a..1b6b921657297 100644 --- a/js/src/vm/NativeObject.h +++ b/js/src/vm/NativeObject.h @@ -1058,6 +1058,15 @@ class NativeObject : public JSObject { return addProperty(cx, obj, id, flags, slotOut); } +#ifdef JS_EXTERNAL_COMPILER_HOOKS + // Add the property that `newShape` (from + // SharedShape::getShapeWithPropertyAtSlot on the object's shape) adds, in + // that shape's slot, which is returned in *slot holding undefined. Slots + // the new span skips are initialized to undefined. + static bool addPropertyWithShape(JSContext* cx, Handle obj, + SharedShape* newShape, uint32_t* slot); +#endif + static bool addPropertyInReservedSlot(JSContext* cx, Handle obj, HandleId id, uint32_t slot, PropertyFlags flags); diff --git a/js/src/vm/ObjectFlags.h b/js/src/vm/ObjectFlags.h index 9c8951b3557f5..ace5605186324 100644 --- a/js/src/vm/ObjectFlags.h +++ b/js/src/vm/ObjectFlags.h @@ -101,6 +101,16 @@ enum class ObjectFlag : uint32_t { // If set, the object may have an accessor property where the getter or setter // is a non-JSFunction callable object. HasNonFunctionAccessor = 1 << 17, + + // If set, the shape's slot numbers may not follow property insertion order: + // a property was placed in a slot of the embedder's choosing (see + // SharedShape::getShapeWithPropertyAtSlot), so the slot span is the highest + // slot plus one, not the last property's slot plus one, and slots below the + // span may be holes (undefined, owned by no property). Inherited by every + // shape derived from one that has it. Code that assumes slot i holds the + // i-th property, or that the last property holds the top slot, must check + // it. Only set with JS_EXTERNAL_COMPILER_HOOKS. + PermutedSlots = 1 << 18, }; using ObjectFlags = EnumFlags; diff --git a/js/src/vm/ObjectFuse.cpp b/js/src/vm/ObjectFuse.cpp index 1d235675a0fbd..38c0e7be6f769 100644 --- a/js/src/vm/ObjectFuse.cpp +++ b/js/src/vm/ObjectFuse.cpp @@ -9,7 +9,9 @@ #include "gc/Barrier.h" #include "gc/StableCellHasher.h" #include "jit/InvalidationScriptSet.h" +#include "js/ExternalCompilerHooks.h" #include "js/SweepingAPI.h" +#include "vm/JSContext.h" #include "vm/JSScript.h" #include "vm/NativeObject.h" #include "vm/PropertyInfo.h" @@ -50,6 +52,15 @@ bool ObjectFuse::ensurePropertyStateLength(uint32_t length) { return true; } +void ObjectFuse::notifyExternalTier(JSContext* cx, uint32_t propSlot) { +#ifdef JS_EXTERNAL_COMPILER_HOOKS + if (JS::ExternalCompilerHooks* hooks = cx->externalCompilerHooks(); + hooks && hooks->objectFuseInvalidated) { + hooks->objectFuseInvalidated(cx, this, propSlot); + } +#endif +} + bool ObjectFuse::addDependency(uint32_t propSlot, const jit::IonScriptKey& ionScript) { MOZ_ASSERT(getPropertyState(propSlot) == PropertyState::Constant); @@ -66,6 +77,7 @@ bool ObjectFuse::addDependency(uint32_t propSlot, void ObjectFuse::invalidateDependentIonScriptsForProperty(JSContext* cx, PropertyInfo prop, const char* reason) { + notifyExternalTier(cx, prop.slot()); if (auto p = dependencies_.lookup(prop.slot())) { p->value().invalidateAndClear(cx, reason); dependencies_.remove(p); @@ -74,6 +86,7 @@ void ObjectFuse::invalidateDependentIonScriptsForProperty(JSContext* cx, void ObjectFuse::invalidateAllDependentIonScripts(JSContext* cx, const char* reason) { + notifyExternalTier(cx, UINT32_MAX); for (auto r = dependencies_.all(); !r.empty(); r.popFront()) { r.front().value().invalidateAndClear(cx, reason); } diff --git a/js/src/vm/ObjectFuse.h b/js/src/vm/ObjectFuse.h index 40d84d63e49d1..c09e91476b416 100644 --- a/js/src/vm/ObjectFuse.h +++ b/js/src/vm/ObjectFuse.h @@ -131,6 +131,10 @@ class ObjectFuse { PropertyInfo prop, const char* reason); void invalidateAllDependentIonScripts(JSContext* cx, const char* reason); + // Tell an external tier (JS::ExternalCompilerHooks::objectFuseInvalidated) + // what Ion's dependencies are told: the property in |propSlot|, or every + // property (UINT32_MAX), may no longer be assumed constant. + void notifyExternalTier(JSContext* cx, uint32_t propSlot); static constexpr uint32_t propertyStateShift(uint32_t propSlot) { return (propSlot % NumPropsPerWord) * NumBitsPerProp; diff --git a/js/src/vm/PlainObject.cpp b/js/src/vm/PlainObject.cpp index a4d0e7a19bf50..f6766b4670ed5 100644 --- a/js/src/vm/PlainObject.cpp +++ b/js/src/vm/PlainObject.cpp @@ -309,7 +309,9 @@ static PlainObject* NewPlainObjectWithProperties( } } - if (canCache && !obj->inDictionaryMode()) { + // The cache's users store property i in slot i. + if (canCache && !obj->inDictionaryMode() && + !obj->shape()->hasPermutedSlots()) { MOZ_ASSERT(obj->getDenseInitializedLength() == 0); MOZ_ASSERT(obj->slotSpan() == properties.length()); cache.add(obj->sharedShape()); diff --git a/js/src/vm/PropMap.cpp b/js/src/vm/PropMap.cpp index dacb66fbcd000..9a1ab2b366086 100644 --- a/js/src/vm/PropMap.cpp +++ b/js/src/vm/PropMap.cpp @@ -239,15 +239,62 @@ bool SharedPropMap::addChild(JSContext* cx, SharedPropMapAndIndex child, return true; } +// static +uint32_t SharedPropMap::permutedSlotSpan(const JSClass* clasp, + const SharedPropMap* map, + uint32_t mapLength) { + MOZ_ASSERT(clasp->isNativeObject()); + uint32_t span = JSCLASS_RESERVED_SLOTS(clasp); + const SharedPropMap* cur = map; + uint32_t len = mapLength; + while (cur) { + for (uint32_t i = 0; i < len; i++) { + PropertyInfo prop = cur->getPropertyInfo(i); + if (prop.hasSlot()) { + span = std::max(span, prop.slot() + 1); + } + } + if (!cur->hasPrevious()) { + break; + } + cur = cur->asNormal()->previous(); + len = PropMap::Capacity; + } + return span; +} + +// static +bool SharedPropMap::slotIsUnused(const SharedPropMap* map, uint32_t mapLength, + uint32_t slot) { + const SharedPropMap* cur = map; + uint32_t len = mapLength; + while (cur) { + for (uint32_t i = 0; i < len; i++) { + PropertyInfo prop = cur->getPropertyInfo(i); + if (prop.hasSlot() && prop.slot() == slot) { + return false; + } + } + if (!cur->hasPrevious()) { + break; + } + cur = cur->asNormal()->previous(); + len = PropMap::Capacity; + } + return true; +} + // static bool SharedPropMap::addProperty(JSContext* cx, const JSClass* clasp, MutableHandle map, uint32_t* mapLength, HandleId id, - PropertyFlags flags, ObjectFlags* objectFlags, - uint32_t* slot) { + PropertyFlags flags, uint32_t slotSpan, + ObjectFlags* objectFlags, uint32_t* slot) { MOZ_ASSERT(!flags.isCustomDataProperty()); + MOZ_ASSERT_IF(!objectFlags->hasFlag(ObjectFlag::PermutedSlots), + slotSpan == SharedPropMap::slotSpan(clasp, map, *mapLength)); - *slot = SharedPropMap::slotSpan(clasp, map, *mapLength); + *slot = slotSpan; if (MOZ_UNLIKELY(*slot > SHAPE_MAXIMUM_SLOT)) { ReportAllocationOverflow(cx); @@ -261,6 +308,40 @@ bool SharedPropMap::addProperty(JSContext* cx, const JSClass* clasp, return addPropertyInternal(cx, map, mapLength, id, prop); } +// static +bool SharedPropMap::addPropertyAtSlot(JSContext* cx, const JSClass* clasp, + MutableHandle map, + uint32_t* mapLength, HandleId id, + PropertyFlags flags, uint32_t slot, + uint32_t slotSpan, + ObjectFlags* objectFlags) { + MOZ_ASSERT(!flags.isCustomDataProperty()); + MOZ_ASSERT(slot >= JSCLASS_RESERVED_SLOTS(clasp)); + MOZ_ASSERT(slot <= SHAPE_MAXIMUM_SLOT); + MOZ_ASSERT_IF(slot < slotSpan, slotIsUnused(map, *mapLength, slot)); + + // A new entry in a CompactPropMap (the first map of the chain, or its clone) + // must fit its small slot field; SharedPropMap::create uses one for a first + // property at or below this bound. Generic adds after it take the span, so + // with at most Capacity entries no slot there exceeds MaxSlotNumber. + static constexpr uint32_t MaxCompactSlot = + CompactPropertyInfo::MaxSlotNumber - (PropMap::Capacity - 1); + if (map && map->isCompact() && *mapLength < PropMap::Capacity && + slot > MaxCompactSlot) { + map.set(nullptr); + return true; + } + + *objectFlags = + GetObjectFlagsForNewProperty(clasp, *objectFlags, id, flags, cx); + if (slot != slotSpan) { + objectFlags->setFlag(ObjectFlag::PermutedSlots); + } + + PropertyInfo prop = PropertyInfo(flags, slot); + return addPropertyInternal(cx, map, mapLength, id, prop); +} + // static bool SharedPropMap::addPropertyInReservedSlot( JSContext* cx, const JSClass* clasp, MutableHandle map, @@ -292,7 +373,8 @@ bool SharedPropMap::addPropertyWithKnownSlot(JSContext* cx, objectFlags); } - MOZ_ASSERT(slot == SharedPropMap::slotSpan(clasp, map, *mapLength)); + MOZ_ASSERT_IF(!objectFlags->hasFlag(ObjectFlag::PermutedSlots), + slot == SharedPropMap::slotSpan(clasp, map, *mapLength)); MOZ_RELEASE_ASSERT(slot <= SHAPE_MAXIMUM_SLOT); *objectFlags = @@ -1316,7 +1398,8 @@ void PropMap::checkConsistency(NativeObject* obj) const { if (prop.hasSlot()) { MOZ_ASSERT_IF((curMap != this || index < mapLength), prop.slot() < obj->slotSpan()); - MOZ_ASSERT_IF(nextSlot.isSome(), *nextSlot >= prop.slot()); + MOZ_ASSERT_IF(nextSlot.isSome() && !obj->shape()->hasPermutedSlots(), + *nextSlot >= prop.slot()); nextSlot = mozilla::Some(prop.slot()); } diff --git a/js/src/vm/PropMap.h b/js/src/vm/PropMap.h index 0dcf416f32ccc..689315f9f675c 100644 --- a/js/src/vm/PropMap.h +++ b/js/src/vm/PropMap.h @@ -690,6 +690,18 @@ class SharedPropMap : public PropMap { return std::max(lastSlot + 1, numReserved); } + // The slot span of a map whose slots may not follow insertion order + // (ObjectFlag::PermutedSlots): the highest slot plus one. Linear in the + // number of properties. + static uint32_t permutedSlotSpan(const JSClass* clasp, + const SharedPropMap* map, + uint32_t mapLength); + + // Whether no property of map/mapLength has slot `slot`. Linear in the number + // of properties. + static bool slotIsUnused(const SharedPropMap* map, uint32_t mapLength, + uint32_t slot); + static uint32_t indexOfNextProperty(uint32_t index) { MOZ_ASSERT(index < PropMap::Capacity); return (index + 1) % PropMap::Capacity; @@ -697,10 +709,26 @@ class SharedPropMap : public PropMap { // Add a new property to this map. Returns the new map/mapLength, slot number, // and object flags. + // + // The new property takes slot `slotSpan`, the span of the shape being + // extended (the map's last slot plus one, unless the shape has + // ObjectFlag::PermutedSlots). static bool addProperty(JSContext* cx, const JSClass* clasp, MutableHandle map, uint32_t* mapLength, HandleId id, PropertyFlags flags, - ObjectFlags* objectFlags, uint32_t* slot); + uint32_t slotSpan, ObjectFlags* objectFlags, + uint32_t* slot); + + // Like addProperty, but in a slot of the caller's choosing, which no + // property of the map may use. A slot other than the span sets + // ObjectFlag::PermutedSlots. Returns true with a null map, and no exception + // pending, if the map cannot hold the slot (a CompactPropMap holds small + // slot numbers only). + static bool addPropertyAtSlot(JSContext* cx, const JSClass* clasp, + MutableHandle map, + uint32_t* mapLength, HandleId id, + PropertyFlags flags, uint32_t slot, + uint32_t slotSpan, ObjectFlags* objectFlags); // Like addProperty, but for when the slot number is a reserved slot. A few // builtin objects use this for initial properties. diff --git a/js/src/vm/Shape.cpp b/js/src/vm/Shape.cpp index 4512f7509e623..9e19b273a99c2 100644 --- a/js/src/vm/Shape.cpp +++ b/js/src/vm/Shape.cpp @@ -344,6 +344,19 @@ bool NativeObject::addProperty(JSContext* cx, Handle obj, return false; } +#ifdef JS_EXTERNAL_COMPILER_HOOKS + // The external tier may place the property in a slot of its choosing. + if (obj->externalWord() && !obj->inDictionaryMode()) { + SharedShape* chosen = nullptr; + if (!ExternalShapeForAdd(cx, obj, id, flags.toRaw(), &chosen)) { + return false; + } + if (chosen) { + return addPropertyWithShape(cx, obj, chosen, slot); + } + } +#endif + if (auto* shape = LookupShapeForAdd(obj->shape(), id, flags, slot)) { if (!obj->setShapeAndAddNewSlot(cx, shape, *slot)) { return false; @@ -387,7 +400,8 @@ bool NativeObject::addProperty(JSContext* cx, Handle obj, uint32_t mapLength = obj->shape()->propMapLength(); if (!SharedPropMap::addProperty(cx, clasp, &map, &mapLength, id, flags, - &objectFlags, slot)) { + obj->sharedShape()->slotSpan(), &objectFlags, + slot)) { return false; } @@ -441,6 +455,98 @@ bool NativeObject::addProperty(JSContext* cx, Handle obj, return true; } +#ifdef JS_EXTERNAL_COMPILER_HOOKS +/* static */ +bool SharedShape::getShapeWithPropertyAtSlot(JSContext* cx, + Handle shape, + HandleId id, PropertyFlags flags, + uint32_t slot, + SharedShape** result) { + *result = nullptr; + + const JSClass* clasp = shape->getObjectClass(); + MOZ_ASSERT(!flags.isCustomDataProperty()); + if (slot < JSCLASS_RESERVED_SLOTS(clasp) || slot > SHAPE_MAXIMUM_SLOT || + flags.isCustomDataProperty()) { + return true; + } + + uint32_t index; + if (shape->lookupPure(id, &index)) { + return true; + } + + Rooted map(cx, shape->propMap()); + uint32_t mapLength = shape->propMapLength(); + uint32_t span = shape->slotSpan(); + if (slot < span && !SharedPropMap::slotIsUnused(map, mapLength, slot)) { + return true; + } + + ObjectFlags objectFlags = shape->objectFlags(); + if (!SharedPropMap::addPropertyAtSlot(cx, clasp, &map, &mapLength, id, flags, + slot, span, &objectFlags)) { + return false; + } + if (!map) { + return true; + } + + Rooted base(cx, shape->base()); + SharedShape* newShape = SharedShape::getPropMapShape( + cx, base, shape->numFixedSlots(), map, mapLength, objectFlags); + if (!newShape) { + return false; + } + MOZ_ASSERT(newShape->slotSpan() == std::max(span, slot + 1)); + *result = newShape; + return true; +} + +/* static */ +bool NativeObject::addPropertyWithShape(JSContext* cx, + Handle obj, + SharedShape* newShape, uint32_t* slot) { + MOZ_ASSERT(!obj->inDictionaryMode()); + + // The shape must add exactly one property to the object's shape; the + // external tier made it with getShapeWithPropertyAtSlot from that shape. + SharedShape* oldShape = obj->sharedShape(); + MOZ_RELEASE_ASSERT(newShape->base() == oldShape->base()); + MOZ_RELEASE_ASSERT(newShape->numFixedSlots() == oldShape->numFixedSlots()); + MOZ_RELEASE_ASSERT(newShape->propMapLength() == + (oldShape->propMapLength() == PropMap::Capacity + ? 1 + : oldShape->propMapLength() + 1)); + PropertyInfoWithKey prop = newShape->lastProperty(); + MOZ_RELEASE_ASSERT(prop.hasSlot()); +#ifdef DEBUG + uint32_t index; + MOZ_ASSERT(!oldShape->lookupPure(prop.key(), &index)); +#endif + + uint32_t oldSpan = oldShape->slotSpan(); + uint32_t newSpan = newShape->slotSpan(); + *slot = prop.slot(); + if (newSpan > oldSpan) { + // Skipped slots below the new property's become holes: undefined, as + // the GC traces the whole span. + if (!obj->setShapeAndAddNewSlots(cx, newShape, oldSpan, newSpan)) { + return false; + } + } else { + // A hole below the span: already allocated and holding undefined. + MOZ_RELEASE_ASSERT(newSpan == oldSpan && *slot < oldSpan); + obj->setShape(newShape); + } + + if (obj->externalWord()) { + ExternalPropertyAdded(cx, obj, prop.key(), *slot); + } + return true; +} +#endif + void Shape::maybeCacheIterator(JSContext* cx, PropertyIteratorObject* iter) { if (!cache().isNone() && !cache().isIterator()) { // If we're already caching other shape data, skip caching the iterator. @@ -599,7 +705,12 @@ bool NativeObject::changeProperty(JSContext* cx, Handle obj, bool isLast = propMap == map && propIndex == mapLength - 1; bool nonLastCustomProperty = oldProp.isCustomDataProperty() && !isLast; - if (map->isShared() && !nonLastCustomProperty && hasReasonableGap) { + // A custom data property getting a slot would take the span of the map + // before it, which with permuted slots may be in use. + bool permutedCustomProperty = + oldProp.isCustomDataProperty() && obj->shape()->hasPermutedSlots(); + if (map->isShared() && !nonLastCustomProperty && !permutedCustomProperty && + hasReasonableGap) { // To change a property, we get the previous propmap and then call // addProperty to re-add the changed property with the new flags. If it // is not the last property, we have to re-add all the following @@ -620,8 +731,11 @@ bool NativeObject::changeProperty(JSContext* cx, Handle obj, return false; } } else { + uint32_t span = + SharedPropMap::slotSpan(clasp, resultMap, resultMapLength); if (!SharedPropMap::addProperty(cx, clasp, &resultMap, &resultMapLength, - id, flags, &objectFlags, slotOut)) { + id, flags, span, &objectFlags, + slotOut)) { return false; } } @@ -921,8 +1035,11 @@ bool NativeObject::removeProperty(JSContext* cx, Handle obj, // 4) We add a new property P, reuse Shape S1, and mark P Constant. // 5) We use the SetSlot IC stub again but this is invalid because P is // still marked Constant. + // + // With permuted slots the last property need not hold the top slot; such + // objects take the dictionary path below. if (propMap == map && propIndex == mapLength - 1 && - !wasTrackedObjectFuseProp) { + !wasTrackedObjectFuseProp && !obj->shape()->hasPermutedSlots()) { MOZ_ASSERT(obj->getLastProperty().key() == id); Rooted sharedMap(cx, map->asShared()); diff --git a/js/src/vm/Shape.h b/js/src/vm/Shape.h index 4ddbea5da1d02..9a6730dfcec44 100644 --- a/js/src/vm/Shape.h +++ b/js/src/vm/Shape.h @@ -358,6 +358,10 @@ class Shape : public gc::CellWithTenuredGCPointer { SMALL_SLOTSPAN_MAX = 0x3ff, // 10 bits. SMALL_SLOTSPAN_SHIFT = 11, SMALL_SLOTSPAN_MASK = uint32_t(SMALL_SLOTSPAN_MAX << SMALL_SLOTSPAN_SHIFT), + + // For NativeShape: mirrors ObjectFlag::PermutedSlots, so that code reading + // the slot span from this word can test both at once. + PERMUTED_SLOTS_BIT = 1 << 21, }; uint32_t immutableFlags; // Immutable flags, see above. @@ -401,6 +405,15 @@ class Shape : public gc::CellWithTenuredGCPointer { return objectFlags_.hasFlag(flag); } + // See ObjectFlag::PermutedSlots. + bool hasPermutedSlots() const { +#ifdef JS_EXTERNAL_COMPILER_HOOKS + return objectFlags_.hasFlag(ObjectFlag::PermutedSlots); +#else + return false; +#endif + } + protected: Shape(Kind kind, BaseShape* base, ObjectFlags objectFlags) : CellWithTenuredGCPointer(base), @@ -495,6 +508,9 @@ class NativeShape : public Shape { MOZ_ASSERT(base->clasp()->isNativeObject()); MOZ_ASSERT(mapLength <= PropMap::Capacity); immutableFlags |= (nfixed << FIXED_SLOTS_SHIFT) | mapLength; + if (hasPermutedSlots()) { + immutableFlags |= PERMUTED_SLOTS_BIT; + } } public: @@ -520,6 +536,11 @@ class NativeShape : public Shape { // For JIT usage. static constexpr uint32_t fixedSlotsMask() { return FIXED_SLOTS_MASK; } static constexpr uint32_t fixedSlotsShift() { return FIXED_SLOTS_SHIFT; } + static constexpr uint32_t smallSlotSpanMask() { return SMALL_SLOTSPAN_MASK; } + static constexpr uint32_t smallSlotSpanShift() { + return SMALL_SLOTSPAN_SHIFT; + } + static constexpr uint32_t permutedSlotsBit() { return PERMUTED_SLOTS_BIT; } }; // Shared shape for a NativeObject. @@ -572,6 +593,9 @@ class SharedShape : public NativeShape { uint32_t slotSpanSlow() const { MOZ_ASSERT(isShared()); const JSClass* clasp = getObjectClass(); + if (MOZ_UNLIKELY(hasPermutedSlots())) { + return SharedPropMap::permutedSlotSpan(clasp, propMap(), propMapLength()); + } return SharedPropMap::slotSpan(clasp, propMap(), propMapLength()); } uint32_t slotSpan() const { @@ -585,6 +609,21 @@ class SharedShape : public NativeShape { return slotSpanSlow(); } +#ifdef JS_EXTERNAL_COMPILER_HOOKS + // The shape that adds property `id` with `flags` to `shape` in slot `slot` + // instead of at the slot span: for an external tier that manages object + // layouts itself. `slot` must not be a reserved slot or in use in `shape`; + // slots between the span and `slot` become holes. A slot other than the + // span sets ObjectFlag::PermutedSlots on the result. Returns true with a + // null *result, and no exception, when the placement is not possible (the + // slot is in use, `id` is already present, or the property map cannot hold + // the slot number). + static bool getShapeWithPropertyAtSlot(JSContext* cx, + Handle shape, + HandleId id, PropertyFlags flags, + uint32_t slot, SharedShape** result); +#endif + /* * Lookup an initial shape matching the given parameters, creating an empty * shape if none was found.