diff options
Diffstat (limited to 'src/bun.js')
-rw-r--r-- | src/bun.js/bindings/OnigurumaRegExp.cpp | 64 | ||||
-rw-r--r-- | src/bun.js/bindings/OnigurumaRegExp.h | 64 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 31 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 540 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 6 | ||||
-rw-r--r-- | src/bun.js/bindings/headers-handwritten.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.h | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.zig | 1 | ||||
-rw-r--r-- | src/bun.js/test/jest.zig | 41 |
9 files changed, 685 insertions, 67 deletions
diff --git a/src/bun.js/bindings/OnigurumaRegExp.cpp b/src/bun.js/bindings/OnigurumaRegExp.cpp index 496a3de67..c9dedb09c 100644 --- a/src/bun.js/bindings/OnigurumaRegExp.cpp +++ b/src/bun.js/bindings/OnigurumaRegExp.cpp @@ -376,70 +376,6 @@ private: void finishCreation(JSC::VM&, JSC::JSGlobalObject*); }; -class OnigurumaRegEx final : public JSC::JSDestructibleObject { -public: - using Base = JSC::JSDestructibleObject; - - static OnigurumaRegEx* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) - { - OnigurumaRegEx* ptr = new (NotNull, JSC::allocateCell<OnigurumaRegEx>(vm)) OnigurumaRegEx(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - static OnigurumaRegEx* create(JSC::JSGlobalObject* globalObject, WTF::String&& pattern, WTF::String&& flags) - { - auto* structure = reinterpret_cast<Zig::GlobalObject*>(globalObject)->OnigurumaRegExpStructure(); - auto* object = create(globalObject->vm(), globalObject, structure); - object->m_flagsString = WTFMove(flags); - object->m_patternString = WTFMove(pattern); - - return object; - } - - DECLARE_EXPORT_INFO; - template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - - return WebCore::subspaceForImpl<OnigurumaRegEx, UseCustomHeapCellType::No>( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForOnigurumaRegExp.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForOnigurumaRegExp = WTFMove(space); }, - [](auto& spaces) { return spaces.m_subspaceForOnigurumaRegExp.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForOnigurumaRegExp = WTFMove(space); }); - } - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(RegExpObjectType, StructureFlags), info()); - } - - // static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); - - const WTF::String& flagsString() const { return m_flagsString; } - void setFlagsString(const WTF::String& flagsString) { m_flagsString = flagsString; } - const WTF::String& patternString() const { return m_patternString; } - void setPatternString(const WTF::String& patternString) { m_patternString = patternString; } - - int32_t m_lastIndex = 0; - -private: - OnigurumaRegEx(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) - : Base(vm, structure) - { - } - - void finishCreation(JSC::VM&) - { - Base::finishCreation(vm()); - } - - WTF::String m_patternString = {}; - WTF::String m_flagsString = {}; -}; - const ClassInfo OnigurumaRegExpConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(OnigurumaRegExpConstructor) }; const ClassInfo OnigurumaRegExpPrototype::s_info = { "Object"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(OnigurumaRegExpPrototype) }; const ClassInfo OnigurumaRegEx::s_info = { "RegExp"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(OnigurumaRegEx) }; diff --git a/src/bun.js/bindings/OnigurumaRegExp.h b/src/bun.js/bindings/OnigurumaRegExp.h index 058492844..8ddb65a0c 100644 --- a/src/bun.js/bindings/OnigurumaRegExp.h +++ b/src/bun.js/bindings/OnigurumaRegExp.h @@ -12,6 +12,70 @@ namespace Zig { using namespace JSC; using namespace WebCore; +class OnigurumaRegEx final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + + static OnigurumaRegEx* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + OnigurumaRegEx* ptr = new (NotNull, JSC::allocateCell<OnigurumaRegEx>(vm)) OnigurumaRegEx(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + static OnigurumaRegEx* create(JSC::JSGlobalObject* globalObject, WTF::String&& pattern, WTF::String&& flags) + { + auto* structure = reinterpret_cast<Zig::GlobalObject*>(globalObject)->OnigurumaRegExpStructure(); + auto* object = create(globalObject->vm(), globalObject, structure); + object->m_flagsString = WTFMove(flags); + object->m_patternString = WTFMove(pattern); + + return object; + } + + DECLARE_EXPORT_INFO; + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl<OnigurumaRegEx, UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForOnigurumaRegExp.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForOnigurumaRegExp = WTFMove(space); }, + [](auto& spaces) { return spaces.m_subspaceForOnigurumaRegExp.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForOnigurumaRegExp = WTFMove(space); }); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(RegExpObjectType, StructureFlags), info()); + } + + // static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + + const WTF::String& flagsString() const { return m_flagsString; } + void setFlagsString(const WTF::String& flagsString) { m_flagsString = flagsString; } + const WTF::String& patternString() const { return m_patternString; } + void setPatternString(const WTF::String& patternString) { m_patternString = patternString; } + + int32_t m_lastIndex = 0; + +private: + OnigurumaRegEx(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&) + { + Base::finishCreation(vm()); + } + + WTF::String m_patternString = {}; + WTF::String m_flagsString = {}; +}; + class OnigurumaRegExpConstructor final : public JSC::InternalFunction { public: using Base = JSC::InternalFunction; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 5d7c49292..c1abb8291 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1782,6 +1782,31 @@ JSC_DEFINE_HOST_FUNCTION(functionBunEscapeHTML, (JSC::JSGlobalObject * lexicalGl } } +JSC_DECLARE_HOST_FUNCTION(functionBunDeepEquals); + +JSC_DEFINE_HOST_FUNCTION(functionBunDeepEquals, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto* global = reinterpret_cast<GlobalObject*>(globalObject); + JSC::VM& vm = global->vm(); + + 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 JSValue::encode(jsUndefined()); + } + + JSC::JSValue arg1 = callFrame->argument(0); + JSC::JSValue arg2 = callFrame->argument(1); + + Vector<std::pair<JSValue, JSValue>, 16> stack; + + bool isEqual = Bun__deepEquals(globalObject, arg1, arg2, stack, &scope, true); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsBoolean(isEqual)); +} + JSC_DECLARE_HOST_FUNCTION(functionBunNanoseconds); JSC_DEFINE_HOST_FUNCTION(functionBunNanoseconds, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -3130,6 +3155,12 @@ void GlobalObject::installAPIGlobals(JSClassRef* globals, int count, JSC::VM& vm } { + JSC::Identifier identifier = JSC::Identifier::fromString(vm, "deepEquals"_s); + object->putDirectNativeFunction(vm, this, identifier, 2, functionBunDeepEquals, ImplementationVisibility::Public, NoIntrinsic, + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0); + } + + { JSC::Identifier identifier = JSC::Identifier::fromString(vm, "version"_s); object->putDirect(vm, PropertyName(identifier), JSC::jsOwnedString(vm, makeString(Bun__version + 1)), diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 8c0df075e..4cb163f88 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -77,6 +77,12 @@ #include "HTTPHeaderNames.h" #include "JSDOMPromiseDeferred.h" #include "JavaScriptCore/TestRunnerUtils.h" +#include "JavaScriptCore/DateInstance.h" +#include "JavaScriptCore/RegExpObject.h" +#include "JavaScriptCore/PropertyNameArray.h" +#include "JavaScriptCore/HashMapImpl.h" +#include "JavaScriptCore/HashMapImplInlines.h" +#include "OnigurumaRegExp.h" template<typename UWSResponse> static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res) @@ -122,6 +128,530 @@ static void handlePromise(PromiseType* promise, JSC__JSGlobalObject* globalObjec } } +static bool canPerformFastPropertyEnumerationForObjectAssignBun(Structure* s) +{ + if (s->typeInfo().overridesGetOwnPropertySlot()) + return false; + if (s->typeInfo().overridesAnyFormOfGetOwnPropertyNames()) + return false; + // FIXME: Indexed properties can be handled. + // https://bugs.webkit.org/show_bug.cgi?id=185358 + if (hasIndexedProperties(s->indexingType())) + return false; + if (s->hasGetterSetterProperties()) + return false; + if (s->hasReadOnlyOrGetterSetterPropertiesExcludingProto()) + return false; + if (s->hasCustomGetterSetterProperties()) + return false; + if (s->isUncacheableDictionary()) + return false; + // Cannot perform fast [[Put]] to |target| if the property names of the |source| contain "__proto__". + if (s->hasUnderscoreProtoPropertyExcludingOriginalProto()) + return false; + return true; +} + +// adapted from underscorejs [https://underscorejs.org/docs/modules/isEqual.html] +bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack) +{ + VM& vm = globalObject->vm(); + 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()); + + size_t length = stack.size(); + size_t originalLength = length; + 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) { + stack.append({ v1, v2 }); + } + + JSCell* c1 = v1.asCell(); + JSCell* c2 = v2.asCell(); + JSC::JSType c1Type = c1->type(); + JSC::JSType 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; + } + + IterationRecord iterationRecord1 = iteratorForIterable(globalObject, v1); + bool isEqual = true; + while (true) { + JSValue next1 = iteratorStep(globalObject, iterationRecord1); + if (next1.isFalse()) { + break; + } + + JSValue nextValue1 = iteratorValue(globalObject, next1); + RETURN_IF_EXCEPTION(*scope, false); + + bool found = false; + IterationRecord iterationRecord2 = iteratorForIterable(globalObject, v2); + while (true) { + JSValue next2 = iteratorStep(globalObject, iterationRecord2); + if (UNLIKELY(next2.isFalse())) { + break; + } + + JSValue nextValue2 = iteratorValue(globalObject, next2); + RETURN_IF_EXCEPTION(*scope, false); + + // set has unique values, no need to count + if (Bun__deepEquals(globalObject, nextValue1, nextValue2, stack, scope, false)) { + found = true; + if (!nextValue1.isPrimitive()) { + stack.append({ nextValue1, nextValue2 }); + } + break; + } + } + + if (!found) { + isEqual = false; + break; + } + } + + if (!isEqual) { + return false; + } + + break; + } + 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; + } + + IterationRecord iterationRecord1 = iteratorForIterable(globalObject, v1); + bool isEqual = true; + while (true) { + JSValue next1 = iteratorStep(globalObject, iterationRecord1); + if (next1.isFalse()) { + break; + } + + JSValue nextValue1 = iteratorValue(globalObject, next1); + RETURN_IF_EXCEPTION(*scope, false); + + if (UNLIKELY(!nextValue1.isObject())) { + return false; + } + + JSObject* nextValueObject1 = asObject(nextValue1); + + JSValue key1 = nextValueObject1->getIndex(globalObject, static_cast<unsigned>(0)); + RETURN_IF_EXCEPTION(*scope, false); + + JSValue value1 = nextValueObject1->getIndex(globalObject, static_cast<unsigned>(1)); + RETURN_IF_EXCEPTION(*scope, false); + + bool found = false; + IterationRecord iterationRecord2 = iteratorForIterable(globalObject, v2); + while (true) { + JSValue next2 = iteratorStep(globalObject, iterationRecord2); + if (UNLIKELY(next2.isFalse())) { + break; + } + + JSValue nextValue2 = iteratorValue(globalObject, next2); + RETURN_IF_EXCEPTION(*scope, false); + + if (UNLIKELY(!nextValue2.isObject())) { + return false; + } + + JSObject* nextValueObject2 = asObject(nextValue2); + + JSValue key2 = nextValueObject2->getIndex(globalObject, static_cast<unsigned>(0)); + RETURN_IF_EXCEPTION(*scope, false); + + JSValue value2 = nextValueObject2->getIndex(globalObject, static_cast<unsigned>(1)); + RETURN_IF_EXCEPTION(*scope, false); + + if (Bun__deepEquals(globalObject, key1, key2, stack, scope, false)) { + if (Bun__deepEquals(globalObject, nextValue1, nextValue2, stack, scope, false)) { + found = true; + if (!nextValue1.isPrimitive()) { + stack.append({ nextValue1, nextValue2 }); + } + break; + } + } + } + + if (!found) { + isEqual = false; + break; + } + } + + if (!isEqual) { + return false; + } + + break; + } + 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); + + if (left->structureID() == right->structureID()) { + return left->internalNumber() == right->internalNumber(); + } + break; + } + case RegExpObjectType: { + if (c2Type != RegExpObjectType) { + return false; + } + + if (OnigurumaRegEx* left = jsDynamicCast<OnigurumaRegEx*>(v1)) { + OnigurumaRegEx* right = jsDynamicCast<OnigurumaRegEx*>(v2); + if (UNLIKELY(!right)) { + return false; + } + + if (!equal(left->patternString(), right->patternString())) { + return false; + } + + if (!equal(left->flagsString(), right->flagsString())) { + return false; + } + + return true; + } else 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 Int8ArrayType: + case Uint8ArrayType: + case Uint8ClampedArrayType: + case Int16ArrayType: + case Uint16ArrayType: + case Int32ArrayType: + case Uint32ArrayType: + case Float32ArrayType: + case Float64ArrayType: + case BigInt64ArrayType: + case BigUint64ArrayType: { + if (!isTypedArrayType(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 JSFunctionType: { + return false; + } + + default: { + break; + } + } + + bool v1Array = isArray(globalObject, v1); + RETURN_IF_EXCEPTION(*scope, false); + bool v2Array = isArray(globalObject, v2); + RETURN_IF_EXCEPTION(*scope, false); + + JSObject* o1 = v1.getObject(); + JSObject* o2 = v2.getObject(); + + 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 length = array1->length(); + if (length != array2->length()) { + return false; + } + + if (array1->canDoFastIndexedAccess() && array2->canDoFastIndexedAccess()) { + for (size_t i = 0; i < length; i++) { + JSValue left = o1->getIndexQuickly(i); + RETURN_IF_EXCEPTION(*scope, false); + JSValue right = o2->getIndexQuickly(i); + RETURN_IF_EXCEPTION(*scope, false); + if (!Bun__deepEquals(globalObject, left, right, stack, scope, true)) { + return false; + } + + RETURN_IF_EXCEPTION(*scope, false); + } + + } else { + for (size_t i = 0; i < length; i++) { + JSValue left = o1->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(*scope, false); + JSValue right = o2->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(*scope, false); + + if (!Bun__deepEquals(globalObject, left, right, stack, scope, true)) { + return false; + } + + RETURN_IF_EXCEPTION(*scope, false); + } + } + + JSC::PropertyNameArray a1(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Include); + JSC::PropertyNameArray a2(vm, PropertyNameMode::Symbols, PrivateSymbolMode::Include); + JSObject::getOwnPropertyNames(o1, globalObject, a1, DontEnumPropertiesMode::Exclude); + JSObject::getOwnPropertyNames(o2, globalObject, a2, DontEnumPropertiesMode::Exclude); + + size_t propertyLength = a1.size(); + 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 (!prop2) { + return false; + } + + if (!Bun__deepEquals(globalObject, prop1, prop2, stack, scope, true)) { + return false; + } + + RETURN_IF_EXCEPTION(*scope, false); + } + + if (addToStack) { + stack.remove(originalLength); + } + + RETURN_IF_EXCEPTION(*scope, false); + + return true; + } + + JSC::Structure* o1Structure = o1->structure(); + if (canPerformFastPropertyEnumerationForObjectAssignBun(o1Structure)) { + JSC::Structure* o2Structure = o2->structure(); + if (canPerformFastPropertyEnumerationForObjectAssignBun(o2Structure)) { + + size_t count1 = 0; + + bool result = true; + if (o2Structure->maxOffset() != o1Structure->maxOffset()) { + return false; + } + + o1Structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + if (entry.attributes() & PropertyAttribute::DontEnum) { + return true; + } + count1++; + + JSValue right = o2->getDirect(vm, JSC::PropertyName(entry.key())); + + if (!right) { + result = false; + return false; + } + + JSValue left = o1->getDirect(entry.offset()); + if (left == right || JSC::sameValue(globalObject, left, right)) { + return true; + } + + if (!Bun__deepEquals(globalObject, left, right, stack, scope, true)) { + result = false; + return false; + } + + return true; + }); + + if (result && o2Structure->id() != o1Structure->id()) { + size_t remain = count1; + o2Structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + if (entry.attributes() & PropertyAttribute::DontEnum) { + return true; + } + + if (remain == 0) { + result = false; + return false; + } + + remain--; + return true; + }); + } + + if (addToStack) { + stack.remove(originalLength); + } + + return result; + } + } + + JSC::PropertyNameArray a1(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); + JSC::PropertyNameArray a2(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Include); + o1->getPropertyNames(globalObject, a1, DontEnumPropertiesMode::Exclude); + o2->getPropertyNames(globalObject, a2, DontEnumPropertiesMode::Exclude); + + size_t propertyLength = a1.size(); + 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 (!prop2) { + return false; + } + + if (!Bun__deepEquals(globalObject, prop1, prop2, stack, scope, true)) { + return false; + } + + RETURN_IF_EXCEPTION(*scope, false); + } + + if (addToStack) { + stack.remove(originalLength); + } + + return true; +} + extern "C" { void WebCore__FetchHeaders__toUWSResponse(WebCore__FetchHeaders* arg0, bool is_ssl, void* arg2) @@ -706,6 +1236,16 @@ bool JSC__JSValue__isSameValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1, return JSC::sameValue(globalObject, left, right); } +bool JSC__JSValue__deepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject) +{ + JSC::JSValue v1 = JSC::JSValue::decode(JSValue0); + JSC::JSValue v2 = JSC::JSValue::decode(JSValue1); + + ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm()); + Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16> stack; + return Bun__deepEquals(globalObject, v1, v2, stack, &scope, true); +} + // This is the same as the C API version, except it returns a JSValue which may be a *Exception // We want that so we can return stack traces. JSC__JSValue JSObjectCallAsFunctionReturnValue(JSContextRef ctx, JSObjectRef object, diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index b769625ec..7b9fb74c1 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3423,6 +3423,10 @@ pub const JSValue = enum(JSValueReprInt) { return @enumToInt(this) == @enumToInt(other) or cppFn("isSameValue", .{ this, other, global }); } + pub fn deepEquals(this: JSValue, other: JSValue, global: *JSGlobalObject) bool { + return cppFn("deepEquals", .{ this, other, global }); + } + pub fn asString(this: JSValue) *JSString { return cppFn("asString", .{ this, @@ -3611,7 +3615,7 @@ pub const JSValue = enum(JSValueReprInt) { return this.asNullableVoid().?; } - pub const Extern = [_][]const u8{ "coerceToInt32", "fastGet_", "getStaticProperty", "createUninitializedUint8Array", "fromInt64NoTruncate", "fromUInt64NoTruncate", "toUInt64NoTruncate", "asPromise", "toInt64", "_then", "put", "makeWithNameAndPrototype", "parseJSON", "symbolKeyFor", "symbolFor", "getSymbolDescription", "createInternalPromise", "asInternalPromise", "asArrayBuffer_", "fromEntries", "createTypeError", "createRangeError", "createObject2", "getIfPropertyExistsImpl", "jsType", "jsonStringify", "kind_", "isTerminationException", "isSameValue", "getLengthOfArray", "toZigString", "createStringArray", "createEmptyObject", "putRecord", "asPromise", "isClass", "getNameProperty", "getClassName", "getErrorsProperty", "toInt32", "toBoolean", "isInt32", "isIterable", "forEach", "isAggregateError", "toError", "toZigException", "isException", "toWTFString", "hasProperty", "getPropertyNames", "getDirect", "putDirect", "getIfExists", "asString", "asObject", "asNumber", "isError", "jsNull", "jsUndefined", "jsTDZValue", "jsBoolean", "jsDoubleNumber", "jsNumberFromDouble", "jsNumberFromChar", "jsNumberFromU16", "jsNumberFromInt64", "isBoolean", "isAnyInt", "isUInt32AsAnyInt", "isInt32AsAnyInt", "isNumber", "isString", "isBigInt", "isHeapBigInt", "isBigInt32", "isSymbol", "isPrimitive", "isGetterSetter", "isCustomGetterSetter", "isObject", "isCell", "asCell", "toString", "toStringOrNull", "toPropertyKey", "toPropertyKeyValue", "toObject", "toString", "getPrototype", "getPropertyByPropertyName", "eqlValue", "eqlCell", "isCallable", "toBooleanSlow" }; + pub const Extern = [_][]const u8{ "coerceToInt32", "fastGet_", "getStaticProperty", "createUninitializedUint8Array", "fromInt64NoTruncate", "fromUInt64NoTruncate", "toUInt64NoTruncate", "asPromise", "toInt64", "_then", "put", "makeWithNameAndPrototype", "parseJSON", "symbolKeyFor", "symbolFor", "getSymbolDescription", "createInternalPromise", "asInternalPromise", "asArrayBuffer_", "fromEntries", "createTypeError", "createRangeError", "createObject2", "getIfPropertyExistsImpl", "jsType", "jsonStringify", "kind_", "isTerminationException", "isSameValue", "getLengthOfArray", "toZigString", "createStringArray", "createEmptyObject", "putRecord", "asPromise", "isClass", "getNameProperty", "getClassName", "getErrorsProperty", "toInt32", "toBoolean", "isInt32", "isIterable", "forEach", "isAggregateError", "toError", "toZigException", "isException", "toWTFString", "hasProperty", "getPropertyNames", "getDirect", "putDirect", "getIfExists", "asString", "asObject", "asNumber", "isError", "jsNull", "jsUndefined", "jsTDZValue", "jsBoolean", "jsDoubleNumber", "jsNumberFromDouble", "jsNumberFromChar", "jsNumberFromU16", "jsNumberFromInt64", "isBoolean", "isAnyInt", "isUInt32AsAnyInt", "isInt32AsAnyInt", "isNumber", "isString", "isBigInt", "isHeapBigInt", "isBigInt32", "isSymbol", "isPrimitive", "isGetterSetter", "isCustomGetterSetter", "isObject", "isCell", "asCell", "toString", "toStringOrNull", "toPropertyKey", "toPropertyKeyValue", "toObject", "toString", "getPrototype", "getPropertyByPropertyName", "eqlValue", "eqlCell", "isCallable", "toBooleanSlow", "deepEquals" }; }; extern "c" fn Microtask__run(*Microtask, *JSGlobalObject) void; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 553259ec0..b8d071785 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -271,4 +271,6 @@ extern "C" size_t Bun__encoding__byteLengthUTF16(const UChar* ptr, size_t len, E extern "C" int64_t Bun__encoding__constructFromLatin1(void*, const unsigned char* ptr, size_t len, Encoding encoding); extern "C" int64_t Bun__encoding__constructFromUTF16(void*, const UChar* ptr, size_t len, Encoding encoding); +bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSC::JSValue v1, JSC::JSValue v2, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, JSC::ThrowScope* scope, bool addToStack); + #endif diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index a9d106029..67dc9e393 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format off -//-- AUTOGENERATED FILE -- 1669191472 +//-- AUTOGENERATED FILE -- 1669267948 #pragma once #include <stddef.h> @@ -486,6 +486,7 @@ CPP_DECL JSC__JSValue JSC__JSValue__createRangeError(const ZigString* arg0, cons CPP_DECL JSC__JSValue JSC__JSValue__createStringArray(JSC__JSGlobalObject* arg0, ZigString* arg1, size_t arg2, bool arg3); CPP_DECL JSC__JSValue JSC__JSValue__createTypeError(const ZigString* arg0, const ZigString* arg1, JSC__JSGlobalObject* arg2); CPP_DECL JSC__JSValue JSC__JSValue__createUninitializedUint8Array(JSC__JSGlobalObject* arg0, size_t arg1); +CPP_DECL bool JSC__JSValue__deepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* arg2); CPP_DECL bool JSC__JSValue__eqlCell(JSC__JSValue JSValue0, JSC__JSCell* arg1); CPP_DECL bool JSC__JSValue__eqlValue(JSC__JSValue JSValue0, JSC__JSValue JSValue1); CPP_DECL JSC__JSValue JSC__JSValue__fastGet_(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, unsigned char arg2); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 41f5f7178..7b649f26f 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -279,6 +279,7 @@ pub extern fn JSC__JSValue__createRangeError(arg0: [*c]const ZigString, arg1: [* pub extern fn JSC__JSValue__createStringArray(arg0: ?*JSC__JSGlobalObject, arg1: [*c]ZigString, arg2: usize, arg3: bool) JSC__JSValue; pub extern fn JSC__JSValue__createTypeError(arg0: [*c]const ZigString, arg1: [*c]const ZigString, arg2: ?*JSC__JSGlobalObject) JSC__JSValue; pub extern fn JSC__JSValue__createUninitializedUint8Array(arg0: ?*JSC__JSGlobalObject, arg1: usize) JSC__JSValue; +pub extern fn JSC__JSValue__deepEquals(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, arg2: ?*JSC__JSGlobalObject) bool; pub extern fn JSC__JSValue__eqlCell(JSValue0: JSC__JSValue, arg1: [*c]JSC__JSCell) bool; pub extern fn JSC__JSValue__eqlValue(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) bool; pub extern fn JSC__JSValue__fastGet_(JSValue0: JSC__JSValue, arg1: ?*JSC__JSGlobalObject, arg2: u8) JSC__JSValue; diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index c7372b573..dafb7dc0c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -675,6 +675,46 @@ pub const Expect = struct { return .zero; } + pub fn toEqual(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const thisValue = callFrame.this(); + const _arguments = callFrame.arguments(1); + const arguments: []const JSValue = _arguments.ptr[0.._arguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toEqual() requires 1 argument", .{}); + return .zero; + } + + if (this.scope.tests.items.len <= this.test_id) { + globalObject.throw("toEqual() must be called in a test", .{}); + return .zero; + } + + active_test_expectation_counter.actual += 1; + + const expected = arguments[0]; + const value = Expect.capturedValueGetCached(thisValue) orelse { + globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + value.ensureStillAlive(); + + const not = this.op.contains(.not); + var pass = value.deepEquals(expected, globalObject); + + if (not) pass = !pass; + if (pass) return thisValue; + + // handle failure + var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject }; + if (not) { + globalObject.throw("Expected values to not be equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) }); + } else { + globalObject.throw("Expected values to be equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) }); + } + return .zero; + } + pub const toHaveProperty = notImplementedJSCFn; pub const toHaveBeenCalledTimes = notImplementedJSCFn; pub const toHaveBeenCalledWith = notImplementedJSCFn; @@ -691,7 +731,6 @@ pub const Expect = struct { pub const toBeLessThanOrEqual = notImplementedJSCFn; pub const toBeInstanceOf = notImplementedJSCFn; pub const toContainEqual = notImplementedJSCFn; - pub const toEqual = notImplementedJSCFn; pub const toMatch = notImplementedJSCFn; pub const toMatchObject = notImplementedJSCFn; pub const toMatchSnapshot = notImplementedJSCFn; |