Compare commits

...

15 Commits

Author SHA1 Message Date
pfg
91225b53c1 fix string case 2025-05-16 15:09:24 -07:00
pfg
d533b602e5 still check properties when passing specialObjectsDequal only for node util isDeepStrictEqual 2025-05-16 15:07:22 -07:00
pfg
43f780618d WIP 2025-05-13 13:22:47 -07:00
pfg
3fb9ff5b53 last one 2025-05-12 19:51:07 -07:00
pfg
d2011e4f98 remove comment 2025-05-12 19:37:58 -07:00
pfg
344264dbdc one TODO remains 2025-05-12 19:36:23 -07:00
pfg
5e864d6e47 WIPWIP 2025-05-12 18:57:46 -07:00
pfg
5f05a8ba74 WIP 2025-05-12 18:01:40 -07:00
pfg
5354232e77 String() fix 2025-05-12 17:31:22 -07:00
pfg
ebbb6b63fa webkit version with SymbolObject.h 2025-05-12 17:04:01 -07:00
pfg
94afa51dde Merge branch 'main' into pfg/util-isDeepStrictEqual 2025-05-12 16:53:08 -07:00
pfg
ecbb0384bc . 2025-05-09 19:59:14 -07:00
pfg
16518f54a2 remove symbol waiting on webkit build 2025-05-09 19:56:00 -07:00
pfg
1429f68c99 WIP 2025-05-09 19:48:35 -07:00
pfg
3d59e35db2 TODO 2025-05-09 17:43:00 -07:00
10 changed files with 448 additions and 66 deletions

View File

@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 017930ebf915121f8f593bef61cbbca82d78132d)
set(WEBKIT_VERSION eda8b0fb4fb1aa23db9c2b00933df8b58bcdd289)
endif()
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)

6
repro.js Normal file
View File

@@ -0,0 +1,6 @@
const sym = Symbol();
const obj1 = { [sym]: 1 };
const obj4 = {};
Object.defineProperty(obj4, sym, { value: 1 }); // non-enumerable
console.log(Bun.deepEquals(obj1, obj4)); // should be 'false'

View File

@@ -512,16 +512,40 @@ JSC_DEFINE_HOST_FUNCTION(functionBunDeepEquals, (JSGlobalObject * globalObject,
if (strict.isBoolean() && strict.asBoolean()) {
bool isEqual = Bun__deepEquals<true, false>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
bool isEqual = Bun__deepEquals<true, BunDeepEqualsMode_Bun>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(isEqual));
} else {
bool isEqual = Bun__deepEquals<false, false>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
bool isEqual = Bun__deepEquals<false, BunDeepEqualsMode_Bun>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(isEqual));
}
}
JSC_DEFINE_HOST_FUNCTION(jsFunction_nodeIsDeepStrictEqual, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* global = reinterpret_cast<GlobalObject*>(globalObject);
auto& vm = JSC::getVM(global);
auto scope = DECLARE_THROW_SCOPE(vm);
if (callFrame->argumentCount() < 2) {
auto throwScope = DECLARE_THROW_SCOPE(vm);
throwTypeError(globalObject, throwScope, "Expected 2 values to compare"_s);
return {};
}
JSC::JSValue arg1 = callFrame->uncheckedArgument(0);
JSC::JSValue arg2 = callFrame->uncheckedArgument(1);
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
bool isEqual = Bun__deepEquals<true, BunDeepEqualsMode_Node>(globalObject, arg1, arg2, gcBuffer, stack, &scope, true);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(isEqual));
}
JSC_DEFINE_HOST_FUNCTION(functionBunDeepMatch, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* global = reinterpret_cast<GlobalObject*>(globalObject);
@@ -547,7 +571,7 @@ JSC_DEFINE_HOST_FUNCTION(functionBunDeepMatch, (JSGlobalObject * globalObject, J
std::set<EncodedJSValue> objVisited;
std::set<EncodedJSValue> subsetVisited;
MarkedArgumentBuffer gcBuffer;
bool match = Bun__deepMatch</* enableAsymmetricMatchers */ false>(object, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, false, false);
bool match = Bun__deepMatch<BunDeepEqualsMode_Bun>(object, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, false, false);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(match));
@@ -898,7 +922,6 @@ static void exportBunObject(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC:
exportValues.append(value);
}
}
}
namespace Zig {

View File

@@ -11,6 +11,7 @@ JSC_DECLARE_HOST_FUNCTION(functionBunDeepMatch);
JSC_DECLARE_HOST_FUNCTION(functionBunNanoseconds);
JSC_DECLARE_HOST_FUNCTION(functionPathToFileURL);
JSC_DECLARE_HOST_FUNCTION(functionFileURLToPath);
JSC_DECLARE_HOST_FUNCTION(jsFunction_nodeIsDeepStrictEqual);
JSC::JSValue constructBunFetchObject(VM& vm, JSObject* bunObject);
JSC::JSObject* createBunObject(VM& vm, JSObject* globalObject);

View File

@@ -10,6 +10,8 @@
#include "JavaScriptCore/JSCast.h"
#include "JavaScriptCore/JSType.h"
#include "JavaScriptCore/NumberObject.h"
#include "JavaScriptCore/BigIntObject.h"
#include "JavaScriptCore/SymbolObject.h"
#include "JavaScriptCore/JSCJSValue.h"
#include "JavaScriptCore/JSGlobalObject.h"
#include "JavaScriptCore/JSPromiseConstructor.h"
@@ -456,7 +458,7 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
if (Bun__deepEquals<false, true>(globalObject, expectedValue, otherValue, gcBuffer, stack, &scope, true)) {
if (Bun__deepEquals<false, BunDeepEqualsMode_Jest>(globalObject, expectedValue, otherValue, gcBuffer, stack, &scope, true)) {
found = true;
break;
}
@@ -481,9 +483,9 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global
if (otherProp.isObject()) {
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
// SAFETY: visited property sets are not required when
// `enableAsymmetricMatchers` and `isMatchingObjectContaining`
// `mode == BunDeepEqualsMode_Jest` and `isMatchingObjectContaining`
// are both true
if (Bun__deepMatch<true>(otherProp, nullptr, patternObject, nullptr, globalObject, &scope, nullptr, false, true)) {
if (Bun__deepMatch<BunDeepEqualsMode_Jest>(otherProp, nullptr, patternObject, nullptr, globalObject, &scope, nullptr, false, true)) {
return AsymmetricMatcherResult::PASS;
}
}
@@ -607,10 +609,10 @@ JSValue getIndexWithoutAccessors(JSGlobalObject* globalObject, JSObject* obj, ui
return JSValue();
}
template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, BunDeepEqualsMode mode>
std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2);
template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, BunDeepEqualsMode mode>
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();
@@ -621,7 +623,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
// need to check this before primitives, asymmetric matchers
// can match against any type of value.
if constexpr (enableAsymmetricMatchers) {
if constexpr (mode == BunDeepEqualsMode_Jest) {
if (v2.isCell() && !v2.isEmpty() && v2.asCell()->type() == JSC::JSType(JSDOMWrapperType)) {
switch (matchAsymmetricMatcher(globalObject, v2, v1, scope)) {
case AsymmetricMatcherResult::FAIL:
@@ -685,9 +687,9 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
JSCell* c2 = v2.asCell();
ASSERT(c1);
ASSERT(c2);
std::optional<bool> isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers>(globalObject, gcBuffer, stack, scope, c1, c2);
std::optional<bool> isSpecialEqual = specialObjectsDequal<isStrict, mode>(globalObject, gcBuffer, stack, scope, c1, c2);
if (isSpecialEqual.has_value()) return std::move(*isSpecialEqual);
isSpecialEqual = specialObjectsDequal<isStrict, enableAsymmetricMatchers>(globalObject, gcBuffer, stack, scope, c2, c1);
isSpecialEqual = specialObjectsDequal<isStrict, mode>(globalObject, gcBuffer, stack, scope, c2, c1);
if (isSpecialEqual.has_value()) return std::move(*isSpecialEqual);
JSObject* o1 = v1.getObject();
JSObject* o2 = v2.getObject();
@@ -734,7 +736,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
}
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, left, right, gcBuffer, stack, scope, true)) {
return false;
}
@@ -789,7 +791,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
return false;
}
@@ -801,6 +803,22 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
// Handle boxed primitives
auto v1IsBoxedPrimitive = v1.inherits<NumberObject>() || v1.inherits<StringObject>() || v1.inherits<BooleanObject>() || v1.inherits<BigIntObject>() || v1.inherits<SymbolObject>();
auto v2IsBoxedPrimitive = v2.inherits<NumberObject>() || v2.inherits<StringObject>() || v2.inherits<BooleanObject>() || v2.inherits<BigIntObject>() || v2.inherits<SymbolObject>();
if (v1IsBoxedPrimitive != v2IsBoxedPrimitive) {
return false; // one is a boxed primitive, the other is not
}
if (v1IsBoxedPrimitive && v2IsBoxedPrimitive) {
auto v1Wrapper = jsCast<JSC::JSWrapperObject*>(v1);
auto v2Wrapper = jsCast<JSC::JSWrapperObject*>(v2);
auto v1Value = v1Wrapper->internalValue();
auto v2Value = v2Wrapper->internalValue();
if (!Bun__deepEquals<isStrict, mode>(globalObject, v1Value, v2Value, gcBuffer, stack, scope, addToStack)) {
return false;
}
}
if constexpr (isStrict) {
if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
return false;
@@ -838,7 +856,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, left, right, gcBuffer, stack, scope, true)) {
result = false;
return false;
}
@@ -854,7 +872,12 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
count++;
JSValue left = o1->getDirect(entry.offset());
JSValue right = o2->getDirect(vm, JSC::PropertyName(entry.key()));
JSValue right;
if (o2->hasEnumerableProperty(globalObject, JSC::PropertyName(entry.key()))) {
right = o2->getDirect(vm, JSC::PropertyName(entry.key()));
} else {
right = JSValue();
}
if constexpr (!isStrict) {
if (left.isUndefined() && right.isEmpty()) {
@@ -871,7 +894,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, left, right, gcBuffer, stack, scope, true)) {
result = false;
return false;
}
@@ -957,7 +980,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
return false;
}
@@ -980,11 +1003,18 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
template<bool isStrict, bool enableAsymmetricMatchers>
template bool Bun__deepEquals<false, BunDeepEqualsMode_Node>(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack);
template bool Bun__deepEquals<true, BunDeepEqualsMode_Node>(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack);
template<bool isStrict, BunDeepEqualsMode mode>
std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, MarkedArgumentBuffer& gcBuffer, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, JSCell* _Nonnull c1, JSCell* _Nonnull c2)
{
uint8_t c1Type = c1->type();
uint8_t c2Type = c2->type();
std::optional<bool> matchMaybeCheckProperties = true;
if constexpr (mode == BunDeepEqualsMode_Node) {
matchMaybeCheckProperties = std::nullopt;
}
switch (c1Type) {
case JSSetType: {
@@ -1012,7 +1042,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
JSValue key2;
bool foundMatchingKey = false;
while (iter2->next(globalObject, key2)) {
if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
if (Bun__deepEquals<isStrict, mode>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
foundMatchingKey = true;
break;
}
@@ -1024,7 +1054,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
}
return true;
return matchMaybeCheckProperties;
}
case JSMapType: {
if (c2Type != JSMapType) {
@@ -1051,7 +1081,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
JSValue key2;
bool foundMatchingKey = false;
while (iter2->nextKeyValue(globalObject, key2, value2)) {
if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
if (Bun__deepEquals<isStrict, mode>(globalObject, key1, key2, gcBuffer, stack, scope, false)) {
foundMatchingKey = true;
break;
}
@@ -1065,12 +1095,12 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
// Compare both values below.
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, value1, value2, gcBuffer, stack, scope, false)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, value1, value2, gcBuffer, stack, scope, false)) {
return false;
}
}
return true;
return matchMaybeCheckProperties;
}
case ArrayBufferType: {
if (c2Type != ArrayBufferType) {
@@ -1090,7 +1120,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
if (byteLength == 0)
return true;
return matchMaybeCheckProperties;
if (UNLIKELY(right->isDetached() || left->isDetached())) {
return false;
@@ -1103,7 +1133,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
if (UNLIKELY(vector == rightVector))
return true;
return matchMaybeCheckProperties;
return (memcmp(vector, rightVector, byteLength) == 0);
}
@@ -1176,7 +1206,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
RETURN_IF_EXCEPTION(*scope, false);
auto rightCause = right->get(globalObject, cause);
RETURN_IF_EXCEPTION(*scope, false);
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, leftCause, rightCause, gcBuffer, stack, scope, true)) {
return false;
}
RETURN_IF_EXCEPTION(*scope, false);
@@ -1226,7 +1256,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}
if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
if (!Bun__deepEquals<isStrict, mode>(globalObject, prop1, prop2, gcBuffer, stack, scope, true)) {
return false;
}
@@ -1281,7 +1311,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
if (byteLength == 0)
return true;
return matchMaybeCheckProperties;
if (UNLIKELY(right->isDetached() || left->isDetached())) {
return false;
@@ -1294,7 +1324,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
if (UNLIKELY(vector == rightVector))
return true;
return matchMaybeCheckProperties;
// For Float32Array and Float64Array, when not in strict mode, we need to
// handle +0 and -0 as equal, and NaN as not equal to itself.
@@ -1309,7 +1339,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}
}
return true;
return matchMaybeCheckProperties;
} else if (c1Type == Float32ArrayType) {
auto* leftFloat = static_cast<const float*>(vector);
auto* rightFloat = static_cast<const float*>(rightVector);
@@ -1320,7 +1350,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}
}
return true;
return matchMaybeCheckProperties;
} else { // Float64Array
auto* leftDouble = static_cast<const double*>(vector);
auto* rightDouble = static_cast<const double*>(rightVector);
@@ -1331,11 +1361,15 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}
}
return true;
return matchMaybeCheckProperties;
}
}
return (memcmp(vector, rightVector, byteLength) == 0);
if (memcmp(vector, rightVector, byteLength) == 0) {
return matchMaybeCheckProperties;
}
return false;
}
case StringObjectType: {
if (c2Type != StringObjectType) {
@@ -1349,7 +1383,10 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
JSString* s1 = c1->toStringInline(globalObject);
JSString* s2 = c2->toStringInline(globalObject);
return s1->equal(globalObject, s2);
if (s1->equal(globalObject, s2)) {
return matchMaybeCheckProperties;
}
return false;
}
case JSFunctionType: {
return false;
@@ -1371,7 +1408,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
} else {
if ((url1 == nullptr) != (url2 == nullptr)) {
goto compareAsNormalValue;
return matchMaybeCheckProperties;
}
}
@@ -1384,7 +1421,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
return false;
}
goto compareAsNormalValue;
return matchMaybeCheckProperties;
}
// TODO: FormData.
@@ -1410,7 +1447,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
}
goto compareAsNormalValue;
return matchMaybeCheckProperties;
} else {
if constexpr (isStrict) {
// if one is a URLSearchParams and the other is not a URLSearchParams, toStrictEqual should return false.
@@ -1419,7 +1456,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
} else {
if ((urlSearchParams1 == nullptr) != (urlSearchParams2 == nullptr)) {
goto compareAsNormalValue;
return matchMaybeCheckProperties;
}
}
}
@@ -1449,7 +1486,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
}
goto compareAsNormalValue;
return matchMaybeCheckProperties;
} else {
if constexpr (isStrict) {
// if one is a FetchHeaders and the other is not a FetchHeaders, toStrictEqual should return false.
@@ -1458,17 +1495,12 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
}
} else {
if ((headers1 == nullptr) != (headers2 == nullptr)) {
goto compareAsNormalValue;
return matchMaybeCheckProperties;
}
}
}
}
}
goto compareAsNormalValue;
compareAsNormalValue:
break;
}
// globalThis is only equal to globalThis
// NOTE: Zig::GlobalObject is tagged as GlobalProxyType
@@ -1497,14 +1529,14 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
* @note
* The sets recording already visited properties (`seenObjProperties`,
* `seenSubsetProperties`, and `gcBuffer`) aren not needed when both
* `enableAsymmetricMatchers` and `isMatchingObjectContaining` are true. In
* `mode == BunDeepEqualsMode_Jest` and `isMatchingObjectContaining` are true. In
* this case, it is safe to pass a `nullptr`.
*
* `gcBuffer` ensures JSC's stack scan does not come up empty-handed and free
* properties currently within those stacks. Likely unnecessary, but better to
* be safe tnan sorry
*
* @tparam enableAsymmetricMatchers
* @tparam mode
* @param objValue
* @param seenObjProperties already visited properties of `objValue`.
* @param subsetValue
@@ -1518,7 +1550,7 @@ std::optional<bool> specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark
* @return true
* @return false
*/
template<bool enableAsymmetricMatchers>
template<BunDeepEqualsMode mode>
bool Bun__deepMatch(
JSValue objValue,
std::set<EncodedJSValue>* seenObjProperties,
@@ -1574,7 +1606,7 @@ bool Bun__deepMatch(
JSCell* subsetPropCell = !subsetProp.isEmpty() && subsetProp.isCell() ? subsetProp.asCell() : nullptr;
JSCell* propCell = prop.isCell() ? prop.asCell() : nullptr;
if constexpr (enableAsymmetricMatchers) {
if constexpr (mode == BunDeepEqualsMode_Jest) {
if (subsetPropCell && subsetPropCell->type() == JSC::JSType(JSDOMWrapperType)) {
switch (matchAsymmetricMatcher(globalObject, subsetProp, prop, throwScope)) {
case AsymmetricMatcherResult::FAIL:
@@ -1608,10 +1640,10 @@ bool Bun__deepMatch(
// if this is called from inside an objectContaining asymmetric matcher, it should behave slightly differently:
// in such case, it expects exhaustive matching of any nested object properties, not just a subset,
// and the user would need to opt-in to subset matching by using another nested objectContaining matcher
if (enableAsymmetricMatchers && isMatchingObjectContaining) {
if (mode == BunDeepEqualsMode_Jest && isMatchingObjectContaining) {
Vector<std::pair<JSValue, JSValue>, 16> stack;
MarkedArgumentBuffer gcBuffer;
if (!Bun__deepEquals<false, true>(globalObject, prop, subsetProp, gcBuffer, stack, throwScope, true)) {
if (!Bun__deepEquals<false, BunDeepEqualsMode_Jest>(globalObject, prop, subsetProp, gcBuffer, stack, throwScope, true)) {
return false;
}
} else {
@@ -1624,7 +1656,7 @@ bool Bun__deepMatch(
gcBuffer->append(subsetProp);
// property cycle detected
if (!didInsertProp.second || !didInsertSubset.second) continue;
if (!Bun__deepMatch<enableAsymmetricMatchers>(prop, seenObjProperties, subsetProp, seenSubsetProperties, globalObject, throwScope, gcBuffer, replacePropsWithAsymmetricMatchers, isMatchingObjectContaining)) {
if (!Bun__deepMatch<mode>(prop, seenObjProperties, subsetProp, seenSubsetProperties, globalObject, throwScope, gcBuffer, replacePropsWithAsymmetricMatchers, isMatchingObjectContaining)) {
return false;
}
}
@@ -1640,14 +1672,14 @@ bool Bun__deepMatch(
// anonymous namespace to avoid name collision
namespace {
template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, BunDeepEqualsMode mode>
inline bool deepEqualsWrapperImpl(JSC::EncodedJSValue a, JSC::EncodedJSValue b, JSC::JSGlobalObject* global)
{
auto& vm = global->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16> stack;
MarkedArgumentBuffer args;
return Bun__deepEquals<isStrict, enableAsymmetricMatchers>(global, JSC::JSValue::decode(a), JSC::JSValue::decode(b), args, stack, &scope, true);
return Bun__deepEquals<isStrict, mode>(global, JSC::JSValue::decode(a), JSC::JSValue::decode(b), args, stack, &scope, true);
}
}
@@ -2555,22 +2587,22 @@ bool JSC__JSValue__isSameValue(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue
bool JSC__JSValue__deepEquals(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject)
{
return deepEqualsWrapperImpl<false, false>(JSValue0, JSValue1, globalObject);
return deepEqualsWrapperImpl<false, BunDeepEqualsMode_Bun>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__jestDeepEquals(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject)
{
return deepEqualsWrapperImpl<false, true>(JSValue0, JSValue1, globalObject);
return deepEqualsWrapperImpl<false, BunDeepEqualsMode_Jest>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__strictDeepEquals(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject)
{
return deepEqualsWrapperImpl<true, false>(JSValue0, JSValue1, globalObject);
return deepEqualsWrapperImpl<true, BunDeepEqualsMode_Bun>(JSValue0, JSValue1, globalObject);
}
bool JSC__JSValue__jestStrictDeepEquals(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject)
{
return deepEqualsWrapperImpl<true, true>(JSValue0, JSValue1, globalObject);
return deepEqualsWrapperImpl<true, BunDeepEqualsMode_Jest>(JSValue0, JSValue1, globalObject);
}
#undef IMPL_DEEP_EQUALS_WRAPPER
@@ -2585,7 +2617,7 @@ bool JSC__JSValue__deepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue J
std::set<EncodedJSValue> objVisited;
std::set<EncodedJSValue> subsetVisited;
MarkedArgumentBuffer gcBuffer;
return Bun__deepMatch<false>(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false);
return Bun__deepMatch<BunDeepEqualsMode_Bun>(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false);
}
bool JSC__JSValue__jestDeepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers)
@@ -2598,7 +2630,7 @@ bool JSC__JSValue__jestDeepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSVal
std::set<EncodedJSValue> objVisited;
std::set<EncodedJSValue> subsetVisited;
MarkedArgumentBuffer gcBuffer;
return Bun__deepMatch<true>(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false);
return Bun__deepMatch<BunDeepEqualsMode_Jest>(obj, &objVisited, subset, &subsetVisited, globalObject, &scope, &gcBuffer, replacePropsWithAsymmetricMatchers, false);
}
extern "C" bool Bun__JSValue__isAsyncContextFrame(JSC::EncodedJSValue value)

View File

@@ -276,6 +276,12 @@ typedef struct StringPointer {
} StringPointer;
#endif
enum BunDeepEqualsMode {
BunDeepEqualsMode_Node,
BunDeepEqualsMode_Jest,
BunDeepEqualsMode_Bun,
};
typedef void WebSocketHTTPClient;
typedef void WebSocketHTTPSClient;
typedef void WebSocketClient;
@@ -403,7 +409,7 @@ extern "C" void Bun__EventLoop__runCallback2(JSC::JSGlobalObject* global, JSC::E
extern "C" void Bun__EventLoop__runCallback3(JSC::JSGlobalObject* global, JSC::EncodedJSValue callback, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue arg1, JSC::EncodedJSValue arg2, JSC::EncodedJSValue arg3);
/// @note throws a JS exception and returns false if a stack overflow occurs
template<bool isStrict, bool enableAsymmetricMatchers>
template<bool isStrict, BunDeepEqualsMode mode>
bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, JSC::MarkedArgumentBuffer&, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, JSC::ThrowScope* scope, bool addToStack);
/**
@@ -438,7 +444,7 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JS
* @return true
* @return false
*/
template<bool enableAsymmetricMatchers>
template<BunDeepEqualsMode enableAsymmetricMatchers>
bool Bun__deepMatch(
JSC::JSValue object,
std::set<JSC::EncodedJSValue>* seenObjProperties,

View File

@@ -21,8 +21,7 @@ function isFunction(value) {
return typeof value === "function";
}
const deepEquals = Bun.deepEquals;
const isDeepStrictEqual = (a, b) => deepEquals(a, b, true);
const isDeepStrictEqual = $newCppFunction("BunObject.cpp", "jsFunction_nodeIsDeepStrictEqual", 2);
const parseArgs = $newZigFunction("parse_args.zig", "parseArgs", 1);

View File

@@ -0,0 +1,49 @@
import * as util from "util";
test("boxed number", () => {
expect(new Number(2)).not.toEqual(new Number(1));
expect(2).not.toEqual(new Number(2));
});
test("boxed symbol", () => {
expect(Object(Symbol())).not.toEqual(Object(Symbol()));
});
test("set props on boxed string", () => {
const str1 = new String("abc");
const str2 = new String("abc");
str1.x = 1;
expect(str1).toEqual(str2); // jest doesn't care
expect(util.isDeepStrictEqual(str1, str2)).toBe(false);
expect(Bun.deepEquals(str1, str2)).toBe(true);
});
for (const key of [Symbol(), "abc"]) {
describe(key === "abc" ? "string key" : "symbol key", () => {
const util = require("util");
const sym = Symbol();
const obj1 = {};
const obj4 = {};
Object.defineProperty(obj1, sym, { value: 1, enumerable: true });
Object.defineProperty(obj4, sym, { value: 1, enumerable: false });
test("enumerable 1", () => {
expect(obj1).not.toEqual(obj4);
expect(util.isDeepStrictEqual(obj1, obj4)).toBe(false);
expect(Bun.deepEquals(obj1, obj4)).toBe(false);
expect(Bun.deepEquals(obj1, obj4, false)).toBe(false);
expect(Bun.deepEquals(obj1, obj4, true)).toBe(false);
expect(obj4).not.toEqual(obj1);
expect(util.isDeepStrictEqual(obj4, obj1)).toBe(false);
expect(Bun.deepEquals(obj4, obj1)).toBe(false);
expect(Bun.deepEquals(obj4, obj1, false)).toBe(false);
expect(Bun.deepEquals(obj4, obj1, true)).toBe(false);
});
test("enumerable 2", () => {
const obj1 = {};
const obj2 = {};
Object.defineProperty(obj2, sym, { value: 1 });
expect(util.isDeepStrictEqual(obj1, obj2)).toBe(true);
expect(util.isDeepStrictEqual(obj2, obj1)).toBe(true);
obj1[sym] = 1;
expect(util.isDeepStrictEqual(obj1, obj2)).toBe(false);
expect(util.isDeepStrictEqual(obj2, obj1)).toBe(false);
});
});
}

View File

@@ -0,0 +1,172 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const {
readFileSync,
} = require('fs');
const {
open,
} = require('fs/promises');
const check = readFileSync(__filename, { encoding: 'utf8' });
// Make sure the ReadableStream works...
(async () => {
const dec = new TextDecoder();
const file = await open(__filename);
let data = '';
for await (const chunk of file.readableWebStream())
data += dec.decode(chunk);
assert.strictEqual(check, data);
assert.throws(() => file.readableWebStream(), {
code: 'ERR_INVALID_STATE',
});
await file.close();
})().then(common.mustCall());
// Make sure that acquiring a ReadableStream fails if the
// FileHandle is already closed.
(async () => {
const file = await open(__filename);
await file.close();
assert.throws(() => file.readableWebStream(), {
code: 'ERR_INVALID_STATE',
});
})().then(common.mustCall());
// Make sure that acquiring a ReadableStream fails if the
// FileHandle is already closing.
(async () => {
const file = await open(__filename);
file.close();
assert.throws(() => file.readableWebStream(), {
code: 'ERR_INVALID_STATE',
});
})().then(common.mustCall());
// Make sure the ReadableStream is closed when the underlying
// FileHandle is closed.
(async () => {
const file = await open(__filename);
const readable = file.readableWebStream();
const reader = readable.getReader();
file.close();
await reader.closed;
})().then(common.mustCall());
// Make sure the ReadableStream is closed when the underlying
// FileHandle is closed.
(async () => {
const file = await open(__filename);
const readable = file.readableWebStream();
file.close();
const reader = readable.getReader();
await reader.closed;
})().then(common.mustCall());
// Make sure that the FileHandle is properly marked "in use"
// when a ReadableStream has been acquired for it.
(async () => {
const file = await open(__filename);
file.readableWebStream();
const mc = new MessageChannel();
mc.port1.onmessage = common.mustNotCall();
assert.throws(() => mc.port2.postMessage(file, [file]), {
code: 25,
name: 'DataCloneError',
});
mc.port1.close();
await file.close();
})().then(common.mustCall());
// Make sure 'bytes' stream works
(async () => {
const file = await open(__filename);
const dec = new TextDecoder();
const readable = file.readableWebStream({ type: 'bytes' });
const reader = readable.getReader({ mode: 'byob' });
let data = '';
let result;
do {
const buff = new ArrayBuffer(100);
result = await reader.read(new DataView(buff));
if (result.value !== undefined) {
data += dec.decode(result.value);
assert.ok(result.value.byteLength <= 100);
}
} while (!result.done);
assert.strictEqual(check, data);
assert.throws(() => file.readableWebStream(), {
code: 'ERR_INVALID_STATE',
});
await file.close();
})().then(common.mustCall());
// Make sure that acquiring a ReadableStream 'bytes' stream
// fails if the FileHandle is already closed.
(async () => {
const file = await open(__filename);
await file.close();
assert.throws(() => file.readableWebStream({ type: 'bytes' }), {
code: 'ERR_INVALID_STATE',
});
})().then(common.mustCall());
// Make sure that acquiring a ReadableStream 'bytes' stream
// fails if the FileHandle is already closing.
(async () => {
const file = await open(__filename);
file.close();
assert.throws(() => file.readableWebStream({ type: 'bytes' }), {
code: 'ERR_INVALID_STATE',
});
})().then(common.mustCall());
// Make sure the 'bytes' ReadableStream is closed when the underlying
// FileHandle is closed.
(async () => {
const file = await open(__filename);
const readable = file.readableWebStream({ type: 'bytes' });
const reader = readable.getReader({ mode: 'byob' });
file.close();
await reader.closed;
})().then(common.mustCall());
// Make sure the 'bytes' ReadableStream is closed when the underlying
// FileHandle is closed.
(async () => {
const file = await open(__filename);
const readable = file.readableWebStream({ type: 'bytes' });
file.close();
const reader = readable.getReader({ mode: 'byob' });
await reader.closed;
})().then(common.mustCall());
// Make sure that the FileHandle is properly marked "in use"
// when a 'bytes' ReadableStream has been acquired for it.
(async () => {
const file = await open(__filename);
file.readableWebStream({ type: 'bytes' });
const mc = new MessageChannel();
mc.port1.onmessage = common.mustNotCall();
assert.throws(() => mc.port2.postMessage(file, [file]), {
code: 25,
name: 'DataCloneError',
});
mc.port1.close();
await file.close();
})().then(common.mustCall());

View File

@@ -0,0 +1,94 @@
'use strict';
// Confirm functionality of `util.isDeepStrictEqual()`.
require('../common');
const assert = require('assert');
const util = require('util');
function utilIsDeepStrict(a, b) {
assert.strictEqual(util.isDeepStrictEqual(a, b), true);
assert.strictEqual(util.isDeepStrictEqual(b, a), true);
}
function notUtilIsDeepStrict(a, b) {
assert.strictEqual(util.isDeepStrictEqual(a, b), false);
assert.strictEqual(util.isDeepStrictEqual(b, a), false);
}
// Handle boxed primitives
{
const boxedString = new String('test');
const boxedSymbol = Object(Symbol());
notUtilIsDeepStrict(new Boolean(true), Object(false));
notUtilIsDeepStrict(Object(true), new Number(1));
notUtilIsDeepStrict(new Number(2), new Number(1));
notUtilIsDeepStrict(boxedSymbol, Object(Symbol()));
notUtilIsDeepStrict(boxedSymbol, {});
utilIsDeepStrict(boxedSymbol, boxedSymbol);
utilIsDeepStrict(Object(true), Object(true));
utilIsDeepStrict(Object(2), Object(2));
utilIsDeepStrict(boxedString, Object('test'));
boxedString.slow = true;
notUtilIsDeepStrict(boxedString, Object('test'));
boxedSymbol.slow = true;
notUtilIsDeepStrict(boxedSymbol, {});
utilIsDeepStrict(Object(BigInt(1)), Object(BigInt(1)));
notUtilIsDeepStrict(Object(BigInt(1)), Object(BigInt(2)));
const booleanish = new Boolean(true);
Object.defineProperty(booleanish, Symbol.toStringTag, { value: 'String' });
Object.setPrototypeOf(booleanish, String.prototype);
notUtilIsDeepStrict(booleanish, new String('true'));
const numberish = new Number(42);
Object.defineProperty(numberish, Symbol.toStringTag, { value: 'String' });
Object.setPrototypeOf(numberish, String.prototype);
notUtilIsDeepStrict(numberish, new String('42'));
const stringish = new String('0');
Object.defineProperty(stringish, Symbol.toStringTag, { value: 'Number' });
Object.setPrototypeOf(stringish, Number.prototype);
notUtilIsDeepStrict(stringish, new Number(0));
const bigintish = new Object(BigInt(42));
Object.defineProperty(bigintish, Symbol.toStringTag, { value: 'String' });
Object.setPrototypeOf(bigintish, String.prototype);
notUtilIsDeepStrict(bigintish, new String('42'));
const symbolish = new Object(Symbol('fhqwhgads'));
Object.defineProperty(symbolish, Symbol.toStringTag, { value: 'String' });
Object.setPrototypeOf(symbolish, String.prototype);
notUtilIsDeepStrict(symbolish, new String('fhqwhgads'));
}
// Handle symbols (enumerable only)
{
const symbol1 = Symbol();
const obj1 = { [symbol1]: 1 };
const obj2 = { [symbol1]: 1 };
const obj3 = { [Symbol()]: 1 };
const obj4 = { };
// Add a non enumerable symbol as well. It is going to be ignored!
Object.defineProperty(obj2, Symbol(), { value: 1 });
Object.defineProperty(obj4, symbol1, { value: 1 });
notUtilIsDeepStrict(obj1, obj3);
utilIsDeepStrict(obj1, obj2);
notUtilIsDeepStrict(obj1, obj4);
// TypedArrays have a fast path. Test for this as well.
const a = new Uint8Array(4);
const b = new Uint8Array(4);
a[symbol1] = true;
b[symbol1] = false;
notUtilIsDeepStrict(a, b);
b[symbol1] = true;
utilIsDeepStrict(a, b);
// The same as TypedArrays is valid for boxed primitives
const boxedStringA = new String('test');
const boxedStringB = new String('test');
boxedStringA[symbol1] = true;
notUtilIsDeepStrict(boxedStringA, boxedStringB);
boxedStringA[symbol1] = true;
utilIsDeepStrict(a, b);
}