diff options
Diffstat (limited to 'js/src/vm/NativeObject.cpp')
-rw-r--r-- | js/src/vm/NativeObject.cpp | 2564 |
1 files changed, 2564 insertions, 0 deletions
diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp new file mode 100644 index 000000000..21f73f4a9 --- /dev/null +++ b/js/src/vm/NativeObject.cpp @@ -0,0 +1,2564 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "vm/NativeObject-inl.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Casting.h" + +#include "jswatchpoint.h" + +#include "gc/Marking.h" +#include "js/Value.h" +#include "vm/Debugger.h" +#include "vm/TypedArrayCommon.h" + +#include "jsobjinlines.h" + +#include "gc/Nursery-inl.h" +#include "vm/ArrayObject-inl.h" +#include "vm/EnvironmentObject-inl.h" +#include "vm/Shape-inl.h" + +using namespace js; + +using JS::AutoCheckCannotGC; +using JS::GenericNaN; +using mozilla::ArrayLength; +using mozilla::DebugOnly; +using mozilla::PodCopy; +using mozilla::RoundUpPow2; + +static const ObjectElements emptyElementsHeader(0, 0); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElements = + reinterpret_cast<HeapSlot*>(uintptr_t(&emptyElementsHeader) + sizeof(ObjectElements)); + +static const ObjectElements emptyElementsHeaderShared(0, 0, ObjectElements::SharedMemory::IsShared); + +/* Objects with no elements share one empty set of elements. */ +HeapSlot* const js::emptyObjectElementsShared = + reinterpret_cast<HeapSlot*>(uintptr_t(&emptyElementsHeaderShared) + sizeof(ObjectElements)); + + +#ifdef DEBUG + +bool +NativeObject::canHaveNonEmptyElements() +{ + return !this->is<TypedArrayObject>(); +} + +#endif // DEBUG + +/* static */ bool +ObjectElements::ConvertElementsToDoubles(JSContext* cx, uintptr_t elementsPtr) +{ + /* + * This function is infallible, but has a fallible interface so that it can + * be called directly from Ion code. Only arrays can have their dense + * elements converted to doubles, and arrays never have empty elements. + */ + HeapSlot* elementsHeapPtr = (HeapSlot*) elementsPtr; + MOZ_ASSERT(elementsHeapPtr != emptyObjectElements && + elementsHeapPtr != emptyObjectElementsShared); + + ObjectElements* header = ObjectElements::fromElements(elementsHeapPtr); + MOZ_ASSERT(!header->shouldConvertDoubleElements()); + + // Note: the elements can be mutated in place even for copy on write + // arrays. See comment on ObjectElements. + Value* vp = (Value*) elementsPtr; + for (size_t i = 0; i < header->initializedLength; i++) { + if (vp[i].isInt32()) + vp[i].setDouble(vp[i].toInt32()); + } + + header->setShouldConvertDoubleElements(); + return true; +} + +/* static */ bool +ObjectElements::MakeElementsCopyOnWrite(ExclusiveContext* cx, NativeObject* obj) +{ + static_assert(sizeof(HeapSlot) >= sizeof(GCPtrObject), + "there must be enough room for the owner object pointer at " + "the end of the elements"); + if (!obj->ensureElements(cx, obj->getDenseInitializedLength() + 1)) + return false; + + ObjectElements* header = obj->getElementsHeader(); + + // Note: this method doesn't update type information to indicate that the + // elements might be copy on write. Handling this is left to the caller. + MOZ_ASSERT(!header->isCopyOnWrite()); + MOZ_ASSERT(!header->isFrozen()); + header->flags |= COPY_ON_WRITE; + + header->ownerObject().init(obj); + return true; +} + +/* static */ bool +ObjectElements::FreezeElements(ExclusiveContext* cx, HandleNativeObject obj) +{ + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + if (obj->hasEmptyElements()) + return true; + + ObjectElements* header = obj->getElementsHeader(); + + // Note: this method doesn't update type information to indicate that the + // elements might be frozen. Handling this is left to the caller. + header->freeze(); + + return true; +} + +#ifdef DEBUG +void +js::NativeObject::checkShapeConsistency() +{ + static int throttle = -1; + if (throttle < 0) { + if (const char* var = getenv("JS_CHECK_SHAPE_THROTTLE")) + throttle = atoi(var); + if (throttle < 0) + throttle = 0; + } + if (throttle == 0) + return; + + MOZ_ASSERT(isNative()); + + Shape* shape = lastProperty(); + Shape* prev = nullptr; + + AutoCheckCannotGC nogc; + if (inDictionaryMode()) { + if (ShapeTable* table = shape->maybeTable(nogc)) { + for (uint32_t fslot = table->freeList(); + fslot != SHAPE_INVALID_SLOT; + fslot = getSlot(fslot).toPrivateUint32()) + { + MOZ_ASSERT(fslot < slotSpan()); + } + + for (int n = throttle; --n >= 0 && shape->parent; shape = shape->parent) { + MOZ_ASSERT_IF(lastProperty() != shape, !shape->hasTable()); + + ShapeTable::Entry& entry = table->search<MaybeAdding::NotAdding>(shape->propid(), + nogc); + MOZ_ASSERT(entry.shape() == shape); + } + } + + shape = lastProperty(); + for (int n = throttle; --n >= 0 && shape; shape = shape->parent) { + MOZ_ASSERT_IF(shape->slot() != SHAPE_INVALID_SLOT, shape->slot() < slotSpan()); + if (!prev) { + MOZ_ASSERT(lastProperty() == shape); + MOZ_ASSERT(shape->listp == &shape_); + } else { + MOZ_ASSERT(shape->listp == &prev->parent); + } + prev = shape; + } + } else { + for (int n = throttle; --n >= 0 && shape->parent; shape = shape->parent) { + if (ShapeTable* table = shape->maybeTable(nogc)) { + MOZ_ASSERT(shape->parent); + for (Shape::Range<NoGC> r(shape); !r.empty(); r.popFront()) { + ShapeTable::Entry& entry = + table->search<MaybeAdding::NotAdding>(r.front().propid(), nogc); + MOZ_ASSERT(entry.shape() == &r.front()); + } + } + if (prev) { + MOZ_ASSERT(prev->maybeSlot() >= shape->maybeSlot()); + shape->kids.checkConsistency(prev); + } + prev = shape; + } + } +} +#endif + +void +js::NativeObject::initializeSlotRange(uint32_t start, uint32_t length) +{ + /* + * No bounds check, as this is used when the object's shape does not + * reflect its allocated slots (updateSlotsForSpan). + */ + HeapSlot* fixedStart; + HeapSlot* fixedEnd; + HeapSlot* slotsStart; + HeapSlot* slotsEnd; + getSlotRangeUnchecked(start, length, &fixedStart, &fixedEnd, &slotsStart, &slotsEnd); + + uint32_t offset = start; + for (HeapSlot* sp = fixedStart; sp < fixedEnd; sp++) + sp->init(this, HeapSlot::Slot, offset++, UndefinedValue()); + for (HeapSlot* sp = slotsStart; sp < slotsEnd; sp++) + sp->init(this, HeapSlot::Slot, offset++, UndefinedValue()); +} + +void +js::NativeObject::initSlotRange(uint32_t start, const Value* vector, uint32_t length) +{ + HeapSlot* fixedStart; + HeapSlot* fixedEnd; + HeapSlot* slotsStart; + HeapSlot* slotsEnd; + getSlotRange(start, length, &fixedStart, &fixedEnd, &slotsStart, &slotsEnd); + for (HeapSlot* sp = fixedStart; sp < fixedEnd; sp++) + sp->init(this, HeapSlot::Slot, start++, *vector++); + for (HeapSlot* sp = slotsStart; sp < slotsEnd; sp++) + sp->init(this, HeapSlot::Slot, start++, *vector++); +} + +void +js::NativeObject::copySlotRange(uint32_t start, const Value* vector, uint32_t length) +{ + HeapSlot* fixedStart; + HeapSlot* fixedEnd; + HeapSlot* slotsStart; + HeapSlot* slotsEnd; + getSlotRange(start, length, &fixedStart, &fixedEnd, &slotsStart, &slotsEnd); + for (HeapSlot* sp = fixedStart; sp < fixedEnd; sp++) + sp->set(this, HeapSlot::Slot, start++, *vector++); + for (HeapSlot* sp = slotsStart; sp < slotsEnd; sp++) + sp->set(this, HeapSlot::Slot, start++, *vector++); +} + +#ifdef DEBUG +bool +js::NativeObject::slotInRange(uint32_t slot, SentinelAllowed sentinel) const +{ + uint32_t capacity = numFixedSlots() + numDynamicSlots(); + if (sentinel == SENTINEL_ALLOWED) + return slot <= capacity; + return slot < capacity; +} +#endif /* DEBUG */ + +Shape* +js::NativeObject::lookup(ExclusiveContext* cx, jsid id) +{ + MOZ_ASSERT(isNative()); + return Shape::search(cx, lastProperty(), id); +} + +Shape* +js::NativeObject::lookupPure(jsid id) +{ + MOZ_ASSERT(isNative()); + return Shape::searchNoHashify(lastProperty(), id); +} + +uint32_t +js::NativeObject::numFixedSlotsForCompilation() const +{ + // This is an alternative method for getting the number of fixed slots in an + // object. It requires more logic and memory accesses than numFixedSlots() + // but is safe to be called from the compilation thread, even if the main + // thread is actively mutating the VM. + + // The compiler does not have access to nursery things. + MOZ_ASSERT(!IsInsideNursery(this)); + + if (this->is<ArrayObject>()) + return 0; + + gc::AllocKind kind = asTenured().getAllocKind(); + return gc::GetGCKindSlots(kind, getClass()); +} + +uint32_t +js::NativeObject::dynamicSlotsCount(uint32_t nfixed, uint32_t span, const Class* clasp) +{ + if (span <= nfixed) + return 0; + span -= nfixed; + + // Increase the slots to SLOT_CAPACITY_MIN to decrease the likelihood + // the dynamic slots need to get increased again. ArrayObjects ignore + // this because slots are uncommon in that case. + if (clasp != &ArrayObject::class_ && span <= SLOT_CAPACITY_MIN) + return SLOT_CAPACITY_MIN; + + uint32_t slots = mozilla::RoundUpPow2(span); + MOZ_ASSERT(slots >= span); + return slots; +} + +inline bool +NativeObject::updateSlotsForSpan(ExclusiveContext* cx, size_t oldSpan, size_t newSpan) +{ + MOZ_ASSERT(oldSpan != newSpan); + + size_t oldCount = dynamicSlotsCount(numFixedSlots(), oldSpan, getClass()); + size_t newCount = dynamicSlotsCount(numFixedSlots(), newSpan, getClass()); + + if (oldSpan < newSpan) { + if (oldCount < newCount && !growSlots(cx, oldCount, newCount)) + return false; + + if (newSpan == oldSpan + 1) + initSlotUnchecked(oldSpan, UndefinedValue()); + else + initializeSlotRange(oldSpan, newSpan - oldSpan); + } else { + /* Trigger write barriers on the old slots before reallocating. */ + prepareSlotRangeForOverwrite(newSpan, oldSpan); + invalidateSlotRange(newSpan, oldSpan - newSpan); + + if (oldCount > newCount) + shrinkSlots(cx, oldCount, newCount); + } + + return true; +} + +bool +NativeObject::setLastProperty(ExclusiveContext* cx, Shape* shape) +{ + MOZ_ASSERT(!inDictionaryMode()); + MOZ_ASSERT(!shape->inDictionary()); + MOZ_ASSERT(shape->zone() == zone()); + MOZ_ASSERT(shape->numFixedSlots() == numFixedSlots()); + MOZ_ASSERT(shape->getObjectClass() == getClass()); + + size_t oldSpan = lastProperty()->slotSpan(); + size_t newSpan = shape->slotSpan(); + + if (oldSpan == newSpan) { + shape_ = shape; + return true; + } + + if (!updateSlotsForSpan(cx, oldSpan, newSpan)) + return false; + + shape_ = shape; + return true; +} + +void +NativeObject::setLastPropertyShrinkFixedSlots(Shape* shape) +{ + MOZ_ASSERT(!inDictionaryMode()); + MOZ_ASSERT(!shape->inDictionary()); + MOZ_ASSERT(shape->zone() == zone()); + MOZ_ASSERT(lastProperty()->slotSpan() == shape->slotSpan()); + MOZ_ASSERT(shape->getObjectClass() == getClass()); + + DebugOnly<size_t> oldFixed = numFixedSlots(); + DebugOnly<size_t> newFixed = shape->numFixedSlots(); + MOZ_ASSERT(newFixed < oldFixed); + MOZ_ASSERT(shape->slotSpan() <= oldFixed); + MOZ_ASSERT(shape->slotSpan() <= newFixed); + MOZ_ASSERT(dynamicSlotsCount(oldFixed, shape->slotSpan(), getClass()) == 0); + MOZ_ASSERT(dynamicSlotsCount(newFixed, shape->slotSpan(), getClass()) == 0); + + shape_ = shape; +} + +void +NativeObject::setLastPropertyMakeNonNative(Shape* shape) +{ + MOZ_ASSERT(!inDictionaryMode()); + MOZ_ASSERT(!shape->getObjectClass()->isNative()); + MOZ_ASSERT(shape->zone() == zone()); + MOZ_ASSERT(shape->slotSpan() == 0); + MOZ_ASSERT(shape->numFixedSlots() == 0); + + if (hasDynamicElements()) + js_free(getElementsHeader()); + if (hasDynamicSlots()) { + js_free(slots_); + slots_ = nullptr; + } + + shape_ = shape; +} + +void +NativeObject::setLastPropertyMakeNative(ExclusiveContext* cx, Shape* shape) +{ + MOZ_ASSERT(getClass()->isNative()); + MOZ_ASSERT(shape->getObjectClass()->isNative()); + MOZ_ASSERT(!shape->inDictionary()); + + // This method is used to convert unboxed objects into native objects. In + // this case, the shape_ field was previously used to store other data and + // this should be treated as an initialization. + shape_.init(shape); + + slots_ = nullptr; + elements_ = emptyObjectElements; + + size_t oldSpan = shape->numFixedSlots(); + size_t newSpan = shape->slotSpan(); + + initializeSlotRange(0, oldSpan); + + // A failure at this point will leave the object as a mutant, and we + // can't recover. + AutoEnterOOMUnsafeRegion oomUnsafe; + if (oldSpan != newSpan && !updateSlotsForSpan(cx, oldSpan, newSpan)) + oomUnsafe.crash("NativeObject::setLastPropertyMakeNative"); +} + +bool +NativeObject::setSlotSpan(ExclusiveContext* cx, uint32_t span) +{ + MOZ_ASSERT(inDictionaryMode()); + + size_t oldSpan = lastProperty()->base()->slotSpan(); + if (oldSpan == span) + return true; + + if (!updateSlotsForSpan(cx, oldSpan, span)) + return false; + + lastProperty()->base()->setSlotSpan(span); + return true; +} + +bool +NativeObject::growSlots(ExclusiveContext* cx, uint32_t oldCount, uint32_t newCount) +{ + MOZ_ASSERT(newCount > oldCount); + MOZ_ASSERT_IF(!is<ArrayObject>(), newCount >= SLOT_CAPACITY_MIN); + + /* + * Slot capacities are determined by the span of allocated objects. Due to + * the limited number of bits to store shape slots, object growth is + * throttled well before the slot capacity can overflow. + */ + NativeObject::slotsSizeMustNotOverflow(); + MOZ_ASSERT(newCount <= MAX_SLOTS_COUNT); + + if (!oldCount) { + MOZ_ASSERT(!slots_); + slots_ = AllocateObjectBuffer<HeapSlot>(cx, this, newCount); + if (!slots_) + return false; + Debug_SetSlotRangeToCrashOnTouch(slots_, newCount); + return true; + } + + HeapSlot* newslots = ReallocateObjectBuffer<HeapSlot>(cx, this, slots_, oldCount, newCount); + if (!newslots) + return false; /* Leave slots at its old size. */ + + slots_ = newslots; + + Debug_SetSlotRangeToCrashOnTouch(slots_ + oldCount, newCount - oldCount); + + return true; +} + +/* static */ bool +NativeObject::growSlotsDontReportOOM(ExclusiveContext* cx, NativeObject* obj, uint32_t newCount) +{ + if (!obj->growSlots(cx, obj->numDynamicSlots(), newCount)) { + cx->recoverFromOutOfMemory(); + return false; + } + return true; +} + +static void +FreeSlots(ExclusiveContext* cx, HeapSlot* slots) +{ + // Note: threads without a JSContext do not have access to GGC nursery allocated things. + if (cx->isJSContext()) + return cx->asJSContext()->runtime()->gc.nursery.freeBuffer(slots); + js_free(slots); +} + +void +NativeObject::shrinkSlots(ExclusiveContext* cx, uint32_t oldCount, uint32_t newCount) +{ + MOZ_ASSERT(newCount < oldCount); + + if (newCount == 0) { + FreeSlots(cx, slots_); + slots_ = nullptr; + return; + } + + MOZ_ASSERT_IF(!is<ArrayObject>(), newCount >= SLOT_CAPACITY_MIN); + + HeapSlot* newslots = ReallocateObjectBuffer<HeapSlot>(cx, this, slots_, oldCount, newCount); + if (!newslots) { + cx->recoverFromOutOfMemory(); + return; /* Leave slots at its old size. */ + } + + slots_ = newslots; +} + +/* static */ bool +NativeObject::sparsifyDenseElement(ExclusiveContext* cx, HandleNativeObject obj, uint32_t index) +{ + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + RootedValue value(cx, obj->getDenseElement(index)); + MOZ_ASSERT(!value.isMagic(JS_ELEMENTS_HOLE)); + + removeDenseElementForSparseIndex(cx, obj, index); + + uint32_t slot = obj->slotSpan(); + + RootedId id(cx, INT_TO_JSID(index)); + + AutoKeepShapeTables keep(cx); + ShapeTable::Entry* entry = nullptr; + if (obj->inDictionaryMode()) { + ShapeTable* table = obj->lastProperty()->ensureTableForDictionary(cx, keep); + if (!table) + return false; + entry = &table->search<MaybeAdding::Adding>(id, keep); + } + + // NOTE: We don't use addDataProperty because we don't want the + // extensibility check if we're, for example, sparsifying frozen objects.. + if (!addPropertyInternal(cx, obj, id, nullptr, nullptr, slot, + obj->getElementsHeader()->elementAttributes(), + 0, entry, true, keep)) { + obj->setDenseElementUnchecked(index, value); + return false; + } + + MOZ_ASSERT(slot == obj->slotSpan() - 1); + obj->initSlot(slot, value); + + return true; +} + +/* static */ bool +NativeObject::sparsifyDenseElements(js::ExclusiveContext* cx, HandleNativeObject obj) +{ + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + uint32_t initialized = obj->getDenseInitializedLength(); + + /* Create new properties with the value of non-hole dense elements. */ + for (uint32_t i = 0; i < initialized; i++) { + if (obj->getDenseElement(i).isMagic(JS_ELEMENTS_HOLE)) + continue; + + if (!sparsifyDenseElement(cx, obj, i)) + return false; + } + + if (initialized) + obj->setDenseInitializedLengthUnchecked(0); + + /* + * Reduce storage for dense elements which are now holes. Explicitly mark + * the elements capacity as zero, so that any attempts to add dense + * elements will be caught in ensureDenseElements. + */ + if (obj->getDenseCapacity()) { + obj->shrinkElements(cx, 0); + obj->getElementsHeader()->capacity = 0; + } + + return true; +} + +bool +NativeObject::willBeSparseElements(uint32_t requiredCapacity, uint32_t newElementsHint) +{ + MOZ_ASSERT(isNative()); + MOZ_ASSERT(requiredCapacity > MIN_SPARSE_INDEX); + + uint32_t cap = getDenseCapacity(); + MOZ_ASSERT(requiredCapacity >= cap); + + if (requiredCapacity > MAX_DENSE_ELEMENTS_COUNT) + return true; + + uint32_t minimalDenseCount = requiredCapacity / SPARSE_DENSITY_RATIO; + if (newElementsHint >= minimalDenseCount) + return false; + minimalDenseCount -= newElementsHint; + + if (minimalDenseCount > cap) + return true; + + uint32_t len = getDenseInitializedLength(); + const Value* elems = getDenseElements(); + for (uint32_t i = 0; i < len; i++) { + if (!elems[i].isMagic(JS_ELEMENTS_HOLE) && !--minimalDenseCount) + return false; + } + return true; +} + +/* static */ DenseElementResult +NativeObject::maybeDensifySparseElements(js::ExclusiveContext* cx, HandleNativeObject obj) +{ + /* + * Wait until after the object goes into dictionary mode, which must happen + * when sparsely packing any array with more than MIN_SPARSE_INDEX elements + * (see PropertyTree::MAX_HEIGHT). + */ + if (!obj->inDictionaryMode()) + return DenseElementResult::Incomplete; + + /* + * Only measure the number of indexed properties every log(n) times when + * populating the object. + */ + uint32_t slotSpan = obj->slotSpan(); + if (slotSpan != RoundUpPow2(slotSpan)) + return DenseElementResult::Incomplete; + + /* Watch for conditions under which an object's elements cannot be dense. */ + if (!obj->nonProxyIsExtensible() || obj->watched()) + return DenseElementResult::Incomplete; + + /* + * The indexes in the object need to be sufficiently dense before they can + * be converted to dense mode. + */ + uint32_t numDenseElements = 0; + uint32_t newInitializedLength = 0; + + RootedShape shape(cx, obj->lastProperty()); + while (!shape->isEmptyShape()) { + uint32_t index; + if (IdIsIndex(shape->propid(), &index)) { + if (shape->attributes() == JSPROP_ENUMERATE && + shape->hasDefaultGetter() && + shape->hasDefaultSetter()) + { + numDenseElements++; + newInitializedLength = Max(newInitializedLength, index + 1); + } else { + /* + * For simplicity, only densify the object if all indexed + * properties can be converted to dense elements. + */ + return DenseElementResult::Incomplete; + } + } + shape = shape->previous(); + } + + if (numDenseElements * SPARSE_DENSITY_RATIO < newInitializedLength) + return DenseElementResult::Incomplete; + + if (newInitializedLength > MAX_DENSE_ELEMENTS_COUNT) + return DenseElementResult::Incomplete; + + /* + * This object meets all necessary restrictions, convert all indexed + * properties into dense elements. + */ + + if (!obj->maybeCopyElementsForWrite(cx)) + return DenseElementResult::Failure; + + if (newInitializedLength > obj->getDenseCapacity()) { + if (!obj->growElements(cx, newInitializedLength)) + return DenseElementResult::Failure; + } + + obj->ensureDenseInitializedLength(cx, newInitializedLength, 0); + + RootedValue value(cx); + + shape = obj->lastProperty(); + while (!shape->isEmptyShape()) { + jsid id = shape->propid(); + uint32_t index; + if (IdIsIndex(id, &index)) { + value = obj->getSlot(shape->slot()); + + /* + * When removing a property from a dictionary, the specified + * property will be removed from the dictionary list and the + * last property will then be changed due to reshaping the object. + * Compute the next shape in the traverse, watching for such + * removals from the list. + */ + if (shape != obj->lastProperty()) { + shape = shape->previous(); + if (!obj->removeProperty(cx, id)) + return DenseElementResult::Failure; + } else { + if (!obj->removeProperty(cx, id)) + return DenseElementResult::Failure; + shape = obj->lastProperty(); + } + + obj->setDenseElement(index, value); + } else { + shape = shape->previous(); + } + } + + /* + * All indexed properties on the object are now dense, clear the indexed + * flag so that we will not start using sparse indexes again if we need + * to grow the object. + */ + if (!obj->clearFlag(cx, BaseShape::INDEXED)) + return DenseElementResult::Failure; + + return DenseElementResult::Success; +} + +// Given a requested capacity (in elements) and (potentially) the length of an +// array for which elements are being allocated, compute an actual allocation +// amount (in elements). (Allocation amounts include space for an +// ObjectElements instance, so a return value of |N| implies +// |N - ObjectElements::VALUES_PER_HEADER| usable elements.) +// +// The requested/actual allocation distinction is meant to: +// +// * preserve amortized O(N) time to add N elements; +// * minimize the number of unused elements beyond an array's length, and +// * provide at least SLOT_CAPACITY_MIN elements no matter what (so adding +// the first several elements to small arrays only needs one allocation). +// +// Note: the structure and behavior of this method follow along with +// UnboxedArrayObject::chooseCapacityIndex. Changes to the allocation strategy +// in one should generally be matched by the other. +/* static */ bool +NativeObject::goodElementsAllocationAmount(ExclusiveContext* cx, uint32_t reqCapacity, + uint32_t length, uint32_t* goodAmount) +{ + if (reqCapacity > MAX_DENSE_ELEMENTS_COUNT) { + ReportOutOfMemory(cx); + return false; + } + + uint32_t reqAllocated = reqCapacity + ObjectElements::VALUES_PER_HEADER; + + // Handle "small" requests primarily by doubling. + const uint32_t Mebi = 1 << 20; + if (reqAllocated < Mebi) { + uint32_t amount = mozilla::AssertedCast<uint32_t>(RoundUpPow2(reqAllocated)); + + // If |amount| would be 2/3 or more of the array's length, adjust + // it (up or down) to be equal to the array's length. This avoids + // allocating excess elements that aren't likely to be needed, either + // in this resizing or a subsequent one. The 2/3 factor is chosen so + // that exceptional resizings will at most triple the capacity, as + // opposed to the usual doubling. + uint32_t goodCapacity = amount - ObjectElements::VALUES_PER_HEADER; + if (length >= reqCapacity && goodCapacity > (length / 3) * 2) + amount = length + ObjectElements::VALUES_PER_HEADER; + + if (amount < SLOT_CAPACITY_MIN) + amount = SLOT_CAPACITY_MIN; + + *goodAmount = amount; + + return true; + } + + // The almost-doubling above wastes a lot of space for larger bucket sizes. + // For large amounts, switch to bucket sizes that obey this formula: + // + // count(n+1) = Math.ceil(count(n) * 1.125) + // + // where |count(n)| is the size of the nth bucket, measured in 2**20 slots. + // These bucket sizes still preserve amortized O(N) time to add N elements, + // just with a larger constant factor. + // + // The bucket size table below was generated with this JavaScript (and + // manual reformatting): + // + // for (let n = 1, i = 0; i < 34; i++) { + // print('0x' + (n * (1 << 20)).toString(16) + ', '); + // n = Math.ceil(n * 1.125); + // } + static const uint32_t BigBuckets[] = { + 0x100000, 0x200000, 0x300000, 0x400000, 0x500000, 0x600000, 0x700000, + 0x800000, 0x900000, 0xb00000, 0xd00000, 0xf00000, 0x1100000, 0x1400000, + 0x1700000, 0x1a00000, 0x1e00000, 0x2200000, 0x2700000, 0x2c00000, + 0x3200000, 0x3900000, 0x4100000, 0x4a00000, 0x5400000, 0x5f00000, + 0x6b00000, 0x7900000, 0x8900000, 0x9b00000, 0xaf00000, 0xc500000, + 0xde00000, 0xfa00000 + }; + MOZ_ASSERT(BigBuckets[ArrayLength(BigBuckets) - 1] <= MAX_DENSE_ELEMENTS_ALLOCATION); + + // Pick the first bucket that'll fit |reqAllocated|. + for (uint32_t b : BigBuckets) { + if (b >= reqAllocated) { + *goodAmount = b; + return true; + } + } + + // Otherwise, return the maximum bucket size. + *goodAmount = MAX_DENSE_ELEMENTS_ALLOCATION; + return true; +} + +bool +NativeObject::growElements(ExclusiveContext* cx, uint32_t reqCapacity) +{ + MOZ_ASSERT(nonProxyIsExtensible()); + MOZ_ASSERT(canHaveNonEmptyElements()); + MOZ_ASSERT(!denseElementsAreFrozen()); + if (denseElementsAreCopyOnWrite()) + MOZ_CRASH(); + + uint32_t oldCapacity = getDenseCapacity(); + MOZ_ASSERT(oldCapacity < reqCapacity); + + uint32_t newAllocated = 0; + if (is<ArrayObject>() && !as<ArrayObject>().lengthIsWritable()) { + MOZ_ASSERT(reqCapacity <= as<ArrayObject>().length()); + MOZ_ASSERT(reqCapacity <= MAX_DENSE_ELEMENTS_COUNT); + // Preserve the |capacity <= length| invariant for arrays with + // non-writable length. See also js::ArraySetLength which initially + // enforces this requirement. + newAllocated = reqCapacity + ObjectElements::VALUES_PER_HEADER; + } else { + if (!goodElementsAllocationAmount(cx, reqCapacity, getElementsHeader()->length, &newAllocated)) + return false; + } + + uint32_t newCapacity = newAllocated - ObjectElements::VALUES_PER_HEADER; + MOZ_ASSERT(newCapacity > oldCapacity && newCapacity >= reqCapacity); + + // If newCapacity exceeds MAX_DENSE_ELEMENTS_COUNT, the array should become + // sparse. + MOZ_ASSERT(newCapacity <= MAX_DENSE_ELEMENTS_COUNT); + + uint32_t initlen = getDenseInitializedLength(); + + HeapSlot* oldHeaderSlots = reinterpret_cast<HeapSlot*>(getElementsHeader()); + HeapSlot* newHeaderSlots; + if (hasDynamicElements()) { + MOZ_ASSERT(oldCapacity <= MAX_DENSE_ELEMENTS_COUNT); + uint32_t oldAllocated = oldCapacity + ObjectElements::VALUES_PER_HEADER; + + newHeaderSlots = ReallocateObjectBuffer<HeapSlot>(cx, this, oldHeaderSlots, oldAllocated, newAllocated); + if (!newHeaderSlots) + return false; // Leave elements at its old size. + } else { + newHeaderSlots = AllocateObjectBuffer<HeapSlot>(cx, this, newAllocated); + if (!newHeaderSlots) + return false; // Leave elements at its old size. + PodCopy(newHeaderSlots, oldHeaderSlots, ObjectElements::VALUES_PER_HEADER + initlen); + } + + ObjectElements* newheader = reinterpret_cast<ObjectElements*>(newHeaderSlots); + newheader->capacity = newCapacity; + elements_ = newheader->elements(); + + Debug_SetSlotRangeToCrashOnTouch(elements_ + initlen, newCapacity - initlen); + + return true; +} + +void +NativeObject::shrinkElements(ExclusiveContext* cx, uint32_t reqCapacity) +{ + uint32_t oldCapacity = getDenseCapacity(); + MOZ_ASSERT(reqCapacity < oldCapacity); + + MOZ_ASSERT(canHaveNonEmptyElements()); + if (denseElementsAreCopyOnWrite()) + MOZ_CRASH(); + + if (!hasDynamicElements()) + return; + + uint32_t newAllocated = 0; + MOZ_ALWAYS_TRUE(goodElementsAllocationAmount(cx, reqCapacity, 0, &newAllocated)); + MOZ_ASSERT(oldCapacity <= MAX_DENSE_ELEMENTS_COUNT); + uint32_t oldAllocated = oldCapacity + ObjectElements::VALUES_PER_HEADER; + if (newAllocated == oldAllocated) + return; // Leave elements at its old size. + + MOZ_ASSERT(newAllocated > ObjectElements::VALUES_PER_HEADER); + uint32_t newCapacity = newAllocated - ObjectElements::VALUES_PER_HEADER; + MOZ_ASSERT(newCapacity <= MAX_DENSE_ELEMENTS_COUNT); + + HeapSlot* oldHeaderSlots = reinterpret_cast<HeapSlot*>(getElementsHeader()); + HeapSlot* newHeaderSlots = ReallocateObjectBuffer<HeapSlot>(cx, this, oldHeaderSlots, + oldAllocated, newAllocated); + if (!newHeaderSlots) { + cx->recoverFromOutOfMemory(); + return; // Leave elements at its old size. + } + + ObjectElements* newheader = reinterpret_cast<ObjectElements*>(newHeaderSlots); + newheader->capacity = newCapacity; + elements_ = newheader->elements(); +} + +/* static */ bool +NativeObject::CopyElementsForWrite(ExclusiveContext* cx, NativeObject* obj) +{ + MOZ_ASSERT(obj->denseElementsAreCopyOnWrite()); + MOZ_ASSERT(!obj->denseElementsAreFrozen()); + + // The original owner of a COW elements array should never be modified. + MOZ_ASSERT(obj->getElementsHeader()->ownerObject() != obj); + + uint32_t initlen = obj->getDenseInitializedLength(); + uint32_t newAllocated = 0; + if (!goodElementsAllocationAmount(cx, initlen, 0, &newAllocated)) + return false; + + uint32_t newCapacity = newAllocated - ObjectElements::VALUES_PER_HEADER; + + // COPY_ON_WRITE flags is set only if obj is a dense array. + MOZ_ASSERT(newCapacity <= MAX_DENSE_ELEMENTS_COUNT); + + JSObject::writeBarrierPre(obj->getElementsHeader()->ownerObject()); + + HeapSlot* newHeaderSlots = AllocateObjectBuffer<HeapSlot>(cx, obj, newAllocated); + if (!newHeaderSlots) + return false; + ObjectElements* newheader = reinterpret_cast<ObjectElements*>(newHeaderSlots); + js_memcpy(newheader, obj->getElementsHeader(), + (ObjectElements::VALUES_PER_HEADER + initlen) * sizeof(Value)); + + newheader->capacity = newCapacity; + newheader->clearCopyOnWrite(); + obj->elements_ = newheader->elements(); + + Debug_SetSlotRangeToCrashOnTouch(obj->elements_ + initlen, newCapacity - initlen); + + return true; +} + +/* static */ bool +NativeObject::allocSlot(ExclusiveContext* cx, HandleNativeObject obj, uint32_t* slotp) +{ + uint32_t slot = obj->slotSpan(); + MOZ_ASSERT(slot >= JSSLOT_FREE(obj->getClass())); + + // If this object is in dictionary mode, try to pull a free slot from the + // shape table's slot-number free list. Shapes without a ShapeTable have an + // empty free list, because we only purge ShapeTables with an empty free + // list. + if (obj->inDictionaryMode()) { + AutoCheckCannotGC nogc; + if (ShapeTable* table = obj->lastProperty()->maybeTable(nogc)) { + uint32_t last = table->freeList(); + if (last != SHAPE_INVALID_SLOT) { +#ifdef DEBUG + MOZ_ASSERT(last < slot); + uint32_t next = obj->getSlot(last).toPrivateUint32(); + MOZ_ASSERT_IF(next != SHAPE_INVALID_SLOT, next < slot); +#endif + + *slotp = last; + + const Value& vref = obj->getSlot(last); + table->setFreeList(vref.toPrivateUint32()); + obj->setSlot(last, UndefinedValue()); + return true; + } + } + } + + if (slot >= SHAPE_MAXIMUM_SLOT) { + ReportOutOfMemory(cx); + return false; + } + + *slotp = slot; + + if (obj->inDictionaryMode() && !obj->setSlotSpan(cx, slot + 1)) + return false; + + return true; +} + +void +NativeObject::freeSlot(ExclusiveContext* cx, uint32_t slot) +{ + MOZ_ASSERT(slot < slotSpan()); + + if (inDictionaryMode()) { + // Ensure we have a ShapeTable as it stores the object's free list (the + // list of available slots in dictionary objects). + AutoCheckCannotGC nogc; + if (ShapeTable* table = lastProperty()->ensureTableForDictionary(cx, nogc)) { + uint32_t last = table->freeList(); + + // Can't afford to check the whole free list, but let's check the head. + MOZ_ASSERT_IF(last != SHAPE_INVALID_SLOT, last < slotSpan() && last != slot); + + // Place all freed slots other than reserved slots (bug 595230) on the + // dictionary's free list. + if (JSSLOT_FREE(getClass()) <= slot) { + MOZ_ASSERT_IF(last != SHAPE_INVALID_SLOT, last < slotSpan()); + setSlot(slot, PrivateUint32Value(last)); + table->setFreeList(slot); + return; + } + } else { + // OOM while creating the ShapeTable holding the free list. We can + // recover from it - it just means we won't be able to reuse this + // slot later. + cx->recoverFromOutOfMemory(); + } + } + setSlot(slot, UndefinedValue()); +} + +Shape* +NativeObject::addDataProperty(ExclusiveContext* cx, jsid idArg, uint32_t slot, unsigned attrs) +{ + MOZ_ASSERT(!(attrs & (JSPROP_GETTER | JSPROP_SETTER))); + RootedNativeObject self(cx, this); + RootedId id(cx, idArg); + return addProperty(cx, self, id, nullptr, nullptr, slot, attrs, 0); +} + +Shape* +NativeObject::addDataProperty(ExclusiveContext* cx, HandlePropertyName name, + uint32_t slot, unsigned attrs) +{ + MOZ_ASSERT(!(attrs & (JSPROP_GETTER | JSPROP_SETTER))); + RootedNativeObject self(cx, this); + RootedId id(cx, NameToId(name)); + return addProperty(cx, self, id, nullptr, nullptr, slot, attrs, 0); +} + +template <AllowGC allowGC> +bool +js::NativeLookupOwnProperty(ExclusiveContext* cx, + typename MaybeRooted<NativeObject*, allowGC>::HandleType obj, + typename MaybeRooted<jsid, allowGC>::HandleType id, + typename MaybeRooted<Shape*, allowGC>::MutableHandleType propp) +{ + bool done; + return LookupOwnPropertyInline<allowGC>(cx, obj, id, propp, &done); +} + +template bool +js::NativeLookupOwnProperty<CanGC>(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + MutableHandleShape propp); + +template bool +js::NativeLookupOwnProperty<NoGC>(ExclusiveContext* cx, NativeObject* const& obj, const jsid& id, + FakeMutableHandle<Shape*> propp); + +/*** [[DefineOwnProperty]] ***********************************************************************/ + +static inline bool +CallAddPropertyHook(ExclusiveContext* cx, HandleNativeObject obj, HandleShape shape, + HandleValue value) +{ + if (JSAddPropertyOp addProperty = obj->getClass()->getAddProperty()) { + if (!cx->shouldBeJSContext()) + return false; + + RootedId id(cx, shape->propid()); + if (!CallJSAddPropertyOp(cx->asJSContext(), addProperty, obj, id, value)) { + obj->removeProperty(cx, shape->propid()); + return false; + } + } + return true; +} + +static inline bool +CallAddPropertyHookDense(ExclusiveContext* cx, HandleNativeObject obj, uint32_t index, + HandleValue value) +{ + // Inline addProperty for array objects. + if (obj->is<ArrayObject>()) { + ArrayObject* arr = &obj->as<ArrayObject>(); + uint32_t length = arr->length(); + if (index >= length) + arr->setLength(cx, index + 1); + return true; + } + + if (JSAddPropertyOp addProperty = obj->getClass()->getAddProperty()) { + if (!cx->shouldBeJSContext()) + return false; + + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + RootedId id(cx, INT_TO_JSID(index)); + if (!CallJSAddPropertyOp(cx->asJSContext(), addProperty, obj, id, value)) { + obj->setDenseElementHole(cx, index); + return false; + } + } + return true; +} + +static bool +UpdateShapeTypeAndValue(ExclusiveContext* cx, HandleNativeObject obj, HandleShape shape, const Value& value) +{ + jsid id = shape->propid(); + if (shape->hasSlot()) { + obj->setSlotWithType(cx, shape, value, /* overwriting = */ false); + + // Per the acquired properties analysis, when the shape of a partially + // initialized object is changed to its fully initialized shape, its + // group can be updated as well. + if (TypeNewScript* newScript = obj->groupRaw()->newScript()) { + if (newScript->initializedShape() == shape) + obj->setGroup(newScript->initializedGroup()); + } + } + if (!shape->hasSlot() || !shape->hasDefaultGetter() || !shape->hasDefaultSetter()) + MarkTypePropertyNonData(cx, obj, id); + if (!shape->writable()) + MarkTypePropertyNonWritable(cx, obj, id); + return true; +} + +static bool +PurgeProtoChain(ExclusiveContext* cx, JSObject* objArg, HandleId id) +{ + /* Root locally so we can re-assign. */ + RootedObject obj(cx, objArg); + + RootedShape shape(cx); + while (obj) { + /* Lookups will not be cached through non-native protos. */ + if (!obj->isNative()) + break; + + shape = obj->as<NativeObject>().lookup(cx, id); + if (shape) + return obj->as<NativeObject>().shadowingShapeChange(cx, *shape); + + obj = obj->staticPrototype(); + } + + return true; +} + +static bool +PurgeEnvironmentChainHelper(ExclusiveContext* cx, HandleObject objArg, HandleId id) +{ + /* Re-root locally so we can re-assign. */ + RootedObject obj(cx, objArg); + + MOZ_ASSERT(obj->isNative()); + MOZ_ASSERT(obj->isDelegate()); + + /* Lookups on integer ids cannot be cached through prototypes. */ + if (JSID_IS_INT(id)) + return true; + + if (!PurgeProtoChain(cx, obj->staticPrototype(), id)) + return false; + + /* + * We must purge the environment chain only for Call objects as they are + * the only kind of cacheable non-global object that can gain properties + * after outer properties with the same names have been cached or + * traced. Call objects may gain such properties via eval introducing new + * vars; see bug 490364. + */ + if (obj->is<CallObject>()) { + while ((obj = obj->enclosingEnvironment()) != nullptr) { + if (!PurgeProtoChain(cx, obj, id)) + return false; + } + } + + return true; +} + +/* + * PurgeEnvironmentChain does nothing if obj is not itself a prototype or + * parent environment, else it reshapes the scope and prototype chains it + * links. It calls PurgeEnvironmentChainHelper, which asserts that obj is + * flagged as a delegate (i.e., obj has ever been on a prototype or parent + * chain). + */ +static inline bool +PurgeEnvironmentChain(ExclusiveContext* cx, HandleObject obj, HandleId id) +{ + if (obj->isDelegate() && obj->isNative()) + return PurgeEnvironmentChainHelper(cx, obj, id); + return true; +} + +static bool +AddOrChangeProperty(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + Handle<PropertyDescriptor> desc) +{ + desc.assertComplete(); + + if (!PurgeEnvironmentChain(cx, obj, id)) + return false; + + // Use dense storage for new indexed properties where possible. + if (JSID_IS_INT(id) && + !desc.getter() && + !desc.setter() && + desc.attributes() == JSPROP_ENUMERATE && + (!obj->isIndexed() || !obj->containsPure(id)) && + !obj->is<TypedArrayObject>()) + { + uint32_t index = JSID_TO_INT(id); + DenseElementResult edResult = obj->ensureDenseElements(cx, index, 1); + if (edResult == DenseElementResult::Failure) + return false; + if (edResult == DenseElementResult::Success) { + obj->setDenseElementWithType(cx, index, desc.value()); + if (!CallAddPropertyHookDense(cx, obj, index, desc.value())) + return false; + return true; + } + } + + RootedShape shape(cx, NativeObject::putProperty(cx, obj, id, desc.getter(), desc.setter(), + SHAPE_INVALID_SLOT, desc.attributes(), 0)); + if (!shape) + return false; + + if (!UpdateShapeTypeAndValue(cx, obj, shape, desc.value())) + return false; + + // Clear any existing dense index after adding a sparse indexed property, + // and investigate converting the object to dense indexes. + if (JSID_IS_INT(id)) { + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + uint32_t index = JSID_TO_INT(id); + NativeObject::removeDenseElementForSparseIndex(cx, obj, index); + DenseElementResult edResult = + NativeObject::maybeDensifySparseElements(cx, obj); + if (edResult == DenseElementResult::Failure) + return false; + if (edResult == DenseElementResult::Success) { + MOZ_ASSERT(!desc.setter()); + return CallAddPropertyHookDense(cx, obj, index, desc.value()); + } + } + + return CallAddPropertyHook(cx, obj, shape, desc.value()); +} + +static bool IsConfigurable(unsigned attrs) { return (attrs & JSPROP_PERMANENT) == 0; } +static bool IsEnumerable(unsigned attrs) { return (attrs & JSPROP_ENUMERATE) != 0; } +static bool IsWritable(unsigned attrs) { return (attrs & JSPROP_READONLY) == 0; } + +static bool IsAccessorDescriptor(unsigned attrs) { + return (attrs & (JSPROP_GETTER | JSPROP_SETTER)) != 0; +} + +static bool IsDataDescriptor(unsigned attrs) { + MOZ_ASSERT((attrs & (JSPROP_IGNORE_VALUE | JSPROP_IGNORE_READONLY)) == 0); + return !IsAccessorDescriptor(attrs); +} + +template <AllowGC allowGC> +static MOZ_ALWAYS_INLINE bool +GetExistingProperty(JSContext* cx, + typename MaybeRooted<Value, allowGC>::HandleType receiver, + typename MaybeRooted<NativeObject*, allowGC>::HandleType obj, + typename MaybeRooted<Shape*, allowGC>::HandleType shape, + typename MaybeRooted<Value, allowGC>::MutableHandleType vp); + +static bool +GetExistingPropertyValue(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + HandleShape shape, MutableHandleValue vp) +{ + if (IsImplicitDenseOrTypedArrayElement(shape)) { + vp.set(obj->getDenseOrTypedArrayElement(JSID_TO_INT(id))); + return true; + } + if (!cx->shouldBeJSContext()) + return false; + + MOZ_ASSERT(shape->propid() == id); + MOZ_ASSERT(obj->contains(cx, shape)); + + RootedValue receiver(cx, ObjectValue(*obj)); + return GetExistingProperty<CanGC>(cx->asJSContext(), receiver, obj, shape, vp); +} + +/* + * If ES6 draft rev 37 9.1.6.3 ValidateAndApplyPropertyDescriptor step 4 would + * return early, because desc is redundant with an existing own property obj[id], + * then set *redundant = true and return true. + */ +static bool +DefinePropertyIsRedundant(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + HandleShape shape, unsigned shapeAttrs, + Handle<PropertyDescriptor> desc, bool *redundant) +{ + *redundant = false; + + if (desc.hasConfigurable() && desc.configurable() != ((shapeAttrs & JSPROP_PERMANENT) == 0)) + return true; + if (desc.hasEnumerable() && desc.enumerable() != ((shapeAttrs & JSPROP_ENUMERATE) != 0)) + return true; + if (desc.isDataDescriptor()) { + if ((shapeAttrs & (JSPROP_GETTER | JSPROP_SETTER)) != 0) + return true; + if (desc.hasWritable() && desc.writable() != ((shapeAttrs & JSPROP_READONLY) == 0)) + return true; + if (desc.hasValue()) { + // Get the current value of the existing property. + RootedValue currentValue(cx); + if (!IsImplicitDenseOrTypedArrayElement(shape) && + shape->hasSlot() && + shape->hasDefaultGetter()) + { + // Inline GetExistingPropertyValue in order to omit a type + // correctness assertion that's too strict for this particular + // call site. For details, see bug 1125624 comments 13-16. + currentValue.set(obj->getSlot(shape->slot())); + } else { + if (!GetExistingPropertyValue(cx, obj, id, shape, ¤tValue)) + return false; + } + + // The specification calls for SameValue here, but it seems to be a + // bug. See <https://bugs.ecmascript.org/show_bug.cgi?id=3508>. + if (desc.value() != currentValue) + return true; + } + + GetterOp existingGetterOp = + IsImplicitDenseOrTypedArrayElement(shape) ? nullptr : shape->getter(); + if (desc.getter() != existingGetterOp) + return true; + + SetterOp existingSetterOp = + IsImplicitDenseOrTypedArrayElement(shape) ? nullptr : shape->setter(); + if (desc.setter() != existingSetterOp) + return true; + } else { + if (desc.hasGetterObject()) { + if (!(shapeAttrs & JSPROP_GETTER) || desc.getterObject() != shape->getterObject()) + return true; + } + if (desc.hasSetterObject()) { + if (!(shapeAttrs & JSPROP_SETTER) || desc.setterObject() != shape->setterObject()) + return true; + } + } + + *redundant = true; + return true; +} + +bool +js::NativeDefineProperty(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + Handle<PropertyDescriptor> desc_, + ObjectOpResult& result) +{ + desc_.assertValid(); + + // Section numbers and step numbers below refer to ES6 draft rev 36 + // (17 March 2015). + // + // This function aims to implement 9.1.6 [[DefineOwnProperty]] as well as + // the [[DefineOwnProperty]] methods described in 9.4.2.1 (arrays), 9.4.4.2 + // (arguments), and 9.4.5.3 (typed array views). + + // Dispense with custom behavior of exotic native objects first. + if (obj->is<ArrayObject>()) { + // 9.4.2.1 step 2. Redefining an array's length is very special. + Rooted<ArrayObject*> arr(cx, &obj->as<ArrayObject>()); + if (id == NameToId(cx->names().length)) { + if (!cx->shouldBeJSContext()) + return false; + return ArraySetLength(cx->asJSContext(), arr, id, desc_.attributes(), desc_.value(), + result); + } + + // 9.4.2.1 step 3. Don't extend a fixed-length array. + uint32_t index; + if (IdIsIndex(id, &index)) { + if (WouldDefinePastNonwritableLength(obj, index)) + return result.fail(JSMSG_CANT_DEFINE_PAST_ARRAY_LENGTH); + } + } else if (obj->is<TypedArrayObject>()) { + // 9.4.5.3 step 3. Indexed properties of typed arrays are special. + uint64_t index; + if (IsTypedArrayIndex(id, &index)) { + if (!cx->shouldBeJSContext()) + return false; + return DefineTypedArrayElement(cx->asJSContext(), obj, index, desc_, result); + } + } else if (obj->is<ArgumentsObject>()) { + if (id == NameToId(cx->names().length)) { + // Either we are resolving the .length property on this object, or + // redefining it. In the latter case only, we must set a bit. To + // distinguish the two cases, we note that when resolving, the + // property won't already exist; whereas the first time it is + // redefined, it will. + if ((desc_.attributes() & JSPROP_RESOLVING) == 0) + obj->as<ArgumentsObject>().markLengthOverridden(); + } else if (JSID_IS_SYMBOL(id) && JSID_TO_SYMBOL(id) == cx->wellKnownSymbols().iterator) { + // Do same thing as .length for [@@iterator]. + if ((desc_.attributes() & JSPROP_RESOLVING) == 0) + obj->as<ArgumentsObject>().markIteratorOverridden(); + } else if (JSID_IS_INT(id)) { + if ((desc_.attributes() & JSPROP_RESOLVING) == 0) + obj->as<ArgumentsObject>().markElementOverridden(); + } + } + + // 9.1.6.1 OrdinaryDefineOwnProperty steps 1-2. + RootedShape shape(cx); + if (desc_.attributes() & JSPROP_RESOLVING) { + // We are being called from a resolve or enumerate hook to reify a + // lazily-resolved property. To avoid reentering the resolve hook and + // recursing forever, skip the resolve hook when doing this lookup. + NativeLookupOwnPropertyNoResolve(cx, obj, id, &shape); + } else { + if (!NativeLookupOwnProperty<CanGC>(cx, obj, id, &shape)) + return false; + } + + // From this point, the step numbers refer to + // 9.1.6.3, ValidateAndApplyPropertyDescriptor. + // Step 1 is a redundant assertion. + + // Filling in desc: Here we make a copy of the desc_ argument. We will turn + // it into a complete descriptor before updating obj. The spec algorithm + // does not explicitly do this, but the end result is the same. Search for + // "fill in" below for places where the filling-in actually occurs. + Rooted<PropertyDescriptor> desc(cx, desc_); + + // Step 2. + if (!shape) { + if (!obj->nonProxyIsExtensible()) + return result.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + + // Fill in missing desc fields with defaults. + CompletePropertyDescriptor(&desc); + + if (!AddOrChangeProperty(cx, obj, id, desc)) + return false; + return result.succeed(); + } + + MOZ_ASSERT(shape); + + // Steps 3-4. (Step 3 is a special case of step 4.) We use shapeAttrs as a + // stand-in for shape in many places below, since shape might not be a + // pointer to a real Shape (see IsImplicitDenseOrTypedArrayElement). + unsigned shapeAttrs = GetShapeAttributes(obj, shape); + bool redundant; + if (!DefinePropertyIsRedundant(cx, obj, id, shape, shapeAttrs, desc, &redundant)) + return false; + if (redundant) { + // In cases involving JSOP_NEWOBJECT and JSOP_INITPROP, obj can have a + // type for this property that doesn't match the value in the slot. + // Update the type here, even though this DefineProperty call is + // otherwise a no-op. (See bug 1125624 comment 13.) + if (!IsImplicitDenseOrTypedArrayElement(shape) && desc.hasValue()) { + if (!UpdateShapeTypeAndValue(cx, obj, shape, desc.value())) + return false; + } + return result.succeed(); + } + + // Non-standard hack: Allow redefining non-configurable properties if + // JSPROP_REDEFINE_NONCONFIGURABLE is set _and_ the object is a non-DOM + // global. The idea is that a DOM object can never have such a thing on + // its proto chain directly on the web, so we should be OK optimizing + // access to accessors found on such an object. Bug 1105518 contemplates + // removing this hack. + bool skipRedefineChecks = (desc.attributes() & JSPROP_REDEFINE_NONCONFIGURABLE) && + obj->is<GlobalObject>() && + !obj->getClass()->isDOMClass(); + + // Step 5. + if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks) { + if (desc.hasConfigurable() && desc.configurable()) + return result.fail(JSMSG_CANT_REDEFINE_PROP); + if (desc.hasEnumerable() && desc.enumerable() != IsEnumerable(shapeAttrs)) + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + // Fill in desc.[[Configurable]] and desc.[[Enumerable]] if missing. + if (!desc.hasConfigurable()) + desc.setConfigurable(IsConfigurable(shapeAttrs)); + if (!desc.hasEnumerable()) + desc.setEnumerable(IsEnumerable(shapeAttrs)); + + // Steps 6-9. + if (desc.isGenericDescriptor()) { + // Step 6. No further validation is required. + + // Fill in desc. A generic descriptor has none of these fields, so copy + // everything from shape. + MOZ_ASSERT(!desc.hasValue()); + MOZ_ASSERT(!desc.hasWritable()); + MOZ_ASSERT(!desc.hasGetterObject()); + MOZ_ASSERT(!desc.hasSetterObject()); + if (IsDataDescriptor(shapeAttrs)) { + RootedValue currentValue(cx); + if (!GetExistingPropertyValue(cx, obj, id, shape, ¤tValue)) + return false; + desc.setValue(currentValue); + desc.setWritable(IsWritable(shapeAttrs)); + } else { + desc.setGetterObject(shape->getterObject()); + desc.setSetterObject(shape->setterObject()); + } + } else if (desc.isDataDescriptor() != IsDataDescriptor(shapeAttrs)) { + // Step 7. + if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks) + return result.fail(JSMSG_CANT_REDEFINE_PROP); + + if (IsImplicitDenseOrTypedArrayElement(shape)) { + MOZ_ASSERT(!obj->is<TypedArrayObject>()); + if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id))) + return false; + shape = obj->lookup(cx, id); + } + + // Fill in desc fields with default values (steps 7.b.i and 7.c.i). + CompletePropertyDescriptor(&desc); + } else if (desc.isDataDescriptor()) { + // Step 8. + bool frozen = !IsConfigurable(shapeAttrs) && !IsWritable(shapeAttrs); + if (frozen && desc.hasWritable() && desc.writable() && !skipRedefineChecks) + return result.fail(JSMSG_CANT_REDEFINE_PROP); + + if (frozen || !desc.hasValue()) { + if (IsImplicitDenseOrTypedArrayElement(shape)) { + MOZ_ASSERT(!obj->is<TypedArrayObject>()); + if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id))) + return false; + shape = obj->lookup(cx, id); + } + + RootedValue currentValue(cx); + if (!GetExistingPropertyValue(cx, obj, id, shape, ¤tValue)) + return false; + + if (!desc.hasValue()) { + // Fill in desc.[[Value]]. + desc.setValue(currentValue); + } else { + // Step 8.a.ii.1. + bool same; + if (!cx->shouldBeJSContext()) + return false; + if (!SameValue(cx->asJSContext(), desc.value(), currentValue, &same)) + return false; + if (!same && !skipRedefineChecks) + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + } + + if (!desc.hasWritable()) + desc.setWritable(IsWritable(shapeAttrs)); + } else { + // Step 9. + MOZ_ASSERT(shape->isAccessorDescriptor()); + MOZ_ASSERT(desc.isAccessorDescriptor()); + + // The spec says to use SameValue, but since the values in + // question are objects, we can just compare pointers. + if (desc.hasSetterObject()) { + if (!IsConfigurable(shapeAttrs) && + desc.setterObject() != shape->setterObject() && + !skipRedefineChecks) + { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + } else { + // Fill in desc.[[Set]] from shape. + desc.setSetterObject(shape->setterObject()); + } + if (desc.hasGetterObject()) { + if (!IsConfigurable(shapeAttrs) && + desc.getterObject() != shape->getterObject() && + !skipRedefineChecks) + { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + } else { + // Fill in desc.[[Get]] from shape. + desc.setGetterObject(shape->getterObject()); + } + } + + // Step 10. + if (!AddOrChangeProperty(cx, obj, id, desc)) + return false; + return result.succeed(); +} + +bool +js::NativeDefineProperty(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + HandleValue value, GetterOp getter, SetterOp setter, unsigned attrs, + ObjectOpResult& result) +{ + Rooted<PropertyDescriptor> desc(cx); + desc.initFields(nullptr, value, attrs, getter, setter); + return NativeDefineProperty(cx, obj, id, desc, result); +} + +bool +js::NativeDefineProperty(ExclusiveContext* cx, HandleNativeObject obj, PropertyName* name, + HandleValue value, GetterOp getter, SetterOp setter, unsigned attrs, + ObjectOpResult& result) +{ + RootedId id(cx, NameToId(name)); + return NativeDefineProperty(cx, obj, id, value, getter, setter, attrs, result); +} + +bool +js::NativeDefineElement(ExclusiveContext* cx, HandleNativeObject obj, uint32_t index, + HandleValue value, GetterOp getter, SetterOp setter, unsigned attrs, + ObjectOpResult& result) +{ + RootedId id(cx); + if (index <= JSID_INT_MAX) { + id = INT_TO_JSID(index); + return NativeDefineProperty(cx, obj, id, value, getter, setter, attrs, result); + } + + AutoRooterGetterSetter gsRoot(cx, attrs, &getter, &setter); + + if (!IndexToId(cx, index, &id)) + return false; + + return NativeDefineProperty(cx, obj, id, value, getter, setter, attrs, result); +} + +bool +js::NativeDefineProperty(ExclusiveContext* cx, HandleNativeObject obj, HandleId id, + HandleValue value, JSGetterOp getter, JSSetterOp setter, + unsigned attrs) +{ + ObjectOpResult result; + if (!NativeDefineProperty(cx, obj, id, value, getter, setter, attrs, result)) + return false; + if (!result) { + // Off-main-thread callers should not get here: they must call this + // function only with known-valid arguments. Populating a new + // PlainObject with configurable properties is fine. + if (!cx->shouldBeJSContext()) + return false; + result.reportError(cx->asJSContext(), obj, id); + return false; + } + return true; +} + +bool +js::NativeDefineProperty(ExclusiveContext* cx, HandleNativeObject obj, PropertyName* name, + HandleValue value, JSGetterOp getter, JSSetterOp setter, + unsigned attrs) +{ + RootedId id(cx, NameToId(name)); + return NativeDefineProperty(cx, obj, id, value, getter, setter, attrs); +} + + +/*** [[HasProperty]] *****************************************************************************/ + +// ES6 draft rev31 9.1.7.1 OrdinaryHasProperty +bool +js::NativeHasProperty(JSContext* cx, HandleNativeObject obj, HandleId id, bool* foundp) +{ + RootedNativeObject pobj(cx, obj); + RootedShape shape(cx); + + // This loop isn't explicit in the spec algorithm. See the comment on step + // 7.a. below. + for (;;) { + // Steps 2-3. ('done' is a SpiderMonkey-specific thing, used below.) + bool done; + if (!LookupOwnPropertyInline<CanGC>(cx, pobj, id, &shape, &done)) + return false; + + // Step 4. + if (shape) { + *foundp = true; + return true; + } + + // Step 5-6. The check for 'done' on this next line is tricky. + // done can be true in exactly these unlikely-sounding cases: + // - We're looking up an element, and pobj is a TypedArray that + // doesn't have that many elements. + // - We're being called from a resolve hook to assign to the property + // being resolved. + // What they all have in common is we do not want to keep walking + // the prototype chain, and always claim that the property + // doesn't exist. + RootedObject proto(cx, done ? nullptr : pobj->staticPrototype()); + + // Step 8. + if (!proto) { + *foundp = false; + return true; + } + + // Step 7.a. If the prototype is also native, this step is a + // recursive tail call, and we don't need to go through all the + // plumbing of HasProperty; the top of the loop is where + // we're going to end up anyway. But if pobj is non-native, + // that optimization would be incorrect. + if (!proto->isNative()) + return HasProperty(cx, proto, id, foundp); + + pobj = &proto->as<NativeObject>(); + } +} + + +/*** [[GetOwnPropertyDescriptor]] ****************************************************************/ + +bool +js::NativeGetOwnPropertyDescriptor(JSContext* cx, HandleNativeObject obj, HandleId id, + MutableHandle<PropertyDescriptor> desc) +{ + RootedShape shape(cx); + if (!NativeLookupOwnProperty<CanGC>(cx, obj, id, &shape)) + return false; + if (!shape) { + desc.object().set(nullptr); + return true; + } + + desc.setAttributes(GetShapeAttributes(obj, shape)); + if (desc.isAccessorDescriptor()) { + MOZ_ASSERT(desc.isShared()); + + // The result of GetOwnPropertyDescriptor() must be either undefined or + // a complete property descriptor (per ES6 draft rev 32 (2015 Feb 2) + // 6.1.7.3, Invariants of the Essential Internal Methods). + // + // It is an unfortunate fact that in SM, properties can exist that have + // JSPROP_GETTER or JSPROP_SETTER but not both. In these cases, rather + // than return true with desc incomplete, we fill out the missing + // getter or setter with a null, following CompletePropertyDescriptor. + if (desc.hasGetterObject()) { + desc.setGetterObject(shape->getterObject()); + } else { + desc.setGetterObject(nullptr); + desc.attributesRef() |= JSPROP_GETTER; + } + if (desc.hasSetterObject()) { + desc.setSetterObject(shape->setterObject()); + } else { + desc.setSetterObject(nullptr); + desc.attributesRef() |= JSPROP_SETTER; + } + + desc.value().setUndefined(); + } else { + // This is either a straight-up data property or (rarely) a + // property with a JSGetterOp/JSSetterOp. The latter must be + // reported to the caller as a plain data property, so clear + // desc.getter/setter, and mask away the SHARED bit. + desc.setGetter(nullptr); + desc.setSetter(nullptr); + desc.attributesRef() &= ~JSPROP_SHARED; + + if (IsImplicitDenseOrTypedArrayElement(shape)) { + desc.value().set(obj->getDenseOrTypedArrayElement(JSID_TO_INT(id))); + } else { + if (!NativeGetExistingProperty(cx, obj, obj, shape, desc.value())) + return false; + } + } + + desc.object().set(obj); + desc.assertComplete(); + return true; +} + + +/*** [[Get]] *************************************************************************************/ + +static inline bool +CallGetter(JSContext* cx, HandleObject obj, HandleValue receiver, HandleShape shape, + MutableHandleValue vp) +{ + MOZ_ASSERT(!shape->hasDefaultGetter()); + + if (shape->hasGetterValue()) { + RootedValue getter(cx, shape->getterValue()); + return js::CallGetter(cx, receiver, getter, vp); + } + + // In contrast to normal getters JSGetterOps always want the holder. + RootedId id(cx, shape->propid()); + return CallJSGetterOp(cx, shape->getterOp(), obj, id, vp); +} + +template <AllowGC allowGC> +static MOZ_ALWAYS_INLINE bool +GetExistingProperty(JSContext* cx, + typename MaybeRooted<Value, allowGC>::HandleType receiver, + typename MaybeRooted<NativeObject*, allowGC>::HandleType obj, + typename MaybeRooted<Shape*, allowGC>::HandleType shape, + typename MaybeRooted<Value, allowGC>::MutableHandleType vp) +{ + if (shape->hasSlot()) { + vp.set(obj->getSlot(shape->slot())); + MOZ_ASSERT_IF(!vp.isMagic(JS_UNINITIALIZED_LEXICAL) && + !obj->isSingleton() && + !obj->template is<EnvironmentObject>() && + shape->hasDefaultGetter(), + ObjectGroupHasProperty(cx, obj->group(), shape->propid(), vp)); + } else { + vp.setUndefined(); + } + if (shape->hasDefaultGetter()) + return true; + + { + jsbytecode* pc; + JSScript* script = cx->currentScript(&pc); + if (script && script->hasBaselineScript()) { + switch (JSOp(*pc)) { + case JSOP_GETPROP: + case JSOP_CALLPROP: + case JSOP_LENGTH: + script->baselineScript()->noteAccessedGetter(script->pcToOffset(pc)); + break; + default: + break; + } + } + } + + if (!allowGC) + return false; + + if (!CallGetter(cx, + MaybeRooted<JSObject*, allowGC>::toHandle(obj), + MaybeRooted<Value, allowGC>::toHandle(receiver), + MaybeRooted<Shape*, allowGC>::toHandle(shape), + MaybeRooted<Value, allowGC>::toMutableHandle(vp))) + { + return false; + } + + // Ancient nonstandard extension: via the JSAPI it's possible to create a + // data property that has both a slot and a getter. In that case, copy the + // value returned by the getter back into the slot. + if (shape->hasSlot() && obj->contains(cx, shape)) + obj->setSlot(shape->slot(), vp); + + return true; +} + +bool +js::NativeGetExistingProperty(JSContext* cx, HandleObject receiver, HandleNativeObject obj, + HandleShape shape, MutableHandleValue vp) +{ + RootedValue receiverValue(cx, ObjectValue(*receiver)); + return GetExistingProperty<CanGC>(cx, receiverValue, obj, shape, vp); +} + +/* + * Given pc pointing after a property accessing bytecode, return true if the + * access is "property-detecting" -- that is, if we shouldn't warn about it + * even if no such property is found and strict warnings are enabled. + */ +static bool +Detecting(JSContext* cx, JSScript* script, jsbytecode* pc) +{ + MOZ_ASSERT(script->containsPC(pc)); + + // Skip jump target opcodes. + while (pc < script->codeEnd() && BytecodeIsJumpTarget(JSOp(*pc))) + pc = GetNextPc(pc); + + MOZ_ASSERT(script->containsPC(pc)); + if (pc >= script->codeEnd()) + return false; + + // General case: a branch or equality op follows the access. + JSOp op = JSOp(*pc); + if (CodeSpec[op].format & JOF_DETECTING) + return true; + + jsbytecode* endpc = script->codeEnd(); + + if (op == JSOP_NULL) { + // Special case #1: don't warn about (obj.prop == null). + if (++pc < endpc) { + op = JSOp(*pc); + return op == JSOP_EQ || op == JSOP_NE; + } + return false; + } + + if (op == JSOP_GETGNAME || op == JSOP_GETNAME) { + // Special case #2: don't warn about (obj.prop == undefined). + JSAtom* atom = script->getAtom(GET_UINT32_INDEX(pc)); + if (atom == cx->names().undefined && + (pc += CodeSpec[op].length) < endpc) { + op = JSOp(*pc); + return op == JSOP_EQ || op == JSOP_NE || op == JSOP_STRICTEQ || op == JSOP_STRICTNE; + } + } + + return false; +} + +enum IsNameLookup { NotNameLookup = false, NameLookup = true }; + +/* + * Finish getting the property `receiver[id]` after looking at every object on + * the prototype chain and not finding any such property. + * + * Per the spec, this should just set the result to `undefined` and call it a + * day. However: + * + * 1. We add support for the nonstandard JSClass::getProperty hook. + * + * 2. This function also runs when we're evaluating an expression that's an + * Identifier (that is, an unqualified name lookup), so we need to figure + * out if that's what's happening and throw a ReferenceError if so. + * + * 3. We also emit an optional warning for this. (It's not super useful on the + * web, as there are too many false positives, but anecdotally useful in + * Gecko code.) + */ +static bool +GetNonexistentProperty(JSContext* cx, HandleNativeObject obj, HandleId id, + HandleValue receiver, IsNameLookup nameLookup, MutableHandleValue vp) +{ + vp.setUndefined(); + + // Non-standard extension: Call the getProperty hook. If it sets vp to a + // value other than undefined, we're done. If not, fall through to the + // warning/error checks below. + if (JSGetterOp getProperty = obj->getClass()->getGetProperty()) { + if (!CallJSGetterOp(cx, getProperty, obj, id, vp)) + return false; + + if (!vp.isUndefined()) + return true; + } + + // If we are doing a name lookup, this is a ReferenceError. + if (nameLookup) + return ReportIsNotDefined(cx, id); + + // Give a strict warning if foo.bar is evaluated by a script for an object + // foo with no property named 'bar'. + // + // Don't warn if extra warnings not enabled or for random getprop + // operations. + if (!cx->compartment()->behaviors().extraWarnings(cx)) + return true; + + jsbytecode* pc; + RootedScript script(cx, cx->currentScript(&pc)); + if (!script) + return true; + + if (*pc != JSOP_GETPROP && *pc != JSOP_GETELEM) + return true; + + // Don't warn repeatedly for the same script. + if (script->warnedAboutUndefinedProp()) + return true; + + // Don't warn in self-hosted code (where the further presence of + // JS::RuntimeOptions::werror() would result in impossible-to-avoid + // errors to entirely-innocent client code). + if (script->selfHosted()) + return true; + + // We may just be checking if that object has an iterator. + if (JSID_IS_ATOM(id, cx->names().iteratorIntrinsic)) + return true; + + // Do not warn about tests like (obj[prop] == undefined). + pc += CodeSpec[*pc].length; + if (Detecting(cx, script, pc)) + return true; + + unsigned flags = JSREPORT_WARNING | JSREPORT_STRICT; + script->setWarnedAboutUndefinedProp(); + + // Ok, bad undefined property reference: whine about it. + RootedValue val(cx, IdToValue(id)); + return ReportValueErrorFlags(cx, flags, JSMSG_UNDEFINED_PROP, JSDVG_IGNORE_STACK, val, + nullptr, nullptr, nullptr); +} + +/* The NoGC version of GetNonexistentProperty, present only to make types line up. */ +bool +GetNonexistentProperty(JSContext* cx, NativeObject* const& obj, const jsid& id, const Value& receiver, + IsNameLookup nameLookup, FakeMutableHandle<Value> vp) +{ + return false; +} + +static inline bool +GeneralizedGetProperty(JSContext* cx, HandleObject obj, HandleId id, HandleValue receiver, + IsNameLookup nameLookup, MutableHandleValue vp) +{ + JS_CHECK_RECURSION(cx, return false); + if (nameLookup) { + // When nameLookup is true, GetProperty implements ES6 rev 34 (2015 Feb + // 20) 8.1.1.2.6 GetBindingValue, with step 3 (the call to HasProperty) + // and step 6 (the call to Get) fused so that only a single lookup is + // needed. + // + // If we get here, we've reached a non-native object. Fall back on the + // algorithm as specified, with two separate lookups. (Note that we + // throw ReferenceErrors regardless of strictness, technically a bug.) + + bool found; + if (!HasProperty(cx, obj, id, &found)) + return false; + if (!found) + return ReportIsNotDefined(cx, id); + } + + return GetProperty(cx, obj, receiver, id, vp); +} + +static inline bool +GeneralizedGetProperty(JSContext* cx, JSObject* obj, jsid id, const Value& receiver, + IsNameLookup nameLookup, FakeMutableHandle<Value> vp) +{ + JS_CHECK_RECURSION_DONT_REPORT(cx, return false); + if (nameLookup) + return false; + return GetPropertyNoGC(cx, obj, receiver, id, vp.address()); +} + +template <AllowGC allowGC> +static MOZ_ALWAYS_INLINE bool +NativeGetPropertyInline(JSContext* cx, + typename MaybeRooted<NativeObject*, allowGC>::HandleType obj, + typename MaybeRooted<Value, allowGC>::HandleType receiver, + typename MaybeRooted<jsid, allowGC>::HandleType id, + IsNameLookup nameLookup, + typename MaybeRooted<Value, allowGC>::MutableHandleType vp) +{ + typename MaybeRooted<NativeObject*, allowGC>::RootType pobj(cx, obj); + typename MaybeRooted<Shape*, allowGC>::RootType shape(cx); + + // This loop isn't explicit in the spec algorithm. See the comment on step + // 4.d below. + for (;;) { + // Steps 2-3. ('done' is a SpiderMonkey-specific thing, used below.) + bool done; + if (!LookupOwnPropertyInline<allowGC>(cx, pobj, id, &shape, &done)) + return false; + + if (shape) { + // Steps 5-8. Special case for dense elements because + // GetExistingProperty doesn't support those. + if (IsImplicitDenseOrTypedArrayElement(shape)) { + vp.set(pobj->getDenseOrTypedArrayElement(JSID_TO_INT(id))); + return true; + } + return GetExistingProperty<allowGC>(cx, receiver, pobj, shape, vp); + } + + // Steps 4.a-b. The check for 'done' on this next line is tricky. + // done can be true in exactly these unlikely-sounding cases: + // - We're looking up an element, and pobj is a TypedArray that + // doesn't have that many elements. + // - We're being called from a resolve hook to assign to the property + // being resolved. + // What they all have in common is we do not want to keep walking + // the prototype chain. + RootedObject proto(cx, done ? nullptr : pobj->staticPrototype()); + + // Step 4.c. The spec algorithm simply returns undefined if proto is + // null, but see the comment on GetNonexistentProperty. + if (!proto) + return GetNonexistentProperty(cx, obj, id, receiver, nameLookup, vp); + + // Step 4.d. If the prototype is also native, this step is a + // recursive tail call, and we don't need to go through all the + // plumbing of JSObject::getGeneric; the top of the loop is where + // we're going to end up anyway. But if pobj is non-native, + // that optimization would be incorrect. + if (proto->getOpsGetProperty()) + return GeneralizedGetProperty(cx, proto, id, receiver, nameLookup, vp); + + pobj = &proto->as<NativeObject>(); + } +} + +bool +js::NativeGetProperty(JSContext* cx, HandleNativeObject obj, HandleValue receiver, HandleId id, + MutableHandleValue vp) +{ + return NativeGetPropertyInline<CanGC>(cx, obj, receiver, id, NotNameLookup, vp); +} + +bool +js::NativeGetPropertyNoGC(JSContext* cx, NativeObject* obj, const Value& receiver, jsid id, Value* vp) +{ + AutoAssertNoException noexc(cx); + return NativeGetPropertyInline<NoGC>(cx, obj, receiver, id, NotNameLookup, vp); +} + +bool +js::GetPropertyForNameLookup(JSContext* cx, HandleObject obj, HandleId id, MutableHandleValue vp) +{ + RootedValue receiver(cx, ObjectValue(*obj)); + if (obj->getOpsGetProperty()) + return GeneralizedGetProperty(cx, obj, id, receiver, NameLookup, vp); + return NativeGetPropertyInline<CanGC>(cx, obj.as<NativeObject>(), receiver, id, NameLookup, vp); +} + + +/*** [[Set]] *************************************************************************************/ + +static bool +MaybeReportUndeclaredVarAssignment(JSContext* cx, HandleString propname) +{ + unsigned flags; + { + jsbytecode* pc; + JSScript* script = cx->currentScript(&pc, JSContext::ALLOW_CROSS_COMPARTMENT); + if (!script) + return true; + + // If the code is not strict and extra warnings aren't enabled, then no + // check is needed. + if (IsStrictSetPC(pc)) + flags = JSREPORT_ERROR; + else if (cx->compartment()->behaviors().extraWarnings(cx)) + flags = JSREPORT_WARNING | JSREPORT_STRICT; + else + return true; + } + + JSAutoByteString bytes; + if (!bytes.encodeUtf8(cx, propname)) + return false; + return JS_ReportErrorFlagsAndNumberUTF8(cx, flags, GetErrorMessage, nullptr, + JSMSG_UNDECLARED_VAR, bytes.ptr()); +} + +/* + * Finish assignment to a shapeful data property of a native object obj. This + * conforms to no standard and there is a lot of legacy baggage here. + */ +static bool +NativeSetExistingDataProperty(JSContext* cx, HandleNativeObject obj, HandleShape shape, + HandleValue v, HandleValue receiver, ObjectOpResult& result) +{ + MOZ_ASSERT(obj->isNative()); + MOZ_ASSERT(shape->isDataDescriptor()); + + if (shape->hasDefaultSetter()) { + if (shape->hasSlot()) { + // The common path. Standard data property. + + // Global properties declared with 'var' will be initially + // defined with an undefined value, so don't treat the initial + // assignments to such properties as overwrites. + bool overwriting = !obj->is<GlobalObject>() || !obj->getSlot(shape->slot()).isUndefined(); + obj->setSlotWithType(cx, shape, v, overwriting); + return result.succeed(); + } + + // Bizarre: shared (slotless) property that's writable but has no + // JSSetterOp. JS code can't define such a property, but it can be done + // through the JSAPI. Treat it as non-writable. + return result.fail(JSMSG_GETTER_ONLY); + } + + MOZ_ASSERT(!obj->is<WithEnvironmentObject>()); // See bug 1128681. + + uint32_t sample = cx->runtime()->propertyRemovals; + RootedId id(cx, shape->propid()); + RootedValue value(cx, v); + if (!CallJSSetterOp(cx, shape->setterOp(), obj, id, &value, result)) + return false; + + // Update any slot for the shape with the value produced by the setter, + // unless the setter deleted the shape. + if (shape->hasSlot() && + (MOZ_LIKELY(cx->runtime()->propertyRemovals == sample) || + obj->contains(cx, shape))) + { + obj->setSlot(shape->slot(), value); + } + + return true; // result is populated by CallJSSetterOp above. +} + +/* + * When a [[Set]] operation finds no existing property with the given id + * or finds a writable data property on the prototype chain, we end up here. + * Finish the [[Set]] by defining a new property on receiver. + * + * This implements ES6 draft rev 28, 9.1.9 [[Set]] steps 5.b-f, but it + * is really old code and there are a few barnacles. + */ +bool +js::SetPropertyByDefining(JSContext* cx, HandleId id, HandleValue v, HandleValue receiverValue, + ObjectOpResult& result) +{ + // Step 5.b. + if (!receiverValue.isObject()) + return result.fail(JSMSG_SET_NON_OBJECT_RECEIVER); + RootedObject receiver(cx, &receiverValue.toObject()); + + bool existing; + { + // Steps 5.c-d. + Rooted<PropertyDescriptor> desc(cx); + if (!GetOwnPropertyDescriptor(cx, receiver, id, &desc)) + return false; + + existing = !!desc.object(); + + // Step 5.e. + if (existing) { + // Step 5.e.i. + if (desc.isAccessorDescriptor()) + return result.fail(JSMSG_OVERWRITING_ACCESSOR); + + // Step 5.e.ii. + if (!desc.writable()) + return result.fail(JSMSG_READ_ONLY); + } + } + + // Invalidate SpiderMonkey-specific caches or bail. + const Class* clasp = receiver->getClass(); + + // Purge the property cache of now-shadowed id in receiver's environment chain. + if (!PurgeEnvironmentChain(cx, receiver, id)) + return false; + + // Steps 5.e.iii-iv. and 5.f.i. Define the new data property. + unsigned attrs = + existing + ? JSPROP_IGNORE_ENUMERATE | JSPROP_IGNORE_READONLY | JSPROP_IGNORE_PERMANENT + : JSPROP_ENUMERATE; + JSGetterOp getter = clasp->getGetProperty(); + JSSetterOp setter = clasp->getSetProperty(); + MOZ_ASSERT(getter != JS_PropertyStub); + MOZ_ASSERT(setter != JS_StrictPropertyStub); + if (!DefineProperty(cx, receiver, id, v, getter, setter, attrs, result)) + return false; + + // If the receiver is native, there is one more legacy wrinkle: the class + // JSSetterOp is called after defining the new property. + if (setter && receiver->is<NativeObject>()) { + if (!result) + return true; + + Rooted<NativeObject*> nativeReceiver(cx, &receiver->as<NativeObject>()); + if (!cx->shouldBeJSContext()) + return false; + RootedValue receiverValue(cx, ObjectValue(*receiver)); + + // This lookup is a bit unfortunate, but not nearly the most + // unfortunate thing about Class getters and setters. Since the above + // DefineProperty call succeeded, receiver is native, and the property + // has a setter (and thus can't be a dense element), this lookup is + // guaranteed to succeed. + RootedShape shape(cx, nativeReceiver->lookup(cx, id)); + MOZ_ASSERT(shape); + return NativeSetExistingDataProperty(cx->asJSContext(), nativeReceiver, shape, v, + receiverValue, result); + } + + return true; +} + +// When setting |id| for |receiver| and |obj| has no property for id, continue +// the search up the prototype chain. +bool +js::SetPropertyOnProto(JSContext* cx, HandleObject obj, HandleId id, HandleValue v, + HandleValue receiver, ObjectOpResult& result) +{ + MOZ_ASSERT(!obj->is<ProxyObject>()); + + RootedObject proto(cx, obj->staticPrototype()); + if (proto) + return SetProperty(cx, proto, id, v, receiver, result); + + return SetPropertyByDefining(cx, id, v, receiver, result); +} + +/* + * Implement "the rest of" assignment to a property when no property receiver[id] + * was found anywhere on the prototype chain. + * + * FIXME: This should be updated to follow ES6 draft rev 28, section 9.1.9, + * steps 4.d.i and 5. + */ +static bool +SetNonexistentProperty(JSContext* cx, HandleId id, HandleValue v, HandleValue receiver, + QualifiedBool qualified, ObjectOpResult& result) +{ + if (!qualified && receiver.isObject() && receiver.toObject().isUnqualifiedVarObj()) { + RootedString idStr(cx, JSID_TO_STRING(id)); + if (!MaybeReportUndeclaredVarAssignment(cx, idStr)) + return false; + } + + return SetPropertyByDefining(cx, id, v, receiver, result); +} + +/* + * Set an existing own property obj[index] that's a dense element or typed + * array element. + */ +static bool +SetDenseOrTypedArrayElement(JSContext* cx, HandleNativeObject obj, uint32_t index, HandleValue v, + ObjectOpResult& result) +{ + if (obj->is<TypedArrayObject>()) { + double d; + if (!ToNumber(cx, v, &d)) + return false; + + // Silently do nothing for out-of-bounds sets, for consistency with + // current behavior. (ES6 currently says to throw for this in + // strict mode code, so we may eventually need to change.) + uint32_t len = obj->as<TypedArrayObject>().length(); + if (index < len) + TypedArrayObject::setElement(obj->as<TypedArrayObject>(), index, d); + return result.succeed(); + } + + if (WouldDefinePastNonwritableLength(obj, index)) + return result.fail(JSMSG_CANT_DEFINE_PAST_ARRAY_LENGTH); + + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + obj->setDenseElementWithType(cx, index, v); + return result.succeed(); +} + +/* + * Finish the assignment `receiver[id] = v` when an existing property (shape) + * has been found on a native object (pobj). This implements ES6 draft rev 32 + * (2015 Feb 2) 9.1.9 steps 5 and 6. + * + * It is necessary to pass both id and shape because shape could be an implicit + * dense or typed array element (i.e. not actually a pointer to a Shape). + */ +static bool +SetExistingProperty(JSContext* cx, HandleNativeObject obj, HandleId id, HandleValue v, + HandleValue receiver, HandleNativeObject pobj, HandleShape shape, + ObjectOpResult& result) +{ + // Step 5 for dense elements. + if (IsImplicitDenseOrTypedArrayElement(shape)) { + // Step 5.a. + if (pobj->getElementsHeader()->isFrozen()) + return result.fail(JSMSG_READ_ONLY); + + // Pure optimization for the common case: + if (receiver.isObject() && pobj == &receiver.toObject()) + return SetDenseOrTypedArrayElement(cx, pobj, JSID_TO_INT(id), v, result); + + // Steps 5.b-f. + return SetPropertyByDefining(cx, id, v, receiver, result); + } + + // Step 5 for all other properties. + if (shape->isDataDescriptor()) { + // Step 5.a. + if (!shape->writable()) + return result.fail(JSMSG_READ_ONLY); + + // steps 5.c-f. + if (receiver.isObject() && pobj == &receiver.toObject()) { + // Pure optimization for the common case. There's no point performing + // the lookup in step 5.c again, as our caller just did it for us. The + // result is |shape|. + + // Steps 5.e.i-ii. + if (pobj->is<ArrayObject>() && id == NameToId(cx->names().length)) { + Rooted<ArrayObject*> arr(cx, &pobj->as<ArrayObject>()); + return ArraySetLength(cx, arr, id, shape->attributes(), v, result); + } + return NativeSetExistingDataProperty(cx, pobj, shape, v, receiver, result); + } + + // SpiderMonkey special case: assigning to an inherited slotless + // property causes the setter to be called, instead of shadowing, + // unless the existing property is JSPROP_SHADOWABLE (see bug 552432). + if (!shape->hasSlot() && !shape->hasShadowable()) { + // Even weirder sub-special-case: inherited slotless data property + // with default setter. Wut. + if (shape->hasDefaultSetter()) + return result.succeed(); + + RootedValue valCopy(cx, v); + return CallJSSetterOp(cx, shape->setterOp(), obj, id, &valCopy, result); + } + + // Shadow pobj[id] by defining a new data property receiver[id]. + // Delegate everything to SetPropertyByDefining. + return SetPropertyByDefining(cx, id, v, receiver, result); + } + + // Steps 6-11. + MOZ_ASSERT(shape->isAccessorDescriptor()); + MOZ_ASSERT_IF(!shape->hasSetterObject(), shape->hasDefaultSetter()); + if (shape->hasDefaultSetter()) + return result.fail(JSMSG_GETTER_ONLY); + + RootedValue setter(cx, ObjectValue(*shape->setterObject())); + if (!js::CallSetter(cx, receiver, setter, v)) + return false; + + return result.succeed(); +} + +bool +js::NativeSetProperty(JSContext* cx, HandleNativeObject obj, HandleId id, HandleValue value, + HandleValue receiver, QualifiedBool qualified, ObjectOpResult& result) +{ + // Fire watchpoints, if any. + RootedValue v(cx, value); + if (MOZ_UNLIKELY(obj->watched())) { + WatchpointMap* wpmap = cx->compartment()->watchpointMap; + if (wpmap && !wpmap->triggerWatchpoint(cx, obj, id, &v)) + return false; + } + + // Step numbers below reference ES6 rev 27 9.1.9, the [[Set]] internal + // method for ordinary objects. We substitute our own names for these names + // used in the spec: O -> pobj, P -> id, ownDesc -> shape. + RootedShape shape(cx); + RootedNativeObject pobj(cx, obj); + + // This loop isn't explicit in the spec algorithm. See the comment on step + // 4.c.i below. (There's a very similar loop in the NativeGetProperty + // implementation, but unfortunately not similar enough to common up.) + for (;;) { + // Steps 2-3. ('done' is a SpiderMonkey-specific thing, used below.) + bool done; + if (!LookupOwnPropertyInline<CanGC>(cx, pobj, id, &shape, &done)) + return false; + + if (shape) { + // Steps 5-6. + return SetExistingProperty(cx, obj, id, v, receiver, pobj, shape, result); + } + + // Steps 4.a-b. The check for 'done' on this next line is tricky. + // done can be true in exactly these unlikely-sounding cases: + // - We're looking up an element, and pobj is a TypedArray that + // doesn't have that many elements. + // - We're being called from a resolve hook to assign to the property + // being resolved. + // What they all have in common is we do not want to keep walking + // the prototype chain. + RootedObject proto(cx, done ? nullptr : pobj->staticPrototype()); + if (!proto) { + // Step 4.d.i (and step 5). + return SetNonexistentProperty(cx, id, v, receiver, qualified, result); + } + + // Step 4.c.i. If the prototype is also native, this step is a + // recursive tail call, and we don't need to go through all the + // plumbing of SetProperty; the top of the loop is where we're going to + // end up anyway. But if pobj is non-native, that optimization would be + // incorrect. + if (!proto->isNative()) { + // Unqualified assignments are not specified to go through [[Set]] + // at all, but they do go through this function. So check for + // unqualified assignment to a nonexistent global (a strict error). + if (!qualified) { + bool found; + if (!HasProperty(cx, proto, id, &found)) + return false; + if (!found) + return SetNonexistentProperty(cx, id, v, receiver, qualified, result); + } + + return SetProperty(cx, proto, id, v, receiver, result); + } + pobj = &proto->as<NativeObject>(); + } +} + +bool +js::NativeSetElement(JSContext* cx, HandleNativeObject obj, uint32_t index, HandleValue v, + HandleValue receiver, ObjectOpResult& result) +{ + RootedId id(cx); + if (!IndexToId(cx, index, &id)) + return false; + return NativeSetProperty(cx, obj, id, v, receiver, Qualified, result); +} + +/*** [[Delete]] **********************************************************************************/ + +// ES6 draft rev31 9.1.10 [[Delete]] +bool +js::NativeDeleteProperty(JSContext* cx, HandleNativeObject obj, HandleId id, + ObjectOpResult& result) +{ + // Steps 2-3. + RootedShape shape(cx); + if (!NativeLookupOwnProperty<CanGC>(cx, obj, id, &shape)) + return false; + + // Step 4. + if (!shape) { + // If no property call the class's delProperty hook, passing succeeded + // as the result parameter. This always succeeds when there is no hook. + return CallJSDeletePropertyOp(cx, obj->getClass()->getDelProperty(), obj, id, result); + } + + cx->runtime()->gc.poke(); + + // Step 6. Non-configurable property. + if (GetShapeAttributes(obj, shape) & JSPROP_PERMANENT) + return result.failCantDelete(); + + if (!CallJSDeletePropertyOp(cx, obj->getClass()->getDelProperty(), obj, id, result)) + return false; + if (!result) + return true; + + // Step 5. + if (IsImplicitDenseOrTypedArrayElement(shape)) { + // Typed array elements are non-configurable. + MOZ_ASSERT(!obj->is<TypedArrayObject>()); + + if (!obj->maybeCopyElementsForWrite(cx)) + return false; + + obj->setDenseElementHole(cx, JSID_TO_INT(id)); + } else { + if (!obj->removeProperty(cx, id)) + return false; + } + + return SuppressDeletedProperty(cx, obj, id); +} |