Skip to content

[Fix] compare the values wrapped by two boxed primitives - #111

Open
nimbly1 wants to merge 1 commit into
inspect-js:mainfrom
nimbly1:fix/boxed-primitive-values
Open

nimbly1 wants to merge 1 commit into
inspect-js:mainfrom
nimbly1:fix/boxed-primitive-values

Conversation

@nimbly1

@nimbly1 nimbly1 commented Oct 27, 2026

Copy link
Copy Markdown

deepEqual treats two boxed primitives of the same kind as equal, whatever they wrap:

deepEqual(Object(1), Object(2));                           // true, in loose and strict mode
deepEqual(Object(true), Object(false), { strict: true });  // true
deepEqual([Object(1)], [Object(2)]);                       // true
deepEqual({ a: Object(1) }, { a: Object(2) });             // true

This holds for boxed numbers, booleans, BigInts and symbols. internalDeepEqual compares only the kind of the box (whichBoxedPrimitive) and never the primitive inside it. String objects were already told apart, but only because of their index keys.

The fix compares the wrapped primitives with Object.is. It reads them through the intrinsic valueOf, so an overridden valueOf is never called (the existing "valueOf is called for boxed primitives" test still passes). Node's assert.deepEqual and assert.deepStrictEqual (Node 24) compare boxed primitives the same way in both modes:

pair before after node assert
Object(1), Object(2) equal not equal not equal
Object(true), Object(false) equal not equal not equal
Object(NaN), Object(NaN) equal equal equal
Object(0), Object(-0) equal not equal not equal

One behavior change to check: Object(0) and Object(-0) are now different in loose mode too, as they are in Node. Plain 0 and -0 are still equal in loose mode. Tell me if you would rather keep boxed 0 and -0 equal in loose mode, and I will limit the check.

Checked: 60 new assertions in the "boxed primitives" test in test/cmp.js. 28 of them fail without the change. npx tape 'test/**/*.js' passes (720 tests), and eslint --ext=js,mjs . has no errors (the 13 warnings were there before). index.js is 399 lines, so I kept the change small for the max-lines limit of 400.

I also compared 65 pairs of values against Node's util.isDeepStrictEqual. The differences went from 14 to 9. The remaining 9 are other things, and I did not touch them: properties keyed by symbols, WeakMap and Promise instances, typed arrays that hold NaN or -0, invalid dates, Error cause, and RegExp lastIndex.

Not run: the npm audit step of npm test. The nyc coverage numbers read 0% on my Windows machine, so I am not quoting coverage. With ASSERT=1, 26 tests fail before and after this change (the WeakMap ones among them), and the new assertions pass.

Generated with Claude Code

Two boxed primitives of the same kind were equal whatever they wrapped:
`deepEqual(Object(1), Object(2))` was true, in loose and strict mode, and so
were boxed `true` and `false`, boxed BigInts and boxed Symbols, alone or
nested in an array or an object. Only the kind was compared, and `String`
objects were told apart just because of their index keys.

Compare the wrapped primitives with `Object.is`, read through the intrinsic
`valueOf` so that an overridden one is never called. This is what node's
`assert.deepEqual` and `assert.deepStrictEqual` do: a boxed `NaN` still equals
another boxed `NaN`, and a boxed `0` no longer equals a boxed `-0`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (bd2b5a8) to head (f6cceb4).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #111      +/-   ##
===========================================
+ Coverage   98.30%   100.00%   +1.69%     
===========================================
  Files           2         2              
  Lines         236       239       +3     
  Branches       99       100       +1     
===========================================
+ Hits          232       239       +7     
+ Misses          4         0       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nimbly1

nimbly1 commented Oct 27, 2026

Copy link
Copy Markdown
Author

A note on the red checks. I think neither is caused by this change.

Tests: node.js assert (19 jobs, Node 8 to 26). This workflow already failed on main (run 23129362237, 2026-03-16). The Node 24 job on this PR reports 26 failing tests, and I get the same 26 when I run ASSERT=1 npx tape 'test/**/*.js' on the unmodified code. They are the primitive comparisons, Dates with own properties and WeakMap tests. The new boxed-primitive assertions pass in that mode.

tests / node tests in Tests: node.js >= 10. All 31 test jobs in that run passed (Node 10.24 to 26.12, and the vlt and bun jobs). The gate job only runs exit 1 when one of latest, nonlatest, vlt-latest or bun-latest reports a failure, and nonlatest created no jobs at all in this run. On main in March it created 250 jobs. With range: >= 10 and type: minors, today's nodejs.org/dist/index.json gives 289 minor versions across 17 majors, so 272 non-latest jobs. GitHub allows 256 jobs in one matrix. I could not read the matrix error itself, so this is an inference. If it is right, every PR to this repo will see the same red gate until the matrix is split.

Tests: node.js < 10 passed (Node 0.8 to 9.x, 98 jobs), and so did Tests: pretest/posttest.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant