Repository navigation
Conversation
… per-type comparison `expect(new MyMap()).toStrictEqual(new Map())` passed, where MyMap extends Map. Bun__deepEquals compared class names only after the comparison for Map, Set, Date, RegExp, Error, ArrayBuffer, the typed arrays and the Temporal classes had answered. toStrictEqual() and Bun.deepEquals(a, b, true) now compare classes first, with the rule of Jest's typeEquality: two objects have one class when they have one `constructor`, when both are arrays, or when their constructors are the same built-in of two realms. The step runs no script. Where only a getter or a Proxy in the prototype chain can answer, the class names decide. An own `constructor` that is no function is a key of the data. Two objects with one prototype and no own `constructor` need no read. toEqual(), Bun.deepEquals(a, b), the fourth argument of Bun.deepEquals() and the node:assert modes do not change. A Buffer is not a Uint8Array for these two: the type of RedisClient#getBuffer() now says Buffer, which is what it returns.
Collaborator
Author
Collaborator
Author
|
Status
|
…ct-equal-builtin-subclass
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #37776. Merge after #37764.
Problem
expect(new MyMap()).toStrictEqual(new Map())passes for a subclassMyMapofMap. So does a subclass of Set, Date, Error, RegExp or a typed array, and aBufferagainst aUint8Array. Jest and Vitest reject each pair.Bun__deepEquals(src/jsc/bindings/bindings.cpp:1298) compares class names only after the comparison of those types has answered.Fix
toStrictEqualandBun.deepEquals(a, b, true)compare classes first. The rule is Jest's: oneconstructor, two arrays, or one built-in of two realms.equals. 24 named pairs differ.test/js/bun/test/expect.test.jsandtest/js/bun/bun-object/deep-equals.test.ts(main fails 38 and 8 tests). Also 307 test files and--aot.Cookie#toJSON()producer.Background
Bun__deepEqualsis the native walker behindtoEqual,toStrictEqual,Bun.deepEqualsandnode:assert. 2 of its 7 compiled modes change.Downsides
Bun.deepEquals(a, b, true). ABufferno longer equals aUint8Array, nor a null-prototype object a literal. No user reported the false pass, so a maintainer must decide.constructorcosts +582 to +844, a typed pair under--aot+246.Notes
The rule, in the order the code applies it (
src/jsc/bindings/BunStrictClassEquality.hand.cpp)toStrictEqual, strictthis.equalsandBun.deepEquals(a, b, true)take the step.toEqual,Bun.deepEquals(a, b),Bun.deepEquals(a, b, true, true)and thenode:assertmodes keep their code.Structurethat lists noconstructor: same class. AStructureis JavaScriptCore's hidden class: the own property names of an object, and its prototype. A type that makes up properties is excluded: a Proxy, a module namespace,process.env.constructor: same class. They readconstructorfrom one chain, so the step reads nothing. An object of compiled code (--aot) keeps own properties out of itsStructure, so the step asks the object.constructorvalues with aVMInquiryslot, which runs no script. The two values give one class in three cases:constructorthat is no function, and the prototypes are equal. This is aconstructorkey of plain data.What breaks
test/js/bun/bun-object/deep-equals.test.tsholds 332 pairs. 128 of them change, all from equal to unequal.toEqual,Bun.deepEquals(a, b)andutil.isDeepStrictEqualdo not change for any pair.rejects.toStrictEqual(new NotFound("x"))no longer passes for a plainError.Bufferagainst aUint8Array. These return aBuffer:fs.readFileSync(), theBun.zstd*functions,CryptoHasher#digest(),Bun.spawnSync().stdout, thestdoutof a shell result,Database#serialize(),Archive#bytes(),redis.getBuffer().Uint8Array:Bun.gzipSync()and its siblings,Bun.sha(),Response#bytes(),Bun.file().bytes(),TextEncoder#encode(), abun:sqliteBLOB, andstructuredClone()of aBuffer.groups,querystring.parse(),Object.groupBy(),util.parseArgs().values. Such results of Bun: every map reply ofBun.redissuch ashgetall(),Bun.Cookie#toJSON(), andreq.paramsof a route with no named parameter.{ __proto__: null, ... }as the expected value passes.exportsname one file forimportand another forrequireis loaded twice. So is a package that is installed twice. Node loads two copies too. A package withmainandmodulestays one class.--environment=jsdomorhappy-dom, the globalURLis a subclass of Bun's. With jsdom, so isRequest. SoURL.parse(x)againstnew URL(x)is unequal, andrequest.clone()against the request..not.toStrictEqualbetween aBufferand aUint8Arraywith the same bytes now passes.Bun.deepEquals(a, b, true)outside a test returnsfalsefor each of these pairs and throws nothing. An example isstructuredClone(buffer)against the buffer. The two-argument form does not change.toEqualpasses for every pair above.A suite can take a pair for equal again with a tester in a preload. This one is for a
Bufferand aUint8Array:What stays equal
prototype.constructoris deleted, against its base.MyArray.from([1])against[1].constructorthat is the base class.@sinonjs/fake-timersgives its Date such a property.node:vmcontext, and jsdom and happy-dom with and without--isolate.--aot. Both are unequal now.Where Bun differs from Jest 30.5.2
Seven groups of 19 pairs are tests in
deep-equals.test.ts, because the class step decides them:constructorkey that is no function is data.JSON.parse('{"constructor":{"a":1}}')twice is equal here and in Node, and unequal in Jest.constructorgetter is not called. A class whose prototype has such a getter goes by its class name. So a Node system error still equals anErrorwith the same name and message.MyMap.prototypeno longer equals aMyMap.causeand the own properties of an Error, and the own properties of a typed array. Jest does not.Two groups of 5 pairs answer as on main:
Not in the tests, and as on main:
toEqual(new MyMap(), new Map())passes. Jest'stoEqualrejects a Map, a Set or another iterable of a subclass. A test that this change turns red and that moves totoEqualthen passes on Bun alone.this.equals(a, b, testers, true)compares classes. Jest's does not.Earlier decisions that this change reverses
deepStrictEqual#29037 (closed, not merged) gaveBun.deepEquals(a, b, true)a prototype check and lefttoStrictEqualout. Its reason: the check "broke cross-realm objects (vmrunInContext) and Buffer-vs-Uint8Array content comparisons across the bun:test suite".node:assertentry points. node:assert: compare constructors like node in deepStrictEqual, not [[Prototype]] identity #40131 says thatBun.deepEqualsandexpect()"do not change here".Bufferbreak stays: 8 assertions of Bun's own tests change..claude/docs/landing-prs.mdasks for a flag on a changed default. bun test: Vitest compatibility #44833 changed equality toward Jest with no flag, and listed each change under "Behaviour changes for existing users".Measured (release x64 builds of main at 3f3a9ff, without and with this change)
User-mode instructions of one
Bun.deepEquals(a, b, true)call, counted by single steps in gdb. The pairs are the strict rows that this change adds tobench/snippets/deep-equals.js.{ a, b }twice{ a, b }and{ b, a }JSON.parse()StructureJSON.parse()[1, 2, 3]twice{ id, name }constructor{ a, b }of another realmStructurekeeps a one-word filter of the names it has seen. With up to 3 keys in these runs, the filter rules outconstructor. With more keys, the step does one property-table lookup for eachStructure.constructor. That happens for a value of another realm and for an ownconstructor. With jsdom the global typed arrays are the window's, so aUint8Arraythat Bun makes is of another realm there.--aot: 3,216 to 3,462 for one field, 5,601 to 5,847 for 8 fields.bun build --compile --aotandbun test --aotgive the same numbers. The step asks each compiled object withAOT::PropertyWalk::getDirect.toEqual,node:assertand the 4-argument mode: 0. 5 of the 7Bun__deepEqualsinstantiations and the 8 per-type functions are identical after address normalisation.{ a: 1 }pair makes 2 allocations and 2 frees on main and here (gdb breakpoint counts).constructorgetter and counting Proxy traps give the same counts as on main.Tests
expect.test.js: 40 new tests, 38 fail on main. 37 are not specific to Bun and pass on Jest 30.5.2 and Vitest 4.1.12.deep-equals.test.ts: 308 pairs answer asequalsof@jest/expect-utils30.5.2 with the testers of itstoStrictEqual. Main answers so for 186 of them.test/package.jsonpins the package. One more test loads a package twice.test/aot/parallel/typed-objects/deep-equality-of-classesandtest-aot-deep-equality.test.ts: 75 pairs answer under--aotas in plain Bun, and a snapshot holds the answers. Both fail on main.test-environment.test.ts: strict rows under jsdom and happy-dom, with and without--isolate.dom-nodes.test.tsanddom-nodes-jest.test.js: nodes behind a Proxy, which the step does not type. Jest 30.5.2 passes the second file as it is.zstd.test.tsandvalkey/unit/buffer-operations.test.ts: 8 assertions compared aBufferwith aUint8Array. They now expect aBuffer. The type ofgetBuffer()saysBuffernow, as its docs do.test/ran on the release build of this branch. They are every*.test.*file there that callstoStrictEqual, and the tests of the runner. The 9 files that fail there fail the same tests on main.toStrictEqualalso ran on the debug build, with the same result. The elysia 1.4.28 suite: no change.BUN_JSC_validateExceptionChecks=1on the debug build: no report.Other pull requests on these lines
toEqual. They stay separate.bun test --aotfollows #44833's mocks #47605 changes the class name comparison at the end of the walker. This change does not touch those lines, andgit merge-treefinds no conflict. Nobody has built or run the two together.Not in this change
constructor, the class name comparison at the end repeats work. A pair of plain objects saves about 1,200 instructions without it. That needs its own tests.Cookie#toJSON()returns a null-prototype object, andCookieMap#toJSON()does not. The types ofArchive#bytes()and of thedataof a Terminal sayUint8Arrayfor aBuffer.toStrictEqualof a Map whose prototype isSet.prototypenow fails. The printer then throwsSet operation called on non-Set object, astoBedoes on main.For a maintainer to decide
toStrictEqual()andBun.deepEquals(a, b, true)compare the classes of two objects, as in Jest. ABufferdoes not equal aUint8Array, an object with no prototype does not equal an object literal, and an instance of a subclass does not equal an instance of its base.toEqual()does not change."Bun.deepEquals(a, b, true)takes the same rule astoStrictEqual, because the docs call them one comparison. AnenableAsymmetricMatcherscondition at the call keeps it on names alone.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/util/zstd.test.ts, test/js/bun/bun-object/deep-equals.test.ts