lib/VM/JSObject.cpp (2,480 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/JSObject.h" #include "hermes/VM/BuildMetadata.h" #include "hermes/VM/Callable.h" #include "hermes/VM/HostModel.h" #include "hermes/VM/InternalProperty.h" #include "hermes/VM/JSArray.h" #include "hermes/VM/JSProxy.h" #include "hermes/VM/Operations.h" #include "hermes/VM/PropertyAccessor.h" #include "llvh/ADT/SmallSet.h" namespace hermes { namespace vm { const ObjectVTable JSObject::vt{ VTable( CellKind::JSObjectKind, cellSize<JSObject>(), nullptr, nullptr, nullptr, nullptr, VTable::HeapSnapshotMetadata{ HeapSnapshot::NodeType::Object, JSObject::_snapshotNameImpl, JSObject::_snapshotAddEdgesImpl, nullptr, JSObject::_snapshotAddLocationsImpl}), JSObject::_getOwnIndexedRangeImpl, JSObject::_haveOwnIndexedImpl, JSObject::_getOwnIndexedPropertyFlagsImpl, JSObject::_getOwnIndexedImpl, JSObject::_setOwnIndexedImpl, JSObject::_deleteOwnIndexedImpl, JSObject::_checkAllOwnIndexedImpl, }; void JSObjectBuildMeta(const GCCell *cell, Metadata::Builder &mb) { // This call is just for debugging and consistency purposes. mb.addJSObjectOverlapSlots(JSObject::numOverlapSlots<JSObject>()); const auto *self = static_cast<const JSObject *>(cell); mb.setVTable(&JSObject::vt); mb.addField("parent", &self->parent_); mb.addField("class", &self->clazz_); mb.addField("propStorage", &self->propStorage_); // Declare the direct properties. static const char *directPropName[JSObject::DIRECT_PROPERTY_SLOTS] = { "directProp0", "directProp1", "directProp2", "directProp3", "directProp4"}; for (unsigned i = mb.getJSObjectOverlapSlots(); i < JSObject::DIRECT_PROPERTY_SLOTS; ++i) { mb.addField(directPropName[i], self->directProps() + i); } } PseudoHandle<JSObject> JSObject::create( Runtime &runtime, Handle<JSObject> parentHandle) { auto *cell = runtime.makeAFixed<JSObject>( runtime, parentHandle, runtime.getHiddenClassForPrototype( *parentHandle, numOverlapSlots<JSObject>()), GCPointerBase::NoBarriers()); return JSObjectInit::initToPseudoHandle(runtime, cell); } PseudoHandle<JSObject> JSObject::create(Runtime &runtime) { return create(runtime, Handle<JSObject>::vmcast(&runtime.objectPrototype)); } PseudoHandle<JSObject> JSObject::create( Runtime &runtime, unsigned propertyCount) { auto self = create(runtime); return runtime.ignoreAllocationFailure( JSObject::allocatePropStorage(std::move(self), runtime, propertyCount)); } PseudoHandle<JSObject> JSObject::create( Runtime &runtime, Handle<HiddenClass> clazz) { auto obj = JSObject::create(runtime, clazz->getNumProperties()); obj->clazz_.setNonNull(runtime, *clazz, &runtime.getHeap()); // If the hidden class has index like property, we need to clear the fast path // flag. if (LLVM_UNLIKELY( obj->clazz_.getNonNull(runtime)->getHasIndexLikeProperties())) obj->flags_.fastIndexProperties = false; return obj; } void JSObject::initializeLazyObject( Runtime &runtime, Handle<JSObject> lazyObject) { assert(lazyObject->flags_.lazyObject && "object must be lazy"); // object is now assumed to be a regular object. lazyObject->flags_.lazyObject = 0; // only functions can be lazy. assert(vmisa<Callable>(lazyObject.get()) && "unexpected lazy object"); Callable::defineLazyProperties(Handle<Callable>::vmcast(lazyObject), runtime); } ObjectID JSObject::getObjectID(JSObject *self, Runtime &runtime) { if (LLVM_LIKELY(self->flags_.objectID)) return self->flags_.objectID; // Object ID does not yet exist, get next unique global ID.. self->flags_.objectID = runtime.generateNextObjectID(); // Make sure it is not zero. if (LLVM_UNLIKELY(!self->flags_.objectID)) --self->flags_.objectID; return self->flags_.objectID; } CallResult<PseudoHandle<JSObject>> JSObject::getPrototypeOf( PseudoHandle<JSObject> selfHandle, Runtime &runtime) { if (LLVM_LIKELY(!selfHandle->isProxyObject())) { return createPseudoHandle(selfHandle->getParent(runtime)); } return JSProxy::getPrototypeOf( runtime.makeHandle(std::move(selfHandle)), runtime); } namespace { CallResult<bool> proxyOpFlags( Runtime &runtime, PropOpFlags opFlags, const char *msg, CallResult<bool> res) { if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!*res && opFlags.getThrowOnError()) { return runtime.raiseTypeError(msg); } return res; } } // namespace CallResult<bool> JSObject::setParent( JSObject *self, Runtime &runtime, JSObject *parent, PropOpFlags opFlags) { if (LLVM_UNLIKELY(self->isProxyObject())) { return proxyOpFlags( runtime, opFlags, "Object is not extensible.", JSProxy::setPrototypeOf( runtime.makeHandle(self), runtime, runtime.makeHandle(parent))); } // ES9 9.1.2 // 4. if (self->parent_.get(runtime) == parent) return true; // 5. if (!self->isExtensible()) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError("Object is not extensible."); } else { return false; } } // 6-8. Check for a prototype cycle. for (JSObject *cur = parent; cur; cur = cur->parent_.get(runtime)) { if (cur == self) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError("Prototype cycle detected"); } else { return false; } } else if (LLVM_UNLIKELY(cur->isProxyObject())) { // TODO this branch should also be used for module namespace and // immutable prototype exotic objects. break; } } // 9. self->parent_.set(runtime, parent, &runtime.getHeap()); // 10. return true; } void JSObject::allocateNewSlotStorage( Handle<JSObject> selfHandle, Runtime &runtime, SlotIndex newSlotIndex, Handle<> valueHandle) { // If it is a direct property, just store the value and we are done. if (LLVM_LIKELY(newSlotIndex < DIRECT_PROPERTY_SLOTS)) { auto shv = SmallHermesValue::encodeHermesValue(*valueHandle, runtime); selfHandle->directProps()[newSlotIndex].set(shv, &runtime.getHeap()); return; } // Make the slot index relative to the indirect storage. newSlotIndex -= DIRECT_PROPERTY_SLOTS; // Allocate a new property storage if not already allocated. if (LLVM_UNLIKELY(!selfHandle->propStorage_)) { // Allocate new storage. assert(newSlotIndex == 0 && "allocated slot must be at end"); auto arrRes = runtime.ignoreAllocationFailure( PropStorage::create(runtime, DEFAULT_PROPERTY_CAPACITY)); selfHandle->propStorage_.setNonNull( runtime, vmcast<PropStorage>(arrRes), &runtime.getHeap()); } else if (LLVM_UNLIKELY( newSlotIndex >= selfHandle->propStorage_.getNonNull(runtime)->capacity())) { // Reallocate the existing one. assert( newSlotIndex == selfHandle->propStorage_.getNonNull(runtime)->size() && "allocated slot must be at end"); auto hnd = runtime.makeMutableHandle(selfHandle->propStorage_); PropStorage::resize(hnd, runtime, newSlotIndex + 1); selfHandle->propStorage_.setNonNull(runtime, *hnd, &runtime.getHeap()); } { NoAllocScope scope{runtime}; auto *const propStorage = selfHandle->propStorage_.getNonNull(runtime); if (newSlotIndex >= propStorage->size()) { assert( newSlotIndex == propStorage->size() && "allocated slot must be at end"); PropStorage::resizeWithinCapacity(propStorage, runtime, newSlotIndex + 1); } } // This must be done after the call to resizeWithinCapacity, since // encodeHermesValue may allocate and cause the ArrayStorage to be trimmed. auto shv = SmallHermesValue::encodeHermesValue(*valueHandle, runtime); // If we don't need to resize, just store it directly. selfHandle->propStorage_.getNonNull(runtime)->set( newSlotIndex, shv, &runtime.getHeap()); } CallResult<PseudoHandle<>> JSObject::getNamedPropertyValue_RJS( Handle<JSObject> selfHandle, Runtime &runtime, Handle<JSObject> propObj, NamedPropertyDescriptor desc) { if (LLVM_LIKELY(!desc.flags.accessor)) return getNamedSlotValue(propObj, runtime, desc); // It's now valid to use the Internal variant because we know it's an // accessor. auto *accessor = vmcast<PropertyAccessor>( getNamedSlotValueUnsafe(propObj.get(), runtime, desc).getObject(runtime)); if (!accessor->getter) return createPseudoHandle(HermesValue::encodeUndefinedValue()); // Execute the accessor on this object. return accessor->getter.getNonNull(runtime)->executeCall0( runtime.makeHandle(accessor->getter), runtime, selfHandle); } CallResult<PseudoHandle<>> JSObject::getComputedPropertyValueInternal_RJS( Handle<JSObject> selfHandle, Runtime &runtime, Handle<JSObject> propObj, ComputedPropertyDescriptor desc) { assert( !selfHandle->flags_.proxyObject && !propObj->flags_.proxyObject && "getComputedPropertyValue_RJS cannot be used with proxy objects"); if (LLVM_LIKELY(!desc.flags.accessor)) return createPseudoHandle(getComputedSlotValueUnsafe( createPseudoHandle(propObj.get()), runtime, desc)); auto *accessor = vmcast<PropertyAccessor>(getComputedSlotValueUnsafe( createPseudoHandle(propObj.get()), runtime, desc)); if (!accessor->getter) return createPseudoHandle(HermesValue::encodeUndefinedValue()); // Execute the accessor on this object. return accessor->getter.getNonNull(runtime)->executeCall0( runtime.makeHandle(accessor->getter), runtime, selfHandle); } CallResult<PseudoHandle<>> JSObject::getComputedPropertyValue_RJS( Handle<JSObject> selfHandle, Runtime &runtime, Handle<JSObject> propObj, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor desc, Handle<> nameValHandle) { if (!propObj) { return createPseudoHandle(HermesValue::encodeEmptyValue()); } if (LLVM_LIKELY(!desc.flags.proxyObject)) { return JSObject::getComputedPropertyValueInternal_RJS( selfHandle, runtime, propObj, desc); } CallResult<Handle<>> keyRes = toPropertyKey(runtime, nameValHandle); if (LLVM_UNLIKELY(keyRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } CallResult<bool> hasRes = JSProxy::hasComputed(propObj, runtime, *keyRes); if (LLVM_UNLIKELY(hasRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!*hasRes) { return createPseudoHandle(HermesValue::encodeEmptyValue()); } return JSProxy::getComputed(propObj, runtime, *keyRes, selfHandle); } CallResult<Handle<JSArray>> JSObject::getOwnPropertyKeys( Handle<JSObject> selfHandle, Runtime &runtime, OwnKeysFlags okFlags) { assert( (okFlags.getIncludeNonSymbols() || okFlags.getIncludeSymbols()) && "Can't exclude symbols and strings"); if (LLVM_UNLIKELY( selfHandle->flags_.lazyObject || selfHandle->flags_.proxyObject)) { if (selfHandle->flags_.proxyObject) { CallResult<PseudoHandle<JSArray>> proxyRes = JSProxy::ownPropertyKeys(selfHandle, runtime, okFlags); if (LLVM_UNLIKELY(proxyRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return runtime.makeHandle(std::move(*proxyRes)); } assert(selfHandle->flags_.lazyObject && "descriptor flags are impossible"); initializeLazyObject(runtime, selfHandle); } auto range = getOwnIndexedRange(selfHandle.get(), runtime); // Estimate the capacity of the output array. This estimate is only // reasonable for the non-symbol case. uint32_t capacity = okFlags.getIncludeNonSymbols() ? (selfHandle->clazz_.getNonNull(runtime)->getNumProperties() + range.second - range.first) : 0; auto arrayRes = JSArray::create(runtime, capacity, 0); if (LLVM_UNLIKELY(arrayRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto array = *arrayRes; // Optional array of SymbolIDs reported via host object API llvh::Optional<Handle<JSArray>> hostObjectSymbols; size_t hostObjectSymbolCount = 0; // If current object is a host object we need to deduplicate its properties llvh::SmallSet<SymbolID::RawType, 16> dedupSet; // Output index. uint32_t index = 0; // Avoid allocating a new handle per element. MutableHandle<> tmpHandle{runtime}; // Number of indexed properties. uint32_t numIndexed = 0; // Regular properties with names that are array indexes are stashed here, if // encountered. llvh::SmallVector<uint32_t, 8> indexNames{}; // Iterate the named properties excluding those which use Symbols. if (okFlags.getIncludeNonSymbols()) { // Get host object property names if (LLVM_UNLIKELY(selfHandle->flags_.hostObject)) { assert( range.first == range.second && "Host objects cannot own indexed range"); auto hostSymbolsRes = vmcast<HostObject>(selfHandle.get())->getHostPropertyNames(); if (hostSymbolsRes == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } if ((hostObjectSymbolCount = (**hostSymbolsRes)->getEndIndex()) != 0) { Handle<JSArray> hostSymbols = *hostSymbolsRes; hostObjectSymbols = std::move(hostSymbols); capacity += hostObjectSymbolCount; } } // Iterate the indexed properties. GCScopeMarkerRAII marker{runtime}; for (auto i = range.first; i != range.second; ++i) { auto res = getOwnIndexedPropertyFlags(selfHandle.get(), runtime, i); if (!res) continue; // If specified, check whether it is enumerable. if (!okFlags.getIncludeNonEnumerable() && !res->enumerable) continue; tmpHandle = HermesValue::encodeDoubleValue(i); JSArray::setElementAt(array, runtime, index++, tmpHandle); marker.flush(); } numIndexed = index; HiddenClass::forEachProperty( runtime.makeHandle(selfHandle->clazz_), runtime, [&runtime, okFlags, array, hostObjectSymbolCount, &index, &indexNames, &tmpHandle, &dedupSet](SymbolID id, NamedPropertyDescriptor desc) { if (!isPropertyNamePrimitive(id)) { return; } // If specified, check whether it is enumerable. if (!okFlags.getIncludeNonEnumerable()) { if (!desc.flags.enumerable) return; } // Host properties might overlap with the ones recognized by the // hidden class. If we're dealing with a host object then keep track // of hidden class properties for the deduplication purposes. if (LLVM_UNLIKELY(hostObjectSymbolCount > 0)) { dedupSet.insert(id.unsafeGetRaw()); } // Check if this property is an integer index. If it is, we stash it // away to deal with it later. This check should be fast since most // property names don't start with a digit. auto propNameAsIndex = toArrayIndex( runtime.getIdentifierTable().getStringView(runtime, id)); if (LLVM_UNLIKELY(propNameAsIndex)) { indexNames.push_back(*propNameAsIndex); return; } tmpHandle = HermesValue::encodeStringValue( runtime.getStringPrimFromSymbolID(id)); JSArray::setElementAt(array, runtime, index++, tmpHandle); }); // Iterate over HostObject properties and append them to the array. Do not // append duplicates. if (LLVM_UNLIKELY(hostObjectSymbols)) { for (size_t i = 0; i < hostObjectSymbolCount; ++i) { assert( (*hostObjectSymbols)->at(runtime, i).isSymbol() && "Host object needs to return array of SymbolIDs"); marker.flush(); SymbolID id = (*hostObjectSymbols)->at(runtime, i).getSymbol(); if (dedupSet.count(id.unsafeGetRaw()) == 0) { dedupSet.insert(id.unsafeGetRaw()); assert( !InternalProperty::isInternal(id) && "host object returned reserved symbol"); auto propNameAsIndex = toArrayIndex( runtime.getIdentifierTable().getStringView(runtime, id)); if (LLVM_UNLIKELY(propNameAsIndex)) { indexNames.push_back(*propNameAsIndex); continue; } tmpHandle = HermesValue::encodeStringValue( runtime.getStringPrimFromSymbolID(id)); JSArray::setElementAt(array, runtime, index++, tmpHandle); } } } } // Now iterate the named properties again, including only Symbols. // We could iterate only once, if we chose to ignore (and disallow) // own properties on HostObjects, as we do with Proxies. if (okFlags.getIncludeSymbols()) { MutableHandle<SymbolID> idHandle{runtime}; HiddenClass::forEachProperty( runtime.makeHandle(selfHandle->clazz_), runtime, [&runtime, okFlags, array, &index, &idHandle]( SymbolID id, NamedPropertyDescriptor desc) { if (!isSymbolPrimitive(id)) { return; } // If specified, check whether it is enumerable. if (!okFlags.getIncludeNonEnumerable()) { if (!desc.flags.enumerable) return; } idHandle = id; JSArray::setElementAt(array, runtime, index++, idHandle); }); } // The end (exclusive) of the named properties. uint32_t endNamed = index; // Properly set the length of the array. auto cr = JSArray::setLength( array, runtime, endNamed + indexNames.size(), PropOpFlags{}); (void)cr; assert( cr != ExecutionStatus::EXCEPTION && *cr && "JSArray::setLength() failed"); // If we have no index-like names, we are done. if (LLVM_LIKELY(indexNames.empty())) return array; // In the unlikely event that we encountered index-like names, we need to sort // them and merge them with the real indexed properties. Note that it is // guaranteed that there are no clashes. std::sort(indexNames.begin(), indexNames.end()); // Also make space for the new elements by shifting all the named properties // to the right. First, resize the array. JSArray::setStorageEndIndex(array, runtime, endNamed + indexNames.size()); // Shift the non-index property names. The region [numIndexed..endNamed) is // moved to [numIndexed+indexNames.size()..array->size()). // TODO: optimize this by implementing memcpy-like functionality in ArrayImpl. for (uint32_t last = endNamed, toLast = array->getEndIndex(); last != numIndexed;) { --last; --toLast; tmpHandle = array->at(runtime, last); JSArray::setElementAt(array, runtime, toLast, tmpHandle); } // Now we need to merge the indexes in indexNames and the array // [0..numIndexed). We start from the end and copy the larger element from // either array. // 1+ the destination position to copy into. for (uint32_t toLast = numIndexed + indexNames.size(), indexNamesLast = indexNames.size(); toLast != 0;) { if (numIndexed) { uint32_t a = (uint32_t)array->at(runtime, numIndexed - 1).getNumber(); uint32_t b; if (indexNamesLast && (b = indexNames[indexNamesLast - 1]) > a) { tmpHandle = HermesValue::encodeDoubleValue(b); --indexNamesLast; } else { tmpHandle = HermesValue::encodeDoubleValue(a); --numIndexed; } } else { assert(indexNamesLast && "prematurely ran out of source values"); tmpHandle = HermesValue::encodeDoubleValue(indexNames[indexNamesLast - 1]); --indexNamesLast; } --toLast; JSArray::setElementAt(array, runtime, toLast, tmpHandle); } return array; } /// Convert a value to string unless already converted /// \param nameValHandle [Handle<>] the value to convert /// \param str [MutableHandle<StringPrimitive>] the string is stored /// there. Must be initialized to null initially. #define LAZY_TO_STRING(runtime, nameValHandle, str) \ do { \ if (!str) { \ auto status = toString_RJS(runtime, nameValHandle); \ assert( \ status != ExecutionStatus::EXCEPTION && \ "toString() of primitive cannot fail"); \ str = status->get(); \ } \ } while (0) /// Convert a value to an identifier unless already converted /// \param nameValHandle [Handle<>] the value to convert /// \param id [SymbolID] the identifier is stored there. Must be initialized /// to INVALID_IDENTIFIER_ID initially. #define LAZY_TO_IDENTIFIER(runtime, nameValHandle, id) \ do { \ if (id.isInvalid()) { \ CallResult<Handle<SymbolID>> idRes = \ valueToSymbolID(runtime, nameValHandle); \ if (LLVM_UNLIKELY(idRes == ExecutionStatus::EXCEPTION)) { \ return ExecutionStatus::EXCEPTION; \ } \ id = **idRes; \ } \ } while (0) /// Convert a value to array index, if possible. /// \param nameValHandle [Handle<>] the value to convert /// \param str [MutableHandle<StringPrimitive>] the string is stored /// there. Must be initialized to null initially. /// \param arrayIndex [OptValue<uint32_t>] the array index is stored /// there. #define TO_ARRAY_INDEX(runtime, nameValHandle, str, arrayIndex) \ do { \ arrayIndex = toArrayIndexFastPath(*nameValHandle); \ if (!arrayIndex && !nameValHandle->isSymbol()) { \ LAZY_TO_STRING(runtime, nameValHandle, str); \ arrayIndex = toArrayIndex(runtime, str); \ } \ } while (0) /// \return true if the flags of a new property make it suitable for indexed /// storage. All new indexed properties are enumerable, writable and /// configurable and have no accessors. static bool canNewPropertyBeIndexed(DefinePropertyFlags dpf) { return dpf.setEnumerable && dpf.enumerable && dpf.setWritable && dpf.writable && dpf.setConfigurable && dpf.configurable && !dpf.setSetter && !dpf.setGetter; } struct JSObject::Helper { public: LLVM_ATTRIBUTE_ALWAYS_INLINE static ObjectFlags &flags(JSObject *self) { return self->flags_; } LLVM_ATTRIBUTE_ALWAYS_INLINE static OptValue<PropertyFlags> getOwnIndexedPropertyFlags(JSObject *self, Runtime &runtime, uint32_t index) { return JSObject::getOwnIndexedPropertyFlags(self, runtime, index); } LLVM_ATTRIBUTE_ALWAYS_INLINE static NamedPropertyDescriptor &castToNamedPropertyDescriptorRef( ComputedPropertyDescriptor &desc) { return desc.castToNamedPropertyDescriptorRef(); } }; namespace { /// ES5.1 8.12.1. /// A helper which takes a SymbolID which caches the conversion of /// nameValHandle if it's needed. It should be default constructed, /// and may or may not be set. This has been measured to be a useful /// perf win. Note that always_inline seems to be ignored on static /// methods, so this function has to be local to the cpp file in order /// to be inlined for the perf win. LLVM_ATTRIBUTE_ALWAYS_INLINE inline CallResult<bool> getOwnComputedPrimitiveDescriptorImpl( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, JSObject::IgnoreProxy ignoreProxy, SymbolID &id, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc) { assert( !nameValHandle->isObject() && "nameValHandle passed to " "getOwnComputedPrimitiveDescriptor " "cannot be an object"); // Try the fast paths first if we have "fast" index properties and the // property name is an obvious index. if (auto arrayIndex = toArrayIndexFastPath(*nameValHandle)) { if (JSObject::Helper::flags(*selfHandle).fastIndexProperties) { auto res = JSObject::Helper::getOwnIndexedPropertyFlags( selfHandle.get(), runtime, *arrayIndex); if (res) { // This a valid array index, residing in our indexed storage. desc.flags = *res; desc.flags.indexed = 1; desc.slot = *arrayIndex; return true; } // This a valid array index, but we don't have it in our indexed storage, // and we don't have index-like named properties. return false; } if (!selfHandle->getClass(runtime)->getHasIndexLikeProperties() && !selfHandle->isHostObject() && !selfHandle->isLazy() && !selfHandle->isProxyObject()) { // Early return to handle the case where an object definitely has no // index-like properties. This avoids allocating a new StringPrimitive and // uniquing it below. return false; } } // Convert the string to a SymbolID LAZY_TO_IDENTIFIER(runtime, nameValHandle, id); // Look for a named property with this name. if (JSObject::getOwnNamedDescriptor( selfHandle, runtime, id, JSObject::Helper::castToNamedPropertyDescriptorRef(desc))) { return true; } if (LLVM_LIKELY( !JSObject::Helper::flags(*selfHandle).indexedStorage && !selfHandle->isLazy() && !selfHandle->isProxyObject())) { return false; } MutableHandle<StringPrimitive> strPrim{runtime}; // If we have indexed storage, perform potentially expensive conversions // to array index and check it. if (JSObject::Helper::flags(*selfHandle).indexedStorage) { // If the name is a valid integer array index, store it here. OptValue<uint32_t> arrayIndex; // Try to convert the property name to an array index. TO_ARRAY_INDEX(runtime, nameValHandle, strPrim, arrayIndex); if (arrayIndex) { auto res = JSObject::Helper::getOwnIndexedPropertyFlags( selfHandle.get(), runtime, *arrayIndex); if (res) { desc.flags = *res; desc.flags.indexed = 1; desc.slot = *arrayIndex; return true; } } return false; } if (selfHandle->isLazy()) { JSObject::initializeLazyObject(runtime, selfHandle); return JSObject::getOwnComputedPrimitiveDescriptor( selfHandle, runtime, nameValHandle, ignoreProxy, tmpSymbolStorage, desc); } assert(selfHandle->isProxyObject() && "descriptor flags are impossible"); if (ignoreProxy == JSObject::IgnoreProxy::Yes) { return false; } return JSProxy::getOwnProperty( selfHandle, runtime, nameValHandle, desc, nullptr); } } // namespace CallResult<bool> JSObject::getOwnComputedPrimitiveDescriptor( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, JSObject::IgnoreProxy ignoreProxy, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc) { SymbolID id{}; return getOwnComputedPrimitiveDescriptorImpl( selfHandle, runtime, nameValHandle, ignoreProxy, id, tmpSymbolStorage, desc); } CallResult<bool> JSObject::getOwnComputedDescriptor( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc) { auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return JSObject::getOwnComputedPrimitiveDescriptor( selfHandle, runtime, *converted, IgnoreProxy::No, tmpSymbolStorage, desc); } CallResult<bool> JSObject::getOwnComputedDescriptor( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc, MutableHandle<> &valueOrAccessor) { auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } // The proxy is ignored here so we can avoid calling // JSProxy::getOwnProperty twice on proxies, since // getOwnComputedPrimitiveDescriptor doesn't pass back the // valueOrAccessor. CallResult<bool> res = JSObject::getOwnComputedPrimitiveDescriptor( selfHandle, runtime, *converted, IgnoreProxy::Yes, tmpSymbolStorage, desc); if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (*res) { // This is safe because we passed IgnoreProxy::Yes above, // meaning that this will return false in proxy cases. valueOrAccessor = getComputedSlotValueUnsafe( createPseudoHandle(selfHandle.get()), runtime, desc); return true; } if (LLVM_UNLIKELY(selfHandle->isProxyObject())) { return JSProxy::getOwnProperty( selfHandle, runtime, nameValHandle, desc, &valueOrAccessor); } return false; } JSObject *JSObject::getNamedDescriptorUnsafe( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, PropertyFlags expectedFlags, NamedPropertyDescriptor &desc) { if (findProperty(selfHandle, runtime, name, expectedFlags, desc)) return *selfHandle; // Check here for host object flag. This means that "normal" own // properties above win over host-defined properties, but there's no // cost imposed on own property lookups. This should do what we // need in practice, and we can define host vs js property // disambiguation however we want. This is here in order to avoid // impacting perf for the common case where an own property exists // in normal storage. if (LLVM_UNLIKELY(selfHandle->flags_.hostObject)) { desc.flags.hostObject = true; desc.flags.writable = true; desc.slot = name.unsafeGetRaw(); return *selfHandle; } if (LLVM_UNLIKELY(selfHandle->flags_.lazyObject)) { assert( !selfHandle->flags_.proxyObject && "Proxy objects should never be lazy"); // Initialize the object and perform the lookup again. JSObject::initializeLazyObject(runtime, selfHandle); if (findProperty(selfHandle, runtime, name, expectedFlags, desc)) return *selfHandle; } if (LLVM_UNLIKELY(selfHandle->flags_.proxyObject)) { desc.flags.proxyObject = true; desc.slot = name.unsafeGetRaw(); return *selfHandle; } if (selfHandle->parent_) { MutableHandle<JSObject> mutableSelfHandle{ runtime, selfHandle->parent_.getNonNull(runtime)}; do { // Check the most common case first, at the cost of some code duplication. if (LLVM_LIKELY( !mutableSelfHandle->flags_.lazyObject && !mutableSelfHandle->flags_.hostObject && !mutableSelfHandle->flags_.proxyObject)) { findProp: if (findProperty( mutableSelfHandle, runtime, name, PropertyFlags::invalid(), desc)) { assert( !selfHandle->flags_.proxyObject && "Proxy object parents should never have own properties"); return *mutableSelfHandle; } } else if (LLVM_UNLIKELY(mutableSelfHandle->flags_.lazyObject)) { JSObject::initializeLazyObject(runtime, mutableSelfHandle); goto findProp; } else if (LLVM_UNLIKELY(mutableSelfHandle->flags_.hostObject)) { desc.flags.hostObject = true; desc.flags.writable = true; desc.slot = name.unsafeGetRaw(); return *mutableSelfHandle; } else { assert( mutableSelfHandle->flags_.proxyObject && "descriptor flags are impossible"); desc.flags.proxyObject = true; desc.slot = name.unsafeGetRaw(); return *mutableSelfHandle; } } while ((mutableSelfHandle = mutableSelfHandle->parent_.get(runtime))); } return nullptr; } ExecutionStatus JSObject::getComputedPrimitiveDescriptor( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, MutableHandle<JSObject> &propObj, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc) { assert( !nameValHandle->isObject() && "nameValHandle passed to " "getComputedPrimitiveDescriptor cannot " "be an object"); propObj = selfHandle.get(); SymbolID id{}; GCScopeMarkerRAII marker{runtime}; do { // A proxy is ignored here so we can check the bit later and // return it back to the caller for additional processing. Handle<JSObject> loopHandle = propObj; CallResult<bool> res = getOwnComputedPrimitiveDescriptorImpl( loopHandle, runtime, nameValHandle, IgnoreProxy::Yes, id, tmpSymbolStorage, desc); if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (*res) { return ExecutionStatus::RETURNED; } if (LLVM_UNLIKELY(propObj->flags_.hostObject)) { desc.flags.hostObject = true; desc.flags.writable = true; desc.slot = id.unsafeGetRaw(); tmpSymbolStorage = id; return ExecutionStatus::RETURNED; } if (LLVM_UNLIKELY(propObj->flags_.proxyObject)) { desc.flags.proxyObject = true; desc.slot = id.unsafeGetRaw(); tmpSymbolStorage = id; return ExecutionStatus::RETURNED; } // This isn't a proxy, so use the faster getParent() instead of // getPrototypeOf. propObj = propObj->getParent(runtime); // Flush at the end of the loop to allow first iteration to be as fast as // possible. marker.flush(); } while (propObj); return ExecutionStatus::RETURNED; } ExecutionStatus JSObject::getComputedDescriptor( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, MutableHandle<JSObject> &propObj, MutableHandle<SymbolID> &tmpSymbolStorage, ComputedPropertyDescriptor &desc) { auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return getComputedPrimitiveDescriptor( selfHandle, runtime, *converted, propObj, tmpSymbolStorage, desc); } CallResult<PseudoHandle<>> JSObject::getNamedWithReceiver_RJS( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, Handle<> receiver, PropOpFlags opFlags, PropertyCacheEntry *cacheEntry) { NamedPropertyDescriptor desc; // Locate the descriptor. propObj contains the object which may be anywhere // along the prototype chain. JSObject *propObj = getNamedDescriptorUnsafe(selfHandle, runtime, name, desc); if (!propObj) { if (LLVM_UNLIKELY(opFlags.getMustExist())) { return runtime.raiseReferenceError( TwineChar16("Property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "' doesn't exist"); } return createPseudoHandle(HermesValue::encodeUndefinedValue()); } if (LLVM_LIKELY( !desc.flags.accessor && !desc.flags.hostObject && !desc.flags.proxyObject)) { // Populate the cache if requested. if (cacheEntry && !propObj->getClass(runtime)->isDictionaryNoCache()) { cacheEntry->clazz = propObj->getClassGCPtr(); cacheEntry->slot = desc.slot; } return createPseudoHandle( getNamedSlotValueUnsafe(propObj, runtime, desc).unboxToHV(runtime)); } if (desc.flags.accessor) { auto *accessor = vmcast<PropertyAccessor>( getNamedSlotValueUnsafe(propObj, runtime, desc).getPointer(runtime)); if (!accessor->getter) return createPseudoHandle(HermesValue::encodeUndefinedValue()); // Execute the accessor on this object. return Callable::executeCall0( runtime.makeHandle(accessor->getter), runtime, receiver); } else if (desc.flags.hostObject) { auto res = vmcast<HostObject>(propObj)->get(name); if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return createPseudoHandle(*res); } else { assert(desc.flags.proxyObject && "descriptor flags are impossible"); return JSProxy::getNamed( runtime.makeHandle(propObj), runtime, name, receiver); } } CallResult<PseudoHandle<>> JSObject::getNamedOrIndexed( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, PropOpFlags opFlags) { if (LLVM_UNLIKELY(selfHandle->flags_.indexedStorage)) { // Note that getStringView can be satisfied without materializing the // Identifier. const auto strView = runtime.getIdentifierTable().getStringView(runtime, name); if (auto nameAsIndex = toArrayIndex(strView)) { return getComputed_RJS( selfHandle, runtime, runtime.makeHandle(HermesValue::encodeNumberValue(*nameAsIndex))); } // Here we have indexed properties but the symbol was not index-like. // Fall through to getNamed(). } return getNamed_RJS(selfHandle, runtime, name, opFlags); } CallResult<PseudoHandle<>> JSObject::getComputedWithReceiver_RJS( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, Handle<> receiver) { // Try the fast-path first: no "index-like" properties and the "name" already // is a valid integer index. if (selfHandle->flags_.fastIndexProperties) { if (auto arrayIndex = toArrayIndexFastPath(*nameValHandle)) { // Do we have this value present in our array storage? If so, return it. PseudoHandle<> ourValue = createPseudoHandle( getOwnIndexed(selfHandle.get(), runtime, *arrayIndex)); if (LLVM_LIKELY(!ourValue->isEmpty())) return ourValue; } } // If nameValHandle is an object, we should convert it to string now, // because toString may have side-effect, and we want to do this only // once. auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto nameValPrimitiveHandle = *converted; ComputedPropertyDescriptor desc; // Locate the descriptor. propObj contains the object which may be anywhere // along the prototype chain. MutableHandle<JSObject> propObj{runtime}; MutableHandle<SymbolID> tmpPropNameStorage{runtime}; if (LLVM_UNLIKELY( getComputedPrimitiveDescriptor( selfHandle, runtime, nameValPrimitiveHandle, propObj, tmpPropNameStorage, desc) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!propObj) return createPseudoHandle(HermesValue::encodeUndefinedValue()); if (LLVM_LIKELY( !desc.flags.accessor && !desc.flags.hostObject && !desc.flags.proxyObject)) { return createPseudoHandle(getComputedSlotValueUnsafe( createPseudoHandle(propObj.get()), runtime, desc)); } if (desc.flags.accessor) { auto *accessor = vmcast<PropertyAccessor>(getComputedSlotValueUnsafe( createPseudoHandle(propObj.get()), runtime, desc)); if (!accessor->getter) return createPseudoHandle(HermesValue::encodeUndefinedValue()); // Execute the accessor on this object. return accessor->getter.getNonNull(runtime)->executeCall0( runtime.makeHandle(accessor->getter), runtime, receiver); } else if (desc.flags.hostObject) { SymbolID id{}; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); auto propRes = vmcast<HostObject>(propObj.get())->get(id); if (propRes == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; return createPseudoHandle(*propRes); } else { assert(desc.flags.proxyObject && "descriptor flags are impossible"); CallResult<Handle<>> key = toPropertyKey(runtime, nameValPrimitiveHandle); if (key == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; return JSProxy::getComputed(propObj, runtime, *key, receiver); } } CallResult<bool> JSObject::hasNamed( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name) { NamedPropertyDescriptor desc; JSObject *propObj = getNamedDescriptorUnsafe(selfHandle, runtime, name, desc); if (propObj == nullptr) { return false; } if (LLVM_UNLIKELY(desc.flags.proxyObject)) { return JSProxy::hasNamed(runtime.makeHandle(propObj), runtime, name); } return true; } CallResult<bool> JSObject::hasNamedOrIndexed( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name) { if (LLVM_UNLIKELY(selfHandle->flags_.indexedStorage)) { const auto strView = runtime.getIdentifierTable().getStringView(runtime, name); if (auto nameAsIndex = toArrayIndex(strView)) { if (haveOwnIndexed(selfHandle.get(), runtime, *nameAsIndex)) { return true; } if (selfHandle->flags_.fastIndexProperties) { return false; } } // Here we have indexed properties but the symbol was not stored in the // indexedStorage. // Fall through to getNamed(). } return hasNamed(selfHandle, runtime, name); } CallResult<bool> JSObject::hasComputed( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle) { // Try the fast-path first: no "index-like" properties and the "name" already // is a valid integer index. if (selfHandle->flags_.fastIndexProperties) { if (auto arrayIndex = toArrayIndexFastPath(*nameValHandle)) { // Do we have this value present in our array storage? If so, return true. if (haveOwnIndexed(selfHandle.get(), runtime, *arrayIndex)) { return true; } } } // If nameValHandle is an object, we should convert it to string now, // because toString may have side-effect, and we want to do this only // once. auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto nameValPrimitiveHandle = *converted; ComputedPropertyDescriptor desc; MutableHandle<SymbolID> tmpPropNameStorage{runtime}; MutableHandle<JSObject> propObj{runtime}; if (getComputedPrimitiveDescriptor( selfHandle, runtime, nameValPrimitiveHandle, propObj, tmpPropNameStorage, desc) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } if (!propObj) { return false; } if (LLVM_UNLIKELY(desc.flags.proxyObject)) { CallResult<Handle<>> key = toPropertyKey(runtime, nameValPrimitiveHandle); if (key == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; return JSProxy::hasComputed(propObj, runtime, *key); } // For compatibility with polyfills we want to pretend that all HostObject // properties are "own" properties in 'in'. Since there is no way to check for // a HostObject property, we must always assume success. In practice the // property name would have been obtained from enumerating the properties in // JS code that looks something like this: // for(key in hostObj) { // if (key in hostObj) // ... // } return true; } static ExecutionStatus raiseErrorForOverridingStaticBuiltin( Handle<JSObject> selfHandle, Runtime &runtime, Handle<SymbolID> name) { Handle<StringPrimitive> methodNameHnd = runtime.makeHandle(runtime.getStringPrimFromSymbolID(name.get())); // If the 'name' property does not exist or is an accessor, we don't display // the name. NamedPropertyDescriptor desc; auto *obj = JSObject::getNamedDescriptorPredefined( selfHandle, runtime, Predefined::name, desc); assert( !selfHandle->isProxyObject() && "raiseErrorForOverridingStaticBuiltin cannot be used with proxy objects"); if (!obj || desc.flags.accessor) { return runtime.raiseTypeError( TwineChar16("Attempting to override read-only builtin method '") + TwineChar16(methodNameHnd.get()) + "'"); } // Display the name property of the builtin object if it is a string. auto objNameRes = JSObject::getNamedSlotValue(createPseudoHandle(obj), runtime, desc); if (LLVM_UNLIKELY(objNameRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } PseudoHandle<StringPrimitive> objName = PseudoHandle<StringPrimitive>::dyn_vmcast(std::move(*objNameRes)); if (!objName) { return runtime.raiseTypeError( TwineChar16("Attempting to override read-only builtin method '") + TwineChar16(methodNameHnd.get()) + "'"); } return runtime.raiseTypeError( TwineChar16("Attempting to override read-only builtin method '") + TwineChar16(objName.get()) + "." + TwineChar16(methodNameHnd.get()) + "'"); } CallResult<bool> JSObject::putNamedWithReceiver_RJS( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, Handle<> valueHandle, Handle<> receiver, PropOpFlags opFlags) { NamedPropertyDescriptor desc; // Look for the property in this object or along the prototype chain. // `name` will not be freed before this function returns, // so it will outlive the lifetime of `desc`. JSObject *propObj = getNamedDescriptorUnsafe( selfHandle, runtime, name, PropertyFlags::defaultNewNamedPropertyFlags(), desc); // If the property exists (or, we hit a proxy/hostobject on the way // up the chain) if (propObj) { // Get the simple case out of the way: If the property already // exists on selfHandle, is not an accessor, selfHandle and // receiver are the same, selfHandle is not a host // object/proxy/internal setter, and the property is writable, // just write into the same slot. if (LLVM_LIKELY( *selfHandle == propObj && selfHandle.getHermesValue().getRaw() == receiver->getRaw() && !desc.flags.accessor && !desc.flags.internalSetter && !desc.flags.hostObject && !desc.flags.proxyObject && desc.flags.writable)) { auto shv = SmallHermesValue::encodeHermesValue(*valueHandle, runtime); setNamedSlotValueUnsafe(*selfHandle, runtime, desc, shv); return true; } if (LLVM_UNLIKELY(desc.flags.accessor)) { auto *accessor = vmcast<PropertyAccessor>( getNamedSlotValueUnsafe(propObj, runtime, desc).getObject(runtime)); // If it is a read-only accessor, fail. if (!accessor->setter) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( TwineChar16("Cannot assign to property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "' which has only a getter"); } return false; } // Execute the accessor on this object. if (accessor->setter.getNonNull(runtime)->executeCall1( runtime.makeHandle(accessor->setter), runtime, receiver, *valueHandle) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } return true; } if (LLVM_UNLIKELY(desc.flags.proxyObject)) { assert( !opFlags.getMustExist() && "MustExist cannot be used with Proxy objects"); CallResult<bool> setRes = JSProxy::setNamed( runtime.makeHandle(propObj), runtime, name, valueHandle, receiver); if (LLVM_UNLIKELY(setRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!*setRes && opFlags.getThrowOnError()) { return runtime.raiseTypeError( TwineChar16("Proxy set returned false for property '") + runtime.getIdentifierTable().getStringView(runtime, name) + "'"); } return setRes; } if (LLVM_UNLIKELY(!desc.flags.writable)) { if (desc.flags.staticBuiltin) { return raiseErrorForOverridingStaticBuiltin( selfHandle, runtime, runtime.makeHandle(name)); } if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( TwineChar16("Cannot assign to read-only property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "'"); } return false; } if (*selfHandle == propObj && desc.flags.internalSetter) { return internalSetter( selfHandle, runtime, name, desc, valueHandle, opFlags); } } // The property does not exist as an conventional own property on // this object. MutableHandle<JSObject> receiverHandle{runtime, *selfHandle}; MutableHandle<SymbolID> tmpSymbolStorage{runtime}; if (selfHandle.getHermesValue().getRaw() != receiver->getRaw() || receiverHandle->isHostObject() || receiverHandle->isProxyObject()) { if (selfHandle.getHermesValue().getRaw() != receiver->getRaw()) { receiverHandle = dyn_vmcast<JSObject>(*receiver); } if (!receiverHandle) { return false; } if (getOwnNamedDescriptor(receiverHandle, runtime, name, desc)) { if (LLVM_UNLIKELY(desc.flags.accessor || !desc.flags.writable)) { return false; } assert( !receiverHandle->isHostObject() && !receiverHandle->isProxyObject() && "getOwnNamedDescriptor never sets hostObject or proxyObject flags"); auto shv = SmallHermesValue::encodeHermesValue(*valueHandle, runtime); setNamedSlotValueUnsafe(*receiverHandle, runtime, desc, shv); return true; } // Now deal with host and proxy object cases. We need to call // getOwnComputedPrimitiveDescriptor because it knows how to call // the [[getOwnProperty]] Proxy impl if needed. if (LLVM_UNLIKELY( receiverHandle->isHostObject() || receiverHandle->isProxyObject())) { if (receiverHandle->isHostObject()) { return vmcast<HostObject>(receiverHandle.get()) ->set(name, *valueHandle); } ComputedPropertyDescriptor desc; Handle<> nameValHandle = runtime.makeHandle(name); CallResult<bool> descDefinedRes = getOwnComputedPrimitiveDescriptor( receiverHandle, runtime, nameValHandle, IgnoreProxy::No, tmpSymbolStorage, desc); if (LLVM_UNLIKELY(descDefinedRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } DefinePropertyFlags dpf; if (*descDefinedRes) { dpf.setValue = 1; } else { dpf = DefinePropertyFlags::getDefaultNewPropertyFlags(); } return JSProxy::defineOwnProperty( receiverHandle, runtime, nameValHandle, dpf, valueHandle, opFlags); } } // Does the caller require it to exist? if (LLVM_UNLIKELY(opFlags.getMustExist())) { return runtime.raiseReferenceError( TwineChar16("Property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "' doesn't exist"); } // Add a new property. return addOwnProperty( receiverHandle, runtime, name, DefinePropertyFlags::getDefaultNewPropertyFlags(), valueHandle, opFlags); } CallResult<bool> JSObject::putNamedOrIndexed( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, Handle<> valueHandle, PropOpFlags opFlags) { if (LLVM_UNLIKELY(selfHandle->flags_.indexedStorage)) { // Note that getStringView can be satisfied without materializing the // Identifier. const auto strView = runtime.getIdentifierTable().getStringView(runtime, name); if (auto nameAsIndex = toArrayIndex(strView)) { return putComputed_RJS( selfHandle, runtime, runtime.makeHandle(HermesValue::encodeNumberValue(*nameAsIndex)), valueHandle, opFlags); } // Here we have indexed properties but the symbol was not index-like. // Fall through to putNamed(). } return putNamed_RJS(selfHandle, runtime, name, valueHandle, opFlags); } CallResult<bool> JSObject::putComputedWithReceiver_RJS( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, Handle<> valueHandle, Handle<> receiver, PropOpFlags opFlags) { assert( !opFlags.getMustExist() && "mustExist flag cannot be used with computed properties"); // Try the fast-path first: has "index-like" properties, the "name" // already is a valid integer index, selfHandle and receiver are the // same, and it is present in storage. if (selfHandle->flags_.fastIndexProperties) { if (auto arrayIndex = toArrayIndexFastPath(*nameValHandle)) { if (selfHandle.getHermesValue().getRaw() == receiver->getRaw()) { if (haveOwnIndexed(selfHandle.get(), runtime, *arrayIndex)) { auto result = setOwnIndexed(selfHandle, runtime, *arrayIndex, valueHandle); if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (LLVM_LIKELY(*result)) return true; if (opFlags.getThrowOnError()) { // TODO: better message. return runtime.raiseTypeError( "Cannot assign to read-only property"); } return false; } } } } // If nameValHandle is an object, we should convert it to string now, // because toString may have side-effect, and we want to do this only // once. auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto nameValPrimitiveHandle = *converted; ComputedPropertyDescriptor desc; // Look for the property in this object or along the prototype chain. MutableHandle<JSObject> propObj{runtime}; MutableHandle<SymbolID> tmpSymbolStorage{runtime}; if (LLVM_UNLIKELY( getComputedPrimitiveDescriptor( selfHandle, runtime, nameValPrimitiveHandle, propObj, tmpSymbolStorage, desc) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } // If the property exists (or, we hit a proxy/hostobject on the way // up the chain) if (propObj) { // Get the simple case out of the way: If the property already // exists on selfHandle, is not an accessor, selfHandle and // receiver are the same, selfHandle is not a host // object/proxy/internal setter, and the property is writable, // just write into the same slot. if (LLVM_LIKELY( selfHandle == propObj && selfHandle.getHermesValue().getRaw() == receiver->getRaw() && !desc.flags.accessor && !desc.flags.internalSetter && !desc.flags.hostObject && !desc.flags.proxyObject && desc.flags.writable)) { if (LLVM_UNLIKELY( setComputedSlotValueUnsafe( selfHandle, runtime, desc, valueHandle) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return true; } // Is it an accessor? if (LLVM_UNLIKELY(desc.flags.accessor)) { auto *accessor = vmcast<PropertyAccessor>( getComputedSlotValueUnsafe(propObj, runtime, desc)); // If it is a read-only accessor, fail. if (!accessor->setter) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeErrorForValue( "Cannot assign to property ", nameValPrimitiveHandle, " which has only a getter"); } return false; } // Execute the accessor on this object. if (accessor->setter.getNonNull(runtime)->executeCall1( runtime.makeHandle(accessor->setter), runtime, receiver, valueHandle.get()) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } return true; } if (LLVM_UNLIKELY(desc.flags.proxyObject)) { assert( !opFlags.getMustExist() && "MustExist cannot be used with Proxy objects"); CallResult<Handle<>> key = toPropertyKey(runtime, nameValPrimitiveHandle); if (key == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; CallResult<bool> setRes = JSProxy::setComputed(propObj, runtime, *key, valueHandle, receiver); if (LLVM_UNLIKELY(setRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!*setRes && opFlags.getThrowOnError()) { // TODO: better message. return runtime.raiseTypeError( TwineChar16("Proxy trap returned false for property")); } return setRes; } if (LLVM_UNLIKELY(!desc.flags.writable)) { if (desc.flags.staticBuiltin) { SymbolID id{}; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); return raiseErrorForOverridingStaticBuiltin( selfHandle, runtime, runtime.makeHandle(id)); } if (opFlags.getThrowOnError()) { return runtime.raiseTypeErrorForValue( "Cannot assign to read-only property ", nameValPrimitiveHandle, ""); } return false; } if (selfHandle == propObj && desc.flags.internalSetter) { SymbolID id{}; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); return internalSetter( selfHandle, runtime, id, desc.castToNamedPropertyDescriptorRef(), valueHandle, opFlags); } } // The property does not exist as an conventional own property on // this object. MutableHandle<JSObject> receiverHandle{runtime, *selfHandle}; if (selfHandle.getHermesValue().getRaw() != receiver->getRaw() || receiverHandle->isHostObject() || receiverHandle->isProxyObject()) { if (selfHandle.getHermesValue().getRaw() != receiver->getRaw()) { receiverHandle = dyn_vmcast<JSObject>(*receiver); } if (!receiverHandle) { return false; } ComputedPropertyDescriptor existingDesc; CallResult<bool> descDefinedRes = getOwnComputedPrimitiveDescriptor( receiverHandle, runtime, nameValPrimitiveHandle, IgnoreProxy::No, tmpSymbolStorage, existingDesc); if (LLVM_UNLIKELY(descDefinedRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } DefinePropertyFlags dpf; if (*descDefinedRes) { if (LLVM_UNLIKELY( existingDesc.flags.accessor || !existingDesc.flags.writable)) { return false; } if (LLVM_LIKELY( !existingDesc.flags.internalSetter && !receiverHandle->isHostObject() && !receiverHandle->isProxyObject())) { if (LLVM_UNLIKELY( setComputedSlotValueUnsafe( receiverHandle, runtime, existingDesc, valueHandle) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return true; } } // At this point, either the descriptor exists on the receiver, // but it's a corner case; or, there was no descriptor. if (LLVM_UNLIKELY( existingDesc.flags.internalSetter || receiverHandle->isHostObject() || receiverHandle->isProxyObject())) { // If putComputed is called on a proxy whose target's prototype // is an array with a propname of 'length', then internalSetter // will be true, and the receiver will be a proxy. In that case, // proxy wins. if (receiverHandle->isProxyObject()) { if (*descDefinedRes) { dpf.setValue = 1; } else { dpf = DefinePropertyFlags::getDefaultNewPropertyFlags(); } return JSProxy::defineOwnProperty( receiverHandle, runtime, nameValPrimitiveHandle, dpf, valueHandle, opFlags); } SymbolID id{}; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); if (existingDesc.flags.internalSetter) { return internalSetter( receiverHandle, runtime, id, existingDesc.castToNamedPropertyDescriptorRef(), valueHandle, opFlags); } assert( receiverHandle->isHostObject() && "descriptor flags are impossible"); return vmcast<HostObject>(receiverHandle.get())->set(id, *valueHandle); } } /// Can we add more properties? if (LLVM_UNLIKELY(!receiverHandle->isExtensible())) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "cannot add a new property"); // TODO: better message. } return false; } // If we have indexed storage we must check whether the property is an index, // and if it is, store it in indexed storage. if (receiverHandle->flags_.indexedStorage) { OptValue<uint32_t> arrayIndex; MutableHandle<StringPrimitive> strPrim{runtime}; TO_ARRAY_INDEX(runtime, nameValPrimitiveHandle, strPrim, arrayIndex); if (arrayIndex) { // Check whether we need to update array's ".length" property. if (auto *array = dyn_vmcast<JSArray>(receiverHandle.get())) { if (LLVM_UNLIKELY(*arrayIndex >= JSArray::getLength(array, runtime))) { auto cr = putNamed_RJS( receiverHandle, runtime, Predefined::getSymbolID(Predefined::length), runtime.makeHandle( HermesValue::encodeNumberValue(*arrayIndex + 1)), opFlags); if (LLVM_UNLIKELY(cr == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (LLVM_UNLIKELY(!*cr)) return false; } } auto result = setOwnIndexed(receiverHandle, runtime, *arrayIndex, valueHandle); if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (LLVM_LIKELY(*result)) return true; if (opFlags.getThrowOnError()) { // TODO: better message. return runtime.raiseTypeError("Cannot assign to read-only property"); } return false; } } SymbolID id{}; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); // Add a new named property. return addOwnProperty( receiverHandle, runtime, id, DefinePropertyFlags::getDefaultNewPropertyFlags(), valueHandle, opFlags); } CallResult<bool> JSObject::deleteNamed( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, PropOpFlags opFlags) { assert( !opFlags.getMustExist() && "mustExist cannot be specified when deleting"); // Find the property by name. NamedPropertyDescriptor desc; auto pos = findProperty(selfHandle, runtime, name, desc); // If the property doesn't exist in this object, return success. if (!pos) { if (LLVM_LIKELY( !selfHandle->flags_.lazyObject && !selfHandle->flags_.proxyObject)) { return true; } else if (selfHandle->flags_.lazyObject) { // object is lazy, initialize and read again. initializeLazyObject(runtime, selfHandle); pos = findProperty(selfHandle, runtime, name, desc); if (!pos) // still not there, return true. return true; } else { assert(selfHandle->flags_.proxyObject && "object flags are impossible"); return proxyOpFlags( runtime, opFlags, "Proxy delete returned false", JSProxy::deleteNamed(selfHandle, runtime, name)); } } // If the property isn't configurable, fail. if (LLVM_UNLIKELY(!desc.flags.configurable)) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( TwineChar16("Property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "' is not configurable"); } return false; } // Clear the deleted property value to prevent memory leaks. setNamedSlotValueUnsafe( *selfHandle, runtime, desc, SmallHermesValue::encodeEmptyValue()); // Perform the actual deletion. auto newClazz = HiddenClass::deleteProperty( runtime.makeHandle(selfHandle->clazz_), runtime, *pos); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); return true; } CallResult<bool> JSObject::deleteComputed( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, PropOpFlags opFlags) { assert( !opFlags.getMustExist() && "mustExist cannot be specified when deleting"); // If nameValHandle is an object, we should convert it to string now, // because toString may have side-effect, and we want to do this only // once. auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto nameValPrimitiveHandle = *converted; // If the name is a valid integer array index, store it here. OptValue<uint32_t> arrayIndex; // If we have indexed storage, we must attempt to convert the name to array // index, even if the conversion is expensive. if (selfHandle->flags_.indexedStorage) { MutableHandle<StringPrimitive> strPrim{runtime}; TO_ARRAY_INDEX(runtime, nameValPrimitiveHandle, strPrim, arrayIndex); } // Try the fast-path first: the "name" is a valid array index and we don't // have "index-like" named properties. if (arrayIndex && selfHandle->flags_.fastIndexProperties) { // Delete the indexed property. if (deleteOwnIndexed(selfHandle, runtime, *arrayIndex)) return true; // Cannot delete property (for example this may be a typed array). if (opFlags.getThrowOnError()) { // TODO: better error message. return runtime.raiseTypeError("Cannot delete property"); } return false; } // slow path, check if object is lazy before continuing. if (LLVM_UNLIKELY(selfHandle->flags_.lazyObject)) { // initialize and try again. initializeLazyObject(runtime, selfHandle); return deleteComputed(selfHandle, runtime, nameValHandle, opFlags); } // Convert the string to an SymbolID; SymbolID id; LAZY_TO_IDENTIFIER(runtime, nameValPrimitiveHandle, id); // Find the property by name. NamedPropertyDescriptor desc; auto pos = findProperty(selfHandle, runtime, id, desc); // If the property exists, make sure it is configurable. if (pos) { // If the property isn't configurable, fail. if (LLVM_UNLIKELY(!desc.flags.configurable)) { if (opFlags.getThrowOnError()) { // TODO: a better message. return runtime.raiseTypeError("Property is not configurable"); } return false; } } // At this point we know that the named property either doesn't exist, or // is configurable and so can be deleted, or the object is a Proxy. // If it is an "index-like" property, we must also delete the "shadow" indexed // property in order to keep Array.length correct. if (arrayIndex) { if (!deleteOwnIndexed(selfHandle, runtime, *arrayIndex)) { // Cannot delete property (for example this may be a typed array). if (opFlags.getThrowOnError()) { // TODO: better error message. return runtime.raiseTypeError("Cannot delete property"); } return false; } } if (pos) { // delete the named property (if it exists). // Clear the deleted property value to prevent memory leaks. setNamedSlotValueUnsafe( *selfHandle, runtime, desc, SmallHermesValue::encodeEmptyValue()); // Remove the property descriptor. auto newClazz = HiddenClass::deleteProperty( runtime.makeHandle(selfHandle->clazz_), runtime, *pos); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); } else if (LLVM_UNLIKELY(selfHandle->flags_.proxyObject)) { CallResult<Handle<>> key = toPropertyKey(runtime, nameValPrimitiveHandle); if (key == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; return proxyOpFlags( runtime, opFlags, "Proxy delete returned false", JSProxy::deleteComputed(selfHandle, runtime, *key)); } return true; } CallResult<bool> JSObject::defineOwnPropertyInternal( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, DefinePropertyFlags dpFlags, Handle<> valueOrAccessor, PropOpFlags opFlags) { assert( !opFlags.getMustExist() && "cannot use mustExist with defineOwnProperty"); assert( !(dpFlags.setValue && dpFlags.isAccessor()) && "Cannot set both value and accessor"); assert( (dpFlags.setValue || dpFlags.isAccessor() || valueOrAccessor.get().isUndefined()) && "value must be undefined when all of setValue/setSetter/setGetter are " "false"); #ifndef NDEBUG if (dpFlags.isAccessor()) { assert(valueOrAccessor.get().isPointer() && "accessor must be non-empty"); assert( !dpFlags.setWritable && !dpFlags.writable && "writable must not be set with accessors"); } #endif // Is it an existing property. NamedPropertyDescriptor desc; auto pos = findProperty(selfHandle, runtime, name, desc); if (pos) { return updateOwnProperty( selfHandle, runtime, name, *pos, desc, dpFlags, valueOrAccessor, opFlags); } if (LLVM_UNLIKELY( selfHandle->flags_.lazyObject || selfHandle->flags_.proxyObject)) { if (selfHandle->flags_.proxyObject) { return JSProxy::defineOwnProperty( selfHandle, runtime, name.isUniqued() ? runtime.makeHandle(HermesValue::encodeStringValue( runtime.getStringPrimFromSymbolID(name))) : runtime.makeHandle(name), dpFlags, valueOrAccessor, opFlags); } assert(selfHandle->flags_.lazyObject && "descriptor flags are impossible"); // if the property was not found and the object is lazy we need to // initialize it and try again. JSObject::initializeLazyObject(runtime, selfHandle); return defineOwnPropertyInternal( selfHandle, runtime, name, dpFlags, valueOrAccessor, opFlags); } return addOwnProperty( selfHandle, runtime, name, dpFlags, valueOrAccessor, opFlags); } ExecutionStatus JSObject::defineNewOwnProperty( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, PropertyFlags propertyFlags, Handle<> valueOrAccessor) { assert( !selfHandle->flags_.proxyObject && "definedNewOwnProperty cannot be used with proxy objects"); assert( !(propertyFlags.accessor && !valueOrAccessor.get().isPointer()) && "accessor must be non-empty"); assert( !(propertyFlags.accessor && propertyFlags.writable) && "writable must not be set with accessors"); assert( !HiddenClass::debugIsPropertyDefined( selfHandle->clazz_.get(runtime), runtime, name) && "new property is already defined"); return addOwnPropertyImpl( selfHandle, runtime, name, propertyFlags, valueOrAccessor); } CallResult<bool> JSObject::defineOwnComputedPrimitive( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, DefinePropertyFlags dpFlags, Handle<> valueOrAccessor, PropOpFlags opFlags) { assert( !nameValHandle->isObject() && "nameValHandle passed to " "defineOwnComputedPrimitive() cannot be " "an object"); assert( !opFlags.getMustExist() && "cannot use mustExist with defineOwnProperty"); assert( !(dpFlags.setValue && dpFlags.isAccessor()) && "Cannot set both value and accessor"); assert( (dpFlags.setValue || dpFlags.isAccessor() || valueOrAccessor.get().isUndefined()) && "value must be undefined when all of setValue/setSetter/setGetter are " "false"); assert( !dpFlags.enableInternalSetter && "Cannot set internalSetter on a computed property"); #ifndef NDEBUG if (dpFlags.isAccessor()) { assert(valueOrAccessor.get().isPointer() && "accessor must be non-empty"); assert( !dpFlags.setWritable && !dpFlags.writable && "writable must not be set with accessors"); } #endif // If the name is a valid integer array index, store it here. OptValue<uint32_t> arrayIndex; // If we have indexed storage, we must attempt to convert the name to array // index, even if the conversion is expensive. if (selfHandle->flags_.indexedStorage) { MutableHandle<StringPrimitive> strPrim{runtime}; TO_ARRAY_INDEX(runtime, nameValHandle, strPrim, arrayIndex); } SymbolID id{}; // If not storing a property with an array index name, or if we don't have // indexed storage, just pass to the named routine. if (!arrayIndex) { LAZY_TO_IDENTIFIER(runtime, nameValHandle, id); return defineOwnPropertyInternal( selfHandle, runtime, id, dpFlags, valueOrAccessor, opFlags); } // At this point we know that we have indexed storage and that the property // has an index-like name. // First check if a named property with the same name exists. if (selfHandle->clazz_.getNonNull(runtime)->getHasIndexLikeProperties()) { LAZY_TO_IDENTIFIER(runtime, nameValHandle, id); NamedPropertyDescriptor desc; auto pos = findProperty(selfHandle, runtime, id, desc); // If we found a named property, update it. if (pos) { return updateOwnProperty( selfHandle, runtime, id, *pos, desc, dpFlags, valueOrAccessor, opFlags); } } // Does an indexed property with that index exist? auto indexedPropPresent = getOwnIndexedPropertyFlags(selfHandle.get(), runtime, *arrayIndex); if (indexedPropPresent) { // The current value of the property. HermesValue curValueOrAccessor = getOwnIndexed(selfHandle.get(), runtime, *arrayIndex); auto updateStatus = checkPropertyUpdate( runtime, *indexedPropPresent, dpFlags, curValueOrAccessor, valueOrAccessor, opFlags); if (updateStatus == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; if (updateStatus->first == PropertyUpdateStatus::failed) return false; // The property update is valid, but can the property remain an "indexed" // property, or do we need to convert it to a named property? // If the property flags didn't change, the property remains indexed. if (updateStatus->second == *indexedPropPresent) { // If the value doesn't change, we are done. if (updateStatus->first == PropertyUpdateStatus::done) return true; // If we successfully updated the value, we are done. auto result = setOwnIndexed(selfHandle, runtime, *arrayIndex, valueOrAccessor); if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (*result) return true; if (opFlags.getThrowOnError()) { // TODO: better error message. return runtime.raiseTypeError("cannot change read-only property value"); } return false; } // OK, we need to convert an indexed property to a named one. // Check whether to use the supplied value, or to reuse the old one, as we // are simply reconfiguring it. MutableHandle<> value{runtime}; if (dpFlags.setValue || dpFlags.isAccessor()) { value = valueOrAccessor.get(); } else { value = curValueOrAccessor; } // Update dpFlags to match the existing property flags. dpFlags.setEnumerable = 1; dpFlags.setWritable = 1; dpFlags.setConfigurable = 1; dpFlags.enumerable = updateStatus->second.enumerable; dpFlags.writable = updateStatus->second.writable; dpFlags.configurable = updateStatus->second.configurable; // Delete the existing indexed property. if (!deleteOwnIndexed(selfHandle, runtime, *arrayIndex)) { if (opFlags.getThrowOnError()) { // TODO: better error message. return runtime.raiseTypeError("Cannot define property"); } return false; } // Add the new named property. LAZY_TO_IDENTIFIER(runtime, nameValHandle, id); return addOwnProperty(selfHandle, runtime, id, dpFlags, value, opFlags); } /// Can we add new properties? if (!selfHandle->isExtensible()) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "cannot add a new property"); // TODO: better message. } return false; } // This is a new property with an index-like name. // Check whether we need to update array's ".length" property. bool updateLength = false; if (auto arrayHandle = Handle<JSArray>::dyn_vmcast(selfHandle)) { if (LLVM_UNLIKELY( *arrayIndex >= JSArray::getLength(*arrayHandle, runtime))) { NamedPropertyDescriptor lengthDesc; bool lengthPresent = getOwnNamedDescriptor( arrayHandle, runtime, Predefined::getSymbolID(Predefined::length), lengthDesc); (void)lengthPresent; assert(lengthPresent && ".length must be present in JSArray"); if (!lengthDesc.flags.writable) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "Cannot assign to read-only 'length' property of array"); } return false; } updateLength = true; } } bool newIsIndexed = canNewPropertyBeIndexed(dpFlags); if (newIsIndexed) { auto result = setOwnIndexed( selfHandle, runtime, *arrayIndex, dpFlags.setValue ? valueOrAccessor : Runtime::getUndefinedValue()); if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; if (!*result) { if (opFlags.getThrowOnError()) { // TODO: better error message. return runtime.raiseTypeError("Cannot define property"); } return false; } } // If this is an array and we need to update ".length", do so. if (updateLength) { // This should always succeed since we are simply enlarging the length. auto res = JSArray::setLength( Handle<JSArray>::vmcast(selfHandle), runtime, *arrayIndex + 1, opFlags); (void)res; assert( res != ExecutionStatus::EXCEPTION && *res && "JSArray::setLength() failed unexpectedly"); } if (newIsIndexed) return true; // We are adding a new property with an index-like name. LAZY_TO_IDENTIFIER(runtime, nameValHandle, id); return addOwnProperty( selfHandle, runtime, id, dpFlags, valueOrAccessor, opFlags); } CallResult<bool> JSObject::defineOwnComputed( Handle<JSObject> selfHandle, Runtime &runtime, Handle<> nameValHandle, DefinePropertyFlags dpFlags, Handle<> valueOrAccessor, PropOpFlags opFlags) { auto converted = toPropertyKeyIfObject(runtime, nameValHandle); if (LLVM_UNLIKELY(converted == ExecutionStatus::EXCEPTION)) return ExecutionStatus::EXCEPTION; return defineOwnComputedPrimitive( selfHandle, runtime, *converted, dpFlags, valueOrAccessor, opFlags); } std::string JSObject::getHeuristicTypeName(GC *gc) { PointerBase &base = gc->getPointerBase(); if (auto constructorVal = tryGetNamedNoAlloc( this, base, Predefined::getSymbolID(Predefined::constructor))) { if (auto *constructor = dyn_vmcast<JSObject>( constructorVal->unboxToHV(gc->getPointerBase()))) { auto name = constructor->getNameIfExists(base); // If the constructor's name doesn't exist, or it is just the object // constructor, attempt to find a different name. if (!name.empty() && name != "Object") return name; } } std::string name = getVT()->snapshotMetaData.defaultNameForNode(this); // A constructor's name was not found, check if the object is in dictionary // mode. if (getClass(base)->isDictionary()) { return name + "(Dictionary)"; } // If it's not an Object, the CellKind is most likely good enough on its own if (getKind() != CellKind::JSObjectKind) { return name; } // If the object isn't a dictionary, and it has only a few property names, // make the name based on those property names. std::vector<std::string> propertyNames; HiddenClass::forEachPropertyNoAlloc( getClass(base), base, [gc, &propertyNames](SymbolID id, NamedPropertyDescriptor) { if (InternalProperty::isInternal(id)) { // Internal properties aren't user-visible, skip them. return; } propertyNames.emplace_back(gc->convertSymbolToUTF8(id)); }); // NOTE: One option is to sort the property names before truncation, to // reduce the number of groups; however, by not sorting them it makes it // easier to spot sets of objects with the same properties but in different // orders, and thus find HiddenClass optimizations to make. // For objects with a lot of properties but aren't in dictionary mode yet, // keep the number displayed small. constexpr int kMaxPropertiesForTypeName = 5; bool truncated = false; if (propertyNames.size() > kMaxPropertiesForTypeName) { propertyNames.erase( propertyNames.begin() + kMaxPropertiesForTypeName, propertyNames.end()); truncated = true; } // The final name should look like Object(a, b, c). if (propertyNames.empty()) { // Don't add parentheses for objects with no properties. return name; } name += "("; bool first = true; for (const auto &prop : propertyNames) { if (!first) { name += ", "; } first = false; name += prop; } if (truncated) { // No need to check for comma edge case because this only happens for // greater than one property. static_assert( kMaxPropertiesForTypeName >= 1, "Property truncation should not happen for 0 properties"); name += ", ..."; } name += ")"; return name; } std::string JSObject::getNameIfExists(PointerBase &base) { // Try "displayName" first, if it is defined. if (auto nameVal = tryGetNamedNoAlloc( this, base, Predefined::getSymbolID(Predefined::displayName))) { if (auto *name = dyn_vmcast<StringPrimitive>(nameVal->unboxToHV(base))) { return converter(name); } } // Next, use "name" if it is defined. if (auto nameVal = tryGetNamedNoAlloc( this, base, Predefined::getSymbolID(Predefined::name))) { if (auto *name = dyn_vmcast<StringPrimitive>(nameVal->unboxToHV(base))) { return converter(name); } } // There is no other way to access the "name" property on an object. return ""; } std::string JSObject::_snapshotNameImpl(GCCell *cell, GC *gc) { auto *const self = vmcast<JSObject>(cell); return self->getHeuristicTypeName(gc); } void JSObject::_snapshotAddEdgesImpl(GCCell *cell, GC *gc, HeapSnapshot &snap) { auto *const self = vmcast<JSObject>(cell); // Add the prototype as a property edge, so it's easy for JS developers to // walk the prototype chain on their own. if (self->parent_) { snap.addNamedEdge( HeapSnapshot::EdgeType::Property, // __proto__ chosen for similarity to V8. "__proto__", gc->getObjectID(self->parent_)); } HiddenClass::forEachPropertyNoAlloc( self->clazz_.get(gc->getPointerBase()), gc->getPointerBase(), [self, gc, &snap](SymbolID id, NamedPropertyDescriptor desc) { if (InternalProperty::isInternal(id)) { // Internal properties aren't user-visible, skip them. return; } // Else, it's a user-visible property. HermesValue prop = getNamedSlotValueUnsafe(self, gc->getPointerBase(), desc.slot) .unboxToHV(gc->getPointerBase()); const llvh::Optional<HeapSnapshot::NodeID> idForProp = gc->getSnapshotID(prop); if (!idForProp) { return; } std::string propName = gc->convertSymbolToUTF8(id); // If the property name is a valid array index, display it as an // "element" instead of a "property". This will put square brackets // around the number and sort it numerically rather than // alphabetically. if (auto index = ::hermes::toArrayIndex(propName)) { snap.addIndexedEdge( HeapSnapshot::EdgeType::Element, index.getValue(), idForProp.getValue()); } else { snap.addNamedEdge( HeapSnapshot::EdgeType::Property, propName, idForProp.getValue()); } }); } void JSObject::_snapshotAddLocationsImpl( GCCell *cell, GC *gc, HeapSnapshot &snap) { auto *const self = vmcast<JSObject>(cell); PointerBase &base = gc->getPointerBase(); // Add the location of the constructor function for this object, if that // constructor is a user-defined JS function. if (auto constructorVal = tryGetNamedNoAlloc( self, base, Predefined::getSymbolID(Predefined::constructor))) { if (constructorVal->isObject()) { if (auto *constructor = dyn_vmcast<JSFunction>(constructorVal->getObject(base))) { constructor->addLocationToSnapshot(snap, gc->getObjectID(self)); } } } } std::pair<uint32_t, uint32_t> JSObject::_getOwnIndexedRangeImpl( JSObject *self, Runtime &runtime) { return {0, 0}; } bool JSObject::_haveOwnIndexedImpl(JSObject *self, Runtime &, uint32_t) { return false; } OptValue<PropertyFlags> JSObject::_getOwnIndexedPropertyFlagsImpl( JSObject *self, Runtime &runtime, uint32_t) { return llvh::None; } HermesValue JSObject::_getOwnIndexedImpl(JSObject *, Runtime &, uint32_t) { return HermesValue::encodeEmptyValue(); } CallResult<bool> JSObject::_setOwnIndexedImpl(Handle<JSObject>, Runtime &, uint32_t, Handle<>) { return false; } bool JSObject::_deleteOwnIndexedImpl(Handle<JSObject>, Runtime &, uint32_t) { return false; } bool JSObject::_checkAllOwnIndexedImpl( JSObject * /*self*/, Runtime & /*runtime*/, ObjectVTable::CheckAllOwnIndexedMode /*mode*/) { return true; } void JSObject::preventExtensions(JSObject *self) { assert( !self->flags_.proxyObject && "[[Extensible]] slot cannot be set directly on Proxy objects"); self->flags_.noExtend = true; } CallResult<bool> JSObject::preventExtensions( Handle<JSObject> selfHandle, Runtime &runtime, PropOpFlags opFlags) { if (LLVM_UNLIKELY(selfHandle->isProxyObject())) { return JSProxy::preventExtensions(selfHandle, runtime, opFlags); } JSObject::preventExtensions(*selfHandle); return true; } ExecutionStatus JSObject::seal(Handle<JSObject> selfHandle, Runtime &runtime) { CallResult<bool> statusRes = JSObject::preventExtensions( selfHandle, runtime, PropOpFlags().plusThrowOnError()); if (LLVM_UNLIKELY(statusRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } assert( *statusRes && "seal preventExtensions with ThrowOnError returned false"); // Already sealed? if (selfHandle->flags_.sealed) return ExecutionStatus::RETURNED; auto newClazz = HiddenClass::makeAllNonConfigurable( runtime.makeHandle(selfHandle->clazz_), runtime); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); selfHandle->flags_.sealed = true; return ExecutionStatus::RETURNED; } ExecutionStatus JSObject::freeze( Handle<JSObject> selfHandle, Runtime &runtime) { CallResult<bool> statusRes = JSObject::preventExtensions( selfHandle, runtime, PropOpFlags().plusThrowOnError()); if (LLVM_UNLIKELY(statusRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } assert( *statusRes && "freeze preventExtensions with ThrowOnError returned false"); // Already frozen? if (selfHandle->flags_.frozen) return ExecutionStatus::RETURNED; auto newClazz = HiddenClass::makeAllReadOnly( runtime.makeHandle(selfHandle->clazz_), runtime); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); selfHandle->flags_.frozen = true; selfHandle->flags_.sealed = true; return ExecutionStatus::RETURNED; } void JSObject::updatePropertyFlagsWithoutTransitions( Handle<JSObject> selfHandle, Runtime &runtime, PropertyFlags flagsToClear, PropertyFlags flagsToSet, OptValue<llvh::ArrayRef<SymbolID>> props) { auto newClazz = HiddenClass::updatePropertyFlagsWithoutTransitions( runtime.makeHandle(selfHandle->clazz_), runtime, flagsToClear, flagsToSet, props); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); } CallResult<bool> JSObject::isExtensible( PseudoHandle<JSObject> self, Runtime &runtime) { if (LLVM_UNLIKELY(self->isProxyObject())) { return JSProxy::isExtensible(runtime.makeHandle(std::move(self)), runtime); } return self->isExtensible(); } bool JSObject::isSealed(PseudoHandle<JSObject> self, Runtime &runtime) { if (self->flags_.sealed) return true; if (!self->flags_.noExtend) return false; auto selfHandle = runtime.makeHandle(std::move(self)); if (!HiddenClass::areAllNonConfigurable( runtime.makeHandle(selfHandle->clazz_), runtime)) { return false; } if (!checkAllOwnIndexed( *selfHandle, runtime, ObjectVTable::CheckAllOwnIndexedMode::NonConfigurable)) { return false; } // Now that we know we are sealed, set the flag. selfHandle->flags_.sealed = true; return true; } bool JSObject::isFrozen(PseudoHandle<JSObject> self, Runtime &runtime) { if (self->flags_.frozen) return true; if (!self->flags_.noExtend) return false; auto selfHandle = runtime.makeHandle(std::move(self)); if (!HiddenClass::areAllReadOnly( runtime.makeHandle(selfHandle->clazz_), runtime)) { return false; } if (!checkAllOwnIndexed( *selfHandle, runtime, ObjectVTable::CheckAllOwnIndexedMode::ReadOnly)) { return false; } // Now that we know we are sealed, set the flag. selfHandle->flags_.frozen = true; selfHandle->flags_.sealed = true; return true; } CallResult<bool> JSObject::addOwnProperty( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, DefinePropertyFlags dpFlags, Handle<> valueOrAccessor, PropOpFlags opFlags) { /// Can we add more properties? if (!selfHandle->isExtensible() && !opFlags.getInternalForce()) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( TwineChar16("Cannot add new property '") + runtime.getIdentifierTable().getStringViewForDev(runtime, name) + "'"); } return false; } PropertyFlags flags{}; // Accessors don't set writeable. if (dpFlags.isAccessor()) { dpFlags.setWritable = 0; flags.accessor = 1; } // Override the default flags if specified. if (dpFlags.setEnumerable) flags.enumerable = dpFlags.enumerable; if (dpFlags.setWritable) flags.writable = dpFlags.writable; if (dpFlags.setConfigurable) flags.configurable = dpFlags.configurable; flags.internalSetter = dpFlags.enableInternalSetter; if (LLVM_UNLIKELY( addOwnPropertyImpl( selfHandle, runtime, name, flags, valueOrAccessor) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return true; } ExecutionStatus JSObject::addOwnPropertyImpl( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, PropertyFlags propertyFlags, Handle<> valueOrAccessor) { assert( !selfHandle->flags_.proxyObject && "Internal properties cannot be added to Proxy objects"); // Add a new property to the class. // TODO: if we check for OOM here in the future, we must undo the slot // allocation. auto addResult = HiddenClass::addProperty( runtime.makeHandle(selfHandle->clazz_), runtime, name, propertyFlags); if (LLVM_UNLIKELY(addResult == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } selfHandle->clazz_.setNonNull(runtime, *addResult->first, &runtime.getHeap()); allocateNewSlotStorage( selfHandle, runtime, addResult->second, valueOrAccessor); // If this is an index-like property, we need to clear the fast path flags. if (LLVM_UNLIKELY( selfHandle->clazz_.getNonNull(runtime)->getHasIndexLikeProperties())) selfHandle->flags_.fastIndexProperties = false; return ExecutionStatus::RETURNED; } CallResult<bool> JSObject::updateOwnProperty( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, HiddenClass::PropertyPos propertyPos, NamedPropertyDescriptor desc, const DefinePropertyFlags dpFlags, Handle<> valueOrAccessor, PropOpFlags opFlags) { auto updateStatus = checkPropertyUpdate( runtime, desc.flags, dpFlags, getNamedSlotValueUnsafe(selfHandle.get(), runtime, desc) .unboxToHV(runtime), valueOrAccessor, opFlags); if (updateStatus == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; if (updateStatus->first == PropertyUpdateStatus::failed) return false; // If the property flags changed, update them. if (updateStatus->second != desc.flags) { desc.flags = updateStatus->second; auto newClazz = HiddenClass::updateProperty( runtime.makeHandle(selfHandle->clazz_), runtime, propertyPos, desc.flags); selfHandle->clazz_.setNonNull(runtime, *newClazz, &runtime.getHeap()); } if (updateStatus->first == PropertyUpdateStatus::done) return true; assert( updateStatus->first == PropertyUpdateStatus::needSet && "unexpected PropertyUpdateStatus"); if (dpFlags.setValue) { if (LLVM_LIKELY(!desc.flags.internalSetter)) { auto shv = SmallHermesValue::encodeHermesValue(*valueOrAccessor, runtime); setNamedSlotValueUnsafe(selfHandle.get(), runtime, desc, shv); } else { return internalSetter( selfHandle, runtime, name, desc, valueOrAccessor, opFlags); } } else if (dpFlags.isAccessor()) { auto shv = SmallHermesValue::encodeHermesValue(*valueOrAccessor, runtime); setNamedSlotValueUnsafe(selfHandle.get(), runtime, desc, shv); } else { // If checkPropertyUpdate() returned needSet, but there is no value or // accessor, clear the value. setNamedSlotValueUnsafe( selfHandle.get(), runtime, desc, SmallHermesValue::encodeUndefinedValue()); } return true; } CallResult<std::pair<JSObject::PropertyUpdateStatus, PropertyFlags>> JSObject::checkPropertyUpdate( Runtime &runtime, const PropertyFlags currentFlags, DefinePropertyFlags dpFlags, const HermesValue curValueOrAccessor, Handle<> valueOrAccessor, PropOpFlags opFlags) { // 8.12.9 [5] Return true, if every field in Desc is absent. if (dpFlags.isEmpty()) return std::make_pair(PropertyUpdateStatus::done, currentFlags); assert( (!dpFlags.isAccessor() || (!dpFlags.setWritable && !dpFlags.writable)) && "can't set both accessor and writable"); assert( !dpFlags.enableInternalSetter && "cannot change the value of internalSetter"); // 8.12.9 [6] Return true, if every field in Desc also occurs in current and // the value of every field in Desc is the same value as the corresponding // field in current when compared using the SameValue algorithm (9.12). // TODO: this would probably be much more efficient with bitmasks. if ((!dpFlags.setEnumerable || dpFlags.enumerable == currentFlags.enumerable) && (!dpFlags.setConfigurable || dpFlags.configurable == currentFlags.configurable)) { if (dpFlags.isAccessor()) { if (currentFlags.accessor) { auto *curAccessor = vmcast<PropertyAccessor>(curValueOrAccessor); auto *newAccessor = vmcast<PropertyAccessor>(valueOrAccessor.get()); if ((!dpFlags.setGetter || curAccessor->getter == newAccessor->getter) && (!dpFlags.setSetter || curAccessor->setter == newAccessor->setter)) { return std::make_pair(PropertyUpdateStatus::done, currentFlags); } } } else { if (!currentFlags.accessor && (!dpFlags.setValue || isSameValue(curValueOrAccessor, valueOrAccessor.get())) && (!dpFlags.setWritable || dpFlags.writable == currentFlags.writable)) { return std::make_pair(PropertyUpdateStatus::done, currentFlags); } } } // 8.12.9 [7] // If the property is not configurable, some aspects are not changeable. if (!currentFlags.configurable) { // Trying to change non-configurable to configurable? if (dpFlags.configurable) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not configurable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } // Trying to change the enumerability of non-configurable property? if (dpFlags.setEnumerable && dpFlags.enumerable != currentFlags.enumerable) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not configurable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } } PropertyFlags newFlags = currentFlags; // 8.12.9 [8] If IsGenericDescriptor(Desc) is true, then no further validation // is required. if (!(dpFlags.setValue || dpFlags.setWritable || dpFlags.setGetter || dpFlags.setSetter)) { // Do nothing } // 8.12.9 [9] // Changing between accessor and data descriptor? else if (currentFlags.accessor != dpFlags.isAccessor()) { if (!currentFlags.configurable) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not configurable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } // If we change from accessor to data descriptor, Preserve the existing // values of the converted property’s [[Configurable]] and [[Enumerable]] // attributes and set the rest of the property’s attributes to their default // values. // If it's the other way around, since the accessor doesn't have the // [[Writable]] attribute, do nothing. newFlags.writable = 0; // If we are changing from accessor to non-accessor, we must set a new // value. if (!dpFlags.isAccessor()) dpFlags.setValue = 1; } // 8.12.9 [10] if both are data descriptors. else if (!currentFlags.accessor) { if (!currentFlags.configurable) { if (!currentFlags.writable) { // If the current property is not writable, but the new one is. if (dpFlags.writable) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not configurable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } // If we are setting a different value. if (dpFlags.setValue && !isSameValue(curValueOrAccessor, valueOrAccessor.get())) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not writable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } } } } // 8.12.9 [11] Both are accessors. else { auto *curAccessor = vmcast<PropertyAccessor>(curValueOrAccessor); auto *newAccessor = vmcast<PropertyAccessor>(valueOrAccessor.get()); // If not configurable, make sure that nothing is changing. if (!currentFlags.configurable) { if ((dpFlags.setGetter && newAccessor->getter != curAccessor->getter) || (dpFlags.setSetter && newAccessor->setter != curAccessor->setter)) { if (opFlags.getThrowOnError()) { return runtime.raiseTypeError( "property is not configurable"); // TODO: better message. } return std::make_pair(PropertyUpdateStatus::failed, PropertyFlags{}); } } // If not setting the getter or the setter, re-use the current one. if (!dpFlags.setGetter) newAccessor->getter.set(runtime, curAccessor->getter, &runtime.getHeap()); if (!dpFlags.setSetter) newAccessor->setter.set(runtime, curAccessor->setter, &runtime.getHeap()); } // 8.12.9 [12] For each attribute field of Desc that is present, set the // correspondingly named attribute of the property named P of object O to the // value of the field. if (dpFlags.setEnumerable) newFlags.enumerable = dpFlags.enumerable; if (dpFlags.setWritable) newFlags.writable = dpFlags.writable; if (dpFlags.setConfigurable) newFlags.configurable = dpFlags.configurable; if (dpFlags.setValue) newFlags.accessor = false; else if (dpFlags.isAccessor()) newFlags.accessor = true; else return std::make_pair(PropertyUpdateStatus::done, newFlags); return std::make_pair(PropertyUpdateStatus::needSet, newFlags); } CallResult<bool> JSObject::internalSetter( Handle<JSObject> selfHandle, Runtime &runtime, SymbolID name, NamedPropertyDescriptor /*desc*/, Handle<> value, PropOpFlags opFlags) { if (vmisa<JSArray>(selfHandle.get())) { if (name == Predefined::getSymbolID(Predefined::length)) { return JSArray::setLength( Handle<JSArray>::vmcast(selfHandle), runtime, value, opFlags); } } llvm_unreachable("unhandled property in Object::internalSetter()"); } namespace { /// Helper function to add all the property names of an object to an /// array, starting at the given index. Only enumerable properties are /// included. Returns the index after the last property added, but... CallResult<uint32_t> appendAllPropertyNames( Handle<JSObject> obj, Runtime &runtime, MutableHandle<BigStorage> &arr, uint32_t beginIndex) { uint32_t size = beginIndex; // We know that duplicate property names can only exist between objects in // the prototype chain. Hence there should not be duplicated properties // before we start to look at any prototype. bool needDedup = false; MutableHandle<> prop(runtime); MutableHandle<JSObject> head(runtime, obj.get()); MutableHandle<StringPrimitive> tmpVal{runtime}; while (head.get()) { GCScope gcScope(runtime); // enumerableProps will contain all enumerable own properties from obj. // Impl note: this is the only place where getOwnPropertyKeys will be // called without IncludeNonEnumerable on a Proxy. Everywhere else, // trap ordering is specified but ES9 13.7.5.15 says "The mechanics and // order of enumerating the properties is not specified", which is // unusual. auto cr = JSObject::getOwnPropertyNames(head, runtime, true /* onlyEnumerable */); if (LLVM_UNLIKELY(cr == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto enumerableProps = *cr; auto marker = gcScope.createMarker(); for (unsigned i = 0, e = enumerableProps->getEndIndex(); i < e; ++i) { gcScope.flushToMarker(marker); prop = enumerableProps->at(runtime, i); if (!needDedup) { // If no dedup is needed, add it directly. if (LLVM_UNLIKELY( BigStorage::push_back(arr, runtime, prop) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } ++size; continue; } // Otherwise loop through all existing properties and check if we // have seen it before. bool dupFound = false; if (prop->isNumber()) { for (uint32_t j = beginIndex; j < size && !dupFound; ++j) { HermesValue val = arr->at(j); if (val.isNumber()) { dupFound = val.getNumber() == prop->getNumber(); } else { // val is string, prop is number. tmpVal = val.getString(); auto valNum = toArrayIndex( StringPrimitive::createStringView(runtime, tmpVal)); dupFound = valNum && valNum.getValue() == prop->getNumber(); } } } else { for (uint32_t j = beginIndex; j < size && !dupFound; ++j) { HermesValue val = arr->at(j); if (val.isNumber()) { // val is number, prop is string. auto propNum = toArrayIndex(StringPrimitive::createStringView( runtime, Handle<StringPrimitive>::vmcast(prop))); dupFound = propNum && (propNum.getValue() == val.getNumber()); } else { dupFound = val.getString()->equals(prop->getString()); } } } if (LLVM_LIKELY(!dupFound)) { if (LLVM_UNLIKELY( BigStorage::push_back(arr, runtime, prop) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } ++size; } } // Continue to follow the prototype chain. CallResult<PseudoHandle<JSObject>> parentRes = JSObject::getPrototypeOf(head, runtime); if (LLVM_UNLIKELY(parentRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } head = parentRes->get(); needDedup = true; } return size; } /// Adds the hidden classes of the prototype chain of obj to arr, /// starting with the prototype of obj at index 0, etc., and /// terminates with null. /// /// \param obj The object whose prototype chain should be output /// \param[out] arr The array where the classes will be appended. This /// array is cleared if any object is unsuitable for caching. ExecutionStatus setProtoClasses( Runtime &runtime, Handle<JSObject> obj, MutableHandle<BigStorage> &arr) { // Layout of a JSArray stored in the for-in cache: // [class(proto(obj)), class(proto(proto(obj))), ..., null, prop0, prop1, ...] if (!obj->shouldCacheForIn(runtime)) { arr->clear(runtime); return ExecutionStatus::RETURNED; } MutableHandle<JSObject> head(runtime, obj->getParent(runtime)); MutableHandle<> clazz(runtime); GCScopeMarkerRAII marker{runtime}; while (head.get()) { if (!head->shouldCacheForIn(runtime)) { arr->clear(runtime); return ExecutionStatus::RETURNED; } if (JSObject::Helper::flags(*head).lazyObject) { // Ensure all properties have been initialized before caching the hidden // class. Not doing this will result in changes to the hidden class // when getOwnPropertyKeys is called later. JSObject::initializeLazyObject(runtime, head); } clazz = HermesValue::encodeObjectValue(head->getClass(runtime)); if (LLVM_UNLIKELY( BigStorage::push_back(arr, runtime, clazz) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } head = head->getParent(runtime); marker.flush(); } clazz = HermesValue::encodeNullValue(); return BigStorage::push_back(arr, runtime, clazz); } /// Verifies that the classes of obj's prototype chain still matches those /// previously prefixed to arr by setProtoClasses. /// /// \param obj The object whose prototype chain should be verified /// \param arr Array previously populated by setProtoClasses /// \return The index after the terminating null if everything matches, /// otherwise 0. uint32_t matchesProtoClasses( Runtime &runtime, Handle<JSObject> obj, Handle<BigStorage> arr) { MutableHandle<JSObject> head(runtime, obj->getParent(runtime)); uint32_t i = 0; while (head.get()) { HermesValue protoCls = arr->at(i++); if (protoCls.isNull() || protoCls.getObject() != head->getClass(runtime) || head->isProxyObject()) { return 0; } head = head->getParent(runtime); } // The chains must both end at the same point. if (head || !arr->at(i++).isNull()) { return 0; } assert(i > 0 && "success should be positive"); return i; } } // namespace CallResult<Handle<BigStorage>> getForInPropertyNames( Runtime &runtime, Handle<JSObject> obj, uint32_t &beginIndex, uint32_t &endIndex) { Handle<HiddenClass> clazz(runtime, obj->getClass(runtime)); // Fast case: Check the cache. MutableHandle<BigStorage> arr(runtime); if (obj->shouldCacheForIn(runtime)) { arr = clazz->getForInCache(runtime); if (arr) { beginIndex = matchesProtoClasses(runtime, obj, arr); if (beginIndex) { // Cache is valid for this object, so use it. endIndex = arr->size(); return arr; } // Invalid for this object. We choose to clear the cache since the // changes to the prototype chain probably affect other objects too. clazz->clearForInCache(runtime); // Clear arr to slightly reduce risk of OOM from allocation below. arr = nullptr; } } // Slow case: Build the array of properties. auto ownPropEstimate = clazz->getNumProperties(); auto arrRes = obj->shouldCacheForIn(runtime) ? BigStorage::createLongLived(runtime, ownPropEstimate) : BigStorage::create(runtime, ownPropEstimate); if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } arr = std::move(*arrRes); if (setProtoClasses(runtime, obj, arr) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } beginIndex = arr->size(); // If obj or any of its prototypes are unsuitable for caching, then // beginIndex is 0 and we return an array with only the property names. bool canCache = beginIndex; auto end = appendAllPropertyNames(obj, runtime, arr, beginIndex); if (end == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } endIndex = *end; // Avoid degenerate memory explosion: if > 75% of the array is properties // or classes from prototypes, then don't cache it. const bool tooMuchProto = *end / 4 > ownPropEstimate; if (canCache && !tooMuchProto) { assert(beginIndex > 0 && "cached array must start with proto classes"); #ifdef HERMES_SLOW_DEBUG assert(beginIndex == matchesProtoClasses(runtime, obj, arr) && "matches"); #endif clazz->setForInCache(*arr, runtime); } return arr; } } // namespace vm } // namespace hermes