/* * Copyright (C) 1999-2000 Harri Porten (porten@kde.org) * Copyright (C) 2003-2020 Apple Inc. All rights reserved. * Copyright (C) 2003 Peter Kelly (pmk@post.com) * Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "config.h" #include "JSArray.h" #include "ArrayPrototype.h" #include "JSArrayInlines.h" #include "JSCInlines.h" #include "PropertyNameArray.h" #include "TypeError.h" #include namespace JSC { const ASCIILiteral LengthExceededTheMaximumArrayLengthError { "Length exceeded the maximum array length"_s }; STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSArray); const ClassInfo JSArray::s_info = {"Array", &JSNonFinalObject::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSArray)}; JSArray* JSArray::tryCreateUninitializedRestricted(ObjectInitializationScope& scope, GCDeferralContext* deferralContext, Structure* structure, unsigned initialLength) { VM& vm = scope.vm(); if (UNLIKELY(initialLength > MAX_STORAGE_VECTOR_LENGTH)) return nullptr; unsigned outOfLineStorage = structure->outOfLineCapacity(); Butterfly* butterfly; IndexingType indexingType = structure->indexingType(); if (LIKELY(!hasAnyArrayStorage(indexingType))) { ASSERT( hasUndecided(indexingType) || hasInt32(indexingType) || hasDouble(indexingType) || hasContiguous(indexingType)); unsigned vectorLength = Butterfly::optimalContiguousVectorLength(structure, initialLength); void* temp = vm.jsValueGigacageAuxiliarySpace.allocateNonVirtual( vm, Butterfly::totalSize(0, outOfLineStorage, true, vectorLength * sizeof(EncodedJSValue)), deferralContext, AllocationFailureMode::ReturnNull); if (UNLIKELY(!temp)) return nullptr; butterfly = Butterfly::fromBase(temp, 0, outOfLineStorage); butterfly->setVectorLength(vectorLength); butterfly->setPublicLength(initialLength); if (hasDouble(indexingType)) { for (unsigned i = initialLength; i < vectorLength; ++i) butterfly->contiguousDouble().atUnsafe(i) = PNaN; } else { for (unsigned i = initialLength; i < vectorLength; ++i) butterfly->contiguous().atUnsafe(i).clear(); } } else { ASSERT( indexingType == ArrayWithSlowPutArrayStorage || indexingType == ArrayWithArrayStorage); static constexpr unsigned indexBias = 0; unsigned vectorLength = ArrayStorage::optimalVectorLength(indexBias, structure, initialLength); void* temp = vm.jsValueGigacageAuxiliarySpace.allocateNonVirtual( vm, Butterfly::totalSize(indexBias, outOfLineStorage, true, ArrayStorage::sizeFor(vectorLength)), deferralContext, AllocationFailureMode::ReturnNull); if (UNLIKELY(!temp)) return nullptr; butterfly = Butterfly::fromBase(temp, indexBias, outOfLineStorage); *butterfly->indexingHeader() = indexingHeaderForArrayStorage(initialLength, vectorLength); ArrayStorage* storage = butterfly->arrayStorage(); storage->m_indexBias = indexBias; storage->m_sparseMap.clear(); storage->m_numValuesInVector = initialLength; for (unsigned i = initialLength; i < vectorLength; ++i) storage->m_vector[i].clear(); } JSArray* result = createWithButterfly(vm, deferralContext, structure, butterfly); scope.notifyAllocated(result); return result; } void JSArray::eagerlyInitializeButterfly(ObjectInitializationScope& scope, JSArray* array, unsigned initialLength) { Structure* structure = array->structure(scope.vm()); IndexingType indexingType = structure->indexingType(); Butterfly* butterfly = array->butterfly(); // This function only serves as a companion to tryCreateUninitializedRestricted() // in the event that we really can't defer initialization of the butterfly after all. // tryCreateUninitializedRestricted() already initialized the elements between // initialLength and vector length. We just need to do 0 - initialLength. // ObjectInitializationScope::notifyInitialized() will verify that all elements are // initialized. if (LIKELY(!hasAnyArrayStorage(indexingType))) { if (hasDouble(indexingType)) { for (unsigned i = 0; i < initialLength; ++i) butterfly->contiguousDouble().atUnsafe(i) = PNaN; } else { for (unsigned i = 0; i < initialLength; ++i) butterfly->contiguous().atUnsafe(i).clear(); } } else { ArrayStorage* storage = butterfly->arrayStorage(); for (unsigned i = 0; i < initialLength; ++i) storage->m_vector[i].clear(); } scope.notifyInitialized(array); } void JSArray::setLengthWritable(JSGlobalObject* globalObject, bool writable) { ASSERT(isLengthWritable() || !writable); if (!isLengthWritable() || writable) return; enterDictionaryIndexingMode(globalObject->vm()); SparseArrayValueMap* map = arrayStorage()->m_sparseMap.get(); ASSERT(map); map->setLengthIsReadOnly(); } // https://tc39.es/ecma262/#sec-array-exotic-objects-defineownproperty-p-desc bool JSArray::defineOwnProperty(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, const PropertyDescriptor& descriptor, bool throwException) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSArray* array = jsCast(object); // 2. If P is "length", then // https://tc39.es/ecma262/#sec-arraysetlength if (propertyName == vm.propertyNames->length) { // FIXME: Nothing prevents this from being called on a RuntimeArray, and the length function will always return 0 in that case. unsigned newLength = array->length(); if (descriptor.value()) { newLength = descriptor.value().toUInt32(globalObject); RETURN_IF_EXCEPTION(scope, false); double valueAsNumber = descriptor.value().toNumber(globalObject); RETURN_IF_EXCEPTION(scope, false); if (valueAsNumber != static_cast(newLength)) { throwRangeError(globalObject, scope, "Invalid array length"_s); return false; } } // OrdinaryDefineOwnProperty (https://tc39.es/ecma262/#sec-validateandapplypropertydescriptor) at steps 1.a, 11.a, and 15 is now performed: // 4. If current.[[Configurable]] is false, then // 4.a. If Desc.[[Configurable]] is present and its value is true, return false. if (descriptor.configurablePresent() && descriptor.configurable()) return typeError(globalObject, scope, throwException, UnconfigurablePropertyChangeConfigurabilityError); // 4.b. If Desc.[[Enumerable]] is present and SameValue(Desc.[[Enumerable]], current.[[Enumerable]]) is false, return false. if (descriptor.enumerablePresent() && descriptor.enumerable()) return typeError(globalObject, scope, throwException, UnconfigurablePropertyChangeEnumerabilityError); // 6. Else if SameValue(IsDataDescriptor(current), IsDataDescriptor(Desc)) is false, then // 6.a. If current.[[Configurable]] is false, return false. if (descriptor.isAccessorDescriptor()) return typeError(globalObject, scope, throwException, UnconfigurablePropertyChangeAccessMechanismError); // 7. Else if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then // 7.a. If current.[[Configurable]] is false and current.[[Writable]] is false, then if (!array->isLengthWritable()) { // 7.a.i. If Desc.[[Writable]] is present and Desc.[[Writable]] is true, return false. // This check is unaffected by steps 13-14 of ArraySetLength as they change non-writable descriptors only. if (descriptor.writablePresent() && descriptor.writable()) return typeError(globalObject, scope, throwException, UnconfigurablePropertyChangeWritabilityError); // 7.a.ii. If Desc.[[Value]] is present and SameValue(Desc.[[Value]], current.[[Value]]) is false, return false. // This check also covers step 12 of ArraySetLength, which is only reachable if newLen < oldLen. if (newLength != array->length()) return typeError(globalObject, scope, throwException, ReadonlyPropertyChangeError); } // setLength() clears indices >= newLength and sets correct "length" value if [[Delete]] fails (step 17.b.i) bool success = array->setLength(globalObject, newLength, throwException); EXCEPTION_ASSERT(!scope.exception() || !success); if (descriptor.writablePresent()) array->setLengthWritable(globalObject, descriptor.writable()); return success; } // 4. Else if P is an array index (15.4), then // a. Let index be ToUint32(P). if (Optional optionalIndex = parseIndex(propertyName)) { // b. Reject if index >= oldLen and oldLenDesc.[[Writable]] is false. uint32_t index = optionalIndex.value(); // FIXME: Nothing prevents this from being called on a RuntimeArray, and the length function will always return 0 in that case. if (index >= array->length() && !array->isLengthWritable()) return typeError(globalObject, scope, throwException, "Attempting to define numeric property on array with non-writable length property."_s); // c. Let succeeded be the result of calling the default [[DefineOwnProperty]] internal method (8.12.9) on A passing P, Desc, and false as arguments. // d. Reject if succeeded is false. // e. If index >= oldLen // e.i. Set oldLenDesc.[[Value]] to index + 1. // e.ii. Call the default [[DefineOwnProperty]] internal method (8.12.9) on A passing "length", oldLenDesc, and false as arguments. This call will always return true. // f. Return true. RELEASE_AND_RETURN(scope, array->defineOwnIndexedProperty(globalObject, index, descriptor, throwException)); } RELEASE_AND_RETURN(scope, array->JSObject::defineOwnNonIndexProperty(globalObject, propertyName, descriptor, throwException)); } bool JSArray::getOwnPropertySlot(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot) { VM& vm = globalObject->vm(); JSArray* thisObject = jsCast(object); if (propertyName == vm.propertyNames->length) { unsigned attributes = thisObject->isLengthWritable() ? PropertyAttribute::DontDelete | PropertyAttribute::DontEnum : PropertyAttribute::DontDelete | PropertyAttribute::DontEnum | PropertyAttribute::ReadOnly; slot.setValue(thisObject, attributes, jsNumber(thisObject->length())); return true; } return JSObject::getOwnPropertySlot(thisObject, globalObject, propertyName, slot); } // https://tc39.es/ecma262/#sec-array-exotic-objects-defineownproperty-p-desc bool JSArray::put(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSArray* thisObject = jsCast(cell); if (UNLIKELY(isThisValueAltered(slot, thisObject))) RELEASE_AND_RETURN(scope, ordinarySetSlow(globalObject, thisObject, propertyName, value, slot.thisValue(), slot.isStrictMode())); thisObject->ensureWritable(vm); if (propertyName == vm.propertyNames->length) { if (!thisObject->isLengthWritable()) { if (slot.isStrictMode()) throwTypeError(globalObject, scope, "Array length is not writable"_s); return false; } unsigned newLength = value.toUInt32(globalObject); RETURN_IF_EXCEPTION(scope, false); double valueAsNumber = value.toNumber(globalObject); RETURN_IF_EXCEPTION(scope, false); if (valueAsNumber != static_cast(newLength)) { throwException(globalObject, scope, createRangeError(globalObject, "Invalid array length"_s)); return false; } RELEASE_AND_RETURN(scope, thisObject->setLength(globalObject, newLength, slot.isStrictMode())); } RELEASE_AND_RETURN(scope, JSObject::put(thisObject, globalObject, propertyName, value, slot)); } bool JSArray::deleteProperty(JSCell* cell, JSGlobalObject* globalObject, PropertyName propertyName, DeletePropertySlot& slot) { VM& vm = globalObject->vm(); JSArray* thisObject = jsCast(cell); if (propertyName == vm.propertyNames->length) return false; return JSObject::deleteProperty(thisObject, globalObject, propertyName, slot); } static int compareKeysForQSort(const void* a, const void* b) { unsigned da = *static_cast(a); unsigned db = *static_cast(b); return (da > db) - (da < db); } void JSArray::getOwnSpecialPropertyNames(JSObject*, JSGlobalObject* globalObject, PropertyNameArray& propertyNames, DontEnumPropertiesMode mode) { VM& vm = globalObject->vm(); if (mode == DontEnumPropertiesMode::Include) propertyNames.add(vm.propertyNames->length); } // This method makes room in the vector, but leaves the new space for count slots uncleared. bool JSArray::unshiftCountSlowCase(const AbstractLocker&, VM& vm, DeferGC&, bool addToFront, unsigned count) { ASSERT(cellLock().isLocked()); ArrayStorage* storage = ensureArrayStorage(vm); Butterfly* butterfly = storage->butterfly(); Structure* structure = this->structure(vm); unsigned propertyCapacity = structure->outOfLineCapacity(); unsigned propertySize = structure->outOfLineSize(); // If not, we should have handled this on the fast path. ASSERT(!addToFront || count > storage->m_indexBias); // Step 1: // Gather 4 key metrics: // * usedVectorLength - how many entries are currently in the vector (conservative estimate - fewer may be in use in sparse vectors). // * requiredVectorLength - how many entries are will there be in the vector, after allocating space for 'count' more. // * currentCapacity - what is the current size of the vector, including any pre-capacity. // * desiredCapacity - how large should we like to grow the vector to - based on 2x requiredVectorLength. unsigned length = storage->length(); unsigned oldVectorLength = storage->vectorLength(); unsigned usedVectorLength = std::min(oldVectorLength, length); ASSERT(usedVectorLength <= MAX_STORAGE_VECTOR_LENGTH); // Check that required vector length is possible, in an overflow-safe fashion. if (count > MAX_STORAGE_VECTOR_LENGTH - usedVectorLength) return false; unsigned requiredVectorLength = usedVectorLength + count; ASSERT(requiredVectorLength <= MAX_STORAGE_VECTOR_LENGTH); // The sum of m_vectorLength and m_indexBias will never exceed MAX_STORAGE_VECTOR_LENGTH. ASSERT(storage->vectorLength() <= MAX_STORAGE_VECTOR_LENGTH && (MAX_STORAGE_VECTOR_LENGTH - storage->vectorLength()) >= storage->m_indexBias); unsigned currentCapacity = storage->vectorLength() + storage->m_indexBias; // The calculation of desiredCapacity won't overflow, due to the range of MAX_STORAGE_VECTOR_LENGTH. // FIXME: This code should be fixed to avoid internal fragmentation. It's not super high // priority since increaseVectorLength() will "fix" any mistakes we make, but it would be cool // to get this right eventually. unsigned desiredCapacity = std::min(MAX_STORAGE_VECTOR_LENGTH, std::max(BASE_ARRAY_STORAGE_VECTOR_LEN, requiredVectorLength) << 1); // Step 2: // We're either going to choose to allocate a new ArrayStorage, or we're going to reuse the existing one. void* newAllocBase = nullptr; unsigned newStorageCapacity; bool allocatedNewStorage; // If the current storage array is sufficiently large (but not too large!) then just keep using it. if (currentCapacity > desiredCapacity && isDenseEnoughForVector(currentCapacity, requiredVectorLength)) { newAllocBase = butterfly->base(structure); newStorageCapacity = currentCapacity; allocatedNewStorage = false; } else { const unsigned preCapacity = 0; Butterfly* newButterfly = Butterfly::tryCreateUninitialized(vm, this, preCapacity, propertyCapacity, true, ArrayStorage::sizeFor(desiredCapacity)); if (!newButterfly) return false; newAllocBase = newButterfly->base(preCapacity, propertyCapacity); newStorageCapacity = desiredCapacity; allocatedNewStorage = true; } // Step 3: // Work out where we're going to move things to. // Determine how much of the vector to use as pre-capacity, and how much as post-capacity. // If we're adding to the end, we'll add all the new space to the end. // If the vector had no free post-capacity (length >= m_vectorLength), don't give it any. // If it did, we calculate the amount that will remain based on an atomic decay - leave the // vector with half the post-capacity it had previously. unsigned postCapacity = 0; if (!addToFront) postCapacity = newStorageCapacity - requiredVectorLength; else if (length < storage->vectorLength()) { // Atomic decay, + the post-capacity cannot be greater than what is available. postCapacity = std::min((storage->vectorLength() - length) >> 1, newStorageCapacity - requiredVectorLength); // If we're moving contents within the same allocation, the post-capacity is being reduced. ASSERT(newAllocBase != butterfly->base(structure) || postCapacity < storage->vectorLength() - length); } unsigned newVectorLength = requiredVectorLength + postCapacity; RELEASE_ASSERT(newVectorLength <= MAX_STORAGE_VECTOR_LENGTH); unsigned preCapacity = newStorageCapacity - newVectorLength; Butterfly* newButterfly = Butterfly::fromBase(newAllocBase, preCapacity, propertyCapacity); { // When moving Butterfly's head to adjust property-storage, we must take a structure lock. // Otherwise, concurrent JIT compiler accesses to a property storage which is half-baked due to move for shift / unshift. // If the butterfly is newly allocated one, we do not need to take a lock since this is not changing the old butterfly. ConcurrentJSLocker structureLock(allocatedNewStorage ? nullptr : &structure->lock()); if (addToFront) { ASSERT(count + usedVectorLength <= newVectorLength); gcSafeMemmove(newButterfly->arrayStorage()->m_vector + count, storage->m_vector, sizeof(JSValue) * usedVectorLength); gcSafeMemmove(newButterfly->propertyStorage() - propertySize, butterfly->propertyStorage() - propertySize, sizeof(JSValue) * propertySize + sizeof(IndexingHeader) + ArrayStorage::sizeFor(0)); // We don't need to zero the pre-capacity for the concurrent GC because it is not available to use as property storage. gcSafeZeroMemory(static_cast(newButterfly->base(0, propertyCapacity)), (propertyCapacity - propertySize) * sizeof(JSValue)); if (allocatedNewStorage) { // We will set the vectorLength to newVectorLength. We populated requiredVectorLength // (usedVectorLength + count), which is less. Clear the difference. for (unsigned i = requiredVectorLength; i < newVectorLength; ++i) newButterfly->arrayStorage()->m_vector[i].clear(); } } else if ((newAllocBase != butterfly->base(structure)) || (preCapacity != storage->m_indexBias)) { gcSafeMemmove(newButterfly->propertyStorage() - propertyCapacity, butterfly->propertyStorage() - propertyCapacity, sizeof(JSValue) * propertyCapacity + sizeof(IndexingHeader) + ArrayStorage::sizeFor(0)); gcSafeMemmove(newButterfly->arrayStorage()->m_vector, storage->m_vector, sizeof(JSValue) * usedVectorLength); for (unsigned i = requiredVectorLength; i < newVectorLength; i++) newButterfly->arrayStorage()->m_vector[i].clear(); } newButterfly->arrayStorage()->setVectorLength(newVectorLength); newButterfly->arrayStorage()->m_indexBias = preCapacity; setButterfly(vm, newButterfly); } return true; } bool JSArray::setLengthWithArrayStorage(JSGlobalObject* globalObject, unsigned newLength, bool throwException, ArrayStorage* storage) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned length = storage->length(); if (newLength == length) return true; // If the length is read only then we enter sparse mode, so should enter the following 'if'. ASSERT(isLengthWritable() || storage->m_sparseMap); if (SparseArrayValueMap* map = storage->m_sparseMap.get()) { // Fail if the length is not writable. if (map->lengthIsReadOnly()) return typeError(globalObject, scope, throwException, ReadonlyPropertyWriteError); if (newLength < length) { // Copy any keys we might be interested in into a vector. Vector keys; keys.reserveInitialCapacity(std::min(map->size(), static_cast(length - newLength))); SparseArrayValueMap::const_iterator end = map->end(); for (SparseArrayValueMap::const_iterator it = map->begin(); it != end; ++it) { unsigned index = static_cast(it->key); if (index < length && index >= newLength) keys.append(index); } // Check if the array is in sparse mode. If so there may be non-configurable // properties, so we have to perform deletion with caution, if not we can // delete values in any order. if (map->sparseMode()) { qsort(keys.begin(), keys.size(), sizeof(unsigned), compareKeysForQSort); unsigned i = keys.size(); while (i) { unsigned index = keys[--i]; SparseArrayValueMap::iterator it = map->find(index); ASSERT(it != map->notFound()); if (it->value.attributes() & PropertyAttribute::DontDelete) { storage->setLength(index + 1); return typeError(globalObject, scope, throwException, UnableToDeletePropertyError); } map->remove(it); } } else { for (unsigned i = 0; i < keys.size(); ++i) map->remove(keys[i]); if (map->isEmpty()) deallocateSparseIndexMap(); } } } if (newLength < length) { // Delete properties from the vector. unsigned usedVectorLength = std::min(length, storage->vectorLength()); for (unsigned i = newLength; i < usedVectorLength; ++i) { WriteBarrier& valueSlot = storage->m_vector[i]; bool hadValue = !!valueSlot; valueSlot.clear(); storage->m_numValuesInVector -= hadValue; } } storage->setLength(newLength); return true; } bool JSArray::appendMemcpy(JSGlobalObject* globalObject, VM& vm, unsigned startIndex, JSC::JSArray* otherArray) { auto scope = DECLARE_THROW_SCOPE(vm); if (!canFastCopy(vm, otherArray)) return false; IndexingType type = indexingType(); IndexingType otherType = otherArray->indexingType(); IndexingType copyType = mergeIndexingTypeForCopying(otherType); if (type == ArrayWithUndecided && copyType != NonArray) { if (copyType == ArrayWithInt32) convertUndecidedToInt32(vm); else if (copyType == ArrayWithDouble) convertUndecidedToDouble(vm); else if (copyType == ArrayWithContiguous) convertUndecidedToContiguous(vm); else { ASSERT(copyType == ArrayWithUndecided); return true; } } else if (type != copyType) return false; unsigned otherLength = otherArray->length(); Checked checkedNewLength = startIndex; checkedNewLength += otherLength; unsigned newLength; if (checkedNewLength.safeGet(newLength) == CheckedState::DidOverflow) { throwException(globalObject, scope, createRangeError(globalObject, LengthExceededTheMaximumArrayLengthError)); return false; } if (newLength >= MIN_SPARSE_ARRAY_INDEX) return false; if (!ensureLength(vm, newLength)) { throwOutOfMemoryError(globalObject, scope); return false; } ASSERT(copyType == indexingType()); if (UNLIKELY(otherType == ArrayWithUndecided)) { auto* butterfly = this->butterfly(); if (type == ArrayWithDouble) { for (unsigned i = startIndex; i < newLength; ++i) butterfly->contiguousDouble().at(this, i) = PNaN; } else { for (unsigned i = startIndex; i < newLength; ++i) butterfly->contiguousInt32().at(this, i).setWithoutWriteBarrier(JSValue()); } } else if (type == ArrayWithDouble) gcSafeMemcpy(butterfly()->contiguousDouble().data() + startIndex, otherArray->butterfly()->contiguousDouble().data(), sizeof(JSValue) * otherLength); else { gcSafeMemcpy(butterfly()->contiguous().data() + startIndex, otherArray->butterfly()->contiguous().data(), sizeof(JSValue) * otherLength); vm.heap.writeBarrier(this); } return true; } bool JSArray::setLength(JSGlobalObject* globalObject, unsigned newLength, bool throwException) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); Butterfly* butterfly = this->butterfly(); switch (indexingMode()) { case ArrayClass: if (!newLength) return true; if (newLength >= MIN_SPARSE_ARRAY_INDEX) { RELEASE_AND_RETURN(scope, setLengthWithArrayStorage( globalObject, newLength, throwException, ensureArrayStorage(vm))); } createInitialUndecided(vm, newLength); return true; case CopyOnWriteArrayWithInt32: case CopyOnWriteArrayWithDouble: case CopyOnWriteArrayWithContiguous: if (newLength == butterfly->publicLength()) return true; convertFromCopyOnWrite(vm); butterfly = this->butterfly(); FALLTHROUGH; case ArrayWithUndecided: case ArrayWithInt32: case ArrayWithDouble: case ArrayWithContiguous: { if (newLength == butterfly->publicLength()) return true; if (newLength > MAX_STORAGE_VECTOR_LENGTH // This check ensures that we can do fast push. || (newLength >= MIN_SPARSE_ARRAY_INDEX && !isDenseEnoughForVector(newLength, countElements()))) { RELEASE_AND_RETURN(scope, setLengthWithArrayStorage( globalObject, newLength, throwException, ensureArrayStorage(vm))); } if (newLength > butterfly->publicLength()) { if (!ensureLength(vm, newLength)) { throwOutOfMemoryError(globalObject, scope); return false; } return true; } unsigned lengthToClear = butterfly->publicLength() - newLength; unsigned costToAllocateNewButterfly = 64; // a heuristic. if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) { reallocateAndShrinkButterfly(vm, newLength); return true; } if (indexingType() == ArrayWithDouble) { for (unsigned i = butterfly->publicLength(); i-- > newLength;) butterfly->contiguousDouble().at(this, i) = PNaN; } else { for (unsigned i = butterfly->publicLength(); i-- > newLength;) butterfly->contiguous().at(this, i).clear(); } butterfly->setPublicLength(newLength); return true; } case ArrayWithArrayStorage: case ArrayWithSlowPutArrayStorage: RELEASE_AND_RETURN(scope, setLengthWithArrayStorage(globalObject, newLength, throwException, arrayStorage())); default: CRASH(); return false; } } JSValue JSArray::pop(JSGlobalObject* globalObject) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); ensureWritable(vm); Butterfly* butterfly = this->butterfly(); switch (indexingType()) { case ArrayClass: return jsUndefined(); case ArrayWithUndecided: if (!butterfly->publicLength()) return jsUndefined(); // We have nothing but holes. So, drop down to the slow version. break; case ArrayWithInt32: case ArrayWithContiguous: { unsigned length = butterfly->publicLength(); if (!length--) return jsUndefined(); RELEASE_ASSERT(length < butterfly->vectorLength()); JSValue value = butterfly->contiguous().at(this, length).get(); if (value) { butterfly->contiguous().at(this, length).clear(); butterfly->setPublicLength(length); return value; } break; } case ArrayWithDouble: { unsigned length = butterfly->publicLength(); if (!length--) return jsUndefined(); RELEASE_ASSERT(length < butterfly->vectorLength()); double value = butterfly->contiguousDouble().at(this, length); if (value == value) { butterfly->contiguousDouble().at(this, length) = PNaN; butterfly->setPublicLength(length); return JSValue(JSValue::EncodeAsDouble, value); } break; } case ARRAY_WITH_ARRAY_STORAGE_INDEXING_TYPES: { ArrayStorage* storage = butterfly->arrayStorage(); unsigned length = storage->length(); if (!length) { if (!isLengthWritable()) throwTypeError(globalObject, scope, ReadonlyPropertyWriteError); return jsUndefined(); } unsigned index = length - 1; if (index < storage->vectorLength()) { WriteBarrier& valueSlot = storage->m_vector[index]; if (valueSlot) { --storage->m_numValuesInVector; JSValue element = valueSlot.get(); valueSlot.clear(); RELEASE_ASSERT(isLengthWritable()); storage->setLength(index); return element; } } break; } default: CRASH(); return JSValue(); } unsigned index = getArrayLength() - 1; // Let element be the result of calling the [[Get]] internal method of O with argument indx. JSValue element = get(globalObject, index); RETURN_IF_EXCEPTION(scope, JSValue()); // Call the [[Delete]] internal method of O with arguments indx and true. bool success = deletePropertyByIndex(this, globalObject, index); RETURN_IF_EXCEPTION(scope, JSValue()); if (!success) { throwTypeError(globalObject, scope, UnableToDeletePropertyError); return jsUndefined(); } // Call the [[Put]] internal method of O with arguments "length", indx, and true. scope.release(); setLength(globalObject, index, true); // Return element. return element; } // Push & putIndex are almost identical, with two small differences. // - we always are writing beyond the current array bounds, so it is always necessary to update m_length & m_numValuesInVector. // - pushing to an array of length 2^32-1 stores the property, but throws a range error. NEVER_INLINE void JSArray::push(JSGlobalObject* globalObject, JSValue value) { pushInline(globalObject, value); } JSArray* JSArray::fastSlice(JSGlobalObject* globalObject, unsigned startIndex, unsigned count) { VM& vm = globalObject->vm(); ensureWritable(vm); auto arrayType = indexingMode(); switch (arrayType) { case ArrayWithDouble: case ArrayWithInt32: case ArrayWithContiguous: { if (count >= MIN_SPARSE_ARRAY_INDEX || structure(vm)->holesMustForwardToPrototype(vm, this)) return nullptr; Structure* resultStructure = globalObject->arrayStructureForIndexingTypeDuringAllocation(arrayType); if (UNLIKELY(hasAnyArrayStorage(resultStructure->indexingType()))) return nullptr; ASSERT(!globalObject->isHavingABadTime()); ObjectInitializationScope scope(vm); JSArray* resultArray = JSArray::tryCreateUninitializedRestricted(scope, resultStructure, count); if (UNLIKELY(!resultArray)) return nullptr; auto& resultButterfly = *resultArray->butterfly(); if (arrayType == ArrayWithDouble) gcSafeMemcpy(resultButterfly.contiguousDouble().data(), butterfly()->contiguousDouble().data() + startIndex, sizeof(JSValue) * count); else gcSafeMemcpy(resultButterfly.contiguous().data(), butterfly()->contiguous().data() + startIndex, sizeof(JSValue) * count); ASSERT(resultButterfly.publicLength() == count); return resultArray; } default: return nullptr; } } bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) { unsigned oldLength = storage->length(); RELEASE_ASSERT(count <= oldLength); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if (storage->hasHoles() || hasSparseMap() || shouldUseSlowPut(indexingType())) { return false; } if (!oldLength) return true; unsigned length = oldLength - count; storage->m_numValuesInVector -= count; storage->setLength(length); unsigned vectorLength = storage->vectorLength(); if (!vectorLength) return true; if (startIndex >= vectorLength) return true; DisallowGC disallowGC; auto locker = holdLock(cellLock()); if (startIndex + count > vectorLength) count = vectorLength - startIndex; unsigned usedVectorLength = std::min(vectorLength, oldLength); unsigned numElementsBeforeShiftRegion = startIndex; unsigned firstIndexAfterShiftRegion = startIndex + count; unsigned numElementsAfterShiftRegion = usedVectorLength - firstIndexAfterShiftRegion; ASSERT(numElementsBeforeShiftRegion + count + numElementsAfterShiftRegion == usedVectorLength); // The point of this comparison seems to be to minimize the amount of elements that have to // be moved during a shift operation. if (numElementsBeforeShiftRegion < numElementsAfterShiftRegion) { // The number of elements before the shift region is less than the number of elements // after the shift region, so we move the elements before to the right. if (numElementsBeforeShiftRegion) { RELEASE_ASSERT(count + startIndex <= vectorLength); gcSafeMemmove(storage->m_vector + count, storage->m_vector, sizeof(JSValue) * startIndex); } { // When moving Butterfly's head to adjust property-storage, we must take a structure lock. // Otherwise, concurrent JIT compiler accesses to a property storage which is half-baked due to move for shift / unshift. Structure* structure = this->structure(vm); ConcurrentJSLocker structureLock(structure->lock()); // Adjust the Butterfly and the index bias. We only need to do this here because we're changing // the start of the Butterfly, which needs to point at the first indexed property in the used // portion of the vector. Butterfly* butterfly = this->butterfly()->shift(structure, count); storage = butterfly->arrayStorage(); storage->m_indexBias += count; // Since we're consuming part of the vector by moving its beginning to the left, // we need to modify the vector length appropriately. storage->setVectorLength(vectorLength - count); setButterfly(vm, butterfly); } } else { // The number of elements before the shift region is greater than or equal to the number // of elements after the shift region, so we move the elements after the shift region to the left. gcSafeMemmove(storage->m_vector + startIndex, storage->m_vector + firstIndexAfterShiftRegion, sizeof(JSValue) * numElementsAfterShiftRegion); // Clear the slots of the elements we just moved. unsigned startOfEmptyVectorTail = usedVectorLength - count; for (unsigned i = startOfEmptyVectorTail; i < usedVectorLength; ++i) storage->m_vector[i].clear(); // We don't modify the index bias or the Butterfly pointer in this case because we're not changing // the start of the Butterfly, which needs to point at the first indexed property in the used // portion of the vector. We also don't modify the vector length because we're not actually changing // its length; we're just using less of it. } return true; } bool JSArray::shiftCountWithAnyIndexingType(JSGlobalObject* globalObject, unsigned& startIndex, unsigned count) { VM& vm = globalObject->vm(); RELEASE_ASSERT(count > 0); ensureWritable(vm); Butterfly* butterfly = this->butterfly(); auto indexingType = this->indexingType(); switch (indexingType) { case ArrayClass: return true; case ArrayWithUndecided: // Don't handle this because it's confusing and it shouldn't come up. return false; case ArrayWithInt32: case ArrayWithContiguous: { unsigned oldLength = butterfly->publicLength(); RELEASE_ASSERT(count <= oldLength); // We may have to walk the entire array to do the shift. We're willing to do // so only if it's not horribly slow. if (oldLength - (startIndex + count) >= MIN_SPARSE_ARRAY_INDEX) return shiftCountWithArrayStorage(vm, startIndex, count, ensureArrayStorage(vm)); // Storing to a hole is fine since we're still having a good time. But reading from a hole // is totally not fine, since we might have to read from the proto chain. // We have to check for holes before we start moving things around so that we don't get halfway // through shifting and then realize we should have been in ArrayStorage mode. unsigned end = oldLength - count; if (this->structure(vm)->holesMustForwardToPrototype(vm, this)) { for (unsigned i = startIndex; i < end; ++i) { JSValue v = butterfly->contiguous().at(this, i + count).get(); if (UNLIKELY(!v)) { startIndex = i; return shiftCountWithArrayStorage(vm, startIndex, count, ensureArrayStorage(vm)); } butterfly->contiguous().at(this, i).setWithoutWriteBarrier(v); } } else { gcSafeMemmove(butterfly->contiguous().data() + startIndex, butterfly->contiguous().data() + startIndex + count, sizeof(JSValue) * (end - startIndex)); } for (unsigned i = end; i < oldLength; ++i) butterfly->contiguous().at(this, i).clear(); butterfly->setPublicLength(oldLength - count); // Our memmoving of values around in the array could have concealed some of them from // the collector. Let's make sure that the collector scans this object again. if (indexingType == ArrayWithContiguous) vm.heap.writeBarrier(this); return true; } case ArrayWithDouble: { unsigned oldLength = butterfly->publicLength(); RELEASE_ASSERT(count <= oldLength); // We may have to walk the entire array to do the shift. We're willing to do // so only if it's not horribly slow. if (oldLength - (startIndex + count) >= MIN_SPARSE_ARRAY_INDEX) return shiftCountWithArrayStorage(vm, startIndex, count, ensureArrayStorage(vm)); // Storing to a hole is fine since we're still having a good time. But reading from a hole // is totally not fine, since we might have to read from the proto chain. // We have to check for holes before we start moving things around so that we don't get halfway // through shifting and then realize we should have been in ArrayStorage mode. unsigned end = oldLength - count; if (this->structure(vm)->holesMustForwardToPrototype(vm, this)) { for (unsigned i = startIndex; i < end; ++i) { double v = butterfly->contiguousDouble().at(this, i + count); if (UNLIKELY(v != v)) { startIndex = i; return shiftCountWithArrayStorage(vm, startIndex, count, ensureArrayStorage(vm)); } butterfly->contiguousDouble().at(this, i) = v; } } else { gcSafeMemmove(butterfly->contiguousDouble().data() + startIndex, butterfly->contiguousDouble().data() + startIndex + count, sizeof(JSValue) * (end - startIndex)); } for (unsigned i = end; i < oldLength; ++i) butterfly->contiguousDouble().at(this, i) = PNaN; butterfly->setPublicLength(oldLength - count); return true; } case ArrayWithArrayStorage: case ArrayWithSlowPutArrayStorage: return shiftCountWithArrayStorage(vm, startIndex, count, arrayStorage()); default: CRASH(); return false; } } // Returns true if the unshift can be handled, false to fallback. bool JSArray::unshiftCountWithArrayStorage(JSGlobalObject* globalObject, unsigned startIndex, unsigned count, ArrayStorage* storage) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned length = storage->length(); RELEASE_ASSERT(startIndex <= length); // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType())) return false; bool moveFront = !startIndex || startIndex < length / 2; unsigned vectorLength = storage->vectorLength(); // Need to have GC deferred around the unshiftCountSlowCase(), since that leaves the butterfly in // a weird state: some parts of it will be left uninitialized, which we will fill in here. DeferGC deferGC(vm.heap); auto locker = holdLock(cellLock()); if (moveFront && storage->m_indexBias >= count) { // When moving Butterfly's head to adjust property-storage, we must take a structure lock. // Otherwise, concurrent JIT compiler accesses to a property storage which is half-baked due to move for shift / unshift. Structure* structure = this->structure(vm); ConcurrentJSLocker structureLock(structure->lock()); Butterfly* newButterfly = storage->butterfly()->unshift(structure, count); storage = newButterfly->arrayStorage(); storage->m_indexBias -= count; storage->setVectorLength(vectorLength + count); setButterfly(vm, newButterfly); } else if (!moveFront && vectorLength - length >= count) storage = storage->butterfly()->arrayStorage(); else if (unshiftCountSlowCase(locker, vm, deferGC, moveFront, count)) storage = arrayStorage(); else { throwOutOfMemoryError(globalObject, scope); return true; } WriteBarrier* vector = storage->m_vector; if (startIndex) { if (moveFront) gcSafeMemmove(vector, vector + count, startIndex * sizeof(JSValue)); else if (length - startIndex) gcSafeMemmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue)); } for (unsigned i = 0; i < count; i++) vector[i + startIndex].clear(); return true; } bool JSArray::unshiftCountWithAnyIndexingType(JSGlobalObject* globalObject, unsigned startIndex, unsigned count) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); ensureWritable(vm); Butterfly* butterfly = this->butterfly(); switch (indexingType()) { case ArrayClass: case ArrayWithUndecided: // We could handle this. But it shouldn't ever come up, so we won't. return false; case ArrayWithInt32: case ArrayWithContiguous: { unsigned oldLength = butterfly->publicLength(); // We may have to walk the entire array to do the unshift. We're willing to do so // only if it's not horribly slow. if (oldLength - startIndex >= MIN_SPARSE_ARRAY_INDEX) RELEASE_AND_RETURN(scope, unshiftCountWithArrayStorage(globalObject, startIndex, count, ensureArrayStorage(vm))); Checked checkedLength(oldLength); checkedLength += count; unsigned newLength; if (CheckedState::DidOverflow == checkedLength.safeGet(newLength)) { throwOutOfMemoryError(globalObject, scope); return true; } if (newLength > MAX_STORAGE_VECTOR_LENGTH) return false; if (!ensureLength(vm, newLength)) { throwOutOfMemoryError(globalObject, scope); return true; } butterfly = this->butterfly(); // We have to check for holes before we start moving things around so that we don't get halfway // through shifting and then realize we should have been in ArrayStorage mode. for (unsigned i = oldLength; i-- > startIndex;) { JSValue v = butterfly->contiguous().at(this, i).get(); if (UNLIKELY(!v)) RELEASE_AND_RETURN(scope, unshiftCountWithArrayStorage(globalObject, startIndex, count, ensureArrayStorage(vm))); } for (unsigned i = oldLength; i-- > startIndex;) { JSValue v = butterfly->contiguous().at(this, i).get(); ASSERT(v); butterfly->contiguous().at(this, i + count).setWithoutWriteBarrier(v); } // Our memmoving of values around in the array could have concealed some of them from // the collector. Let's make sure that the collector scans this object again. vm.heap.writeBarrier(this); // NOTE: we're leaving being garbage in the part of the array that we shifted out // of. This is fine because the caller is required to store over that area, and // in contiguous mode storing into a hole is guaranteed to behave exactly the same // as storing over an existing element. return true; } case ArrayWithDouble: { unsigned oldLength = butterfly->publicLength(); // We may have to walk the entire array to do the unshift. We're willing to do so // only if it's not horribly slow. if (oldLength - startIndex >= MIN_SPARSE_ARRAY_INDEX) RELEASE_AND_RETURN(scope, unshiftCountWithArrayStorage(globalObject, startIndex, count, ensureArrayStorage(vm))); Checked checkedLength(oldLength); checkedLength += count; unsigned newLength; if (CheckedState::DidOverflow == checkedLength.safeGet(newLength)) { throwOutOfMemoryError(globalObject, scope); return true; } if (newLength > MAX_STORAGE_VECTOR_LENGTH) return false; if (!ensureLength(vm, newLength)) { throwOutOfMemoryError(globalObject, scope); return true; } butterfly = this->butterfly(); // We have to check for holes before we start moving things around so that we don't get halfway // through shifting and then realize we should have been in ArrayStorage mode. for (unsigned i = oldLength; i-- > startIndex;) { double v = butterfly->contiguousDouble().at(this, i); if (UNLIKELY(v != v)) RELEASE_AND_RETURN(scope, unshiftCountWithArrayStorage(globalObject, startIndex, count, ensureArrayStorage(vm))); } for (unsigned i = oldLength; i-- > startIndex;) { double v = butterfly->contiguousDouble().at(this, i); ASSERT(v == v); butterfly->contiguousDouble().at(this, i + count) = v; } // NOTE: we're leaving being garbage in the part of the array that we shifted out // of. This is fine because the caller is required to store over that area, and // in contiguous mode storing into a hole is guaranteed to behave exactly the same // as storing over an existing element. return true; } case ArrayWithArrayStorage: case ArrayWithSlowPutArrayStorage: RELEASE_AND_RETURN(scope, unshiftCountWithArrayStorage(globalObject, startIndex, count, arrayStorage())); default: CRASH(); return false; } } void JSArray::fillArgList(JSGlobalObject* globalObject, MarkedArgumentBuffer& args) { unsigned i = 0; unsigned vectorEnd; WriteBarrier* vector; Butterfly* butterfly = this->butterfly(); switch (indexingType()) { case ArrayClass: return; case ArrayWithUndecided: { vector = nullptr; vectorEnd = 0; break; } case ArrayWithInt32: case ArrayWithContiguous: { vectorEnd = butterfly->publicLength(); vector = butterfly->contiguous().data(); break; } case ArrayWithDouble: { vector = nullptr; vectorEnd = 0; for (; i < butterfly->publicLength(); ++i) { double v = butterfly->contiguousDouble().at(this, i); if (v != v) break; args.append(JSValue(JSValue::EncodeAsDouble, v)); } break; } case ARRAY_WITH_ARRAY_STORAGE_INDEXING_TYPES: { ArrayStorage* storage = butterfly->arrayStorage(); vector = storage->m_vector; vectorEnd = std::min(storage->length(), storage->vectorLength()); break; } default: CRASH(); #if COMPILER_QUIRK(CONSIDERS_UNREACHABLE_CODE) vector = 0; vectorEnd = 0; break; #endif } for (; i < vectorEnd; ++i) { WriteBarrier& v = vector[i]; if (!v) break; args.append(v.get()); } // FIXME: What prevents this from being called with a RuntimeArray? The length function will always return 0 in that case. for (; i < length(); ++i) args.append(get(globalObject, i)); } void JSArray::copyToArguments(JSGlobalObject* globalObject, JSValue* firstElementDest, unsigned offset, unsigned length) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned i = offset; WriteBarrier* vector; unsigned vectorEnd; length += offset; // We like to think of the length as being our length, rather than the output length. // FIXME: What prevents this from being called with a RuntimeArray? The length function will always return 0 in that case. ASSERT(length == this->length()); Butterfly* butterfly = this->butterfly(); switch (indexingType()) { case ArrayClass: return; case ArrayWithUndecided: { vector = nullptr; vectorEnd = 0; break; } case ArrayWithInt32: case ArrayWithContiguous: { vector = butterfly->contiguous().data(); vectorEnd = butterfly->publicLength(); break; } case ArrayWithDouble: { vector = nullptr; vectorEnd = 0; for (; i < butterfly->publicLength(); ++i) { ASSERT(i < butterfly->vectorLength()); double v = butterfly->contiguousDouble().at(this, i); if (v != v) break; firstElementDest[i - offset] = JSValue(JSValue::EncodeAsDouble, v); } break; } case ARRAY_WITH_ARRAY_STORAGE_INDEXING_TYPES: { ArrayStorage* storage = butterfly->arrayStorage(); vector = storage->m_vector; vectorEnd = std::min(length, storage->vectorLength()); break; } default: CRASH(); #if COMPILER_QUIRK(CONSIDERS_UNREACHABLE_CODE) vector = 0; vectorEnd = 0; break; #endif } for (; i < vectorEnd; ++i) { WriteBarrier& v = vector[i]; if (!v) break; firstElementDest[i - offset] = v.get(); } for (; i < length; ++i) { firstElementDest[i - offset] = get(globalObject, i); RETURN_IF_EXCEPTION(scope, void()); } } bool JSArray::isIteratorProtocolFastAndNonObservable() { JSGlobalObject* globalObject = this->globalObject(); if (!globalObject->isArrayPrototypeIteratorProtocolFastAndNonObservable()) return false; VM& vm = globalObject->vm(); Structure* structure = this->structure(vm); // This is the fast case. Many arrays will be an original array. if (globalObject->isOriginalArrayStructure(structure)) return true; if (structure->mayInterceptIndexedAccesses()) return false; if (getPrototypeDirect(vm) != globalObject->arrayPrototype()) return false; if (getDirectOffset(vm, vm.propertyNames->iteratorSymbol) != invalidOffset) return false; return true; } inline JSArray* constructArray(ObjectInitializationScope& scope, Structure* arrayStructure, unsigned length) { JSArray* array = JSArray::tryCreateUninitializedRestricted(scope, arrayStructure, length); // FIXME: we should probably throw an out of memory error here, but // when making this change we should check that all clients of this // function will correctly handle an exception being thrown from here. // https://bugs.webkit.org/show_bug.cgi?id=169786 RELEASE_ASSERT(array); // FIXME: We only need this for subclasses of Array because we might need to allocate a new structure to change // indexing types while initializing. If this triggered a GC then we might scan our currently uninitialized // array and crash. https://bugs.webkit.org/show_bug.cgi?id=186811 if (!arrayStructure->globalObject()->isOriginalArrayStructure(arrayStructure)) JSArray::eagerlyInitializeButterfly(scope, array, length); return array; } JSArray* constructArray(JSGlobalObject* globalObject, Structure* arrayStructure, const ArgList& values) { VM& vm = globalObject->vm(); unsigned length = values.size(); ObjectInitializationScope scope(vm); JSArray* array = constructArray(scope, arrayStructure, length); for (unsigned i = 0; i < length; ++i) array->initializeIndex(scope, i, values.at(i)); return array; } JSArray* constructArray(JSGlobalObject* globalObject, Structure* arrayStructure, const JSValue* values, unsigned length) { VM& vm = globalObject->vm(); ObjectInitializationScope scope(vm); JSArray* array = constructArray(scope, arrayStructure, length); for (unsigned i = 0; i < length; ++i) array->initializeIndex(scope, i, values[i]); return array; } JSArray* constructArrayNegativeIndexed(JSGlobalObject* globalObject, Structure* arrayStructure, const JSValue* values, unsigned length) { VM& vm = globalObject->vm(); ObjectInitializationScope scope(vm); JSArray* array = constructArray(scope, arrayStructure, length); for (int i = 0; i < static_cast(length); ++i) array->initializeIndex(scope, i, values[-i]); return array; } } // namespace JSC