Bug 1494537: Add CacheIR stub for out-of-capacity-bounds assignments to arrays. r=tcampbell
☠☠ backed out by b8601c0c0614 ☠ ☠
authorKannan Vijayan <kvijayan@mozilla.com>
Sun, 14 Oct 2018 16:33:09 -0400
changeset 489494 6213dd2a20f2c544722ff18d62863cbf6e031b8e
parent 489493 4afcdb551984b452e41847bc2a2690f46bdbbefe
child 489495 00351b000d8835beb11192c939f3953699799940
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerstcampbell
bugs1494537
milestone64.0a1
Bug 1494537: Add CacheIR stub for out-of-capacity-bounds assignments to arrays. r=tcampbell
js/src/builtin/Array.cpp
js/src/builtin/Array.h
js/src/jit-test/tests/cacheir/bug1494537.js
js/src/jit/BaselineCacheIRCompiler.cpp
js/src/jit/CacheIR.cpp
js/src/jit/CacheIR.h
js/src/jit/CacheIRCompiler.cpp
js/src/jit/CacheIRCompiler.h
js/src/jit/IonCacheIRCompiler.cpp
js/src/jit/VMFunctions.cpp
js/src/jit/VMFunctions.h
js/src/vm/NativeObject.cpp
js/src/vm/NativeObject.h
js/src/vm/Shape.h
js/src/vm/ShapedObject.h
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -407,19 +407,16 @@ js::GetElementsWithAdder(JSContext* cx, 
         if (!adder->append(cx, val)) {
             return false;
         }
     }
 
     return true;
 }
 
-static bool
-ObjectMayHaveExtraIndexedProperties(JSObject* obj);
-
 static inline bool
 IsPackedArrayOrNoExtraIndexedProperties(JSObject* obj, uint64_t length)
 {
     return (IsPackedArray(obj) && obj->as<ArrayObject>().length() == length) ||
            !ObjectMayHaveExtraIndexedProperties(obj);
 }
 
 static bool
@@ -1045,18 +1042,18 @@ ObjectMayHaveExtraIndexedOwnProperties(J
                              obj->getClass(), INT_TO_JSID(0), obj);
 }
 
 /*
  * Whether obj may have indexed properties anywhere besides its dense
  * elements. This includes other indexed properties in its shape hierarchy, and
  * indexed properties or elements along its prototype chain.
  */
