Skip to content

Bun.deepEquals: drop the unused fourth skipPrototype argument - #37836

Open
robobun wants to merge 2 commits into
mainfrom
farm/4af35e29/deep-equals-drop-4th-arg
Open

Bun.deepEquals: drop the unused fourth skipPrototype argument#37836
robobun wants to merge 2 commits into
mainfrom
farm/4af35e29/deep-equals-drop-4th-arg

Conversation

@robobun

@robobun robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Remove the fourth argument branch and the explicit template instantiation that existed only for it. Anything after strict is now ignored, as in every stable release.
  • skipPrototype stays reachable where node exposes it, util.isDeepStrictEqual(a, b, true) and new Assert({ skipPrototype: true }); their entry point is untouched.
  • Verification: a new test passes extra values after strict and expects them to be ignored; five of its assertions fail on main and pass with this change. The node assert, node util and expect suites were also run on a debug build.
  • Merge note: Remove dead code from the FFI shim layer, link_interface tables, jssink/jest codegen, and REPL shims #37788 adds a sibling instantiation next to the removed one, so whichever lands second has a one line conflict.

Background

  • Bun.deepEquals(a, b, strict) is Bun's public deep equality. Strict mode also compares constructors, so a class instance never equals an object literal, and counts a property set to undefined as present.
  • skipPrototype is node's option on util.isDeepStrictEqual and the Assert class: strict comparison minus the constructor check. Bun implements it as a flag on the shared native comparer, reached by those node APIs through their own binding.
  • The native comparer is a C++ template, one instantiation per flag combination. A combination used only from another file needs an explicit instantiation line; the one removed here served only the dropped branch.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/bun-object/deep-equals.test.ts

Original description

Repro

class Foo { a = 1 }
Bun.deepEquals(new Foo(), { a: 1 }, true);       // false (documented strict mode)
Bun.deepEquals(new Foo(), { a: 1 }, true, true); // true on current main

Bun.deepEquals is declared as (a, b, strict?) in packages/bun-types/bun.d.ts and documented with three parameters, but the runtime reads a fourth one. The four argument call above does not type check, is mentioned nowhere in the docs, and has no test; the only in-tree caller of Bun.deepEquals is the three argument loose call in src/js/node/assert.ts.

Cause

#34434 added callFrame->argument(3) to functionBunDeepEquals in src/jsc/bindings/BunObject.cpp so that the JS implementation of util.isDeepStrictEqual(a, b, skipPrototype) could reach the skip-prototype instantiation of Bun__deepEquals without a separate binding. The review thread on that PR states the intent: the argument is internal plumbing and the public contract of Bun.deepEquals stays at three arguments (#34434 (comment)).

#34660 then gave util.isDeepStrictEqual and the Assert class their own native entry point (Bun__deepEqualsNodeStrictSkipProto, called from jsFunctionIsDeepStrictEqual in NodeUtilTypesModule.cpp). That left the fourth argument of Bun.deepEquals, and the Bun__deepEquals<true, false, false, true> explicit instantiation in bindings.cpp that existed only for it, with no consumer.

Fix

Remove the argument(3) branch from functionBunDeepEquals and the explicit instantiation it was the only user of. Bun.deepEquals(a, b, true, <anything>) is now the same as Bun.deepEquals(a, b, true), which is what every release so far has done: the argument landed after 1.3.14 and has not shipped in a stable release, so no user code can depend on it.

Removing rather than documenting it because that is the intent recorded when it was added, and because the mode it selected (strict, minus the constructor/class name check) is node's skipPrototype option, which Bun exposes where node does: util.isDeepStrictEqual(a, b, true) and new Assert({ skipPrototype: true }). Those still use the skipPrototypeIdentity template parameter through Bun__deepEqualsNodeStrictSkipProto; that parameter and entry point are untouched.

