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;
}