diff --git a/include/hermes/VM/ArrayStorage.h b/include/hermes/VM/ArrayStorage.h index 983b2267d70..608cf3d87d0 100644 --- a/include/hermes/VM/ArrayStorage.h +++ b/include/hermes/VM/ArrayStorage.h @@ -28,9 +28,8 @@ class ArrayStorageBase final : public VariableSizeRuntimeCell, private llvh::TrailingObjects< ArrayStorageBase, GCHermesValueInLargeObjImpl> { - using GCHVType = GCHermesValueInLargeObjImpl; - public: + using GCHVType = GCHermesValueInLargeObjImpl; using size_type = uint32_t; using iterator = GCHVType *; diff --git a/lib/VM/JSLib/Array.cpp b/lib/VM/JSLib/Array.cpp index 9bde390650e..2dc9affe90b 100644 --- a/lib/VM/JSLib/Array.cpp +++ b/lib/VM/JSLib/Array.cpp @@ -520,6 +520,79 @@ bool arrayFastPathCheck( return true; } +/// Check whether elements of \p O can be read directly out of JSArray +/// indexed storage. +/// \param O the object whose elements would be read. +/// \param len the value of the "length" property of \p O. +/// \return \p O as a JSArray if the fast-path read is possible, nullptr +/// otherwise. This function does not allocate, but the returned pointer is +/// unrooted and must not be used across an allocation. +static JSArray *canReadElementsFast( + Runtime &runtime, + Handle O, + uint64_t len) { + JSArray *arr = dyn_vmcast(O.get()); + if (arr && len <= UINT32_MAX && + arrayFastPathCheck(runtime, arr, nullptr, (uint32_t)len)) + return arr; + return nullptr; +} + +/// Perform "Let O be ? ToObject(this value)" followed by +/// "Let len be ? ToLength(? Get(O, "length"))". Used by the slow paths of the +/// Array.prototype functions that have a fast path for retrieving the length. +/// \param thisArg the "this" value of the call. +/// \param[in,out] O if null, set to ToObject(\p thisArg); if already non-null, +/// do nothing, since the caller has already obtained \p O and \p len. +/// \param[out] len set to the "length" property of \p O, if it was retrieved +/// here. +LLVM_ATTRIBUTE_NOINLINE +static ExecutionStatus ensureObjectAndLength( + Runtime &runtime, + Handle<> thisArg, + PinnedValue &O, + uint64_t &len) { + if (*O) + return ExecutionStatus::RETURNED; + + CallResult objRes = toObject(runtime, thisArg); + if (LLVM_UNLIKELY(objRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + O.castAndSetHermesValue(*objRes); + + CallResult lenRes = + lengthOfArrayLike(runtime, O, Runtime::makeNullHandle()); + if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + len = *lenRes; + return ExecutionStatus::RETURNED; +} + +/// Check whether an Array.prototype mutation method (pop/shift/unshift) can +/// use its fast path on the "this" value of \p args. If the "this" value is a +/// JSArray, \p O and \p len are populated even when the check fails, so that +/// the slow path can reuse the length instead of performing a second +/// observable Get (the "length" of a JSArray can never be an accessor). +/// \param[out] O set to the "this" value if it is a JSArray, left null +/// otherwise. +/// \param[out] len set to the array length if the "this" value is a JSArray, +/// left unmodified otherwise. +/// \return true if the caller may take the fast path. +static bool canUseArrayFastPath( + Runtime &runtime, + NativeArgs args, + PinnedValue &O, + uint64_t &len) { + if (LLVM_UNLIKELY(!vmisa(args.getThisArg()))) + return false; + JSArray *arr = vmcast(args.getThisArg()); + O = args.vmcastThis(); + len = JSArray::getLength(arr, runtime); + return arrayFastPathCheck(runtime, arr, *runtime.arrayClass, len); +} + CallResult arrayConstructor(void *, Runtime &runtime) { NativeArgs args = runtime.getCurrentFrame().getNativeArgs(); // NativeConstructors create their own this when called with new. The array @@ -1117,8 +1190,7 @@ CallResult arrayPrototypeJoin(void *, Runtime &runtime) { uint32_t fastPathEnd = 0; // 1. Fast Path: Process as many elements as possible quickly. - if (JSArray *arr = dyn_vmcast(lv.O.get()); - arr && arrayFastPathCheck(runtime, arr, nullptr, (uint32_t)len)) { + if (JSArray *arr = canReadElementsFast(runtime, lv.O, len)) { // Accumulate the size of the strings in the array, stopping at the first // element that is not a string, null, or undefined. @@ -1272,12 +1344,12 @@ CallResult arrayPrototypeJoin(void *, Runtime &runtime) { } /// Fast path for Array.prototype.push() when the array is a normal array. -/// \pre \p arr is an array with fast index properties and there are no -/// index-like properties in any parents. -/// \pre \p The storage for \p arr starts at 0 and ends at its length. /// \param arr the array to push onto. /// \param len the length of \p arr (before pushing). /// \param args the original NativeArgs to push(). +/// \pre \p arr is an array with fast index properties and there are no +/// index-like properties in any parents. +/// \pre The storage for \p arr starts at 0 and ends at its length. static CallResult arrayPrototypePushFastPath( Runtime &runtime, Handle arr, @@ -1328,9 +1400,6 @@ static CallResult arrayPrototypePushSlowPath( NativeArgs args = runtime.getCurrentFrame().getNativeArgs(); uint32_t argCount = args.getArgCount(); - // Ensure the fast path does not leak any handles. - NoLeakHandleScope noLeaks{runtime}; - struct : Locals { PinnedValue O; PinnedValue arr; @@ -1416,9 +1485,6 @@ static CallResult arrayPrototypePushSlowPath( CallResult arrayPrototypePush(void *, Runtime &runtime) { NativeArgs args = runtime.getCurrentFrame().getNativeArgs(); - // Ensure the fast path does not leak any handles. - NoLeakHandleScope noLeaks{runtime}; - // 3. Let items be a List whose elements are, in left to right order, the // arguments that were passed to this function invocation. // 4. Let argCount be the number of elements in items. @@ -1433,6 +1499,8 @@ CallResult arrayPrototypePush(void *, Runtime &runtime) { if (LLVM_LIKELY(len < UINT32_MAX - argCount) && arrayFastPathCheck(runtime, arr, *runtime.arrayClass, len)) { + // Ensure the fast path does not leak any handles. + NoLeakHandleScope noLeaks{runtime}; return arrayPrototypePushFastPath( runtime, args.vmcastThis(), len, args); } @@ -1950,6 +2018,92 @@ CallResult sortSparse( return O.getHermesValue(); } + +/// Reads successive elements of an object, taking them directly out of the +/// underlying JSArray indexed storage when possible. Used by the +/// Array.prototype functions that run a user callback per element +/// (forEach/map/filter/every/some). +/// +/// Whether the fast path is possible is determined once, on construction, even +/// though callbacks run between reads: at() re-checks the bounds against the +/// current storage on every read, hasFastIndexProperties() is re-checked so a +/// non-empty slot can never be shadowed by an index-like named property (adding +/// one clears the flag), and converting an indexed property to an accessor +/// empties its slot first. An empty slot must still go through the slow path, +/// because a callback may have added index-like properties to the prototype +/// chain since construction. +class ElementReader { + private: + /// Runtime to read in. + Runtime &runtime_; + + /// Object to read elements from. + Handle obj_; + + /// Whether elements can be read directly out of obj_'s indexed storage. + bool fastPath_; + + /// Temporary storage for a property name, used by the slow path of read(). + PinnedValue &tmpNameStorage_; + + /// Storage for the object a value is retrieved from, used by the slow path + /// of read(). + PinnedValue &descObj_; + + public: + /// \param obj the object to read elements from. + /// \param len the value of the "length" property of \p obj. + /// \param tmpNameStorage temporary storage for a property name. Owned by the + /// caller so that read() does not have to allocate it on every call. + /// \param descObj storage for the object a value is retrieved from, owned by + /// the caller for the same reason as \p tmpNameStorage. + ElementReader( + Runtime &runtime, + Handle obj, + uint64_t len, + PinnedValue &tmpNameStorage, + PinnedValue &descObj) + : runtime_(runtime), + obj_(obj), + fastPath_(len != 0 && canReadElementsFast(runtime, obj, len)), + tmpNameStorage_(tmpNameStorage), + descObj_(descObj) {} + + /// Load the value of property \p kHandle of the object into \p kValue. + /// \param kHandle a Handle containing the index to read, encoded as a number. + /// \param[out] kValue set to the value of the property if it is present. + /// \return true if the property is present, false otherwise. + CallResult read(Handle<> kHandle, PinnedValue<> &kValue); +}; + +CallResult ElementReader::read( + Handle<> kHandle, + PinnedValue<> &kValue) { + if (fastPath_ && LLVM_LIKELY(obj_->hasFastIndexProperties())) { + double kNum = kHandle->getNumber(); + assert(kNum <= UINT32_MAX && "fast path index must fit in uint32_t"); + uint32_t k = (uint32_t)kNum; + SmallHermesValue elem = vmcast(obj_.get())->at(runtime_, k); + if (LLVM_LIKELY(!elem.isEmpty())) { + kValue = elem.unboxToHV(runtime_); + return true; + } + } + ComputedPropertyDescWithSymStorage desc{tmpNameStorage_}; + JSObject::getComputedPrimitiveDescriptor( + obj_, runtime_, kHandle, descObj_, desc); + CallResult> propRes = JSObject::getComputedPropertyValue_RJS( + obj_, runtime_, descObj_, desc.get(), kHandle); + if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + if (LLVM_LIKELY(!(*propRes)->isEmpty())) { + kValue = std::move(*propRes); + return true; + } + return false; +} + } // anonymous namespace /// ES5.1 15.4.4.11. @@ -2018,6 +2172,7 @@ inline CallResult arrayPrototypeForEach(void *, Runtime &runtime) { PinnedValue O; PinnedValue<> lenProp; PinnedValue<> k; + PinnedValue<> kValue; PinnedValue descObj; PinnedValue tmpPropNameStorage; } lv; @@ -2051,28 +2206,26 @@ inline CallResult arrayPrototypeForEach(void *, Runtime &runtime) { // Index to execute the callback on. lv.k = HermesValue::encodeTrustedNumberValue(0); + // Reads elements directly out of the indexed storage when possible. + ElementReader reader{ + runtime, lv.O, len, lv.tmpPropNameStorage, lv.descObj}; + // Loop through and execute the callback on all existing values. - // TODO: Implement a fast path for actual arrays. auto marker = gcScope.createMarker(); while (lv.k->getDouble() < len) { gcScope.flushToMarker(marker); - ComputedPropertyDescWithSymStorage desc{lv.tmpPropNameStorage}; - JSObject::getComputedPrimitiveDescriptor( - lv.O, runtime, lv.k, lv.descObj, desc); - CallResult> propRes = JSObject::getComputedPropertyValue_RJS( - lv.O, runtime, lv.descObj, desc.get(), lv.k); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { + CallResult kPresent = reader.read(lv.k, lv.kValue); + if (LLVM_UNLIKELY(kPresent == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - if (LLVM_LIKELY(!(*propRes)->isEmpty())) { - auto kValue = std::move(*propRes); + if (*kPresent) { if (LLVM_UNLIKELY( Callable::executeCall3( callbackFn, runtime, args.getArgHandle(1), - kValue.get(), + lv.kValue.get(), lv.k.get(), lv.O.getHermesValue()) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; @@ -2457,13 +2610,46 @@ CallResult arrayPrototypeSlice(void *, Runtime &runtime) { } lv.A = std::move(*arrRes); + // An empty slice reads nothing from O on either path below, and A already + // has length 0, so return it without paying for the fast path check. + if (count32 == 0) + return lv.A.getHermesValue(); + + // Fast path: copy the elements directly out of the array's storage. The + // check fails if the argument coercions above changed the element count so + // that it no longer matches len. Mutations that preserve the element count + // are fine: we copy the current values, which is what the spec requires, + // since elements are read after argument coercion. There is no need to set + // the length of A, since it was already set when A was allocated, and holes + // (empty values) can be copied like ordinary values because the fast path + // check has ensured there are no index-like properties in the prototype + // chain. + if (JSArray *arr = canReadElementsFast(runtime, lv.O, (uint64_t)len)) { + NoAllocScope noAlloc{runtime}; + // k can be cast to uint32_t because it is clamped to [0, len] and + // canReadElementsFast has checked that len fits in uint32_t. + assert(k >= 0 && k <= len && "k must be in [0, len]"); + uint32_t start = (uint32_t)k; + auto *aStorage = lv.A->getIndexedStorageUnsafe(runtime); + auto *oStorage = arr->getIndexedStorageUnsafe(runtime); + assert( + start + count32 <= oStorage->size() && + "slice range must be within the source storage"); + JSArray::StorageType::GCHVType::copy( + oStorage->data() + start, + oStorage->data() + start + count32, + aStorage->data(), + aStorage, + runtime.getHeap()); + return lv.A.getHermesValue(); + } + // Next index in A to write to. uint32_t n = 0; auto marker = gcScope.createMarker(); // Copy the elements between the actual start and end indices into A. - // TODO: Implement a fast path for actual arrays. while (k < fin) { lv.k = HermesValue::encodeTrustedNumberValue(k); ComputedPropertyDescWithSymStorage desc{lv.tmpPropNameStorage}; @@ -2494,10 +2680,7 @@ CallResult arrayPrototypeSlice(void *, Runtime &runtime) { return lv.A.getHermesValue(); } -/// Fast path for Array.prototype.slice() when the array is a normal array. -/// \pre \p O is an array with fast index properties and there are no index-like -/// properties in any parents. -/// \pre \p The storage for \p O starts at 0 and ends at its length. +/// Fast path for Array.prototype.splice() when the array is a normal array. /// \param O the array to splice. /// \param len the length of O. /// \param A the array to populate with deleted elements. @@ -2505,6 +2688,9 @@ CallResult arrayPrototypeSlice(void *, Runtime &runtime) { /// \param actualDeleteCount the number of elements to delete. /// \param itemCount the number of elements to insert from \p args. /// \param args the original NativeArgs to splice(). +/// \pre \p O is an array with fast index properties and there are no index-like +/// properties in any parents. +/// \pre The storage for \p O starts at 0 and ends at its length. /// \return the result of the splice (\p A). static CallResult arrayPrototypeSpliceFastPath( Runtime &runtime, @@ -2528,16 +2714,20 @@ static CallResult arrayPrototypeSpliceFastPath( ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - { + if (actualDeleteCount > 0) { NoAllocScope noAlloc(runtime); JSArray::StorageType *aStorage = A->getIndexedStorageNullable(runtime); JSArray::StorageType *oStorage = O->getIndexedStorageNullable(runtime); - for (uint32_t j = 0; j < actualDeleteCount; ++j) { - assert(aStorage && oStorage && "storage must not be null"); - uint32_t from = actualStart + j; - SmallHermesValue elem = oStorage->at(from); - aStorage->set(j, elem, runtime.getHeap()); - } + assert(aStorage && oStorage && "storage must not be null"); + assert( + actualStart + actualDeleteCount <= oStorage->size() && + "deleted range must be within the source storage"); + JSArray::StorageType::GCHVType::copy( + oStorage->data() + actualStart, + oStorage->data() + actualStart + actualDeleteCount, + aStorage->data(), + aStorage, + runtime.getHeap()); } // Final length of O. @@ -2556,16 +2746,22 @@ static CallResult arrayPrototypeSpliceFastPath( NoAllocScope noAlloc(runtime); JSArray::StorageType *oStorage = O->getIndexedStorageNullable(runtime); - for (uint32_t k = actualStart; k < len - actualDeleteCount; ++k) { - assert(oStorage && "storage must not be null"); - uint32_t from = k + actualDeleteCount; - uint32_t to = k + itemCount; - SmallHermesValue elem = oStorage->at(from); - oStorage->set(to, elem, runtime.getHeap()); - } + assert(oStorage && "storage must not be null"); + assert(len <= oStorage->size() && "moved range must be within the storage"); + JSArray::StorageType::GCHVType::copy( + oStorage->data() + actualStart + actualDeleteCount, + oStorage->data() + len, + oStorage->data() + actualStart + itemCount, + oStorage, + runtime.getHeap()); // Shrink the array size to the new length. JSArray::StorageType::resizeWithinCapacity(oStorage, runtime, finalLen); + // Update the elemCount immediately so it never exceeds the storage size: + // the argument copy loop below may allocate, and anything observing the + // array during a GC must not see elemCount past the end of the storage. + assert(oStorage->size() == finalLen && O->getBeginIndex() == 0); + O->setElemCountUnsafe(finalLen); } else if (itemCount > actualDeleteCount) { // Inserting more items than deleting. @@ -2577,20 +2773,22 @@ static CallResult arrayPrototypeSpliceFastPath( return ExecutionStatus::EXCEPTION; } - // Start from the right, and copy elements to the right. - // This makes space to insert the elements from the arguments. - // Loop k from (len - actualDeleteCount) to actualStart (exclusive), - // just like the spec does for the slow path. + // Copy elements to the right, backwards, to avoid overwriting elements + // that have not been moved yet. This makes space to insert the elements + // from the arguments. NoAllocScope noAlloc(runtime); JSArray::StorageType *oStorage = O->getIndexedStorageNullable(runtime); - for (uint32_t k = len - actualDeleteCount; k > actualStart; --k) { - assert(oStorage && "storage must not be null"); - uint32_t from = k + actualDeleteCount - 1; - uint32_t to = k + itemCount - 1; - SmallHermesValue elem = oStorage->at(from); - oStorage->set(to, elem, runtime.getHeap()); - } + assert(oStorage && "storage must not be null"); + assert( + finalLen <= oStorage->size() && + "destination range must be within the storage"); + JSArray::StorageType::GCHVType::copy_backward( + oStorage->data() + actualStart + actualDeleteCount, + oStorage->data() + len, + oStorage->data() + finalLen, + oStorage, + runtime.getHeap()); } // Finally, just copy the elements from the args into the array @@ -3089,6 +3287,12 @@ CallResult arrayPrototypeCopyWithin(void *, Runtime &runtime) { return lv.O.getHermesValue(); } +/// Fast path for Array.prototype.pop() when the array is a normal array. +/// \param arr the array to pop the last element from. +/// \param len the length of \p arr (before popping). +/// \pre \p arr is an array with fast index properties and there are no +/// index-like properties in any parents. +/// \pre The storage for \p arr starts at 0 and ends at its length. static CallResult arrayPrototypePopFastPath(Runtime &runtime, Handle arr, uint32_t len) { if (LLVM_UNLIKELY(len == 0)) { @@ -3126,23 +3330,11 @@ CallResult arrayPrototypePop(void *, Runtime &runtime) { } lv; LocalsRAII lraii{runtime, &lv}; - // Ensure the fast path does not leak any handles. - NoLeakHandleScope noLeaks{runtime}; - uint64_t len; - if (LLVM_LIKELY(vmisa(args.getThisArg()))) { - // Fast path for getting the length. - JSArray *arr = vmcast(args.getThisArg()); - len = JSArray::getLength(arr, runtime); - - if (arrayFastPathCheck(runtime, arr, *runtime.arrayClass, len)) { - return arrayPrototypePopFastPath( - runtime, args.vmcastThis(), len); - } - - // Fast path check failed, populate the O Local so it can be used in the - // slow path. - lv.O = args.vmcastThis(); + if (canUseArrayFastPath(runtime, args, lv.O, len)) { + // Ensure the fast path does not leak any handles. + NoLeakHandleScope noLeaks{runtime}; + return arrayPrototypePopFastPath(runtime, args.vmcastThis(), len); } // The slow path may create additional handles, so create a GCScope to avoid @@ -3150,23 +3342,10 @@ CallResult arrayPrototypePop(void *, Runtime &runtime) { GCScope gcScope(runtime); // If the fast path has not populated the object, do that now. - if (!*lv.O) { - auto res = toObject(runtime, args.getThisHandle()); - if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; - } - lv.O.castAndSetHermesValue(*res); - auto propRes = JSObject::getNamed_RJS( - lv.O, runtime, Predefined::getSymbolID(Predefined::length)); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; - } - lv.lenProp = std::move(*propRes); - auto intRes = toLengthU64(runtime, lv.lenProp); - if (LLVM_UNLIKELY(intRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; - } - len = *intRes; + if (LLVM_UNLIKELY( + ensureObjectAndLength(runtime, args.getThisHandle(), lv.O, len) == + ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; } if (len == 0) { @@ -3207,6 +3386,53 @@ CallResult arrayPrototypePop(void *, Runtime &runtime) { return lv.element.get(); } +/// Fast path for Array.prototype.shift() when the array is a normal array. +/// \param arr the array to shift the first element from. +/// \param len the length of \p arr (before shifting). +/// \pre \p arr is an array with fast index properties and there are no +/// index-like properties in any parents. +/// \pre The storage for \p arr starts at 0 and ends at its length. +static CallResult arrayPrototypeShiftFastPath( + Runtime &runtime, + Handle arr, + uint32_t len) { + if (LLVM_UNLIKELY(len == 0)) { + return HermesValue::encodeUndefinedValue(); + } + + // May allocate, do the encoding outside NoAllocScope. + auto newLen = SmallHermesValue::encodeNumberValue(len - 1, runtime); + + // Perform the actual shift. + NoAllocScope noAlloc{runtime}; + auto *storage = arr->getIndexedStorageUnsafe(runtime); + assert(len <= storage->size() && "shifted range must be within the storage"); + SmallHermesValue first = storage->at(0); + // Move every element to the left one slot. Holes (empty values) are moved + // like ordinary values, which is unobservable because the fast path check + // has ensured there are no index-like properties in the prototype chain. + JSArray::StorageType::GCHVType::copy( + storage->data() + 1, + storage->data() + len, + storage->data(), + storage, + runtime.getHeap()); + JSArray::StorageType::resizeWithinCapacity(storage, runtime, len - 1); + // Set the elemCount to the end of the storage, which we know is correct + // because we just shrank the storage to len-1 elements and we've already + // checked the bounds of the storage in the fast path check. + assert(storage->size() == len - 1 && arr->getBeginIndex() == 0); + arr->setElemCountUnsafe(len - 1); + // We've already checked that the length is not readonly. + JSArray::putLengthUnsafe(*arr, runtime, newLen); + + // Fast path check has ensured there's no other elements up the prototype + // chain that can have values at index-like property names, so if we see + // 'empty' in the storage we need to return 'undefined'. + return first.isEmpty() ? HermesValue::encodeUndefinedValue() + : first.unboxToHV(runtime); +} + CallResult arrayPrototypeShift(void *, Runtime &runtime) { NativeArgs args = runtime.getCurrentFrame().getNativeArgs(); struct : Locals { @@ -3221,24 +3447,24 @@ CallResult arrayPrototypeShift(void *, Runtime &runtime) { } lv; LocalsRAII lraii{runtime, &lv}; - GCScope gcScope(runtime); - auto objRes = toObject(runtime, args.getThisHandle()); - if (LLVM_UNLIKELY(objRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; + uint64_t len; + if (canUseArrayFastPath(runtime, args, lv.O, len)) { + // Ensure the fast path does not leak any handles. + NoLeakHandleScope noLeaks{runtime}; + return arrayPrototypeShiftFastPath( + runtime, args.vmcastThis(), len); } - lv.O.castAndSetHermesValue(*objRes); - auto propRes = JSObject::getNamed_RJS( - lv.O, runtime, Predefined::getSymbolID(Predefined::length)); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; - } - lv.lenProp = std::move(*propRes); - auto intRes = toLengthU64(runtime, lv.lenProp); - if (LLVM_UNLIKELY(intRes == ExecutionStatus::EXCEPTION)) { + // The slow path may create additional handles, so create a GCScope to avoid + // leaking them. + GCScope gcScope(runtime); + + // If the fast path has not populated the object, do that now. + if (LLVM_UNLIKELY( + ensureObjectAndLength(runtime, args.getThisHandle(), lv.O, len) == + ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - uint64_t len = *intRes; if (len == 0) { // Need to set length to 0 per spec. @@ -3253,9 +3479,8 @@ CallResult arrayPrototypeShift(void *, Runtime &runtime) { return HermesValue::encodeUndefinedValue(); } - if (LLVM_UNLIKELY( - (propRes = getIndexed_RJS(runtime, lv.O, 0)) == - ExecutionStatus::EXCEPTION)) { + CallResult> propRes = getIndexed_RJS(runtime, lv.O, 0); + if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } lv.first = std::move(*propRes); @@ -3263,7 +3488,6 @@ CallResult arrayPrototypeShift(void *, Runtime &runtime) { lv.from = HermesValue::encodeTrustedNumberValue(1); // Move every element to the left one slot. - // TODO: Add a fast path for actual arrays. while (lv.from->getDouble() < len) { GCScopeMarkerRAII marker{gcScope}; @@ -3436,10 +3660,7 @@ indexOfHelper(Runtime &runtime, NativeArgs args, const bool reverse) { // Search for the element. auto searchElement = args.getArgHandle(0); // Fast path. - if (LLVM_LIKELY( - arrHandle && - arrayFastPathCheck( - runtime, arrHandle.get(), nullptr, (uint32_t)len))) { + if (LLVM_LIKELY(arrHandle && canReadElementsFast(runtime, arrHandle, len))) { // SmallHermesValue::getRaw() may return uintptr_t, which can be a // different type from uint32_t/uint64_t on some platforms (e.g. // unsigned long vs unsigned long long). @@ -3590,6 +3811,71 @@ indexOfHelper(Runtime &runtime, NativeArgs args, const bool reverse) { return HermesValue::encodeTrustedNumberValue(-1); } +/// Fast path for Array.prototype.unshift() when the array is a normal array. +/// \param arr the array to prepend the arguments to. +/// \param len the length of \p arr (before prepending). +/// \param args the original NativeArgs to unshift(). +/// \pre \p arr is an array with fast index properties and there are no +/// index-like properties in any parents. +/// \pre The storage for \p arr starts at 0 and ends at its length. +/// \pre There is at least one argument, and the resulting length fits in +/// uint32. +static CallResult arrayPrototypeUnshiftFastPath( + Runtime &runtime, + Handle arr, + uint32_t len, + const NativeArgs &args) { + uint32_t argCount = args.getArgCount(); + assert(argCount > 0 && "fast path requires at least one argument"); + assert( + UINT32_MAX - len >= argCount && + "integer overflow checked before calling fast path"); + uint32_t finalLen = len + argCount; + + // Expand the array to make room for the new items. + // Length property will be set at the end. + if (LLVM_UNLIKELY( + JSArray::increaseStorageEndIndex(arr, runtime, finalLen) == + ExecutionStatus::EXCEPTION)) { + return ExecutionStatus::EXCEPTION; + } + + { + // Move every element to the right by argCount, copying backwards to + // avoid overwriting elements that have not been moved yet. Holes + // (empty values) are moved like ordinary values, which is unobservable + // because the fast path check has ensured there are no index-like + // properties in the prototype chain. + NoAllocScope noAlloc{runtime}; + auto *storage = arr->getIndexedStorageUnsafe(runtime); + assert( + finalLen <= storage->size() && + "destination range must be within the storage"); + JSArray::StorageType::GCHVType::copy_backward( + storage->data(), + storage->data() + len, + storage->data() + finalLen, + storage, + runtime.getHeap()); + } + + // Copy the arguments into the beginning of the array. + for (uint32_t i = 0; i < argCount; ++i) { + // Perform potential allocation before dereferencing arr. + SmallHermesValue shv = + SmallHermesValue::encodeHermesValue(args.getArg(i), runtime); + JSArray::unsafeSetExistingElementAt(*arr, runtime, i, shv); + } + + auto shv = SmallHermesValue::encodeNumberValue(finalLen, runtime); + // Since we have already checked that the hidden class is unchanged, and + // updated the storage end index, we can just directly store the new length + // to the corresponding slot in the JSArray. + JSArray::putLengthUnsafe(*arr, runtime, shv); + + return HermesValue::encodeTrustedNumberValue(finalLen); +} + CallResult arrayPrototypeUnshift(void *, Runtime &runtime) { NativeArgs args = runtime.getCurrentFrame().getNativeArgs(); struct : Locals { @@ -3603,26 +3889,29 @@ CallResult arrayPrototypeUnshift(void *, Runtime &runtime) { PinnedValue fromNameTmpStorage; } lv; LocalsRAII lraii{runtime, &lv}; - GCScope gcScope(runtime); - auto objRes = toObject(runtime, args.getThisHandle()); - if (LLVM_UNLIKELY(objRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; + size_t argCount = args.getArgCount(); + uint64_t len; + // Note that canUseArrayFastPath() is called first, so that it populates O + // and len for the slow path even when the checks after it fail. + if (canUseArrayFastPath(runtime, args, lv.O, len) && argCount > 0 && + LLVM_LIKELY(len < UINT32_MAX - argCount)) { + // Ensure the fast path does not leak any handles. + NoLeakHandleScope noLeaks{runtime}; + return arrayPrototypeUnshiftFastPath( + runtime, args.vmcastThis(), len, args); } - lv.O.castAndSetHermesValue(*objRes); - auto propRes = JSObject::getNamed_RJS( - lv.O, runtime, Predefined::getSymbolID(Predefined::length)); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { - return ExecutionStatus::EXCEPTION; - } - lv.lenProp = std::move(*propRes); - auto intRes = toLengthU64(runtime, lv.lenProp); - if (LLVM_UNLIKELY(intRes == ExecutionStatus::EXCEPTION)) { + // The slow path may create additional handles, so create a GCScope to avoid + // leaking them. + GCScope gcScope(runtime); + + // If the fast path has not populated the object, do that now. + if (LLVM_UNLIKELY( + ensureObjectAndLength(runtime, args.getThisHandle(), lv.O, len) == + ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - uint64_t len = *intRes; - size_t argCount = args.getArgCount(); // 4. If argCount > 0, then if (argCount > 0) { @@ -3637,7 +3926,6 @@ CallResult arrayPrototypeUnshift(void *, Runtime &runtime) { uint64_t j = 0; // Move elements to the right by argCount to account for the new elements. - // TODO: Add a fast path for actual arrays. auto marker = gcScope.createMarker(); while (k > 0) { gcScope.flushToMarker(marker); @@ -3755,6 +4043,10 @@ everySomeHelper(Runtime &runtime, NativeArgs args, const bool every) { "Array.prototype.every() requires a callable argument"); } + // Reads elements directly out of the indexed storage when possible. + ElementReader reader{ + runtime, lv.O, len, lv.tmpPropNameStorage, lv.descObj}; + // Loop through and run the callback. auto marker = gcScope.createMarker(); // Index to check the callback on. @@ -3762,18 +4054,13 @@ everySomeHelper(Runtime &runtime, NativeArgs args, const bool every) { while (k < len) { gcScope.flushToMarker(marker); - ComputedPropertyDescWithSymStorage desc{lv.tmpPropNameStorage}; lv.k = HermesValue::encodeTrustedNumberValue(k); - JSObject::getComputedPrimitiveDescriptor( - lv.O, runtime, lv.k, lv.descObj, desc); - CallResult> propRes = JSObject::getComputedPropertyValue_RJS( - lv.O, runtime, lv.descObj, desc, lv.k); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { + CallResult kPresent = reader.read(lv.k, lv.kValue); + if (LLVM_UNLIKELY(kPresent == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - if (LLVM_LIKELY(!(*propRes)->isEmpty())) { + if (*kPresent) { // kPresent is true, call the callback on the kth element. - lv.kValue = std::move(*propRes); auto callRes = Callable::executeCall3( callbackFn, runtime, @@ -3823,9 +4110,10 @@ CallResult arrayPrototypeMap(void *, Runtime &runtime) { PinnedValue<> lenProp; PinnedValue A; PinnedValue<> k; + PinnedValue<> kValue; + PinnedValue<> value; PinnedValue descObj; PinnedValue tmpPropNameStorage; - PinnedValue<> value; } lv; LocalsRAII lraii{runtime, &lv}; @@ -3867,28 +4155,26 @@ CallResult arrayPrototypeMap(void *, Runtime &runtime) { // Current index to execute callback on. lv.k = HermesValue::encodeTrustedNumberValue(0); + // Reads elements directly out of the indexed storage when possible. + ElementReader reader{ + runtime, lv.O, len, lv.tmpPropNameStorage, lv.descObj}; + // Main loop to execute callback and store the results in A. - // TODO: Implement a fast path for actual arrays. auto marker = gcScope.createMarker(); while (lv.k->getDouble() < len) { gcScope.flushToMarker(marker); - ComputedPropertyDescWithSymStorage desc{lv.tmpPropNameStorage}; - JSObject::getComputedPrimitiveDescriptor( - lv.O, runtime, lv.k, lv.descObj, desc); - CallResult> propRes = JSObject::getComputedPropertyValue_RJS( - lv.O, runtime, lv.descObj, desc.get(), lv.k); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { + CallResult kPresent = reader.read(lv.k, lv.kValue); + if (LLVM_UNLIKELY(kPresent == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - if (LLVM_LIKELY(!(*propRes)->isEmpty())) { + if (*kPresent) { // kPresent is true, execute callback and store result in A[k]. - auto kValue = std::move(*propRes); auto callRes = Callable::executeCall3( callbackFn, runtime, args.getArgHandle(1), - kValue.get(), + lv.kValue.get(), lv.k.get(), lv.O.getHermesValue()); if (LLVM_UNLIKELY(callRes == ExecutionStatus::EXCEPTION)) { @@ -3960,21 +4246,20 @@ CallResult arrayPrototypeFilter(void *, Runtime &runtime) { // Index to copy to in the new array. uint32_t to = 0; + // Reads elements directly out of the indexed storage when possible. + ElementReader reader{ + runtime, lv.O, len, lv.tmpPropNameStorage, lv.descObj}; + auto marker = gcScope.createMarker(); while (k < len) { gcScope.flushToMarker(marker); - ComputedPropertyDescWithSymStorage desc{lv.tmpPropNameStorage}; lv.k = HermesValue::encodeTrustedNumberValue(k); - JSObject::getComputedPrimitiveDescriptor( - lv.O, runtime, lv.k, lv.descObj, desc); - CallResult> propRes = JSObject::getComputedPropertyValue_RJS( - lv.O, runtime, lv.descObj, desc.get(), lv.k); - if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { + CallResult kPresent = reader.read(lv.k, lv.kValue); + if (LLVM_UNLIKELY(kPresent == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } - if (LLVM_LIKELY(!(*propRes)->isEmpty())) { - lv.kValue = std::move(*propRes); + if (*kPresent) { // Call the callback. auto callRes = Callable::executeCall3( callbackFn, @@ -4537,10 +4822,7 @@ CallResult arrayPrototypeIncludes(void *, Runtime &runtime) { } // Fast path. - if (LLVM_LIKELY( - arrHandle && - arrayFastPathCheck( - runtime, arrHandle.get(), nullptr, (uint32_t)len))) { + if (LLVM_LIKELY(arrHandle && canReadElementsFast(runtime, arrHandle, len))) { using SearchType = std::conditional_t< sizeof(SmallHermesValue::RawType) == 4, uint32_t, diff --git a/test/hermes/array-fastpath.js b/test/hermes/array-fastpath.js new file mode 100644 index 00000000000..b7956a97d57 --- /dev/null +++ b/test/hermes/array-fastpath.js @@ -0,0 +1,553 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// RUN: %hermes -target=HBC -O %s | %FileCheck --match-full-lines %s +// RUN: %hermes -target=HBC -O -emit-binary -out %t.hbc %s && %hermes %t.hbc | %FileCheck --match-full-lines %s +// RUN: %shermes -exec %s | %FileCheck --match-full-lines %s +"use strict"; + +// Tests for the fast paths of the Array.prototype methods that access the +// indexed storage directly: shift/unshift/slice/splice, and the ones that run +// a callback per element (forEach/map/filter/every/some). Covers the fast +// paths themselves and the conditions that must make them fall back to the +// generic path: holes, mutation from an argument coercion or from a callback +// mid-iteration, accessors, modified prototypes, and frozen/sealed arrays. + +print('shift'); +// CHECK-LABEL: shift +var a = [1, 2, 3]; +print(a.shift(), a, a.length); +// CHECK-NEXT: 1 2,3 2 +a = []; +print(a.shift(), a.length); +// CHECK-NEXT: undefined 0 +a = [42]; +print(a.shift(), a.length); +// CHECK-NEXT: 42 0 +a.push(7); +print(a); +// CHECK-NEXT: 7 +a = [, 1]; +print(a.shift(), a, 0 in a); +// CHECK-NEXT: undefined 1 true +a = [1, , 3]; +print(a.shift(), a, 0 in a, 1 in a); +// CHECK-NEXT: 1 ,3 false true +a = [1.5, 2.5, 3.5]; +print(a.shift(), a); +// CHECK-NEXT: 1.5 2.5,3.5 +// Length extended past the storage (elemCount != len) must take the +// generic path. +a = [1, 2]; +a.length = 5; +print(a.shift(), a.length, a); +// CHECK-NEXT: 1 4 2,,, +// Storage that does not begin at index 0 must take the generic path. +a = []; +a[3] = 'x'; +print(a.shift(), a.length, 3 in a, a[2]); +// CHECK-NEXT: undefined 3 false x +a = [1, 2]; +Object.defineProperty(a, "length", {writable: false}); +try { + a.shift(); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true +a = Object.freeze([1, 2]); +try { + a.shift(); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true +var obj = {0: 'a', 1: 'b', length: 2}; +print(Array.prototype.shift.call(obj), obj[0], obj.length); +// CHECK-NEXT: a b 1 +// Heap-allocated elements (strings, boxed doubles) are moved with write +// barriers by the fast path; the contents must survive a GC. +a = ['x', 'y', 1.5, 'z']; +print(a.shift(), a); +// CHECK-NEXT: x y,1.5,z +gc(); +print(a); +// CHECK-NEXT: y,1.5,z +// An own accessor clears fast index properties; the generic path calls the +// getter while moving elements down. +a = [1, 2, 3]; +Object.defineProperty( + a, 2, {get: function() { return 42; }, configurable: true}); +print(a.shift(), a, a.length); +// CHECK-NEXT: 1 2,42 2 +// this coercion and length access failures propagate from the slow path. +try { + Array.prototype.shift.call(null); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true +try { + Array.prototype.shift.call({get length() { throw new Error('len boom'); }}); +} catch (e) { + print(e.message); +} +// CHECK-NEXT: len boom + +print('unshift'); +// CHECK-LABEL: unshift +a = [3, 4]; +print(a.unshift(1, 2), a); +// CHECK-NEXT: 4 1,2,3,4 +a = []; +print(a.unshift(1), a); +// CHECK-NEXT: 1 1 +a = [1, 2]; +print(a.unshift(), a); +// CHECK-NEXT: 2 1,2 +a = [1, , 3]; +print(a.unshift(0), a, 2 in a, 3 in a); +// CHECK-NEXT: 4 0,1,,3 false true +a = [1, 2, 3, 4]; +a.unshift(-3, -2, -1, 0); +print(a); +// CHECK-NEXT: -3,-2,-1,0,1,2,3,4 +// Shrinking keeps the storage capacity, so this unshift grows within +// capacity and takes the inline (no realloc) path. +a = [1, 2, 3]; +a.length = 2; +print(a.unshift(9), a); +// CHECK-NEXT: 3 9,1,2 +// Length extended past the storage (elemCount != len) must take the +// generic path; the trailing holes stay holes. +a = [1, 2]; +a.length = 4; +print(a.unshift(0), a, 3 in a); +// CHECK-NEXT: 5 0,1,2,, false +a = Object.seal([1]); +try { + a.unshift(0); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true +// A hole at index 0 is moved right like an ordinary value. +a = [, 'b']; +print(a.unshift('z'), a, 1 in a); +// CHECK-NEXT: 3 z,,b false +// Heap values and boxed doubles are moved/encoded with write barriers. +a = ['c', 'd']; +print(a.unshift('a', 1.5), a); +// CHECK-NEXT: 4 a,1.5,c,d +gc(); +print(a); +// CHECK-NEXT: a,1.5,c,d +// Read-only length changes the hidden class, so the fast path is skipped +// and the generic path throws when growing the array. +a = [1, 2]; +Object.defineProperty(a, "length", {writable: false}); +try { + a.unshift(0); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true +// Generic path on a plain object, and this coercion failure. +obj = {0: 'a', 1: 'b', length: 2}; +print(Array.prototype.unshift.call(obj, 'z'), + obj[0], obj[1], obj[2], obj.length); +// CHECK-NEXT: 3 z a b 3 +try { + Array.prototype.unshift.call(undefined, 1); +} catch (e) { + print(e instanceof TypeError); +} +// CHECK-NEXT: true + +print('slice'); +// CHECK-LABEL: slice +a = [1, 2, 3, 4, 5]; +print(a.slice(1, 3), a.slice(-2), a.slice(0, -1), a.slice(3, 1).length, a); +// CHECK-NEXT: 2,3 4,5 1,2,3,4 0 1,2,3,4,5 +a = [1, , 3]; +var s = a.slice(0, 3); +print(s.length, 0 in s, 1 in s, 2 in s); +// CHECK-NEXT: 3 true false true +// Heap-allocated elements (strings, boxed doubles) are copied with write +// barriers by the fast path. +a = ['x', 1.5, 'y']; +print(a.slice(1, 3)); +// CHECK-NEXT: 1.5,y +s = a.slice(0); +gc(); +print(s); +// CHECK-NEXT: x,1.5,y +// The fast path must copy the storage, not alias it. +a = [1, 2, 3]; +s = a.slice(0); +a[0] = 99; +s[2] = 'i'; +print(s, a); +// CHECK-NEXT: 1,2,i 99,2,3 +// Length extended past the storage (elemCount != len) must take the +// generic path. +a = [1, 2]; +a.length = 4; +s = a.slice(0); +print(s.length, s[0], s[1], 2 in s); +// CHECK-NEXT: 4 1 2 false +// Mutating the array from valueOf during argument coercion must not use a +// stale fast path; the captured length is used with per-index HasProperty. +a = [1, 2, 3, 4, 5]; +s = a.slice({valueOf: function() { a.length = 2; return 0; }}); +print(s.length, s[0], s[1], 2 in s); +// CHECK-NEXT: 5 1 2 false +// Growing the array from valueOf also fails the fast-path check; the +// captured length bounds the result. +a = [1, 2, 3]; +s = a.slice({valueOf: function() { a.push(4, 5); return 0; }}); +print(s.length, s); +// CHECK-NEXT: 3 1,2,3 +// Installing an accessor from valueOf clears fast index properties; the +// generic path calls the getter. +a = [1, 2, 3]; +s = a.slice({valueOf: function() { + Object.defineProperty(a, 1, {get: function() { return 'got'; }}); + return 0; +}}); +print(s); +// CHECK-NEXT: 1,got,3 +// Mutation from valueOf that keeps the same shape still permits the fast +// path, which must observe the new values. +a = [1, 2, 3]; +s = a.slice({valueOf: function() { a[1] = 'mut'; return 1; }}); +print(s); +// CHECK-NEXT: mut,3 + +print('forEach'); +// CHECK-LABEL: forEach +a = [10, 20, 30]; +var acc = []; +a.forEach(function(v, i) { acc.push(v, i); }); +print(acc); +// CHECK-NEXT: 10,0,20,1,30,2 +a = [1, , 3]; +acc = []; +a.forEach(function(v, i) { acc.push(i); }); +print(acc); +// CHECK-NEXT: 0,2 +// Length extended past the storage (elemCount != len) must take the +// generic path; the trailing holes are not visited. +a = [1, 2]; +a.length = 4; +acc = []; +a.forEach(function(v, i) { acc.push(v, i); }); +print(acc); +// CHECK-NEXT: 1,0,2,1 +// Callback shrinks the array mid-iteration. +a = [1, 2, 3, 4, 5]; +acc = []; +a.forEach(function(v, i) { acc.push(v); if (i === 1) a.length = 2; }); +print(acc); +// CHECK-NEXT: 1,2 +// Callback grows the array mid-iteration; new elements are not visited. +a = [1, 2]; +acc = []; +a.forEach(function(v) { acc.push(v); a.push(99); }); +print(acc); +// CHECK-NEXT: 1,2 +// Callback installs an accessor mid-iteration. +a = [1, 2, 3]; +acc = []; +a.forEach(function(v, i) { + acc.push(v); + if (i === 0) { + Object.defineProperty( + a, 2, {get: function() { return 42; }, configurable: true}); + } +}); +print(acc); +// CHECK-NEXT: 1,2,42 +// Callback swaps the prototype for a Proxy mid-iteration; the hole at index +// 1 must then be read through the Proxy. +a = [1, , 3]; +acc = []; +a.forEach(function(v, i) { + if (i === 0) { + Object.setPrototypeOf(a, new Proxy({}, { + has: function(t, k) { return k === '1'; }, + get: function(t, k) { return k === '1' ? 111 : undefined; }, + })); + } + acc.push(v); +}); +print(acc); +// CHECK-NEXT: 1,111,3 +// Callback throws; iteration stops and the exception propagates. +a = [1, 2, 3]; +acc = []; +try { + a.forEach(function(v, i) { + acc.push(v); + if (i === 1) + throw new Error('forEach boom'); + }); +} catch (e) { + print(acc, e.message); +} +// CHECK-NEXT: 1,2 forEach boom +// Callback triggers GC mid-iteration; the fast path must re-read the +// indexed storage on every iteration rather than caching a raw pointer. +a = [1.5, 'two', 3.5]; +acc = []; +a.forEach(function(v) { gc(); acc.push(v); }); +print(acc); +// CHECK-NEXT: 1.5,two,3.5 +// Callback deletes an element ahead; the new hole is skipped because +// nothing on the prototype chain provides it. +a = [1, 2, 3]; +acc = []; +a.forEach(function(v, i) { if (i === 0) delete a[2]; acc.push(v); }); +print(acc); +// CHECK-NEXT: 1,2 +// Non-array this takes the generic read path. +acc = []; +Array.prototype.forEach.call({0: 'a', 2: 'c', length: 3}, function(v, i) { + acc.push(v, i); +}); +print(acc); +// CHECK-NEXT: a,0,c,2 + +print('map'); +// CHECK-LABEL: map +a = [1, 2, 3]; +print(a.map(function(x) { return x * 2; })); +// CHECK-NEXT: 2,4,6 +a = [1, , 3]; +var mp = a.map(function(x) { return x + 1; }); +print(mp.length, mp[0], 1 in mp, mp[2]); +// CHECK-NEXT: 3 2 false 4 +// Writes from the callback are observed by later iterations. +a = [1, 2, 3]; +print(a.map(function(x, i) { if (i === 0) a[2] = 30; return x; })); +// CHECK-NEXT: 1,2,30 +// Callback shrinks the array mid-iteration; missing indices become holes +// but the result keeps the original length. +a = [1, 2, 3, 4]; +mp = a.map(function(x, i) { if (i === 0) a.length = 2; return x * 10; }); +print(mp.length, mp[0], mp[1], 2 in mp, 3 in mp); +// CHECK-NEXT: 4 10 20 false false +// Callback throws; the exception propagates. +a = [1, 2, 3]; +try { + a.map(function(x, i) { + if (i === 1) + throw new Error('map boom'); + return x; + }); +} catch (e) { + print(e.message); +} +// CHECK-NEXT: map boom +// Callback triggers GC mid-iteration. +a = [1.5, 'two', 3.5]; +print(a.map(function(v) { gc(); return v; })); +// CHECK-NEXT: 1.5,two,3.5 +// Callback installs an accessor mid-iteration; the emptied indexed slot is +// then read through the property path. +a = [1, 2, 3]; +mp = a.map(function(v, i) { + if (i === 0) { + Object.defineProperty( + a, 2, {get: function() { return 42; }, configurable: true}); + } + return v; +}); +print(mp); +// CHECK-NEXT: 1,2,42 +// Non-array this takes the generic read path. +mp = Array.prototype.map.call({0: 2, 1: 3, length: 2}, function(v) { + return v * v; +}); +print(mp); +// CHECK-NEXT: 4,9 + +print('filter'); +// CHECK-LABEL: filter +a = [1, 2, 3, 4, 5, 6]; +print(a.filter(function(x) { return x % 2 === 0; })); +// CHECK-NEXT: 2,4,6 +a = [1, , 3]; +print(a.filter(function() { return true; })); +// CHECK-NEXT: 1,3 +a = [1, 2, 3, 4]; +print(a.filter(function(x, i) { if (i === 0) a.length = 2; return true; })); +// CHECK-NEXT: 1,2 +// Callback throws; the exception propagates. +a = [1, 2, 3]; +try { + a.filter(function(x, i) { + if (i === 1) + throw new Error('filter boom'); + return true; + }); +} catch (e) { + print(e.message); +} +// CHECK-NEXT: filter boom +// Callback triggers GC mid-iteration. +a = [1.5, 'two', 3.5]; +print(a.filter(function(v) { gc(); return true; })); +// CHECK-NEXT: 1.5,two,3.5 +// Callback pollutes Array.prototype mid-iteration; the hole at index 1 is +// then read through the prototype. +a = [1, , 3]; +var flt = a.filter(function(v, i) { + if (i === 0) + Array.prototype[1] = 'p1'; + return true; +}); +delete Array.prototype[1]; +print(flt); +// CHECK-NEXT: 1,p1,3 + +print('every/some'); +// CHECK-LABEL: every/some +a = [2, 4, 6]; +print(a.every(function(x) { return x % 2 === 0; }), + a.some(function(x) { return x > 5; })); +// CHECK-NEXT: true true +// Holes are skipped. +a = [1, , 3]; +acc = []; +print(a.every(function(v, i) { acc.push(v, i); return true; }), acc); +// CHECK-NEXT: true 1,0,3,2 +// Callback shrinks the array; the removed slots read as holes. +a = [1, 2, 3, 4]; +acc = []; +print(a.some(function(v, i) { + acc.push(v); + if (i === 0) + a.length = 2; + return false; +}), acc); +// CHECK-NEXT: false 1,2 +// Callback throws; the exception propagates. +a = [1, 2, 3]; +try { + a.every(function(x, i) { + if (i === 1) + throw new Error('every boom'); + return true; + }); +} catch (e) { + print(e.message); +} +// CHECK-NEXT: every boom +// Callback triggers GC mid-iteration. +a = [1.5, 'two', 3.5]; +acc = []; +print(a.every(function(v) { gc(); acc.push(v); return true; }), acc); +// CHECK-NEXT: true 1.5,two,3.5 +// Callback pollutes Array.prototype mid-iteration; the hole at index 1 is +// then read through the prototype. +a = [1, , 3]; +acc = []; +print(a.some(function(v, i) { + if (i === 0) + Array.prototype[1] = 'p1'; + acc.push(v); + return false; +}), acc); +delete Array.prototype[1]; +// CHECK-NEXT: false 1,p1,3 +// Non-array receivers take the generic path. +print(Array.prototype.every.call({0: 2, 1: 4, length: 2}, function(v) { + return v % 2 === 0; +})); +// CHECK-NEXT: true + +print('splice'); +// CHECK-LABEL: splice +// Equal insert and delete counts: neither storage-shifting branch of the +// fast path runs. +a = [1, 2, 3, 4]; +print(a.splice(1, 2, 'x', 'y'), a); +// CHECK-NEXT: 2,3 1,x,y,4 +// Insert-only splice in the middle (overlapping backward copy), then a +// delete-only splice; heap values must survive a GC. +a = ['s1', 's2', 's3']; +print(a.splice(1, 0, 1.5, 'n1').length, a.length); +// CHECK-NEXT: 0 5 +gc(); +print(a); +// CHECK-NEXT: s1,1.5,n1,s2,s3 +print(a.splice(2, 2), a); +// CHECK-NEXT: n1,s2 s1,1.5,s3 +// Deleting more than inserting, with at least one item to insert: the +// storage shrinks and only then are the arguments copied in. Encoding a +// double argument may allocate, so the array must be left consistent (its +// element count within the shrunk storage) across that allocation. +a = [1, 2, 3, 4, 5]; +print(a.splice(1, 3, 2.5), a, a.length); +// CHECK-NEXT: 2,3,4 1,2.5,5 3 +gc(); +print(a); +// CHECK-NEXT: 1,2.5,5 +a = ['a', 'b', 'c', 'd', 'e']; +print(a.splice(0, 4, 'z', 1.5), a); +// CHECK-NEXT: a,b,c,d z,1.5,e +gc(); +print(a); +// CHECK-NEXT: z,1.5,e + +print('chained'); +// CHECK-LABEL: chained +// Consecutive fast-path operations must leave the array in a consistent +// state (storage bounds, length property) for the next one. +a = [1, 2, 3, 4]; +print(a.shift(), a, a.length); +// CHECK-NEXT: 1 2,3,4 3 +print(a.unshift(0, 1), a, a.length); +// CHECK-NEXT: 5 0,1,2,3,4 5 +print(a.slice(1, 3), a.map(function(x) { return x * 2; })); +// CHECK-NEXT: 1,2 0,2,4,6,8 +print(a.shift(), a.pop(), a.push(9), a); +// CHECK-NEXT: 0 4 4 1,2,3,9 + +print('subclass'); +// CHECK-LABEL: subclass +// Array subclass instances have a different parent than Array.prototype, +// so they must take the generic path. (Hermes does not implement +// Symbol.species: slice/map/filter return plain arrays on both paths.) +class MyArr extends Array {} +var m = MyArr.of(1, 2, 3); +print(m.shift(), '' + m); +// CHECK-NEXT: 1 2,3 +print(m.unshift(0), '' + m); +// CHECK-NEXT: 3 0,2,3 +print(m.slice(1), m.slice(1) instanceof MyArr, + m.map(function(x) { return x; }) instanceof MyArr); +// CHECK-NEXT: 2,3 false false +acc = []; +m.forEach(function(v) { acc.push(v); }); +print(acc, m.filter(function() { return true; }) instanceof MyArr); +// CHECK-NEXT: 0,2,3 false + +print('prototype pollution'); +// CHECK-LABEL: prototype pollution +// An element on Array.prototype forces the generic path; holes must read +// through the prototype chain. +Array.prototype[1] = 'polluted'; +a = [1, , 3]; +acc = []; +a.forEach(function(v) { acc.push(v); }); +print(acc); +// CHECK-NEXT: 1,polluted,3 +print(a.shift(), a[0]); +// CHECK-NEXT: 1 polluted +delete Array.prototype[1];