-static bool
-ObjectMayHaveExtraIndexedProperties(JSObject* obj)
+bool
+js::ObjectMayHaveExtraIndexedProperties(JSObject* obj)
 {
     MOZ_ASSERT_IF(obj->hasDynamicPrototype(), !obj->isNative());
 
     if (ObjectMayHaveExtraIndexedOwnProperties(obj)) {
         return true;
     }
 
     do {
--- a/js/src/builtin/Array.h
+++ b/js/src/builtin/Array.h
@@ -191,16 +191,19 @@ ArrayConstructor(JSContext* cx, unsigned
 
 // Like Array constructor, but doesn't perform GetPrototypeFromConstructor.
 extern bool
 array_construct(JSContext* cx, unsigned argc, Value* vp);
 
 extern bool
 IsCrossRealmArrayConstructor(JSContext* cx, const Value& v, bool* result);
 
+extern bool
+ObjectMayHaveExtraIndexedProperties(JSObject* obj);
+
 class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final
 {
     /*
      * An ArraySpeciesLookup holds the following:
      *
      *  Array.prototype (arrayProto_)
      *      To ensure that the incoming array has the standard proto.
      *
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/cacheir/bug1494537.js
@@ -0,0 +1,125 @@
+setJitCompilerOption("ion.forceinlineCaches", 1);
+
+let offsets = [213, 559, 255, 515, 30, 507, 252, 329, 487, 7];
+
+function update_index(i, j) {
+    var offset = offsets[j % offsets.length];
+    return i + offset;
+}
+
+function compute_index(initial, count) {
+    for (var i = 0; i < count; i++) {
+        initial = update_index(initial, i);
+    }
+    return initial;
+}
+
+// This is written so that the IC added in the bug activates.
+function mutate_array(array, count, epsilon = 0) {
+    var index = 0;
+    for (var i = 0; i < count; i++) {
+        index = update_index(index, i);
+        array[index] = i + epsilon;
+    }
+    return array[offsets[0]+offsets[1]] === (1 + epsilon) &&
+           array[10] === undefined;
+}
+
+// Monomorphizing mutate_array to ensure we get the IC chains we want
+function create_variant(variant) {
+    var source = mutate_array.toString().replace("mutate_array", "mutate_array_"+variant);
+    return source;
+}
+
+function test_basic() {
+    eval(create_variant("basic"));
+    var x = [];
+
+    var count = 100;
+    assertEq(mutate_array_basic(x, count), true);
+    var end = compute_index(0, count);
+    assertEq(x[end], count - 1);
+    assertEq(x[end - 1], undefined);
+}
+
+// Ensure the IC respects frozen.
+function test_frozen() {
+    eval(create_variant("frozen"));
+    var x = [];
+    Object.freeze(x);
+
+    var count = 100;
+    assertEq(mutate_array_frozen(x, count), false);
+    assertEq(x.length, 0);
+
+    var end = compute_index(0, count);
+
+    var y = [];
+    assertEq(mutate_array_frozen(y, count), true);
+    assertEq(y[end], count - 1);
+    Object.freeze(y);
+
+    // After a mutated array is frozen, can't subsequently modify elements
+    assertEq(mutate_array_frozen(x, count, 10), false);
+    assertEq(y[end], count - 1);
+}
+
+// Let's make sure updates to the array happen as expecte.d
+function test_update() {
+    eval(create_variant("update"));
+
+    var x = [];
+    var count = 100;
+    assertEq(mutate_array_update(x, count), true);
+    var end = compute_index(0, count);
+    assertEq(x[end], count - 1);
+    assertEq(x[end - 1], undefined);
+
+    var epsilon = 2;
+    mutate_array_update(x, 200, epsilon);
+    assertEq(x[end], count -1 + epsilon)
+}
+
+// Elements may be non-writable, let us not write them.
+function test_nonwritable() {
+    eval(create_variant("nonwritable"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 10);
+    Object.defineProperty(x, index, {value: -10, writable: false});
+    mutate_array_nonwritable(x, count);
+    assertEq(x[index], -10);
+}
+
+// Random indices can get setters, let's make sure we honour those.
+function test_setter() {
+    eval(create_variant("setter"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 80);
+    var sigil = 0;
+    Object.defineProperty(x, index, {set(newVal) {sigil++; }});
+    mutate_array_setter(x, count);
+    assertEq(sigil, 1);
+    assertEq(x[index], undefined);
+}
+
+// Ensure indexes on the prototype don't break things;
+//
+function test_proto_indices() {
+    eval(create_variant("proto_indices"));
+    var x = [];
+    var count = 100;
+    var index = compute_index(0, 80);
+    x.__proto__[index] = "hello";
+    mutate_array_proto_indices(x, count);
+    assertEq(x.__proto__[index], "hello");
+    assertEq(x[index], 79);
+}
+
+test_basic();
+test_frozen();
+test_update();
+test_nonwritable();
+test_setter();
+test_proto_indices();
\ No newline at end of file
--- a/js/src/jit/BaselineCacheIRCompiler.cpp
+++ b/js/src/jit/BaselineCacheIRCompiler.cpp
@@ -1875,16 +1875,43 @@ BaselineCacheIRCompiler::emitCallProxySe
         return false;
     }
 
     stubFrame.leave(masm);
     return true;
 }
 
 bool
+BaselineCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register id = allocator.useRegister(masm, reader.int32OperandId());
+    ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
+    bool strict = reader.readBool();
+    AutoScratchRegister scratch(allocator, masm);
+
+    allocator.discardStack(masm);
+
+    AutoStubFrame stubFrame(*this);
+    stubFrame.enter(masm, scratch);
+
+    masm.Push(Imm32(strict));
+    masm.Push(val);
+    masm.Push(id);
+    masm.Push(obj);
+
+    if (!callVM(masm, AddOrUpdateSparseElementHelperInfo)) {
+        return false;
+    }
+    stubFrame.leave(masm);
+    return true;
+}
+
+
+bool
 BaselineCacheIRCompiler::emitMegamorphicSetElement()
 {
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     ValueOperand idVal = allocator.useValueRegister(masm, reader.valOperandId());
     ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
     bool strict = reader.readBool();
 
     allocator.discardStack(masm);
--- a/js/src/jit/CacheIR.cpp
+++ b/js/src/jit/CacheIR.cpp
@@ -3444,16 +3444,19 @@ SetPropIRGenerator::tryAttachStub()
                 return true;
             }
             if (tryAttachSetDenseElementHole(obj, objId, index, indexId, rhsValId)) {
                 return true;
             }
             if (tryAttachSetTypedElement(obj, objId, index, indexId, rhsValId)) {
                 return true;
             }
+            if (tryAttachAddOrUpdateSparseElement(obj, objId, index, indexId, rhsValId)) {
+                return true;
+            }
             return false;
         }
         return false;
     }
     return false;
 }
 
 static void
@@ -4060,16 +4063,99 @@ SetPropIRGenerator::tryAttachSetDenseEle
 
     // Type inference uses JSID_VOID for the element types.
     typeCheckInfo_.set(nobj->group(), JSID_VOID);
 
     trackAttached(isAdd ? "AddDenseElement" : "StoreDenseElementHole");
     return true;
 }
 
+// Add an IC for adding or updating a sparse array element.
+bool
+SetPropIRGenerator::tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId,
+                                                      uint32_t index, Int32OperandId indexId,
+                                                      ValOperandId rhsId)
+{
+    JSOp op = JSOp(*pc_);
+    MOZ_ASSERT(IsPropertySetOp(op) || IsPropertyInitOp(op));
+
+    if (op != JSOP_SETELEM && op != JSOP_STRICTSETELEM) {
+        return false;
+    }
+
+    if (!obj->isNative()) {
+        return false;
+    }
+    NativeObject& nobj = obj->as<NativeObject>();
+
+    // We cannot attach a stub to a non-extensible object
+    if (!nobj.isExtensible()) {
+        return false;
+    }
+
+    // Stub doesn't handle negative indices.
+    if (index > INT_MAX) {
+        return false;
+    }
+
+    // We also need to be past the end of the dense capacity, to ensure sparse.
+    if (index < nobj.getDenseCapacity()) {
+        return false;
+    }
+
+    // Only handle Array objects in this stub.
+    if (!nobj.is<ArrayObject>()) {
+        return false;
+    }
+    RootedArrayObject aobj(cx_, &obj->as<ArrayObject>());
+
+    // Don't attach if we're adding to an array with non-writable length.
+    bool isAdd = (index >= aobj->length());
+    if (isAdd && !aobj->lengthIsWritable()) {
+        return false;
+    }
+
+    // Indexed properties on the prototype chain aren't handled by the helper.
+    if (ObjectMayHaveExtraIndexedProperties(aobj->staticPrototype())) {
+        return false;
+    }
+
+    // Ensure we are still talking about an array class.
+    writer.guardClass(objId, GuardClassKind::Array);
+
+    // The helper we are going to call only applies to non-dense elements.
+    writer.guardIndexGreaterThanDenseCapacity(objId, indexId);
+
+    // Guard extensible: We may be trying to add a new element, and so we'd best
+    // be able to do so safely.
+    writer.guardIsExtensible(objId);
+
+    // Ensures we are able to efficiently able to map to an integral jsid.
+    writer.guardIndexIsNonNegative(indexId);
+
+    // Shape guard the prototype chain to avoid shadowing indexes from appearing.
+    // Dense elements may appear on the prototype chain (and prototypes may
+    // have a different notion of which elements are dense), but they can
+    // only be data properties, so our specialized Set handler is ok to bind
+    // to them.
+    ShapeGuardProtoChain(writer, obj, objId);
+
+    // Ensure that if we're adding an element to the object, the object's
+    // length is writable.
+    writer.guardIndexIsValidUpdateOrAdd(objId, indexId);
+
+    writer.callAddOrUpdateSparseElementHelper(objId, indexId, rhsId,
+                                              /* strict = */op == JSOP_STRICTSETELEM);
+    writer.returnFromIC();
+
+    trackAttached("AddOrUpdateSparseElement");
+    return true;
+}
+
+
 bool
 SetPropIRGenerator::tryAttachSetTypedElement(HandleObject obj, ObjOperandId objId,
                                              uint32_t index, Int32OperandId indexId,
                                              ValOperandId rhsId)
 {
     if (!obj->is<TypedArrayObject>() && !IsPrimitiveArrayTypedObject(obj)) {
         return false;
     }
--- a/js/src/jit/CacheIR.h
+++ b/js/src/jit/CacheIR.h
@@ -196,16 +196,17 @@ extern const char* const CacheKindNames[
     _(GuardIsInt32Index)                  \
     _(GuardType)                          \
     _(GuardShape)                         \
     _(GuardGroup)                         \
     _(GuardProto)                         \
     _(GuardClass)                         /* Guard an object class, per GuardClassKind */ \
     _(GuardAnyClass)                      /* Guard an arbitrary class for an object */ \
     _(GuardCompartment)                   \
+    _(GuardIsExtensible)                  \
     _(GuardIsNativeFunction)              \
     _(GuardIsNativeObject)                \
     _(GuardIsProxy)                       \
     _(GuardHasProxyHandler)               \
     _(GuardNotDOMProxy)                   \
     _(GuardSpecificObject)                \
     _(GuardSpecificAtom)                  \
     _(GuardSpecificSymbol)                \
@@ -217,16 +218,19 @@ extern const char* const CacheKindNames[
     _(GuardNoUnboxedExpando)              \
     _(GuardAndLoadUnboxedExpando)         \
     _(GuardAndGetIndexFromString)         \
     _(GuardAndGetNumberFromString)        \
     _(GuardAndGetIterator)                \
     _(GuardHasGetterSetter)               \
     _(GuardGroupHasUnanalyzedNewScript)   \
     _(GuardIndexIsNonNegative)            \
+    _(GuardIndexGreaterThanDenseCapacity) \
+    _(GuardIndexGreaterThanArrayLength)   \
+    _(GuardIndexIsValidUpdateOrAdd)       \
     _(GuardTagNotEqual)                   \
     _(GuardXrayExpandoShapeAndDefaultProto) \
     _(GuardFunctionPrototype)             \
     _(GuardNoAllocationMetadataBuilder)   \
     _(GuardObjectGroupNotPretenured)      \
     _(LoadStackValue)                     \
     _(LoadObject)                         \
     _(LoadProto)                          \
@@ -261,16 +265,17 @@ extern const char* const CacheKindNames[
     _(ArrayPush)                          \
     _(ArrayJoinResult)                    \
     _(StoreTypedElement)                  \
     _(CallNativeSetter)                   \
     _(CallScriptedSetter)                 \
     _(CallSetArrayLength)                 \
     _(CallProxySet)                       \
     _(CallProxySetByValue)                \
+    _(CallAddOrUpdateSparseElementHelper) \
     _(CallInt32ToString)                  \
     _(CallNumberToString)                 \
                                           \
     /* The *Result ops load a value into the cache's result register. */ \
     _(LoadFixedSlotResult)                \
     _(LoadDynamicSlotResult)              \
     _(LoadUnboxedPropertyResult)          \
     _(LoadTypedObjectResult)              \
@@ -759,16 +764,19 @@ class MOZ_RAII CacheIRWriter : public JS
     void guardCompartment(ObjOperandId obj, JSObject* global, JS::Compartment* compartment) {
         assertSameCompartment(global);
         writeOpWithOperandId(CacheOp::GuardCompartment, obj);
         // Add a reference to a global in the compartment to keep it alive.
         addStubField(uintptr_t(global), StubField::Type::JSObject);
         // Use RawWord, because compartments never move and it can't be GCed.
         addStubField(uintptr_t(compartment), StubField::Type::RawWord);
     }
+    void guardIsExtensible(ObjOperandId obj) {
+        writeOpWithOperandId(CacheOp::GuardIsExtensible, obj);
+    }
     void guardNoDetachedTypedObjects() {
         writeOp(CacheOp::GuardNoDetachedTypedObjects);
     }
     void guardFrameHasNoArgumentsObject() {
         writeOp(CacheOp::GuardFrameHasNoArgumentsObject);
     }
 
     Int32OperandId guardAndGetIndexFromString(StringOperandId str) {
@@ -801,16 +809,28 @@ class MOZ_RAII CacheIRWriter : public JS
     void guardGroupHasUnanalyzedNewScript(ObjectGroup* group) {
         writeOp(CacheOp::GuardGroupHasUnanalyzedNewScript);
         addStubField(uintptr_t(group), StubField::Type::ObjectGroup);
     }
 
     void guardIndexIsNonNegative(Int32OperandId index) {
         writeOpWithOperandId(CacheOp::GuardIndexIsNonNegative, index);
     }
+    void guardIndexGreaterThanDenseCapacity(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexGreaterThanDenseCapacity, obj);
+        writeOperandId(index);
+    }
+    void guardIndexGreaterThanArrayLength(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexGreaterThanArrayLength, obj);
+        writeOperandId(index);
+    }
+    void guardIndexIsValidUpdateOrAdd(ObjOperandId obj, Int32OperandId index) {
+        writeOpWithOperandId(CacheOp::GuardIndexIsValidUpdateOrAdd, obj);
+        writeOperandId(index);
+    }
     void guardTagNotEqual(ValueTagOperandId lhs, ValueTagOperandId rhs) {
         writeOpWithOperandId(CacheOp::GuardTagNotEqual, lhs);
         writeOperandId(rhs);
     }
 
     void loadFrameCalleeResult() {
         writeOp(CacheOp::LoadFrameCalleeResult);
     }
@@ -1031,16 +1051,22 @@ class MOZ_RAII CacheIRWriter : public JS
         buffer_.writeByte(uint32_t(strict));
     }
     void callProxySetByValue(ObjOperandId obj, ValOperandId id, ValOperandId rhs, bool strict) {
         writeOpWithOperandId(CacheOp::CallProxySetByValue, obj);
         writeOperandId(id);
         writeOperandId(rhs);
         buffer_.writeByte(uint32_t(strict));
     }
+    void callAddOrUpdateSparseElementHelper(ObjOperandId obj, Int32OperandId id, ValOperandId rhs, bool strict) {
+        writeOpWithOperandId(CacheOp::CallAddOrUpdateSparseElementHelper, obj);
+        writeOperandId(id);
+        writeOperandId(rhs);
+        buffer_.writeByte(uint32_t(strict));
+    }
     StringOperandId callInt32ToString(Int32OperandId id) {
         StringOperandId res(nextOperandId_++);
         writeOpWithOperandId(CacheOp::CallInt32ToString, id);
         writeOperandId(res);
         return res;
     }
     StringOperandId callNumberToString(ValOperandId id) {
         StringOperandId res(nextOperandId_++);
@@ -1743,16 +1769,20 @@ class MOZ_RAII SetPropIRGenerator : publ
     bool tryAttachSetDenseElement(HandleObject obj, ObjOperandId objId, uint32_t index,
                                   Int32OperandId indexId, ValOperandId rhsId);
     bool tryAttachSetTypedElement(HandleObject obj, ObjOperandId objId, uint32_t index,
                                   Int32OperandId indexId, ValOperandId rhsId);
 
     bool tryAttachSetDenseElementHole(HandleObject obj, ObjOperandId objId, uint32_t index,
                                       Int32OperandId indexId, ValOperandId rhsId);
 
+    bool tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId, uint32_t index,
+                                           Int32OperandId indexId, ValOperandId rhsId);
+
+
     bool tryAttachGenericProxy(HandleObject obj, ObjOperandId objId, HandleId id,
                                ValOperandId rhsId, bool handleDOMProxies);
     bool tryAttachDOMProxyShadowed(HandleObject obj, ObjOperandId objId, HandleId id,
                                    ValOperandId rhsId);
     bool tryAttachDOMProxyUnshadowed(HandleObject obj, ObjOperandId objId, HandleId id,
                                      ValOperandId rhsId);
     bool tryAttachDOMProxyExpando(HandleObject obj, ObjOperandId objId, HandleId id,
                                   ValOperandId rhsId);
--- a/js/src/jit/CacheIRCompiler.cpp
+++ b/js/src/jit/CacheIRCompiler.cpp
@@ -1730,16 +1730,45 @@ CacheIRCompiler::emitGuardClass()
         masm.branchTestObjClassNoSpectreMitigations(Assembler::NotEqual, obj, clasp, scratch,
                                                     failure->label());
     }
 
     return true;
 }
 
 bool
+CacheIRCompiler::emitGuardIsExtensible()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    AutoScratchRegister scratch(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    Address shape(obj, ShapedObject::offsetOfShape());
+    masm.loadPtr(shape, scratch);
+
+    Address baseShape(scratch, Shape::offsetOfBaseShape());
+    masm.loadPtr(baseShape, scratch);
+
+    Address baseShapeFlags(scratch, BaseShape::offsetOfFlags());
+    masm.loadPtr(baseShapeFlags, scratch);
+
+    masm.and32(Imm32(js::BaseShape::NOT_EXTENSIBLE), scratch);
+
+    // Spectre-style checks are not needed here because we do not
+    // interpret data based on this check.
+    masm.branch32(Assembler::Equal, scratch, Imm32(js::BaseShape::NOT_EXTENSIBLE),
+                  failure->label());
+    return true;
+}
+
+bool
 CacheIRCompiler::emitGuardIsNativeFunction()
 {
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     JSNative nativeFunc = reinterpret_cast<JSNative>(reader.pointer());
     AutoScratchRegister scratch(allocator, masm);
 
     FailurePath* failure;
     if (!addFailurePath(&failure)) {
@@ -2816,16 +2845,99 @@ CacheIRCompiler::emitGuardIndexIsNonNega
         return false;
     }
 
     masm.branch32(Assembler::LessThan, index, Imm32(0), failure->label());
     return true;
 }
 
 bool
+CacheIRCompiler::emitGuardIndexGreaterThanDenseCapacity()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    // Ensure index >= capacity.
+    Label outOfBounds;
+    Address capacity(scratch, ObjectElements::offsetOfCapacity());
+    masm.spectreBoundsCheck32(index, capacity, scratch2, &outOfBounds);
+    masm.jump(failure->label());
+    masm.bind(&outOfBounds);
+
+    return true;
+}
+
+bool
+CacheIRCompiler::emitGuardIndexGreaterThanArrayLength()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    // Ensure index >= length;
+    Label outOfBounds;
+    Address length(scratch, ObjectElements::offsetOfLength());
+    masm.spectreBoundsCheck32(index, length, scratch2, &outOfBounds);
+    masm.jump(failure->label());
+    masm.bind(&outOfBounds);
+    return true;
+}
+
+bool
+CacheIRCompiler::emitGuardIndexIsValidUpdateOrAdd()
+{
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register index = allocator.useRegister(masm, reader.int32OperandId());
+    AutoScratchRegister scratch(allocator, masm);
+    AutoScratchRegister scratch2(allocator, masm);
+
+    FailurePath* failure;
+    if (!addFailurePath(&failure)) {
+        return false;
+    }
+
+    // Load obj->elements.
+    masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
+
+    Label success;
+
+    // If length is writable, branch to &success.  All indices are writable.
+    Address flags(scratch, ObjectElements::offsetOfFlags());
+    masm.branchTest32(Assembler::Zero, flags,
+                      Imm32(ObjectElements::Flags::NONWRITABLE_ARRAY_LENGTH),
+                      &success);
+
+    // Otherwise, ensure index is in bounds.
+    Address length(scratch, ObjectElements::offsetOfLength());
+    masm.spectreBoundsCheck32(index, length, scratch2,
+                              /* failure = */ failure->label());
+    masm.bind(&success);
+    return true;
+}
+
+bool
 CacheIRCompiler::emitGuardTagNotEqual()
 {
     Register lhs = allocator.useRegister(masm, reader.valueTagOperandId());
     Register rhs = allocator.useRegister(masm, reader.valueTagOperandId());
 
     FailurePath* failure;
     if (!addFailurePath(&failure)) {
         return false;
--- a/js/src/jit/CacheIRCompiler.h
+++ b/js/src/jit/CacheIRCompiler.h
@@ -27,30 +27,34 @@ namespace jit {
     _(GuardIsString)                      \
     _(GuardIsSymbol)                      \
     _(GuardIsNumber)                      \
     _(GuardIsInt32)                       \
     _(GuardIsInt32Index)                  \
     _(GuardType)                          \
     _(GuardClass)                         \
     _(GuardGroupHasUnanalyzedNewScript)   \
+    _(GuardIsExtensible)                  \
     _(GuardIsNativeFunction)              \
     _(GuardFunctionPrototype)             \
     _(GuardIsNativeObject)                \
     _(GuardIsProxy)                       \
     _(GuardNotDOMProxy)                   \
     _(GuardSpecificInt32Immediate)        \
     _(GuardMagicValue)                    \
     _(GuardNoUnboxedExpando)              \
     _(GuardAndLoadUnboxedExpando)         \
     _(GuardNoDetachedTypedObjects)        \
     _(GuardNoDenseElements)               \
     _(GuardAndGetNumberFromString)        \
     _(GuardAndGetIndexFromString)         \
     _(GuardIndexIsNonNegative)            \
+    _(GuardIndexGreaterThanDenseCapacity) \
+    _(GuardIndexGreaterThanArrayLength)   \
+    _(GuardIndexIsValidUpdateOrAdd)       \
     _(GuardTagNotEqual)                   \
     _(GuardXrayExpandoShapeAndDefaultProto)\
     _(GuardNoAllocationMetadataBuilder)   \
     _(GuardObjectGroupNotPretenured)      \
     _(LoadObject)                         \
     _(LoadProto)                          \
     _(LoadEnclosingEnvironment)           \
     _(LoadWrapperTarget)                  \
--- a/js/src/jit/IonCacheIRCompiler.cpp
+++ b/js/src/jit/IonCacheIRCompiler.cpp
@@ -2261,16 +2261,38 @@ IonCacheIRCompiler::emitCallProxySetByVa
     masm.Push(val);
     masm.Push(idVal);
     masm.Push(obj);
 
     return callVM(masm, ProxySetPropertyByValueInfo);
 }
 
 bool
+IonCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
+{
+    AutoSaveLiveRegisters save(*this);
+
+    Register obj = allocator.useRegister(masm, reader.objOperandId());
+    Register id = allocator.useRegister(masm, reader.int32OperandId());
+    ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
+    bool strict = reader.readBool();
+
+    Label done;
+    prepareVMCall(masm, save);
+
+    masm.Push(Imm32(strict));
+    masm.Push(val);
+    masm.Push(id);
+    masm.Push(obj);
+
+    return callVM(masm, AddOrUpdateSparseElementHelperInfo);
+}
+
+
+bool
 IonCacheIRCompiler::emitMegamorphicSetElement()
 {
     AutoSaveLiveRegisters save(*this);
 
     Register obj = allocator.useRegister(masm, reader.objOperandId());
     ConstantOrRegister idVal = allocator.useConstantOrRegister(masm, reader.valOperandId());
     ConstantOrRegister val = allocator.useConstantOrRegister(masm, reader.valOperandId());
     bool strict = reader.readBool();
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -2067,10 +2067,15 @@ const VMFunction ProxyHasInfo = Function
 typedef bool (*ProxyHasOwnFn)(JSContext*, HandleObject, HandleValue, MutableHandleValue);
 const VMFunction ProxyHasOwnInfo = FunctionInfo<ProxyHasOwnFn>(ProxyHasOwn, "ProxyHasOwn");
 
 typedef bool (*NativeGetElementFn)(JSContext*, HandleNativeObject, HandleValue, int32_t,
                                    MutableHandleValue);
 const VMFunction NativeGetElementInfo =
     FunctionInfo<NativeGetElementFn>(NativeGetElement, "NativeGetProperty");
 
+typedef bool (*AddOrUpdateSparseElementHelperFn)(JSContext* cx, HandleArrayObject obj,
+                                                 int32_t int_id, HandleValue v, bool strict);
+const VMFunction AddOrUpdateSparseElementHelperInfo =
+    FunctionInfo<AddOrUpdateSparseElementHelperFn>(AddOrUpdateSparseElementHelper, "AddOrUpdateSparseElementHelper");
+
 } // namespace jit
 } // namespace js
--- a/js/src/jit/VMFunctions.h
+++ b/js/src/jit/VMFunctions.h
@@ -977,15 +977,17 @@ extern const VMFunction ProxyGetProperty
 extern const VMFunction ProxyGetPropertyByValueInfo;
 extern const VMFunction ProxySetPropertyInfo;
 extern const VMFunction ProxySetPropertyByValueInfo;
 extern const VMFunction ProxyHasInfo;
 extern const VMFunction ProxyHasOwnInfo;
 
 extern const VMFunction NativeGetElementInfo;
 
+extern const VMFunction AddOrUpdateSparseElementHelperInfo;
+
 // TailCall VMFunctions
 extern const VMFunction DoConcatStringObjectInfo;
 
 } // namespace jit
 } // namespace js
 
 #endif /* jit_VMFunctions_h */
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -2102,16 +2102,78 @@ DefineNonexistentProperty(JSContext* cx,
         if (!AddDataProperty(cx, obj, id, v)) {
             return false;
         }
     }
 
     return result.succeed();
 }
 
