bool Bun__deepEquals()

in src/bun.js/bindings/bindings.cpp [652:1264]


bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack)
{
    VM& vm = globalObject->vm();

    // need to check this before primitives, asymmetric matchers
    // can match against any type of value.
    if constexpr (enableAsymmetricMatchers) {
        if (v2.isCell() && !v2.isEmpty() && v2.asCell()->type() == JSC::JSType(JSDOMWrapperType)) {
            switch (matchAsymmetricMatcher(globalObject, v2, v1, scope)) {
            case AsymmetricMatcherResult::FAIL:
                return false;
            case AsymmetricMatcherResult::PASS:
                return true;
            case AsymmetricMatcherResult::NOT_MATCHER:
                // continue comparison
                break;
            }
        } else if (v1.isCell() && !v1.isEmpty() && v1.asCell()->type() == JSC::JSType(JSDOMWrapperType)) {
            switch (matchAsymmetricMatcher(globalObject, v1, v2, scope)) {
            case AsymmetricMatcherResult::FAIL:
                return false;
            case AsymmetricMatcherResult::PASS:
                return true;
            case AsymmetricMatcherResult::NOT_MATCHER:
                // continue comparison
                break;
            }
        }
    }

    if (!v1.isEmpty() && !v2.isEmpty() && JSC::sameValue(globalObject, v1, v2)) {
        return true;
    }

    if (v1.isEmpty() || v2.isEmpty())
        return v1.isEmpty() == v2.isEmpty();

    if (v1.isPrimitive() || v2.isPrimitive())
        return false;

    RELEASE_ASSERT(v1.isCell());
    RELEASE_ASSERT(v2.isCell());

    const size_t length = stack.size();
    const auto originalGCBufferSize = gcBuffer.size();
    for (size_t i = 0; i < length; i++) {
        auto values = stack.at(i);
        if (JSC::JSValue::strictEqual(globalObject, values.first, v1)) {
            return JSC::JSValue::strictEqual(globalObject, values.second, v2);
        } else if (JSC::JSValue::strictEqual(globalObject, values.second, v2))
            return false;
    }

    if (addToStack) {
        gcBuffer.append(v1);
        gcBuffer.append(v2);
        stack.append({ v1, v2 });
    }
    auto removeFromStack = WTF::makeScopeExit([&] {
        if (addToStack) {
            stack.remove(length);
            while (gcBuffer.size() > originalGCBufferSize)
                gcBuffer.removeLast();
        }
    });

    JSCell* c1 = v1.asCell();
    JSCell* c2 = v2.asCell();
    JSObject* o1 = v1.getObject();
    JSObject* o2 = v2.getObject();

    // We use additional values outside the enum
    // so the warning here is unnecessary
    uint8_t c1Type = c1->type();
    uint8_t c2Type = c2->type();

    switch (c1Type) {
    case JSSetType: {
        if (c2Type != JSSetType) {
            return false;
        }

        JSSet* set1 = jsCast<JSSet*>(c1);
        JSSet* set2 = jsCast<JSSet*>(c2);

        if (set1->size() != set2->size()) {
            return false;
        }

        auto iter1 = JSSetIterator::create(globalObject, set1->structure(), set1, IterationKind::Keys);
        JSValue key1;
        while (iter1->next(globalObject, key1)) {
            if (set2->has(globalObject, key1)) {
                continue;
            }

            // We couldn't find the key in the second set. This may be a false positive due to how
            // JSValues are represented in JSC, so we need to fall back to a linear search to be sure.
            auto iter2 = JSSetIterator::create(globalObject, set2->structure(), set2, IterationKind::Keys);
            JSValue key2;
            bool foundMatchingKey = false;
            while (iter2->next(globalObject, key2)) {
                if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
                    foundMatchingKey = true;
                    break;
                }
            }

            if (!foundMatchingKey) {
                return false;
            }
        }

        return true;
    }
    case JSMapType: {
        if (c2Type != JSMapType) {
            return false;
        }

        JSMap* map1 = jsCast<JSMap*>(c1);
        JSMap* map2 = jsCast<JSMap*>(c2);
        size_t leftSize = map1->size();

        if (leftSize != map2->size()) {
            return false;
        }

        auto iter1 = JSMapIterator::create(globalObject, map1->structure(), map1, IterationKind::Entries);
        JSValue key1, value1;
        while (iter1->nextKeyValue(globalObject, key1, value1)) {
            JSValue value2 = map2->get(globalObject, key1);
            if (value2.isUndefined()) {
                // We couldn't find the key in the second map. This may be a false positive due to
                // how JSValues are represented in JSC, so we need to fall back to a linear search
                // to be sure.
                auto iter2 = JSMapIterator::create(globalObject, map2->structure(), map2, IterationKind::Entries);
                JSValue key2;
                bool foundMatchingKey = false;
                while (iter2->nextKeyValue(globalObject, key2, value2)) {
                    if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
                        foundMatchingKey = true;
                        break;
                    }
                }

                if (!foundMatchingKey) {
                    return false;
                }

                // Compare both values below.
            }

            if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, value1, value2, gcBuffer, stack, scope, false)) {
                return false;
            }
        }

        return true;
    }
    case ArrayBufferType: {
        if (c2Type != ArrayBufferType) {
            return false;
        }

        JSC::ArrayBuffer* left = jsCast<JSArrayBuffer*>(v1)->impl();
        JSC::ArrayBuffer* right = jsCast<JSArrayBuffer*>(v2)->impl();
        size_t byteLength = left->byteLength();

        if (right->byteLength() != byteLength) {
            return false;
        }

        if (byteLength == 0)
            return true;

        if (UNLIKELY(right->isDetached() || left->isDetached())) {
            return false;
        }

        const void* vector = left->data();
        const void* rightVector = right->data();
        if (UNLIKELY(!vector || !rightVector)) {
            return false;
        }

        if (UNLIKELY(vector == rightVector))
            return true;

        return (memcmp(vector, rightVector, byteLength) == 0);
    }
    case JSDateType: {
        if (c2Type != JSDateType) {
            return false;
        }

        JSC::DateInstance* left = jsCast<DateInstance*>(v1);
        JSC::DateInstance* right = jsCast<DateInstance*>(v2);

        return left->internalNumber() == right->internalNumber();
    }
    case RegExpObjectType: {
        if (c2Type != RegExpObjectType) {
            return false;
        }

        if (JSC::RegExpObject* left = jsDynamicCast<JSC::RegExpObject*>(v1)) {
            JSC::RegExpObject* right = jsDynamicCast<JSC::RegExpObject*>(v2);

            if (UNLIKELY(!right)) {
                return false;
            }

            return left->regExp()->key() == right->regExp()->key();
        }

        return false;
    }
    case ErrorInstanceType: {
        if (c2Type != ErrorInstanceType) {
            return false;
        }

        if (JSC::ErrorInstance* left = jsDynamicCast<JSC::ErrorInstance*>(v1)) {
            JSC::ErrorInstance* right = jsDynamicCast<JSC::ErrorInstance*>(v2);

            if (UNLIKELY(!right)) {
                return false;
            }

            return (
                left->sanitizedNameString(globalObject) == right->sanitizedNameString(globalObject) && left->sanitizedMessageString(globalObject) == right->sanitizedMessageString(globalObject));
        }
    }
    case Int8ArrayType:
    case Uint8ArrayType:
    case Uint8ClampedArrayType:
    case Int16ArrayType:
    case Uint16ArrayType:
    case Int32ArrayType:
    case Uint32ArrayType:
    case Float16ArrayType:
    case Float32ArrayType:
    case Float64ArrayType:
    case BigInt64ArrayType:
    case BigUint64ArrayType: {
        if (!isTypedArrayType(static_cast<JSC::JSType>(c2Type)) || c1Type != c2Type) {
            return false;
        }

        JSC::JSArrayBufferView* left = jsCast<JSArrayBufferView*>(v1);
        JSC::JSArrayBufferView* right = jsCast<JSArrayBufferView*>(v2);
        size_t byteLength = left->byteLength();

        if (right->byteLength() != byteLength) {
            return false;
        }

        if (byteLength == 0)
            return true;

        if (UNLIKELY(right->isDetached() || left->isDetached())) {
            return false;
        }

        const void* vector = left->vector();
        const void* rightVector = right->vector();
        if (UNLIKELY(!vector || !rightVector)) {
            return false;
        }

        if (UNLIKELY(vector == rightVector))
            return true;

        return (memcmp(vector, rightVector, byteLength) == 0);
    }
    case StringObjectType: {
        if (c2Type != StringObjectType) {
            return false;
        }

        if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
            return false;
        }

        JSString* s1 = c1->toStringInline(globalObject);
        JSString* s2 = c2->toStringInline(globalObject);

        return s1->equal(globalObject, s2);
    }
    case JSFunctionType: {
        return false;
    }

    case JSDOMWrapperType: {
        if (c2Type == JSDOMWrapperType) {
            // https://github.com/oven-sh/bun/issues/4089
            // https://github.com/oven-sh/bun/issues/6492
            auto* url2 = jsDynamicCast<JSDOMURL*>(v2);
            auto* url1 = jsDynamicCast<JSDOMURL*>(v1);

            if constexpr (isStrict) {
                // if one is a URL and the other is not a URL, toStrictEqual returns false.
                if ((url2 == nullptr) != (url1 == nullptr)) {
                    return false;
                }
            }

            if (url2 && url1) {
                // toEqual or toStrictEqual should return false when the URLs' href is not equal
                // But you could have added additional properties onto the
                // url object itself, so we must check those as well
                // But it's definitely not equal if the href() is not the same
                if (url1->wrapped().href() != url2->wrapped().href()) {
                    return false;
                }
            }
        }
        break;
    }

    default: {
        break;
    }
    }

    bool v1Array = isArray(globalObject, v1);
    RETURN_IF_EXCEPTION(*scope, false);
    bool v2Array = isArray(globalObject, v2);
    RETURN_IF_EXCEPTION(*scope, false);

    if (v1Array != v2Array)
        return false;

    if (v1Array && v2Array) {
        JSC::JSArray* array1 = JSC::jsCast<JSC::JSArray*>(v1);
        JSC::JSArray* array2 = JSC::jsCast<JSC::JSArray*>(v2);

        size_t array1Length = array1->length();
        size_t array2Length = array2->length();
        if constexpr (isStrict) {
            if (array1Length != array2Length) {
                return false;
            }
        }

        uint64_t i = 0;
        for (; i < array1Length; i++) {
            JSValue left = getIndexWithoutAccessors(globalObject, o1, i);
            RETURN_IF_EXCEPTION(*scope, false);
            JSValue right = getIndexWithoutAccessors(globalObject, o2, i);
            RETURN_IF_EXCEPTION(*scope, false);

            if constexpr (isStrict) {
                if (left.isEmpty() && right.isEmpty()) {
                    continue;
                }
                if (left.isEmpty() || right.isEmpty()) {
                    return false;
                }
            }

            if constexpr (!isStrict) {
                if (((left.isEmpty() || right.isEmpty()) && (left.isUndefined() || right.isUndefined()))) {
                    continue;
                }
            }

            if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
                return false;
            }

            RETURN_IF_EXCEPTION(*scope, false);
        }

        for (; i < array2Length; i++) {
            JSValue right = getIndexWithoutAccessors(globalObject, o2, i);
            RETURN_IF_EXCEPTION(*scope, false);

            if (((right.isEmpty() || right.isUndefined()))) {
                continue;
            }

            return false;
        }

        JSC::PropertyNameArray a1(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Exclude);
        JSC::PropertyNameArray a2(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Exclude);
        JSObject::getOwnPropertyNames(o1, globalObject, a1, DontEnumPropertiesMode::Exclude);
        JSObject::getOwnPropertyNames(o2, globalObject, a2, DontEnumPropertiesMode::Exclude);

        size_t propertyLength = a1.size();
        if constexpr (isStrict) {
            if (propertyLength != a2.size()) {
                return false;
            }
        }

        // take a property name from one, try to get it from both
        for (size_t i = 0; i < propertyLength; i++) {
            Identifier i1 = a1[i];
            PropertyName propertyName1 = PropertyName(i1);

            JSValue prop1 = o1->get(globalObject, propertyName1);
            RETURN_IF_EXCEPTION(*scope, false);

            if (UNLIKELY(!prop1)) {
                return false;
            }

            JSValue prop2 = o2->getIfPropertyExists(globalObject, propertyName1);
            RETURN_IF_EXCEPTION(*scope, false);

            if constexpr (!isStrict) {
                if (prop1.isUndefined() && prop2.isEmpty()) {
                    continue;
                }
            }

            if (!prop2) {
                return false;
            }

            if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
                return false;
            }

            RETURN_IF_EXCEPTION(*scope, false);
        }

        RETURN_IF_EXCEPTION(*scope, false);

        return true;
    }

    if constexpr (isStrict) {
        if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
            return false;
        }
    }

    JSC::Structure* o1Structure = o1->structure();
    if (!o1Structure->hasNonReifiedStaticProperties() && o1Structure->canPerformFastPropertyEnumeration()) {
        JSC::Structure* o2Structure = o2->structure();
        if (!o2Structure->hasNonReifiedStaticProperties() && o2Structure->canPerformFastPropertyEnumeration()) {

            bool result = true;
            bool sameStructure = o2Structure->id() == o1Structure->id();
            if (sameStructure) {
                o1Structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
                    if (entry.attributes() & PropertyAttribute::DontEnum || PropertyName(entry.key()).isPrivateName()) {
                        return true;
                    }

                    JSValue left = o1->getDirect(entry.offset());
                    JSValue right = o2->getDirect(entry.offset());

                    if constexpr (!isStrict) {
                        if (left.isUndefined() && right.isEmpty()) {
                            return true;
                        }
                    }

                    if (!right) {
                        result = false;
                        return false;
                    }

                    if (left == right || JSC::sameValue(globalObject, left, right)) {
                        return true;
                    }

                    if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
                        result = false;
                        return false;
                    }

                    return true;
                });
            } else {
                size_t count = 0;
                o1Structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
                    if (entry.attributes() & PropertyAttribute::DontEnum || PropertyName(entry.key()).isPrivateName()) {
                        return true;
                    }
                    count++;

                    JSValue left = o1->getDirect(entry.offset());
                    JSValue right = o2->getDirect(vm, JSC::PropertyName(entry.key()));

                    if constexpr (!isStrict) {
                        if (left.isUndefined() && right.isEmpty()) {
                            return true;
                        }
                    }

                    if (!right) {
                        result = false;
                        return false;
                    }

                    if (left == right || JSC::sameValue(globalObject, left, right)) {
                        return true;
                    }

                    if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
                        result = false;
                        return false;
                    }

                    return true;
                });

                if (result) {
                    size_t remain = count;
                    o2Structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
                        if (entry.attributes() & PropertyAttribute::DontEnum || PropertyName(entry.key()).isPrivateName()) {
                            return true;
                        }

                        if constexpr (!isStrict) {
                            if (o2->getDirect(entry.offset()).isUndefined()) {
                                return true;
                            }
                        }

                        // Try to get the right value from the left. We don't need to check if they're equal
                        // because the above loop has already iterated each property in the left. If we've
                        // seen this property before, it was already `deepEquals`ed. If it doesn't exist,
                        // the objects are not equal.
                        if (o1->getDirectOffset(vm, JSC::PropertyName(entry.key())) == invalidOffset) {
                            result = false;
                            return false;
                        }

                        if (remain == 0) {
                            result = false;
                            return false;
                        }

                        remain--;
                        return true;
                    });
                }
            }

            return result;
        }
    }

    JSC::PropertyNameArray a1(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
    JSC::PropertyNameArray a2(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
    o1->getPropertyNames(globalObject, a1, DontEnumPropertiesMode::Exclude);
    RETURN_IF_EXCEPTION(*scope, false);
    o2->getPropertyNames(globalObject, a2, DontEnumPropertiesMode::Exclude);
    RETURN_IF_EXCEPTION(*scope, false);

    const size_t propertyArrayLength1 = a1.size();
    const size_t propertyArrayLength2 = a2.size();
    if constexpr (isStrict) {
        if (propertyArrayLength1 != propertyArrayLength2) {
            return false;
        }
    }

    // take a property name from one, try to get it from both
    size_t i;
    for (i = 0; i < propertyArrayLength1; i++) {
        Identifier i1 = a1[i];
        PropertyName propertyName1 = PropertyName(i1);

        JSValue prop1 = o1->get(globalObject, propertyName1);
        RETURN_IF_EXCEPTION(*scope, false);

        if (UNLIKELY(!prop1)) {
            return false;
        }

        JSValue prop2 = o2->getIfPropertyExists(globalObject, propertyName1);
        RETURN_IF_EXCEPTION(*scope, false);

        if constexpr (!isStrict) {
            if (prop1.isUndefined() && prop2.isEmpty()) {
                continue;
            }
        }

        if (!prop2) {
            return false;
        }

        if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
            return false;
        }

        RETURN_IF_EXCEPTION(*scope, false);
    }

    // for the remaining properties in the other object, make sure they are undefined
    for (; i < propertyArrayLength2; i++) {
        Identifier i2 = a2[i];
        PropertyName propertyName2 = PropertyName(i2);

        JSValue prop2 = o2->getIfPropertyExists(globalObject, propertyName2);
        RETURN_IF_EXCEPTION(*scope, false);

        if (!prop2.isUndefined()) {
            return false;
        }
    }

    return true;
}