/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "builtin/MapObject-inl.h" #include "builtin/MapObject.h" #include "jsapi.h" #include "builtin/OrderedHashTableObject.h" #include "gc/GCContext.h" #include "jit/InlinableNatives.h" #include "js/MapAndSet.h" #include "js/PropertyAndElement.h" // JS_DefineFunctions #include "js/PropertySpec.h" #include "js/Utility.h" #include "vm/BigIntType.h" #include "vm/EqualityOperations.h" // js::SameValue #include "vm/GlobalObject.h" #include "vm/Interpreter.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/SelfHosting.h" #include "vm/SymbolType.h" #include "builtin/OrderedHashTableObject-inl.h" #include "gc/GCContext-inl.h" #include "gc/Marking-inl.h" #include "gc/ObjectKind-inl.h" #include "vm/GeckoProfiler-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using mozilla::NumberEqualsInt32; /*** HashableValue **********************************************************/ static PreBarriered NormalizeDoubleValue(double d) { int32_t i; if (NumberEqualsInt32(d, &i)) { // Normalize int32_t-valued doubles to int32_t for faster hashing and // testing. Note: we use NumberEqualsInt32 here instead of NumberIsInt32 // because we want -0 and 0 to be normalized to the same thing. return Int32Value(i); } // Normalize the sign bit of a NaN. return JS::CanonicalizedDoubleValue(d); } bool HashableValue::setValue(JSContext* cx, const Value& v) { if (v.isString()) { // Atomize so that hash() and operator==() are fast and infallible. JSString* str = AtomizeString(cx, v.toString()); if (!str) { return false; } value = StringValue(str); } else if (v.isDouble()) { value = NormalizeDoubleValue(v.toDouble()); } else { value = v; } MOZ_ASSERT(value.isUndefined() || value.isNull() || value.isBoolean() || value.isNumber() || value.isString() || value.isSymbol() || value.isObject() || value.isBigInt()); return true; } static HashNumber HashValue(const Value& v, const mozilla::HashCodeScrambler& hcs) { // HashableValue::setValue normalizes values so that the SameValue relation // on HashableValues is the same as the == relationship on // value.asRawBits(). So why not just return that? Security. // // To avoid revealing GC of atoms, string-based hash codes are computed // from the string contents rather than any pointer; to avoid revealing // addresses, pointer-based hash codes are computed using the // HashCodeScrambler. if (v.isString()) { return v.toString()->asAtom().hash(); } if (v.isSymbol()) { return v.toSymbol()->hash(); } if (v.isBigInt()) { return MaybeForwarded(v.toBigInt())->hash(); } if (v.isObject()) { return hcs.scramble(v.asRawBits()); } MOZ_ASSERT(!v.isGCThing(), "do not reveal pointers via hash codes"); return mozilla::HashGeneric(v.asRawBits()); } HashNumber HashableValue::hash(const mozilla::HashCodeScrambler& hcs) const { return HashValue(value, hcs); } bool HashableValue::equals(const HashableValue& other) const { // Two HashableValues are equal if they have equal bits. bool b = (value.asRawBits() == other.value.asRawBits()); if (!b && (value.type() == other.value.type())) { if (value.isBigInt()) { // BigInt values are considered equal if they represent the same // mathematical value. b = BigInt::equal(value.toBigInt(), other.value.toBigInt()); } } #ifdef DEBUG bool same; JSContext* cx = TlsContext.get(); MOZ_ASSERT(SameValueZero(cx, value, other.value, &same)); MOZ_ASSERT(same == b); #endif return b; } /*** MapIterator ************************************************************/ namespace {} /* anonymous namespace */ static const JSClassOps MapIteratorObjectClassOps = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve MapIteratorObject::finalize, // finalize nullptr, // call nullptr, // construct nullptr, // trace }; static const ClassExtension MapIteratorObjectClassExtension = { MapIteratorObject::objectMoved, // objectMovedOp }; const JSClass MapIteratorObject::class_ = { "Map Iterator", JSCLASS_HAS_RESERVED_SLOTS(MapIteratorObject::SlotCount) | JSCLASS_FOREGROUND_FINALIZE | JSCLASS_SKIP_NURSERY_FINALIZE, &MapIteratorObjectClassOps, JS_NULL_CLASS_SPEC, &MapIteratorObjectClassExtension, }; const JSFunctionSpec MapIteratorObject::methods[] = { JS_SELF_HOSTED_FN("next", "MapIteratorNext", 0, 0), JS_FS_END, }; /* static */ bool GlobalObject::initMapIteratorProto(JSContext* cx, Handle global) { Rooted base( cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); if (!base) { return false; } Rooted proto( cx, GlobalObject::createBlankPrototypeInheriting(cx, base)); if (!proto) { return false; } if (!JS_DefineFunctions(cx, proto, MapIteratorObject::methods) || !DefineToStringTag(cx, proto, cx->names().Map_Iterator_)) { return false; } if (!JSObject::setHasRealmFuseProperty(cx, proto)) { return false; } global->initBuiltinProto(ProtoKind::MapIteratorProto, proto); return true; } template static inline bool HasRegisteredNurseryIterators(TableObject* t) { Value v = t->getReservedSlot(TableObject::RegisteredNurseryIteratorsSlot); return v.toBoolean(); } template static inline void SetRegisteredNurseryIterators(TableObject* t, bool b) { t->setReservedSlot(TableObject::RegisteredNurseryIteratorsSlot, JS::BooleanValue(b)); } MapIteratorObject* MapIteratorObject::create(JSContext* cx, Handle mapobj, Kind kind) { Rooted global(cx, &mapobj->global()); Rooted proto( cx, GlobalObject::getOrCreateMapIteratorPrototype(cx, global)); if (!proto) { return nullptr; } MapIteratorObject* iterobj = NewObjectWithGivenProto(cx, proto); if (!iterobj) { return nullptr; } if (IsInsideNursery(iterobj) && !HasRegisteredNurseryIterators(mapobj.get())) { if (!cx->nursery().addMapWithNurseryIterators(mapobj)) { ReportOutOfMemory(cx); return nullptr; } SetRegisteredNurseryIterators(mapobj.get(), true); } MapObject::Table(mapobj).initIterator(iterobj, kind); return iterobj; } void MapIteratorObject::finalize(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(gcx->onMainThread()); MOZ_ASSERT(!IsInsideNursery(obj)); if (obj->as().isActive()) { obj->as().unlink(); } } size_t MapIteratorObject::objectMoved(JSObject* obj, JSObject* old) { MapIteratorObject* iter = &obj->as(); if (!iter->isActive()) { return 0; } if (IsInsideNursery(old)) { MapObject* mapObj = iter->target(); MapObject::Table(mapObj).relinkNurseryIterator(iter); } else { iter->updateListAfterMove(&old->as()); } return 0; } MapObject* MapIteratorObject::target() const { MOZ_ASSERT(isActive(), "only active iterators have a target object"); Value value = getFixedSlot(TargetSlot); return &MaybeForwarded(&value.toObject())->as(); } bool MapIteratorObject::next(MapIteratorObject* mapIterator, ArrayObject* resultPairObj) { // IC code calls this directly. AutoUnsafeCallWithABI unsafe; // Check invariants for inlined GetNextMapEntryForIterator. // The array should be tenured, so that post-barrier can be done simply. MOZ_ASSERT(resultPairObj->isTenured()); // The array elements should be fixed. MOZ_ASSERT(resultPairObj->hasFixedElements()); MOZ_ASSERT(resultPairObj->getDenseInitializedLength() == 2); MOZ_ASSERT(resultPairObj->getDenseCapacity() >= 2); if (!mapIterator->isActive()) { // Already done. return true; } auto storeResult = [resultPairObj](Kind kind, const auto& element) { switch (kind) { case Kind::Keys: resultPairObj->setDenseElement(0, element.key.get()); break; case Kind::Values: resultPairObj->setDenseElement(1, element.value); break; case Kind::Entries: { resultPairObj->setDenseElement(0, element.key.get()); resultPairObj->setDenseElement(1, element.value); break; } } }; MapObject* mapObj = mapIterator->target(); return MapObject::Table(mapObj).iteratorNext(mapIterator, storeResult); } /* static */ JSObject* MapIteratorObject::createResultPair(JSContext* cx) { ArrayObject* resultPairObj = NewDenseFullyAllocatedArray(cx, 2, TenuredObject); if (!resultPairObj) { return nullptr; } resultPairObj->setDenseInitializedLength(2); resultPairObj->initDenseElement(0, NullValue()); resultPairObj->initDenseElement(1, NullValue()); return resultPairObj; } /*** Map ********************************************************************/ struct js::UnbarrieredHashPolicy { using Lookup = Value; static HashNumber hash(const Lookup& v, const mozilla::HashCodeScrambler& hcs) { return HashValue(v, hcs); } static bool match(const Value& k, const Lookup& l) { return k == l; } static bool isEmpty(const Value& v) { return v.isMagic(JS_HASH_KEY_EMPTY); } static void makeEmpty(Value* vp) { vp->setMagic(JS_HASH_KEY_EMPTY); } }; // MapObject::Table, ::UnbarrieredTable and ::PreBarrieredTable must all have // the same memory layout. static_assert(sizeof(MapObject::Table::Entry) == sizeof(MapObject::UnbarrieredTable::Entry)); static_assert(sizeof(MapObject::Table::Entry) == sizeof(MapObject::PreBarrieredTable::Entry)); const JSClassOps MapObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize nullptr, // call nullptr, // construct trace, // trace }; const ClassSpec MapObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, MapObject::staticMethods, MapObject::staticProperties, MapObject::methods, MapObject::properties, MapObject::finishInit, }; const ClassExtension MapObject::classExtension_ = { MapObject::objectMoved, // objectMovedOp }; const JSClass MapObject::class_ = { "Map", JSCLASS_DELAY_METADATA_BUILDER | JSCLASS_HAS_RESERVED_SLOTS(MapObject::SlotCount) | JSCLASS_HAS_CACHED_PROTO(JSProto_Map), &MapObject::classOps_, &MapObject::classSpec_, &MapObject::classExtension_}; const JSClass MapObject::protoClass_ = { "Map.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_Map), JS_NULL_CLASS_OPS, &MapObject::classSpec_, }; const JSPropertySpec MapObject::properties[] = { JS_INLINABLE_PSG("size", size, 0, MapSize), JS_STRING_SYM_PS(toStringTag, "Map", JSPROP_READONLY), JS_PS_END, }; const JSFunctionSpec MapObject::methods[] = { JS_INLINABLE_FN("get", get, 1, 0, MapGet), JS_INLINABLE_FN("has", has, 1, 0, MapHas), JS_INLINABLE_FN("set", set, 2, 0, MapSet), JS_INLINABLE_FN("delete", delete_, 1, 0, MapDelete), JS_FN("keys", keys, 0, 0), JS_FN("values", values, 0, 0), JS_FN("clear", clear, 0, 0), JS_SELF_HOSTED_FN("forEach", "MapForEach", 2, 0), JS_FN("getOrInsert", getOrInsert, 2, 0), JS_SELF_HOSTED_FN("getOrInsertComputed", "MapGetOrInsertComputed", 2, 0), JS_FN("entries", entries, 0, 0), // @@iterator is re-defined in finishInit so that it has the // same identity as |entries|. JS_SYM_FN(iterator, entries, 0, 0), JS_FS_END, }; const JSPropertySpec MapObject::staticProperties[] = { JS_SELF_HOSTED_SYM_GET(species, "$MapSpecies", 0), JS_PS_END, }; const JSFunctionSpec MapObject::staticMethods[] = { JS_SELF_HOSTED_FN("groupBy", "MapGroupBy", 2, 0), JS_FS_END, }; /* static */ bool MapObject::finishInit(JSContext* cx, HandleObject ctor, HandleObject proto) { Handle nativeProto = proto.as(); RootedValue entriesFn(cx); RootedId entriesId(cx, NameToId(cx->names().entries)); if (!NativeGetProperty(cx, nativeProto, entriesId, &entriesFn)) { return false; } // 23.1.3.12 Map.prototype[@@iterator]() // The initial value of the @@iterator property is the same function object // as the initial value of the "entries" property. RootedId iteratorId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().iterator)); if (!NativeDefineDataProperty(cx, nativeProto, iteratorId, entriesFn, 0)) { return false; } return JSObject::setHasRealmFuseProperty(cx, nativeProto); } void MapObject::trace(JSTracer* trc, JSObject* obj) { MapObject* mapObj = &obj->as(); Table(mapObj).trace(trc); } using NurseryKeysVector = GCVector; template static NurseryKeysVector* GetNurseryKeys(TableObject* t) { Value value = t->getReservedSlot(TableObject::NurseryKeysSlot); return reinterpret_cast(value.toPrivate()); } template static NurseryKeysVector* AllocNurseryKeys(TableObject* t) { MOZ_ASSERT(!GetNurseryKeys(t)); auto keys = js_new(); if (!keys) { return nullptr; } t->setReservedSlot(TableObject::NurseryKeysSlot, PrivateValue(keys)); return keys; } template static void DeleteNurseryKeys(TableObject* t) { auto keys = GetNurseryKeys(t); MOZ_ASSERT(keys); js_delete(keys); t->setReservedSlot(TableObject::NurseryKeysSlot, PrivateValue(nullptr)); } // A generic store buffer entry that traces all nursery keys for an ordered hash // map or set. template class js::OrderedHashTableRef : public gc::BufferableRef { ObjectT* object; public: explicit OrderedHashTableRef(ObjectT* obj) : object(obj) {} void trace(JSTracer* trc) override { MOZ_ASSERT(!IsInsideNursery(object)); NurseryKeysVector* keys = GetNurseryKeys(object); MOZ_ASSERT(keys); keys->mutableEraseIf([&](Value& key) { MOZ_ASSERT(typename ObjectT::UnbarrieredTable(object).hash(key) == typename ObjectT::Table(object).hash( *reinterpret_cast(&key))); MOZ_ASSERT(IsInsideNursery(key.toGCThing())); auto result = typename ObjectT::UnbarrieredTable(object).rekeyOneEntry( key, [trc](const Value& prior) { Value key = prior; TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key"); return key; }); if (result.isNothing()) { return true; // Key removed. } key = result.value(); return !IsInsideNursery(key.toGCThing()); }); if (!keys->empty()) { trc->runtime()->gc.storeBuffer().putGeneric( OrderedHashTableRef(object)); return; } DeleteNurseryKeys(object); } }; template [[nodiscard]] inline static bool PostWriteBarrier(ObjectT* obj, const Value& keyValue) { MOZ_ASSERT(!IsInsideNursery(obj)); if (MOZ_LIKELY(!keyValue.isObject() && !keyValue.isBigInt())) { MOZ_ASSERT_IF(keyValue.isGCThing(), !IsInsideNursery(keyValue.toGCThing())); return true; } if (!IsInsideNursery(keyValue.toGCThing())) { return true; } NurseryKeysVector* keys = GetNurseryKeys(obj); if (!keys) { keys = AllocNurseryKeys(obj); if (!keys) { return false; } keyValue.toGCThing()->storeBuffer()->putGeneric( OrderedHashTableRef(obj)); } return keys->append(keyValue); } bool MapObject::getKeysAndValuesInterleaved( JS::MutableHandle> entries) { auto appendEntry = [&entries](auto& entry) { return entries.append(entry.key.get()) && entries.append(entry.value); }; return Table(this).forEachEntry(appendEntry); } bool MapObject::set(JSContext* cx, const Value& key, const Value& val) { HashableValue k; if (!k.setValue(cx, key)) { return false; } return setWithHashableKey(cx, k, val); } bool MapObject::setWithHashableKey(JSContext* cx, const HashableValue& key, const Value& value) { bool needsPostBarriers = isTenured(); if (needsPostBarriers) { // Use the Table representation which has post barriers. if (!PostWriteBarrier(this, key)) { ReportOutOfMemory(cx); return false; } if (!Table(this).put(cx, key, value)) { return false; } } else { // Use the PreBarrieredTable representation which does not. if (!PreBarrieredTable(this).put(cx, key, value)) { return false; } } return true; } bool MapObject::getOrInsert(JSContext* cx, const Value& key, const Value& val, MutableHandleValue rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } bool needsPostBarriers = isTenured(); if (needsPostBarriers) { if (!PostWriteBarrier(this, k)) { ReportOutOfMemory(cx); return false; } // Use the Table representation which has post barriers. if (const Table::Entry* p = Table(this).getOrAdd(cx, k, val)) { rval.set(p->value); } else { return false; } } else { // Use the PreBarrieredTable representation which does not. if (const PreBarrieredTable::Entry* p = PreBarrieredTable(this).getOrAdd(cx, k, val)) { rval.set(p->value); } else { return false; } } return true; } MapObject* MapObject::createWithProto(JSContext* cx, HandleObject proto, NewObjectKind newKind) { MOZ_ASSERT(proto); gc::AllocKind allocKind = gc::GetGCObjectKind(SlotCount); AutoSetNewObjectMetadata metadata(cx); auto* mapObj = NewObjectWithGivenProtoAndKinds(cx, proto, allocKind, newKind); if (!mapObj) { return nullptr; } UnbarrieredTable(mapObj).initSlots(); mapObj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); mapObj->initReservedSlot(RegisteredNurseryIteratorsSlot, BooleanValue(false)); return mapObj; } MapObject* MapObject::create(JSContext* cx, HandleObject proto /* = nullptr */) { if (proto) { return createWithProto(cx, proto, GenericObject); } // This is the common case so use the template object's shape to optimize the // allocation. MapObject* templateObj = GlobalObject::getOrCreateMapTemplateObject(cx); if (!templateObj) { return nullptr; } gc::AllocKind allocKind = templateObj->asTenured().getAllocKind(); MOZ_ASSERT(gc::GetGCKindSlots(allocKind) >= SlotCount); MOZ_ASSERT(!gc::IsFinalizedKind(allocKind)); AutoSetNewObjectMetadata metadata(cx); Rooted shape(cx, templateObj->sharedShape()); auto* mapObj = NativeObject::create(cx, allocKind, gc::Heap::Default, shape); if (!mapObj) { return nullptr; } UnbarrieredTable(mapObj).initSlots(); mapObj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); mapObj->initReservedSlot(RegisteredNurseryIteratorsSlot, BooleanValue(false)); return mapObj; } // static MapObject* GlobalObject::getOrCreateMapTemplateObject(JSContext* cx) { GlobalObjectData& data = cx->global()->data(); if (MapObject* obj = data.mapObjectTemplate) { return obj; } Rooted proto(cx, GlobalObject::getOrCreatePrototype(cx, JSProto_Map)); if (!proto) { return nullptr; } auto* mapObj = MapObject::createWithProto(cx, proto, TenuredObject); if (!mapObj) { return nullptr; } data.mapObjectTemplate.init(mapObj); return mapObj; } size_t MapObject::sizeOfBufferData() { return Table(this).sizeOfExcludingObject(); } size_t MapObject::sizeOfMallocData(mozilla::MallocSizeOf mallocSizeOf) { if (NurseryKeysVector* nurseryKeys = GetNurseryKeys(this)) { return nurseryKeys->sizeOfIncludingThis(mallocSizeOf); } return 0; } size_t MapObject::objectMoved(JSObject* obj, JSObject* old) { auto* mapObj = &obj->as(); Table(mapObj).updateIteratorsAfterMove(&old->as()); if (IsInsideNursery(old)) { Nursery& nursery = mapObj->runtimeFromMainThread()->gc.nursery(); Table(mapObj).maybeMoveBufferOnPromotion(nursery); } return 0; } void MapObject::clearNurseryIteratorsBeforeMinorGC() { Table(this).clearNurseryIterators(); } /* static */ MapObject* MapObject::sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj) { Nursery& nursery = gcx->runtime()->gc.nursery(); bool wasInCollectedRegion = nursery.inCollectedRegion(mapobj); if (wasInCollectedRegion && !IsForwarded(mapobj)) { // This MapObject is dead. return nullptr; } mapobj = MaybeForwarded(mapobj); // Keep |mapobj| registered with the nursery if it still has nursery // iterators. bool hasNurseryIterators = Table(mapobj).hasNurseryIterators(); SetRegisteredNurseryIterators(mapobj, hasNurseryIterators); return hasNurseryIterators ? mapobj : nullptr; } bool MapObject::tryOptimizeCtorWithIterable(JSContext* cx, const Value& iterableVal, bool* optimized) { MOZ_ASSERT(!iterableVal.isNullOrUndefined()); MOZ_ASSERT(!*optimized); if (!CanOptimizeMapOrSetCtorWithIterable(MapObject::set, this, cx)) { return true; } if (!iterableVal.isObject()) { return true; } JSObject* iterable = &iterableVal.toObject(); // Fast path for `new Map(array)`. if (IsOptimizableArrayForMapOrSetCtor(iterable, cx)) { ArrayObject* array = &iterable->as(); uint32_t len = array->getDenseInitializedLength(); for (uint32_t index = 0; index < len; index++) { Value element = array->getDenseElement(index); MOZ_ASSERT(IsPackedArray(&element.toObject())); auto* elementArray = &element.toObject().as(); Value key = elementArray->getDenseElement(0); Value value = elementArray->getDenseElement(1); MOZ_ASSERT(!key.isMagic(JS_ELEMENTS_HOLE)); MOZ_ASSERT(!value.isMagic(JS_ELEMENTS_HOLE)); if (!set(cx, key, value)) { return false; } } *optimized = true; return true; } // Fast path for `new Map(map)`. if (IsMapObjectWithDefaultIterator(iterable, cx)) { auto* iterableMap = &iterable->as(); auto addEntry = [cx, this](auto& entry) { return setWithHashableKey(cx, entry.key, entry.value); }; if (!Table(iterableMap).forEachEntry(addEntry)) { return false; } *optimized = true; return true; } return true; } // static MapObject* MapObject::createFromIterable(JSContext* cx, Handle proto, Handle iterable, Handle allocatedFromJit) { // A null-or-undefined |iterable| is quite common and we check for this in JIT // code. MOZ_ASSERT_IF(allocatedFromJit, !iterable.isNullOrUndefined()); Rooted obj(cx, allocatedFromJit); if (!obj) { obj = MapObject::create(cx, proto); if (!obj) { return nullptr; } } if (!iterable.isNullOrUndefined()) { bool optimized = false; if (!obj->tryOptimizeCtorWithIterable(cx, iterable, &optimized)) { return nullptr; } if (!optimized) { FixedInvokeArgs<1> args(cx); args[0].set(iterable); RootedValue thisv(cx, ObjectValue(*obj)); if (!CallSelfHostedFunction(cx, cx->names().MapConstructorInit, thisv, args, args.rval())) { return nullptr; } } } return obj; } bool MapObject::construct(JSContext* cx, unsigned argc, Value* vp) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "Map"); CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "Map")) { return false; } RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Map, &proto)) { return false; } MapObject* obj = MapObject::createFromIterable(cx, proto, args.get(0)); if (!obj) { return false; } args.rval().setObject(*obj); return true; } bool MapObject::is(HandleValue v) { return v.isObject() && v.toObject().hasClass(&class_); } bool MapObject::is(HandleObject o) { return o->hasClass(&class_); } uint32_t MapObject::size() { static_assert(sizeof(Table(this).count()) <= sizeof(uint32_t), "map count must be precisely representable as a JS number"); return Table(this).count(); } bool MapObject::size_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); args.rval().setNumber(mapObj->size()); return true; } bool MapObject::size(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "size"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::get(JSContext* cx, const Value& key, MutableHandleValue rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } if (const Table::Entry* p = Table(this).get(k)) { rval.set(p->value); } else { rval.setUndefined(); } return true; } bool MapObject::get_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); return mapObj->get(cx, args.get(0), args.rval()); } bool MapObject::get(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "get"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::has(JSContext* cx, const Value& key, bool* rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } *rval = Table(this).has(k); return true; } bool MapObject::has_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); bool found; if (!mapObj->has(cx, args.get(0), &found)) { return false; } args.rval().setBoolean(found); return true; } bool MapObject::has(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "has"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::set_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); if (!mapObj->set(cx, args.get(0), args.get(1))) { return false; } args.rval().set(args.thisv()); return true; } bool MapObject::set(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "set"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::getOrInsert_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); return mapObj->getOrInsert(cx, args.get(0), args.get(1), args.rval()); } bool MapObject::getOrInsert(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "getOrInsert"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::delete_(JSContext* cx, const Value& key, bool* rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } if (isTenured()) { *rval = Table(this).remove(cx, k); } else { *rval = PreBarrieredTable(this).remove(cx, k); } return true; } bool MapObject::delete_impl(JSContext* cx, const CallArgs& args) { // MapObject::trace does not trace deleted entries. Incremental GC therefore // requires that no HeapPtr objects pointing to heap values be left // alive in the hash table. // // OrderedHashMapImpl::remove() doesn't destroy the removed entry. It merely // calls OrderedHashMapImpl::MapOps::makeEmpty. But that is sufficient, // because makeEmpty clears the value by doing e->value = Value(), and in the // case of Table, Value() means HeapPtr(), which is the same as // HeapPtr(UndefinedValue()). auto* mapObj = &args.thisv().toObject().as(); bool found; if (!mapObj->delete_(cx, args.get(0), &found)) { return false; } args.rval().setBoolean(found); return true; } bool MapObject::delete_(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "delete"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool MapObject::iterator(JSContext* cx, IteratorKind kind, Handle obj, MutableHandleValue iter) { JSObject* iterobj = MapIteratorObject::create(cx, obj, kind); if (!iterobj) { return false; } iter.setObject(*iterobj); return true; } bool MapObject::iterator_impl(JSContext* cx, const CallArgs& args, IteratorKind kind) { Rooted mapObj(cx, &args.thisv().toObject().as()); return iterator(cx, kind, mapObj, args.rval()); } bool MapObject::keys_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, IteratorKind::Keys); } bool MapObject::keys(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "keys"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, keys_impl, args); } bool MapObject::values_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, IteratorKind::Values); } bool MapObject::values(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "values"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, values_impl, args); } bool MapObject::entries_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, IteratorKind::Entries); } bool MapObject::entries(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "entries"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, entries_impl, args); } bool MapObject::clear_impl(JSContext* cx, const CallArgs& args) { auto* mapObj = &args.thisv().toObject().as(); mapObj->clear(cx); args.rval().setUndefined(); return true; } bool MapObject::clear(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "clear"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, clear_impl, args); } void MapObject::clear(JSContext* cx) { if (isTenured()) { Table(this).clear(cx); } else { PreBarrieredTable(this).clear(cx); } } /*** SetIterator ************************************************************/ static const JSClassOps SetIteratorObjectClassOps = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve SetIteratorObject::finalize, // finalize nullptr, // call nullptr, // construct nullptr, // trace }; static const ClassExtension SetIteratorObjectClassExtension = { SetIteratorObject::objectMoved, // objectMovedOp }; const JSClass SetIteratorObject::class_ = { "Set Iterator", JSCLASS_HAS_RESERVED_SLOTS(SetIteratorObject::SlotCount) | JSCLASS_FOREGROUND_FINALIZE | JSCLASS_SKIP_NURSERY_FINALIZE, &SetIteratorObjectClassOps, JS_NULL_CLASS_SPEC, &SetIteratorObjectClassExtension, }; const JSFunctionSpec SetIteratorObject::methods[] = { JS_SELF_HOSTED_FN("next", "SetIteratorNext", 0, 0), JS_FS_END, }; /* static */ bool GlobalObject::initSetIteratorProto(JSContext* cx, Handle global) { Rooted base( cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); if (!base) { return false; } Rooted proto( cx, GlobalObject::createBlankPrototypeInheriting(cx, base)); if (!proto) { return false; } if (!JS_DefineFunctions(cx, proto, SetIteratorObject::methods) || !DefineToStringTag(cx, proto, cx->names().Set_Iterator_)) { return false; } if (!JSObject::setHasRealmFuseProperty(cx, proto)) { return false; } global->initBuiltinProto(ProtoKind::SetIteratorProto, proto); return true; } SetIteratorObject* SetIteratorObject::create(JSContext* cx, Handle setobj, Kind kind) { MOZ_ASSERT(kind != Kind::Keys); Rooted global(cx, &setobj->global()); Rooted proto( cx, GlobalObject::getOrCreateSetIteratorPrototype(cx, global)); if (!proto) { return nullptr; } SetIteratorObject* iterobj = NewObjectWithGivenProto(cx, proto); if (!iterobj) { return nullptr; } if (IsInsideNursery(iterobj) && !HasRegisteredNurseryIterators(setobj.get())) { if (!cx->nursery().addSetWithNurseryIterators(setobj)) { ReportOutOfMemory(cx); return nullptr; } SetRegisteredNurseryIterators(setobj.get(), true); } SetObject::Table(setobj).initIterator(iterobj, kind); return iterobj; } void SetIteratorObject::finalize(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(gcx->onMainThread()); MOZ_ASSERT(!IsInsideNursery(obj)); if (obj->as().isActive()) { obj->as().unlink(); } } size_t SetIteratorObject::objectMoved(JSObject* obj, JSObject* old) { SetIteratorObject* iter = &obj->as(); if (!iter->isActive()) { return 0; } if (IsInsideNursery(old)) { SetObject* setObj = iter->target(); SetObject::Table(setObj).relinkNurseryIterator(iter); } else { iter->updateListAfterMove(&old->as()); } return 0; } SetObject* SetIteratorObject::target() const { MOZ_ASSERT(isActive(), "only active iterators have a target object"); Value value = getFixedSlot(TargetSlot); return &MaybeForwarded(&value.toObject())->as(); } bool SetIteratorObject::next(SetIteratorObject* setIterator, ArrayObject* resultObj) { // IC code calls this directly. AutoUnsafeCallWithABI unsafe; // Check invariants for inlined _GetNextSetEntryForIterator. // The array should be tenured, so that post-barrier can be done simply. MOZ_ASSERT(resultObj->isTenured()); // The array elements should be fixed. MOZ_ASSERT(resultObj->hasFixedElements()); MOZ_ASSERT(resultObj->getDenseInitializedLength() == 1); MOZ_ASSERT(resultObj->getDenseCapacity() >= 1); if (!setIterator->isActive()) { // Already done. return true; } auto storeResult = [resultObj](Kind kind, const auto& element) { resultObj->setDenseElement(0, element.get()); }; SetObject* setObj = setIterator->target(); return SetObject::Table(setObj).iteratorNext(setIterator, storeResult); } /* static */ JSObject* SetIteratorObject::createResult(JSContext* cx) { ArrayObject* resultObj = NewDenseFullyAllocatedArray(cx, 1, TenuredObject); if (!resultObj) { return nullptr; } resultObj->setDenseInitializedLength(1); resultObj->initDenseElement(0, NullValue()); return resultObj; } /*** Set ********************************************************************/ const JSClassOps SetObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize nullptr, // call nullptr, // construct trace, // trace }; const ClassSpec SetObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, nullptr, SetObject::staticProperties, SetObject::methods, SetObject::properties, SetObject::finishInit, }; const ClassExtension SetObject::classExtension_ = { SetObject::objectMoved, // objectMovedOp }; const JSClass SetObject::class_ = { "Set", JSCLASS_DELAY_METADATA_BUILDER | JSCLASS_HAS_RESERVED_SLOTS(SetObject::SlotCount) | JSCLASS_HAS_CACHED_PROTO(JSProto_Set), &SetObject::classOps_, &SetObject::classSpec_, &SetObject::classExtension_}; const JSClass SetObject::protoClass_ = { "Set.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_Set), JS_NULL_CLASS_OPS, &SetObject::classSpec_, }; const JSPropertySpec SetObject::properties[] = { JS_INLINABLE_PSG("size", size, 0, SetSize), JS_STRING_SYM_PS(toStringTag, "Set", JSPROP_READONLY), JS_PS_END, }; const JSFunctionSpec SetObject::methods[] = { JS_INLINABLE_FN("has", has, 1, 0, SetHas), JS_INLINABLE_FN("add", add, 1, 0, SetAdd), JS_INLINABLE_FN("delete", delete_, 1, 0, SetDelete), JS_FN("entries", entries, 0, 0), JS_FN("clear", clear, 0, 0), JS_SELF_HOSTED_FN("forEach", "SetForEach", 2, 0), JS_SELF_HOSTED_FN("union", "SetUnion", 1, 0), JS_SELF_HOSTED_FN("difference", "SetDifference", 1, 0), JS_SELF_HOSTED_FN("intersection", "SetIntersection", 1, 0), JS_SELF_HOSTED_FN("symmetricDifference", "SetSymmetricDifference", 1, 0), JS_SELF_HOSTED_FN("isSubsetOf", "SetIsSubsetOf", 1, 0), JS_SELF_HOSTED_FN("isSupersetOf", "SetIsSupersetOf", 1, 0), JS_SELF_HOSTED_FN("isDisjointFrom", "SetIsDisjointFrom", 1, 0), JS_FN("values", values, 0, 0), // @@iterator and |keys| re-defined in finishInit so that they have the // same identity as |values|. JS_FN("keys", values, 0, 0), JS_SYM_FN(iterator, values, 0, 0), JS_FS_END, }; // clang-format on const JSPropertySpec SetObject::staticProperties[] = { JS_SELF_HOSTED_SYM_GET(species, "$SetSpecies", 0), JS_PS_END, }; /* static */ bool SetObject::finishInit(JSContext* cx, HandleObject ctor, HandleObject proto) { Handle nativeProto = proto.as(); RootedValue valuesFn(cx); RootedId valuesId(cx, NameToId(cx->names().values)); if (!NativeGetProperty(cx, nativeProto, valuesId, &valuesFn)) { return false; } // 23.2.3.8 Set.prototype.keys() // The initial value of the "keys" property is the same function object // as the initial value of the "values" property. RootedId keysId(cx, NameToId(cx->names().keys)); if (!NativeDefineDataProperty(cx, nativeProto, keysId, valuesFn, 0)) { return false; } // 23.2.3.11 Set.prototype[@@iterator]() // See above. RootedId iteratorId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().iterator)); if (!NativeDefineDataProperty(cx, nativeProto, iteratorId, valuesFn, 0)) { return false; } return JSObject::setHasRealmFuseProperty(cx, nativeProto); } bool SetObject::keys(JS::MutableHandle> keys) { auto appendEntry = [&keys](auto& entry) { return keys.append(entry.get()); }; return Table(this).forEachEntry(appendEntry); } bool SetObject::add(JSContext* cx, const Value& key) { HashableValue k; if (!k.setValue(cx, key)) { return false; } return addHashableValue(cx, k); } bool SetObject::addHashableValue(JSContext* cx, const HashableValue& value) { bool needsPostBarriers = isTenured(); if (needsPostBarriers && !PostWriteBarrier(this, value)) { ReportOutOfMemory(cx); return false; } return Table(this).put(cx, value); } SetObject* SetObject::createWithProto(JSContext* cx, HandleObject proto, NewObjectKind newKind) { MOZ_ASSERT(proto); gc::AllocKind allocKind = gc::GetGCObjectKind(SlotCount); AutoSetNewObjectMetadata metadata(cx); auto* setObj = NewObjectWithGivenProtoAndKinds(cx, proto, allocKind, newKind); if (!setObj) { return nullptr; } UnbarrieredTable(setObj).initSlots(); setObj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); setObj->initReservedSlot(RegisteredNurseryIteratorsSlot, BooleanValue(false)); return setObj; } SetObject* SetObject::create(JSContext* cx, HandleObject proto /* = nullptr */) { if (proto) { return createWithProto(cx, proto, GenericObject); } // This is the common case so use the template object's shape to optimize the // allocation. SetObject* templateObj = GlobalObject::getOrCreateSetTemplateObject(cx); if (!templateObj) { return nullptr; } gc::AllocKind allocKind = templateObj->asTenured().getAllocKind(); MOZ_ASSERT(gc::GetGCKindSlots(allocKind) >= SlotCount); MOZ_ASSERT(!gc::IsFinalizedKind(allocKind)); AutoSetNewObjectMetadata metadata(cx); Rooted shape(cx, templateObj->sharedShape()); auto* setObj = NativeObject::create(cx, allocKind, gc::Heap::Default, shape); if (!setObj) { return nullptr; } UnbarrieredTable(setObj).initSlots(); setObj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); setObj->initReservedSlot(RegisteredNurseryIteratorsSlot, BooleanValue(false)); return setObj; } // static SetObject* GlobalObject::getOrCreateSetTemplateObject(JSContext* cx) { GlobalObjectData& data = cx->global()->data(); if (SetObject* obj = data.setObjectTemplate) { return obj; } Rooted proto(cx, GlobalObject::getOrCreatePrototype(cx, JSProto_Set)); if (!proto) { return nullptr; } auto* setObj = SetObject::createWithProto(cx, proto, TenuredObject); if (!setObj) { return nullptr; } data.setObjectTemplate.init(setObj); return setObj; } void SetObject::trace(JSTracer* trc, JSObject* obj) { SetObject* setobj = static_cast(obj); Table(setobj).trace(trc); } size_t SetObject::sizeOfBufferData() { return Table(this).sizeOfExcludingObject(); } size_t SetObject::sizeOfMallocData(mozilla::MallocSizeOf mallocSizeOf) { if (NurseryKeysVector* nurseryKeys = GetNurseryKeys(this)) { return nurseryKeys->sizeOfIncludingThis(mallocSizeOf); } return 0; } size_t SetObject::objectMoved(JSObject* obj, JSObject* old) { auto* setObj = &obj->as(); Table(setObj).updateIteratorsAfterMove(&old->as()); if (IsInsideNursery(old)) { Nursery& nursery = setObj->runtimeFromMainThread()->gc.nursery(); Table(setObj).maybeMoveBufferOnPromotion(nursery); } return 0; } void SetObject::clearNurseryIteratorsBeforeMinorGC() { Table(this).clearNurseryIterators(); } /* static */ SetObject* SetObject::sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj) { Nursery& nursery = gcx->runtime()->gc.nursery(); bool wasInCollectedRegion = nursery.inCollectedRegion(setobj); if (wasInCollectedRegion && !IsForwarded(setobj)) { // This SetObject is dead. return nullptr; } setobj = MaybeForwarded(setobj); // Keep |setobj| registered with the nursery if it still has nursery // iterators. bool hasNurseryIterators = Table(setobj).hasNurseryIterators(); SetRegisteredNurseryIterators(setobj, hasNurseryIterators); return hasNurseryIterators ? setobj : nullptr; } bool SetObject::tryOptimizeCtorWithIterable(JSContext* cx, const Value& iterableVal, bool* optimized) { MOZ_ASSERT(!iterableVal.isNullOrUndefined()); MOZ_ASSERT(!*optimized); if (!CanOptimizeMapOrSetCtorWithIterable(SetObject::add, this, cx)) { return true; } if (!iterableVal.isObject()) { return true; } JSObject* iterable = &iterableVal.toObject(); // Fast path for `new Set(array)`. if (IsOptimizableArrayForMapOrSetCtor(iterable, cx)) { ArrayObject* array = &iterable->as(); uint32_t len = array->getDenseInitializedLength(); for (uint32_t index = 0; index < len; index++) { Value keyVal = array->getDenseElement(index); MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); if (!add(cx, keyVal)) { return false; } } *optimized = true; return true; } // Fast path for `new Set(set)`. if (IsSetObjectWithDefaultIterator(iterable, cx)) { auto* iterableSet = &iterable->as(); if (!IsSetObjectWithDefaultIterator(iterableSet, cx)) { return true; } auto addEntry = [cx, this](auto& entry) { return addHashableValue(cx, entry); }; if (!Table(iterableSet).forEachEntry(addEntry)) { return false; } *optimized = true; return true; } return true; } // static SetObject* SetObject::createFromIterable(JSContext* cx, Handle proto, Handle iterable, Handle allocatedFromJit) { // A null-or-undefined |iterable| is quite common and we check for this in JIT // code. MOZ_ASSERT_IF(allocatedFromJit, !iterable.isNullOrUndefined()); Rooted obj(cx, allocatedFromJit); if (!obj) { obj = SetObject::create(cx, proto); if (!obj) { return nullptr; } } if (!iterable.isNullOrUndefined()) { bool optimized = false; if (!obj->tryOptimizeCtorWithIterable(cx, iterable, &optimized)) { return nullptr; } if (!optimized) { FixedInvokeArgs<1> args(cx); args[0].set(iterable); RootedValue thisv(cx, ObjectValue(*obj)); if (!CallSelfHostedFunction(cx, cx->names().SetConstructorInit, thisv, args, args.rval())) { return nullptr; } } } return obj; } bool SetObject::construct(JSContext* cx, unsigned argc, Value* vp) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "Set"); CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "Set")) { return false; } RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Set, &proto)) { return false; } SetObject* obj = SetObject::createFromIterable(cx, proto, args.get(0)); if (!obj) { return false; } args.rval().setObject(*obj); return true; } bool SetObject::is(HandleValue v) { return v.isObject() && v.toObject().hasClass(&class_); } bool SetObject::is(HandleObject o) { return o->hasClass(&class_); } uint32_t SetObject::size() { static_assert(sizeof(Table(this).count()) <= sizeof(uint32_t), "set count must be precisely representable as a JS number"); return Table(this).count(); } bool SetObject::size_impl(JSContext* cx, const CallArgs& args) { auto* setObj = &args.thisv().toObject().as(); args.rval().setNumber(setObj->size()); return true; } bool SetObject::size(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "size"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::has_impl(JSContext* cx, const CallArgs& args) { auto* setObj = &args.thisv().toObject().as(); bool found; if (!setObj->has(cx, args.get(0), &found)) { return false; } args.rval().setBoolean(found); return true; } bool SetObject::has(JSContext* cx, const Value& key, bool* rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } *rval = Table(this).has(k); return true; } bool SetObject::has(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "has"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::add_impl(JSContext* cx, const CallArgs& args) { auto* setObj = &args.thisv().toObject().as(); if (!setObj->add(cx, args.get(0))) { return false; } args.rval().set(args.thisv()); return true; } bool SetObject::add(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "add"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::delete_(JSContext* cx, const Value& key, bool* rval) { HashableValue k; if (!k.setValue(cx, key)) { return false; } *rval = Table(this).remove(cx, k); return true; } bool SetObject::delete_impl(JSContext* cx, const CallArgs& args) { auto* setObj = &args.thisv().toObject().as(); bool found; if (!setObj->delete_(cx, args.get(0), &found)) { return false; } args.rval().setBoolean(found); return true; } bool SetObject::delete_(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "delete"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } bool SetObject::iterator(JSContext* cx, IteratorKind kind, Handle obj, MutableHandleValue iter) { JSObject* iterobj = SetIteratorObject::create(cx, obj, kind); if (!iterobj) { return false; } iter.setObject(*iterobj); return true; } bool SetObject::iterator_impl(JSContext* cx, const CallArgs& args, IteratorKind kind) { Rooted setObj(cx, &args.thisv().toObject().as()); return iterator(cx, kind, setObj, args.rval()); } bool SetObject::values_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, IteratorKind::Values); } bool SetObject::values(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "values"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, values_impl, args); } bool SetObject::entries_impl(JSContext* cx, const CallArgs& args) { return iterator_impl(cx, args, IteratorKind::Entries); } bool SetObject::entries(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "entries"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, entries_impl, args); } void SetObject::clear(JSContext* cx) { Table(this).clear(cx); } bool SetObject::clear_impl(JSContext* cx, const CallArgs& args) { auto* setObj = &args.thisv().toObject().as(); setObj->clear(cx); args.rval().setUndefined(); return true; } bool SetObject::clear(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "clear"); CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, is, clear_impl, args); } bool SetObject::copy(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 1); MOZ_ASSERT(SetObject::is(args[0])); auto* result = SetObject::create(cx); if (!result) { return false; } auto* from = &args[0].toObject().as(); auto addToResult = [cx, result](auto& entry) { return result->addHashableValue(cx, entry); }; if (!Table(from).forEachEntry(addToResult)) { return false; } args.rval().setObject(*result); return true; } /*** JS static utility functions ********************************************/ static bool forEach(const char* funcName, JSContext* cx, HandleObject obj, HandleValue callbackFn, HandleValue thisArg) { CHECK_THREAD(cx); RootedId forEachId(cx, NameToId(cx->names().forEach)); RootedFunction forEachFunc( cx, JS::GetSelfHostedFunction(cx, funcName, forEachId, 2)); if (!forEachFunc) { return false; } RootedValue fval(cx, ObjectValue(*forEachFunc)); return Call(cx, fval, obj, callbackFn, thisArg, &fval); } // RAII class that unwraps a wrapped Map or Set object and then enters its // realm. template class MOZ_RAII AutoEnterTableRealm { mozilla::Maybe ar_; Rooted unwrapped_; public: AutoEnterTableRealm(JSContext* cx, JSObject* obj) : unwrapped_(cx) { JSObject* unwrapped = UncheckedUnwrap(obj); MOZ_ASSERT(unwrapped != obj); MOZ_RELEASE_ASSERT(unwrapped->is()); unwrapped_ = &unwrapped->as(); ar_.emplace(cx, unwrapped_); } Handle unwrapped() const { return unwrapped_; } }; /*** JS public APIs *********************************************************/ JS_PUBLIC_API JSObject* JS::NewMapObject(JSContext* cx) { return MapObject::create(cx); } JS_PUBLIC_API uint32_t JS::MapSize(JSContext* cx, HandleObject obj) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { return obj.as()->size(); } AutoEnterTableRealm enter(cx, obj); return enter.unwrapped()->size(); } JS_PUBLIC_API bool JS::MapGet(JSContext* cx, HandleObject obj, HandleValue key, MutableHandleValue rval) { CHECK_THREAD(cx); cx->check(obj, key, rval); if (obj->is()) { return obj.as()->get(cx, key, rval); } { AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } if (!enter.unwrapped()->get(cx, wrappedKey, rval)) { return false; } } return JS_WrapValue(cx, rval); } JS_PUBLIC_API bool JS::MapSet(JSContext* cx, HandleObject obj, HandleValue key, HandleValue val) { CHECK_THREAD(cx); cx->check(obj, key, val); if (obj->is()) { return obj.as()->set(cx, key, val); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); Rooted wrappedValue(cx, val); if (!JS_WrapValue(cx, &wrappedKey) || !JS_WrapValue(cx, &wrappedValue)) { return false; } return enter.unwrapped()->set(cx, wrappedKey, wrappedValue); } JS_PUBLIC_API bool JS::MapHas(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { CHECK_THREAD(cx); cx->check(obj, key); if (obj->is()) { return obj.as()->has(cx, key, rval); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } return enter.unwrapped()->has(cx, wrappedKey, rval); } JS_PUBLIC_API bool JS::MapGetOrInsert(JSContext* cx, HandleObject obj, HandleValue key, HandleValue val, MutableHandleValue rval) { CHECK_THREAD(cx); cx->check(obj, key, val); if (obj->is()) { return obj.as()->getOrInsert(cx, key, val, rval); } { AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); Rooted wrappedValue(cx, val); if (!JS_WrapValue(cx, &wrappedKey) || !JS_WrapValue(cx, &wrappedValue)) { return false; } if (!enter.unwrapped()->getOrInsert(cx, wrappedKey, wrappedValue, rval)) { return false; } } return JS_WrapValue(cx, rval); } JS_PUBLIC_API bool JS::MapDelete(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { CHECK_THREAD(cx); cx->check(obj, key); if (obj->is()) { return obj.as()->delete_(cx, key, rval); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } return enter.unwrapped()->delete_(cx, wrappedKey, rval); } JS_PUBLIC_API bool JS::MapClear(JSContext* cx, HandleObject obj) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { obj.as()->clear(cx); return true; } AutoEnterTableRealm enter(cx, obj); enter.unwrapped()->clear(cx); return true; } template [[nodiscard]] static bool CreateIterator( JSContext* cx, typename TableObject::IteratorKind kind, Handle obj, MutableHandle rval) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { return TableObject::iterator(cx, kind, obj.as(), rval); } { AutoEnterTableRealm enter(cx, obj); if (!TableObject::iterator(cx, kind, enter.unwrapped(), rval)) { return false; } } return JS_WrapValue(cx, rval); } JS_PUBLIC_API bool JS::MapKeys(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CreateIterator(cx, MapObject::IteratorKind::Keys, obj, rval); } JS_PUBLIC_API bool JS::MapValues(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CreateIterator(cx, MapObject::IteratorKind::Values, obj, rval); } JS_PUBLIC_API bool JS::MapEntries(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CreateIterator(cx, MapObject::IteratorKind::Entries, obj, rval); } JS_PUBLIC_API bool JS::MapForEach(JSContext* cx, HandleObject obj, HandleValue callbackFn, HandleValue thisVal) { return forEach("MapForEach", cx, obj, callbackFn, thisVal); } JS_PUBLIC_API JSObject* JS::NewSetObject(JSContext* cx) { return SetObject::create(cx); } JS_PUBLIC_API uint32_t JS::SetSize(JSContext* cx, HandleObject obj) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { return obj.as()->size(); } AutoEnterTableRealm enter(cx, obj); return enter.unwrapped()->size(); } JS_PUBLIC_API bool JS::SetAdd(JSContext* cx, HandleObject obj, HandleValue key) { CHECK_THREAD(cx); cx->check(obj, key); if (obj->is()) { return obj.as()->add(cx, key); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } return enter.unwrapped()->add(cx, wrappedKey); } JS_PUBLIC_API bool JS::SetHas(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { CHECK_THREAD(cx); cx->check(obj, key); if (obj->is()) { return obj.as()->has(cx, key, rval); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } return enter.unwrapped()->has(cx, wrappedKey, rval); } JS_PUBLIC_API bool JS::SetDelete(JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { CHECK_THREAD(cx); cx->check(obj, key); if (obj->is()) { return obj.as()->delete_(cx, key, rval); } AutoEnterTableRealm enter(cx, obj); Rooted wrappedKey(cx, key); if (!JS_WrapValue(cx, &wrappedKey)) { return false; } return enter.unwrapped()->delete_(cx, wrappedKey, rval); } JS_PUBLIC_API bool JS::SetClear(JSContext* cx, HandleObject obj) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { obj.as()->clear(cx); return true; } AutoEnterTableRealm enter(cx, obj); enter.unwrapped()->clear(cx); return true; } JS_PUBLIC_API bool JS::SetKeys(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return SetValues(cx, obj, rval); } JS_PUBLIC_API bool JS::SetValues(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CreateIterator(cx, SetObject::IteratorKind::Values, obj, rval); } JS_PUBLIC_API bool JS::SetEntries(JSContext* cx, HandleObject obj, MutableHandleValue rval) { return CreateIterator(cx, SetObject::IteratorKind::Entries, obj, rval); } JS_PUBLIC_API bool JS::SetForEach(JSContext* cx, HandleObject obj, HandleValue callbackFn, HandleValue thisVal) { return forEach("SetForEach", cx, obj, callbackFn, thisVal); } JS_PUBLIC_API bool js::GetSetObjectKeys( JSContext* cx, JS::HandleObject obj, JS::MutableHandle> keys) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { return obj->as().keys(keys); } { AutoEnterTableRealm enter(cx, obj); if (!enter.unwrapped()->keys(keys)) { return false; } } for (uint32_t i = 0; i < keys.length(); i++) { if (!JS_WrapValue(cx, keys[i])) { return false; } } return true; } JS_PUBLIC_API bool js::GetMapObjectKeysAndValuesInterleaved( JSContext* cx, JS::HandleObject obj, JS::MutableHandle> entries) { CHECK_THREAD(cx); cx->check(obj); if (obj->is()) { return obj->as().getKeysAndValuesInterleaved(entries); } { AutoEnterTableRealm enter(cx, obj); if (!enter.unwrapped()->getKeysAndValuesInterleaved(entries)) { return false; } } for (uint32_t i = 0; i < entries.length(); i++) { if (!JS_WrapValue(cx, entries[i])) { return false; } } return true; }