This came up in #37776, which adds another strict-mode check gated on the same template parameter and skipped testing the four argument form because of the type mismatch. Note for merging: #37788 adds a sibling explicit instantiation next to the one removed here, so whichever of the two lands second has a one-line conflict in bindings.cpp.

Tests

test/js/bun/bun-object/deep-equals.test.ts gets "ignores arguments after strict": a class instance vs a literal (both orders and nested), a boxed String vs a String subclass instance (both orders), with true and a few non-boolean values as the extra argument, plus checks that strict mode otherwise still applies and loose mode is unaffected. On main the five toBe(false) assertions with true as the extra argument all receive true; with this change the file passes.

Also ran test/js/node/assert/deep-equal.test.ts (covers util.isDeepStrictEqual's skipPrototype argument, 305 pass), test/js/node/util/util.test.js (208 pass) and test/js/bun/test/expect.test.js (415 pass) on the debug build.

functionBunDeepEquals read callFrame->argument(3) and, together with
strict, dispatched to Bun__deepEquals<true, false, false, true>, which
bindings.cpp instantiated explicitly for that one call. The argument was
plumbing for util.isDeepStrictEqual's skipPrototype, which has had its
own native entry point (Bun__deepEqualsNodeStrictSkipProto) since #34660.
It was never typed, documented, tested or called in-tree.

Bun.deepEquals(a, b, true, true) now behaves like Bun.deepEquals(a, b,
true); the node skipPrototype instantiation used by util.isDeepStrictEqual
and the Assert class is unchanged.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 62ba98ad-7e3c-4f53-a394-244b2aac0875

📥 Commits

Reviewing files that changed from the base of the PR and between 9a543cc and 6aadf07.

📒 Files selected for processing (3)
  • src/jsc/bindings/BunObject.cpp
  • src/jsc/bindings/bindings.cpp
  • test/js/bun/bun-object/deep-equals.test.ts

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

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on main with Bun.deepEquals(new (class Foo { a = 1 })(), { a: 1 }, true, true) returning true while the three argument strict call returns false; the new "ignores arguments after strict" test in test/js/bun/bun-object/deep-equals.test.ts fails on main and passes with this change.

Self-review done, nothing outstanding. Checked in particular that the node skipPrototype path (util.isDeepStrictEqual(a, b, true) and the Assert class option) still goes through Bun__deepEqualsNodeStrictSkipProto and that its <true, false, true, true> instantiation is still emitted after this change; only <true, false, false, true> disappears.

CI: across the two builds so far the change has compiled on every platform and deep-equals.test.ts passed on every lane that ran it. The red entries are unrelated to this diff: on the first build a bun-install-registry.test.ts hoisting assertion on Windows aarch64, and on the second build (#93424) five build lanes failing to download vendored dependency tarballs from github.com (Failed to download after 5 attempts, HTTP 503), plus two tests that install from the network. The same download failures are hitting most builds in the pipeline as of a few hours later, so I have not re-pushed yet; the failed jobs on #93424 can be retried in place once GitHub downloads recover, or I will retrigger on the next event here. The unrelated test failures have been reported separately.

@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 — removes dead-code fourth argument from Bun.deepEquals and its now-unused explicit template instantiation.

What was reviewed:

  • Confirmed no remaining references to Bun__deepEquals<true, false, false, true> after removal.
  • Verified util.isDeepStrictEqual's skipPrototype path still routes through Bun__deepEqualsNodeStrictSkipProto (NodeUtilTypesModule.cpp:879), which is untouched.
  • Confirmed the only in-tree JS caller of Bun.deepEquals (src/js/node/assert.ts:52) passes three arguments.
  • New test covers both argument orders, nested, boxed-String subclass, and non-boolean extras; strict/loose behavior for the first three args is unchanged.
Extended reasoning...

Overview

This PR removes an undocumented fourth skipPrototype argument from functionBunDeepEquals in src/jsc/bindings/BunObject.cpp (7 lines deleted) and the explicit template instantiation Bun__deepEquals<true, false, false, true> in src/jsc/bindings/bindings.cpp that existed solely for that branch (4 lines deleted). It adds a regression test in test/js/bun/bun-object/deep-equals.test.ts asserting that any argument after strict is ignored.

Security risks

None. Pure removal of an unreachable-in-practice code path from a comparison utility; no user input parsing, no I/O, no auth/crypto surface.

Level of scrutiny

Low-to-medium. The change is a small, mechanical dead-code deletion with a clear provenance story (introduced for internal plumbing in #34434, obsoleted by #34660's dedicated Bun__deepEqualsNodeStrictSkipProto entry point). I verified the key claims: grep confirms no remaining users of the removed instantiation; jsFunctionIsDeepStrictEqual in NodeUtilTypesModule.cpp still calls Bun__deepEqualsNodeStrictSkipProto for the node-compat skipPrototype behavior; and the sole in-tree Bun.deepEquals caller in src/js/node/assert.ts passes three arguments. The public type declaration and docs already describe three parameters, so this brings the runtime back in line with the contract.

Other factors

The new test is well-constructed: it exercises both operand orders, nested objects, and boxed-primitive subclasses (the paths where the constructor/prototype check matters), tries true and several non-boolean fourth arguments, and includes positive controls showing strict mode still works and loose mode is unaffected. The PR description states the five toBe(false) assertions with true as the extra arg fail on main, satisfying the fails-for-the-right-reason requirement. The description also flags a one-line merge conflict with #37788 in bindings.cpp, which is a helpful heads-up but doesn't affect this PR's correctness.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:05 PM PT - Aug 12th, 2026

@robobun, your commit 6aadf07 has 4 failures in Build #93424 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37836

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

bun-37836 --bun

@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 — removes an undocumented, never-shipped fourth argument from Bun.deepEquals and its now-orphaned template instantiation.

What was reviewed

  • Confirmed no remaining consumers of Bun__deepEquals<true, false, false, true> in the tree; the node skipPrototype path goes through Bun__deepEqualsNodeStrictSkipProto (<true, false, true, true>), which is untouched.
  • Checked src/js/ for four-argument deepEquals calls — none.
  • New test covers both argument orders, nested, boxed-String subclass, non-boolean extras, and confirms strict/loose semantics still hold.
Extended reasoning...

Overview

This PR strips an internal-only fourth skipPrototype argument from functionBunDeepEquals in src/jsc/bindings/BunObject.cpp and deletes the Bun__deepEquals<true, false, false, true> explicit instantiation in src/jsc/bindings/bindings.cpp that existed solely for that branch. A regression test is added to test/js/bun/bun-object/deep-equals.test.ts asserting that arguments after strict are ignored. Net native change: -11 lines.

Security risks

None. This narrows the surface of a comparison utility to match its documented and typed contract; no auth, crypto, filesystem, or network paths are involved.

Level of scrutiny

Low. The change is a mechanical dead-code deletion. The PR description traces the history precisely (#34434 added the argument as internal plumbing, #34660 gave the node path its own entry point, leaving this orphaned), and I verified the claims: Bun__deepEqualsNodeStrictSkipProto in bindings.cpp:3103 uses the <true, false, true, true> instantiation and is called from NodeUtilTypesModule.cpp, so util.isDeepStrictEqual's skipPrototype behavior is unaffected. Grepping src/js/ for four-argument deepEquals calls found none, and no other reference to the removed <true, false, false, true> instantiation exists.

Other factors

The argument never shipped in a stable release per the description, so there is no user-facing compat concern. The new test is well-constructed: it fails on main (five toBe(false) assertions receive true when the extra arg is true), covers symmetry, nesting, and non-boolean extras, and confirms the strict-mode contract otherwise holds. The test comment names both node entry points that still expose skipPrototype. The PR also flags a one-line merge conflict with #37788 in bindings.cpp, which is helpful for whoever merges second.

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.

1 participant