diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index c82c4a503b98..a3f7f1f18fee 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -100,6 +100,7 @@ #include "DOMURL.h" #include "JSDOMURL.h" +#include #include #include #include @@ -1380,42 +1381,34 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark if (vector == rightVector) [[unlikely]] return true; - // 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. + // Float arrays are compared element-wise in non-strict mode. Jest's toEqual + // uses Object.is semantics (NaN equals NaN, +0 differs from -0), while node's + // loose deepEqual uses == (NaN differs from NaN, +0 equals -0). if (!isStrict && (c1Type == Float16ArrayType || c1Type == Float32ArrayType || c1Type == Float64ArrayType)) { - if (c1Type == Float16ArrayType) { - auto* leftFloat = static_cast(vector); - auto* rightFloat = static_cast(rightVector); - size_t numElements = byteLength / sizeof(WTF::Float16); - - for (size_t i = 0; i < numElements; i++) { - if (leftFloat[i] != rightFloat[i]) { - return false; - } - } - return true; - } else if (c1Type == Float32ArrayType) { - auto* leftFloat = static_cast(vector); - auto* rightFloat = static_cast(rightVector); - size_t numElements = byteLength / sizeof(float); - + auto floatElementsEqual = [&](const auto* left, const auto* right) -> bool { + size_t numElements = byteLength / sizeof(*left); for (size_t i = 0; i < numElements; i++) { - if (leftFloat[i] != rightFloat[i]) { - return false; + double l = left[i]; + double r = right[i]; + if constexpr (enableAsymmetricMatchers) { + if (l == r ? std::signbit(l) != std::signbit(r) : !(std::isnan(l) && std::isnan(r))) { + return false; + } + } else { + if (l != r) { + return false; + } } } return true; - } else { // Float64Array - auto* leftDouble = static_cast(vector); - auto* rightDouble = static_cast(rightVector); - size_t numElements = byteLength / sizeof(double); + }; - for (size_t i = 0; i < numElements; i++) { - if (leftDouble[i] != rightDouble[i]) { - return false; - } - } - return true; + if (c1Type == Float16ArrayType) { + return floatElementsEqual(static_cast(vector), static_cast(rightVector)); + } else if (c1Type == Float32ArrayType) { + return floatElementsEqual(static_cast(vector), static_cast(rightVector)); + } else { + return floatElementsEqual(static_cast(vector), static_cast(rightVector)); } } diff --git a/test/regression/issue/34815.test.ts b/test/regression/issue/34815.test.ts new file mode 100644 index 000000000000..14a4d19d29b2 --- /dev/null +++ b/test/regression/issue/34815.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from "bun:test"; +import assert from "node:assert"; + +// https://github.com/oven-sh/bun/issues/34815 +// Jest compares typed array elements with Object.is semantics: +// NaN equals NaN, and +0 is distinct from -0. +test("toEqual treats NaN elements of float typed arrays as equal", () => { + expect(new Float16Array([NaN])).toEqual(new Float16Array([NaN])); + expect(new Float32Array([NaN])).toEqual(new Float32Array([NaN])); + expect(new Float64Array([NaN])).toEqual(new Float64Array([NaN])); + expect(new Float16Array([1, NaN, 2])).toEqual(new Float16Array([1, NaN, 2])); + expect(new Float32Array([1, NaN, 2])).toEqual(new Float32Array([1, NaN, 2])); + expect(new Float64Array([1, NaN, 2])).toEqual(new Float64Array([1, NaN, 2])); + + expect(new Float16Array([NaN])).toStrictEqual(new Float16Array([NaN])); + expect(new Float32Array([NaN])).toStrictEqual(new Float32Array([NaN])); + expect(new Float64Array([NaN])).toStrictEqual(new Float64Array([NaN])); + + expect(new Float64Array([NaN])).not.toEqual(new Float64Array([1])); + expect(new Float64Array([1])).not.toEqual(new Float64Array([NaN])); + expect(new Float32Array([1])).not.toEqual(new Float32Array([2])); +}); + +test("toEqual distinguishes +0 and -0 in float typed arrays", () => { + expect(new Float16Array([-0])).not.toEqual(new Float16Array([0])); + expect(new Float32Array([-0])).not.toEqual(new Float32Array([0])); + expect(new Float64Array([-0])).not.toEqual(new Float64Array([0])); + expect(new Float64Array([-0])).toEqual(new Float64Array([-0])); + expect(new Float64Array([0])).toEqual(new Float64Array([0])); +}); + +// node's loose deepEqual keeps == semantics for float typed arrays: +// NaN differs from NaN, +0 equals -0. Strict mode compares bytes. +test("node:assert float typed array semantics are unchanged", () => { + assert.deepEqual(new Float64Array([-0]), new Float64Array([0])); + assert.throws(() => assert.deepEqual(new Float64Array([NaN]), new Float64Array([NaN]))); + assert.deepStrictEqual(new Float64Array([NaN]), new Float64Array([NaN])); + assert.throws(() => assert.deepStrictEqual(new Float64Array([-0]), new Float64Array([0]))); + + expect(Bun.deepEquals(new Float64Array([-0]), new Float64Array([0]), false)).toBe(true); + expect(Bun.deepEquals(new Float64Array([NaN]), new Float64Array([NaN]), false)).toBe(false); + expect(Bun.deepEquals(new Float64Array([NaN]), new Float64Array([NaN]), true)).toBe(true); +});