From aa2c4db6f209d71673a9281d86a5970438de3319 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Tue, 29 Sep 2026 22:25:56 -0700 Subject: [PATCH 1/3] Custom-slot shapes for external tiers that manage object layouts. An external compilation tier may wish to place slots in objects in certain ways to improve commonality or predictability of these slots, especially when they are derived from static analysis. This PR adds the necessary hooks to: - Permit creation of shapes that "permute" slots: assign slots in a different order than the walk up the shape tree. Places in the runtime that assume shape-walk order is the same as property order are modified. - Specify the shape to use when a property is added, overriding the default lookup or lazy shape creation. To support "permuted shapes", we add a new flag, `PermutedSlots`, and we track the shape *span* (max slot + 1) separately from the index of the bottommost shape. Both GC scans and new property additions in generic (not-hooked) paths need this span to choose the next slot to use. With NightMonkey's use of these hooks, all jit-tests (including tests that iterate over properties in added order) continue to pass; the order of shapes up the shape tree still represents the semantically-visible order in which properties were added. It is just the order of assigned slots that can differ. All changes are ifdef'd under JS_EXTERNAL_COMPILER_HOOKS; without it hasPermutedSlots() is false and the engine is unchanged. --- js/public/ExternalCompilerHooks.h | 32 +++++++ js/src/builtin/Object.cpp | 8 +- js/src/builtin/TestingFunctions.cpp | 84 +++++++++++++++++++ js/src/jit/CacheIR.cpp | 5 ++ js/src/jit/VMFunctions.cpp | 3 + js/src/vm/ExternalCompilerHooks.cpp | 36 ++++++++ js/src/vm/NativeObject.h | 9 ++ js/src/vm/ObjectFlags.h | 10 +++ js/src/vm/PlainObject.cpp | 4 +- js/src/vm/PropMap.cpp | 93 +++++++++++++++++++-- js/src/vm/PropMap.h | 30 ++++++- js/src/vm/Shape.cpp | 125 +++++++++++++++++++++++++++- js/src/vm/Shape.h | 39 +++++++++ 13 files changed, 466 insertions(+), 12 deletions(-) diff --git a/js/public/ExternalCompilerHooks.h b/js/public/ExternalCompilerHooks.h index 741ff49a52f11..e638b65d447a2 100644 --- a/js/public/ExternalCompilerHooks.h +++ b/js/public/ExternalCompilerHooks.h @@ -37,6 +37,7 @@ namespace js { class AbstractGeneratorObject; class NativeObject; +class SharedShape; class RegExpShared; class RunState; class VectorMatchPairs; @@ -87,6 +88,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 @@ -153,6 +165,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/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/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. From 175bba52f2ae1d251028d7acdd6a920827e79a2c Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sat, 3 Oct 2026 12:27:40 -0700 Subject: [PATCH 2/3] Report object-fuse invalidations to external tiers. An external tier might wish to specialize on a constant value for a global. Previously, there was no way for the external compiler to subscribe to / have access to a fuse associated with a global's value. This PR adds the necessary hook. --- js/public/ExternalCompilerHooks.h | 10 ++++++++++ js/src/vm/ObjectFuse.cpp | 13 +++++++++++++ js/src/vm/ObjectFuse.h | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/js/public/ExternalCompilerHooks.h b/js/public/ExternalCompilerHooks.h index e638b65d447a2..795f7a3969ec2 100644 --- a/js/public/ExternalCompilerHooks.h +++ b/js/public/ExternalCompilerHooks.h @@ -37,6 +37,7 @@ namespace js { class AbstractGeneratorObject; class NativeObject; +class ObjectFuse; class SharedShape; class RegExpShared; class RunState; @@ -108,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. 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; From 08702e6f7b6c6d9a2ebf9c0cfc37ccebdd395706 Mon Sep 17 00:00:00 2001 From: Chris Fallin Date: Sun, 4 Oct 2026 01:35:02 -0700 Subject: [PATCH 3/3] Expose JSContext::offsetOfExternalCompilerState for external tiers' compiled code. --- js/src/vm/JSContext.h | 5 +++++ 1 file changed, 5 insertions(+) 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; }