Skip to content

Fix toEqual treating NaN elements of float typed arrays as unequal - #34818

Open
robobun wants to merge 2 commits into
mainfrom
farm/2b13d6bd/toequal-float-nan
Open

Fix toEqual treating NaN elements of float typed arrays as unequal#34818
robobun wants to merge 2 commits into
mainfrom
farm/2b13d6bd/toequal-float-nan

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #34815

Repro

import { expect, test } from "bun:test";
test("nan", () => {
  expect(new Float16Array([NaN])).toEqual(new Float16Array([NaN])); // failed
});

Float32Array and Float64Array were affected the same way.

Cause

Since #19285 (v1.2.11), the non-strict deep-equals path compares float typed array elements with IEEE !=, so NaN never equals NaN and +0 equals -0. That matches node's loose assert.deepEqual, but the same code also runs for Jest's toEqual, which compares elements with Object.is semantics (NaN equals NaN, +0 differs from -0).

Fix

In Bun__deepEquals, the float element loop now branches on the jest template parameter: the jest path uses Object.is semantics per element, while the node/Bun.deepEquals path keeps the existing IEEE comparison. Strict mode (toStrictEqual, deepStrictEqual) is unchanged (byte-wise memcmp).

Verification

  • test/regression/issue/34815.test.ts fails on the released build, passes with the fix; it also pins the node loose/strict semantics as unchanged.
  • test/js/bun/test/expect.test.js (407 pass), test/js/bun/bun-object/deep-equals.spec.ts (44 pass), and test/js/node/test/parallel/test-assert-typedarray-deepequal.js (40 pass) all pass locally.

Note this also aligns toEqual with Jest for new Float64Array([-0]) vs new Float64Array([0]): they are now unequal, as in Jest.


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/34815.test.ts"
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (82944b6ef)