+bool
+js::AddOrUpdateSparseElementHelper(JSContext* cx, HandleArrayObject obj, int32_t int_id,
+                                   HandleValue v, bool strict)
+{
+    MOZ_ASSERT(INT_FITS_IN_JSID(int_id));
+    RootedId id(cx, INT_TO_JSID(int_id));
+
+    // This helper doesn't handle the case where the index may be in the dense elements
+    MOZ_ASSERT(int_id >= 0);
+    MOZ_ASSERT(uint32_t(int_id) >= obj->getDenseCapacity());
+
+    // First decide if this is an add or an update. Because the IC guards have
+    // already ensured this exists exterior to the dense array range, and the
+    // prototype checks have ensured there are no indexes on the prototype, we
+    // can use the shape lineage to find the element if it exists:
+    Shape* shape = obj->lastProperty()->search(cx, id);
+
+    // If we didn't find the shape, we're on the add path: delegate to
+    // AddSparseElement:
+    if (shape == nullptr) {
+        Rooted<PropertyDescriptor> desc(cx);
+        desc.setDataDescriptor(v, JSPROP_ENUMERATE);
+        desc.assertComplete();
+
+        if (!ReshapeForShadowedProp(cx, obj, id)) {
+            return false;
+        }
+
+        MOZ_ASSERT(Shape::isDataProperty(desc.attributes(), desc.getter(), desc.setter()));
+
+        shape = NativeObject::addDataProperty(cx, obj, id, SHAPE_INVALID_SLOT,
+                                              desc.attributes());
+        if (!shape) {
+            return false;
+        }
+
+        UpdateShapeTypeAndValueForWritableDataProp(cx, obj, shape, id, desc.value());
+
+        // Do not call `maybeDensifySparseElements` here.  This may theoretically
+        // de-optimize cases where Arrays are being used "partly sparsely", with
+        // a mostly-dense prefix range, and some sparse elements.  That does not
+        // seem to be a likely case, however, and thus this is an explicit decision
+        // to not bother with sparsification.
+
+        return CallAddPropertyHook(cx, obj, id, desc.value());
+    }
+
+    // At this point we're updating a property: See SetExistingProperty
+    if (shape->writable() && shape->isDataProperty()) {
+        // While all JSID_INT properties use a single TI entry,
+        // nothing yet has inspected the updated value so we *must* use setSlotWithType().
+        obj->setSlotWithType(cx, shape, v, /* overwriting = */ true);
+        return true;
+    }
+
+    // We don't know exactly what this object looks like, hit the slowpath.
+    RootedValue receiver(cx, ObjectValue(*obj));
+    JS::ObjectOpResult result;
+    return SetProperty(cx, obj, id, v, receiver, result) &&
+           result.checkStrictErrorOrWarning(cx, obj, id, strict);
+}
+
 
 /*** [[HasProperty]] *****************************************************************************/
 
 // ES6 draft rev31 9.1.7.1 OrdinaryHasProperty
 bool
 js::NativeHasProperty(JSContext* cx, HandleNativeObject obj, HandleId id, bool* foundp)
 {
     RootedNativeObject pobj(cx, obj);
--- a/js/src/vm/NativeObject.h
+++ b/js/src/vm/NativeObject.h
@@ -1617,16 +1617,20 @@ NativeGetElement(JSContext* cx, HandleNa
 bool
 SetPropertyByDefining(JSContext* cx, HandleId id, HandleValue v, HandleValue receiver,
                       ObjectOpResult& result);
 
 bool
 SetPropertyOnProto(JSContext* cx, HandleObject obj, HandleId id, HandleValue v,
                    HandleValue receiver, ObjectOpResult& result);
 
+bool
+AddOrUpdateSparseElementHelper(JSContext* cx, HandleArrayObject obj, int32_t int_id,
+                               HandleValue v, bool strict);
+
 /*
  * Indicates whether an assignment operation is qualified (`x.y = 0`) or
  * unqualified (`y = 0`). In strict mode, the latter is an error if no such
  * variable already exists.
  *
  * Used as an argument to NativeSetProperty.
  */
 enum QualifiedBool {
--- a/js/src/vm/Shape.h
+++ b/js/src/vm/Shape.h
@@ -1154,18 +1154,20 @@ class Shape : public gc::TenuredCell
 
     MOZ_ALWAYS_INLINE Shape* search(JSContext* cx, jsid id);
     MOZ_ALWAYS_INLINE Shape* searchLinear(jsid id);
 
     void fixupAfterMovingGC();
     void fixupGetterSetterForBarrier(JSTracer* trc);
     void updateBaseShapeAfterMovingGC();
 
+    // For JIT usage.
+    static inline size_t offsetOfBaseShape() { return offsetof(Shape, base_); }
+
 #ifdef DEBUG
-    // For JIT usage.
     static inline size_t offsetOfImmutableFlags() { return offsetof(Shape, immutableFlags); }
     static inline uint32_t fixedSlotsMask() { return FIXED_SLOTS_MASK; }
 #endif
 
   private:
     void fixupDictionaryShapeAfterMovingGC();
     void fixupShapeTreeAfterMovingGC();
 
--- a/js/src/vm/ShapedObject.h
+++ b/js/src/vm/ShapedObject.h
@@ -6,16 +6,18 @@
 
 #ifndef vm_ShapedObject_h
 #define vm_ShapedObject_h
 
 #include "vm/JSObject.h"
 
 namespace js {
 
+namespace jit { class CacheIRCompiler; }
+
 /*
  * Shaped objects are a variant of JSObject that use a GCPtrShape for their
  * |shapeOrExpando_| field. All objects that point to a js::Shape as their
  * |shapeOrExpando_| field should use this as their subclass.
  *
  * NOTE: shape()->getObjectClass() must equal getClass().
  */
 class ShapedObject : public JSObject
@@ -53,16 +55,18 @@ class ShapedObject : public JSObject
     static JSObject* fromShapeFieldPointer(uintptr_t p) {
         return reinterpret_cast<JSObject*>(p - ShapedObject::offsetOfShape());
     }
 
   private:
     // See JSObject::offsetOfGroup() comment.
     friend class js::jit::MacroAssembler;
 
+    friend class js::jit::CacheIRCompiler;
+
     static constexpr size_t offsetOfShape() {
         static_assert(offsetOfShapeOrExpando() == offsetof(shadow::Object, shape),
                       "shadow shape must match actual shape");
         return offsetOfShapeOrExpando();
     }
 };
 
 } // namespace js