test/regression/issue/34815.test.ts:
3 | 
4 | // https://github.com/oven-sh/bun/issues/34815
5 | // Jest compares typed array elements with Object.is semantics:
6 | // NaN equals NaN, and +0 is distinct from -0.
7 | test("toEqual treats NaN elements of float typed arrays as equal", () => {
8 |   expect(new Float16Array([NaN])).toEqual(new Float16Array([NaN]));
                                      ^
error: expect(received).toEqual(expected)

  Float16Array [
    NaN,
  ]

- Expected  - 0
+ Received  + 0

      at <anonymous> (/workspace/bun/test/regression/issue/34815.test.ts:8:35)
(fail) toEqual treats NaN elements of float typed arrays as equal [22.92ms]
20 |   expect(new Float64Array([1])).not.toEqual(new Float64A
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (701edc439)

test/regression/issue/34815.test.ts:
(pass) toEqual treats NaN elements of float typed arrays as equal [3.92ms]
(pass) toEqual distinguishes +0 and -0 in float typed arrays [0.09ms]
(pass) node:assert float typed array semantics are unchanged [2.30ms]

 3 pass
 0 fail
 20 expect() calls
Ran 3 tests across 1 file. [160.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/regression/issue/34815.test.ts"
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (82944b6ef)

test/regression/issue/34815.test.ts:
(pass) toEqual treats NaN elements of float typed arrays as equal [26.87ms]
(pass) toEqual distinguishes +0 and -0 in float typed arrays [4.93ms]
(pass) node:assert float typed array semantics are unchanged [124.50ms]

 3 pass
 0 fail
 20 expect() calls
Ran 3 tests across 1 file. [2.52s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 713ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] cxx obj/src/jsc/bindings/bindings.cpp.o
[2/7] gen cpp.rs (cppbind)
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[9
... (truncated)
diff hotspot
src/jsc/bindings/bindings.cpp       | 53 ++++++++++++++++---------------------
 test/regression/issue/34815.test.ts | 43 ++++++++++++++++++++++++++++++
 2 files changed, 66 insertions(+), 30 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                 reads  edits  tests
src/jsc/bindings/bindings.cpp            1      2      0
test/regression/issue/34815.test.ts      0      1      0

root cause · written by the author bot

The root cause was that jest's toEqual on float typed arrays compared elements with plain IEEE !=, under which NaN != NaN, so two arrays containing NaN were reported as unequal. The fix branches the non-strict float typed-array comparison in specialObjectsDequal on the enableAsymmetricMatchers template parameter, applying Object.is semantics (NaN equals NaN, and +0 is distinguished from -0) when the comparison comes from jest matchers. Node's assert.deepEqual and Bun.deepEquals retain the original IEEE comparison, so their behavior is unchanged.

…qual

Jest's toEqual compares typed array elements with Object.is semantics:
NaN equals NaN and +0 differs from -0. The non-strict deepEquals float
path used IEEE comparison for all callers, which matches node's loose
assert.deepEqual but broke expect(new Float16Array([NaN])).toEqual(...).

Keep the IEEE comparison for the node/Bun.deepEquals path and use
Object.is semantics when asymmetric matchers are enabled (jest path).

Fixes #34815
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:27 AM PT - Jul 20th, 2026

@robobun, your commit 82944b6efcb51357075f8b299251914b32a008b5 passed in Build #76253! 🎉


🧪   To try this PR locally:

bunx bun-pr 34818

That installs a local version of the PR into your bun-34818 executable, so you can run:

bun-34818 --bun

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Updated float typed-array deep equality to handle NaN and signed zero according to matcher mode, with regression tests covering Bun matchers, Node assertions, and Bun.deepEquals.

Changes

Float typed-array equality

Layer / File(s) Summary
Typed-array comparison semantics
src/jsc/bindings/bindings.cpp
Float typed-array comparisons use Object.is-style handling for asymmetric matchers while preserving legacy comparisons otherwise.
Regression coverage
test/regression/issue/34815.test.ts
Tests verify NaN, +0, and -0 behavior across float typed arrays, Bun matchers, Node assertions, and Bun.deepEquals.

Suggested reviewers: jarred-sumner, sosukesuzuki

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The fix makes toEqual pass for Float16Array([NaN]) and adds regression coverage for the reported bug.
Out of Scope Changes check ✅ Passed The added test and deep-equals semantics changes stay focused on the typed-array NaN comparison bug.
Title check ✅ Passed The title clearly and concisely describes the main fix to typed-array toEqual NaN handling.
Description check ✅ Passed The description covers the change and verification, though it uses custom headings instead of the template's exact section names.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/regression/issue/34815.test.ts`:
- Around line 5-6: Remove the explanatory comments near the typed-array
assertions in the regression test, including both comment blocks around lines
5-6 and 32-33, while retaining only the GitHub issue URL comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fde7d60e-c9fb-4608-a329-901501e392f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff1454 and 701edc4.

📒 Files selected for processing (2)
  • src/jsc/bindings/bindings.cpp
  • test/regression/issue/34815.test.ts

Comment thread test/regression/issue/34815.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Bun.deepEquals: fix four false-positive equality classes #32872 - Fixes the same NaN float typed array deepEquals bug with the same Object.is semantics approach in specialObjectsDequal
  2. util: node v26.3.0 compat — styleText hex/validateStream, getCallSites, regexp highlighting, vm namespace, tty.WriteStream + deepEquals fixes (test-util 70% → 83%) #34434 - Includes the same typed array deepEquals fixes as part of broader node:util v26.3.0 compat work (self-describes as overlapping with Bun.deepEquals: fix four false-positive equality classes #32872)

🤖 Generated with Claude Code

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed the overlap. #32872 fixes the same float typed array class (same approach: Object.is semantics on the jest path, gated on the asymmetric-matchers template parameter) as part of a broader change covering four deepEquals classes for #29030, and #34434 overlaps both as part of node:util v26 compat work.

This PR is the minimal fix for the regression reported in #34815 (toEqual on float typed arrays with NaN, broken since v1.2.11), with a regression test that also pins the node loose/strict semantics. If #32872 or #34434 lands first, this one can be closed; if this lands first, the float hunk in #32872 becomes a trivial rebase. Leaving the choice of merge order to maintainers.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that branches the non-strict float typed-array comparison on the jest template param so toEqual gets Object.is semantics while node's loose deepEqual/Bun.deepEquals keep IEEE ==.

What was reviewed:

  • Verified enableAsymmetricMatchers correctly discriminates jest matchers (jestDeepEquals, <false, true>) from Bun.deepEquals/assert.deepEqual (<false, false>) — bindings.cpp:2750-2768.
  • Traced the ternary against Object.is truth table (NaN/NaN, ±0, mixed) — correct; signbit check is only reachable when l == r, so non-zero equal values can't spuriously differ.
  • Confirmed strict path (memcmp at line 1415) is untouched, so toStrictEqual/deepStrictEqual behavior is unchanged; test pins that too.
Extended reasoning...

Overview

Single-function change in specialObjectsDequal (src/jsc/bindings/bindings.cpp) inside the typed-array case. The three copy-pasted per-width float loops are collapsed into one generic lambda that widens each element to double and, when enableAsymmetricMatchers is true (jest matchers), applies Object.is semantics (l == r ? signbit(l) != signbit(r) : !(isnan(l) && isnan(r))). The non-jest path keeps the existing IEEE !=. A regression test covers Float16/32/64 for NaN and ±0 under toEqual, toStrictEqual, assert.deepEqual, assert.deepStrictEqual, and Bun.deepEquals.

Security risks

None. Pure numeric comparison logic in the test-runner deep-equals path; no untrusted input parsing, allocation, or side effects.

Level of scrutiny

Low-medium. The change is confined to a !isStrict branch that already existed and only alters element-wise comparison semantics for one of the two template instantiations. I confirmed via the export table at bindings.cpp:2750-2768 that enableAsymmetricMatchers=true maps exactly to jestDeepEquals/jestStrictDeepEquals (and toEqual uses the former via the test_runner crate), while Bun.deepEquals and node assert use false. Widening WTF::Float16/float to double preserves NaN-ness and sign, so the semantics carry through; sizeof(*left) on WTF::Float16 is 2, matching the previous sizeof(WTF::Float16) divisor.

Other factors

  • The refactor also deduplicates three near-identical loops, which is the kind of within-diff dedup REVIEW.md asks for.
  • Tests are appropriately placed in test/regression/issue/34815.test.ts with the issue URL, cover all three float widths and both directions (equal/not-equal), and pin the unchanged node/Bun.deepEquals semantics so a future edit that accidentally flips the non-jest path will fail.
  • The PR notes a deliberate secondary behavior change (toEqual now distinguishes ±0 in float arrays) and correctly attributes it to Jest parity — this follows directly from Object.is and is covered by a test.
  • Existing suites (expect.test.js, deep-equals.spec.ts, test-assert-typedarray-deepequal.js) reported passing; the change surface is small enough that those are the right regression net.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that branches float typed-array element comparison on the jest template parameter, giving toEqual Object.is semantics while leaving Bun.deepEquals/node loose deepEqual and strict memcmp paths untouched.

What was reviewed:

  • Object.is predicate (l == r ? signbit differs : !(both NaN)) — correct for NaN-equals-NaN and +0≠-0; widening Float16/float to double preserves NaN and signed-zero.
  • Template-parameter routing: jestDeepEquals = <false, true> hits the new branch; deepEquals = <false, false> keeps IEEE !=; both strict variants still fall through to memcmp.
  • Regression test pins all four contracts (toEqual/toStrictEqual/assert.deepEqual/deepStrictEqual + Bun.deepEquals) across Float16/32/64.

Note: overlaps with #32872/#34434 as already flagged in-thread — merge order is a maintainer call, but this change is self-contained and rebases trivially either way.

Extended reasoning...

Overview

The PR fixes #34815: expect(new Float16Array([NaN])).toEqual(new Float16Array([NaN])) was failing because the non-strict float typed-array path in specialObjectsDequal (bindings.cpp) compared elements with IEEE !=, under which NaN never equals NaN. The fix collapses the three per-width loops into a single generic lambda that widens each element to double and, when enableAsymmetricMatchers is set (the jest path), applies Object.is semantics per element; the node/Bun.deepEquals path keeps the original IEEE comparison. A new <cmath> include supplies std::signbit/std::isnan. Net -7 lines in bindings.cpp plus a 43-line regression test.

Security risks

None. This is a pure comparison-semantics change in the test-matcher deep-equality helper; no untrusted input parsing, allocation sizing, or FFI ownership is touched.

Level of scrutiny

Moderate. Bun__deepEquals backs four user-facing contracts (jest toEqual/toStrictEqual, node assert.deepEqual/deepStrictEqual, and Bun.deepEquals), so I traced the four deepEqualsWrapperImpl<...> instantiations at bindings.cpp:2752-2767 to confirm only <false, true> (jest non-strict) reaches the new Object.is branch, <false, false> still gets IEEE !=, and both <true, *> variants skip the block entirely and hit memcmp. I also walked the Object.is predicate: when l == r under IEEE, signbits can only differ for ±0, so the signbit check is exactly the +0/-0 case; when l != r, only both-NaN survives as equal. Widening Float16/float to double is exact and preserves NaN and signed zero, so the else branch is behavior-identical to the old per-type !=.

Other factors

The regression test covers all three float widths, NaN in isolation and mid-array, +0 vs -0 both directions, toStrictEqual, node assert.deepEqual/deepStrictEqual, and Bun.deepEquals in both modes — so it pins the unchanged paths as well as the fixed one. The PR evidence shows the test failing on the debug/ASAN build without the fix and passing with it (the "release without fix" block in the gate output shows the same commit hash as the fix, which looks like a harness labeling artifact rather than a real pass-without-fix). The author also re-ran expect.test.js, deep-equals.spec.ts, and test-assert-typedarray-deepequal.js clean. The CodeRabbit comment about test comments was resolved. The known overlap with #32872/#34434 is a coordination question the author already surfaced; it doesn't affect correctness of this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun test toEqual incorrect comparison on Float16Array with NaN.

1 participant