1import { assert } from "@effect/vitest"
2import * as Equal from "effect/Equal"
3import * as Hash from "effect/Hash"
4import * as HashMap from "effect/HashMap"
5import * as Option from "effect/Option"
6import { describe, expect, it, vi } from "vitest"
7
8class Key implements Equal.Equal, Hash.Hash {
9 constructor(readonly group: string) {}
10
11 [Equal.symbol](that: unknown) {
12 return that instanceof Key && this.group === that.group
13 }
14
15 [Hash.symbol]() {
16 return 0
17 }
18}
19
20describe("Equal.equals", () => {
21 it("does not cache an equality found through a provisional circular pair", () => {
22 interface Child {
23 parent?: Parent
24 }
25
26 class Parent implements Hash.Hash {
27 constructor(readonly child: Child, readonly x: number, readonly y: number) {}
28
29 [Hash.symbol](): number {
30 return 0
31 }
32 }
33
34 const p: Child = {}
35 const q: Child = {}
36 const a = new Parent(p, 1, 2)
37 const b = new Parent(q, 2, 1)
38 p.parent = a
39 q.parent = b
40
41 expect(Equal.equals(a, b)).toBe(false)
42 expect(Equal.equals(p, q)).toBe(false)
43 })
44
45 describe("cyclic values", () => {
46 class GraphNode implements Hash.Hash {
47 v: number
48 a: unknown = undefined
49 b: unknown = undefined
50 constructor(v: number) {
51 this.v = v
52 }
53 [Hash.symbol](): number {
54 return 0
55 }
56 }
57
58 type Spec = ReadonlyArray<{ readonly v: number; readonly a: number; readonly b: number }>
59
60 const graph = (spec: Spec): Array<GraphNode> => {
61 const nodes = spec.map(({ v }) => new GraphNode(v))
62 spec.forEach(({ a, b }, i) => {
63 nodes[i].a = nodes[a]
64 nodes[i].b = nodes[b]
65 })
66 return nodes
67 }
68
69 it("does not let an earlier comparison change a later result", () => {
70 const [, n2] = graph([{ v: 1, a: 1, b: 0 }, { v: 1, a: 0, b: 0 }])
71 const [m0, m1] = graph([{ v: 1, a: 1, b: 1 }, { v: 1, a: 1, b: 0 }])
72 Equal.equals(n2, m1)
73 assert.isTrue(Equal.equals({ w: n2, k: 1 }, { w: m0, k: 1 }))
74 })
75
76 it("assumes cycles equal pair by pair", () => {
77 const [loop] = graph([{ v: 0, a: 0, b: 0 }])
78 const [, entry] = graph([{ v: 0, a: 0, b: 0 }, { v: 0, a: 0, b: 0 }])
79 assert.isTrue(Equal.equals(loop, entry))
80 const [, p1] = graph([{ v: 1, a: 0, b: 0 }, { v: 0, a: 0, b: 0 }])
81 const [, q1] = graph([{ v: 1, a: 1, b: 0 }, { v: 0, a: 0, b: 0 }])
82 assert.isFalse(Equal.equals(p1, q1))
83 })
84
85 it("agrees with bisimilarity on random cyclic graphs", () => {
86 // Compute the greatest bisimulation by eliminating invalid pairs.
87 const bisimilar = (left: Spec, right: Spec): ReadonlyArray<ReadonlyArray<boolean>> => {
88 const related = left.map((l) => right.map((r) => l.v === r.v))
89 let changed = true
90 while (changed) {
91 changed = false
92 left.forEach((l, i) =>
93 right.forEach((r, j) => {
94 if (related[i][j] && !(related[l.a][r.a] && related[l.b][r.b])) {
95 related[i][j] = false
96 changed = true
97 }
98 })
99 )
100 }
101 return related
102 }
103 let seed = 1
104 const random = (n: number): number => {
105 seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
106 return (seed >>> 16) % n
107 }
108 const randomSpec = (): Spec => {
109 const size = 1 + random(4)
110 return Array.from({ length: size }, () => ({ v: random(2), a: random(size), b: random(size) }))
111 }
112 const disagreements: Array<string> = []
113 for (let run = 0; run < 500; run++) {
114 const left = randomSpec()
115 const right = randomSpec()
116 const expected = bisimilar(left, right)
117 for (let x = 0; x < left.length; x++) {
118 for (let y = 0; y < right.length; y++) {
119 if (Equal.equals(graph(left)[x], graph(right)[y]) !== expected[x][y]) {
120 disagreements.push(`${JSON.stringify(left)}[${x}] vs ${JSON.stringify(right)}[${y}]`)
121 }
122 }
123 }
124 }
125 assert.deepStrictEqual(disagreements, [])
126 })
127 })
128
129 describe("plain objects", () => {
130 it("should return true for structurally identical objects (structural equality)", () => {
131 const obj1 = { a: 1, b: 2 }
132 const obj2 = { a: 1, b: 2 }
133 expect(Equal.equals(obj1, obj2)).toBe(true)
134 })
135
136 it("should return true for same reference", () => {
137 const obj = { a: 1, b: 2 }
138 expect(Equal.equals(obj, obj)).toBe(true)
139 })
140
141 it("should return true for empty objects", () => {
142 expect(Equal.equals({}, {})).toBe(true)
143 })
144
145 it("should return false for objects with different values", () => {
146 const obj1 = { a: 1, b: 2 }
147 const obj2 = { a: 1, b: 3 }
148 expect(Equal.equals(obj1, obj2)).toBe(false)
149 })
150
151 it("should return false for objects with different keys", () => {
152 const obj1 = { a: 1, b: 2 }
153 const obj2 = { a: 1, c: 2 }
154 expect(Equal.equals(obj1, obj2)).toBe(false)
155 })
156
157 describe("symbol properties", () => {
158 it("should return true for objects with identical symbol keys and values", () => {
159 const sym1 = Symbol("test")
160 const sym2 = Symbol("other")
161 const obj1 = { [sym1]: 1, [sym2]: 2 }
162 const obj2 = { [sym1]: 1, [sym2]: 2 }
163 expect(Equal.equals(obj1, obj2)).toBe(true)
164 })
165
166 it("should return false for objects with different symbol keys", () => {
167 const sym1 = Symbol("test")
168 const sym2 = Symbol("test") // Different symbol with same description
169 const obj1 = { [sym1]: 1 }
170 const obj2 = { [sym2]: 1 }
171 expect(Equal.equals(obj1, obj2)).toBe(false)
172 })
173
174 it("should return false for objects with different symbol values", () => {
175 const sym = Symbol("test")
176 const obj1 = { [sym]: 1 }
177 const obj2 = { [sym]: 2 }
178 expect(Equal.equals(obj1, obj2)).toBe(false)
179 })
180
181 it("should return true for objects with mixed string and symbol keys", () => {
182 const sym = Symbol("test")
183 const obj1 = { a: 1, [sym]: 2 }
184 const obj2 = { a: 1, [sym]: 2 }
185 expect(Equal.equals(obj1, obj2)).toBe(true)
186 })
187
188 it("should return false when one object has symbol keys and the other doesn't", () => {
189 const sym = Symbol("test")
190 const obj1 = { a: 1, [sym]: 2 }
191 const obj2 = { a: 1 }
192 expect(Equal.equals(obj1, obj2)).toBe(false)
193 })
194
195 it("should consider non-enumerable symbol properties", () => {
196 const sym1 = Symbol("enumerable")
197 const sym2 = Symbol("non-enumerable")
198 const obj1: any = { a: 1 }
199 const obj2: any = { a: 1 }
200
201 // Add enumerable symbol property
202 obj1[sym1] = "visible"
203 obj2[sym1] = "visible"
204
205 // Add non-enumerable symbol property with same value
206 Object.defineProperty(obj1, sym2, { value: "hidden", enumerable: false })
207 Object.defineProperty(obj2, sym2, { value: "hidden", enumerable: false })
208
209 expect(Equal.equals(obj1, obj2)).toBe(true)
210 })
211
212 it("should consider non-enumerable symbol properties even when they differ", () => {
213 const sym1 = Symbol("enumerable")
214 const sym2 = Symbol("non-enumerable")
215 const obj1: any = { a: 1 }
216 const obj2: any = { a: 1 }
217
218 // Add same enumerable symbol property
219 obj1[sym1] = "visible"
220 obj2[sym1] = "visible"
221
222 // Add non-enumerable symbol properties with different values
223 Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false })
224 Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false })
225
226 expect(Equal.equals(obj1, obj2)).toBe(false)
227 })
228
229 it("should consider enumerable symbol properties even when string properties are the same", () => {
230 const sym = Symbol("test")
231 const obj1: any = { a: 1, b: 2 }
232 const obj2: any = { a: 1, b: 2 }
233
234 obj1[sym] = "different"
235 obj2[sym] = "values"
236
237 expect(Equal.equals(obj1, obj2)).toBe(false)
238 })
239 })
240
241 describe("non-enumerable properties", () => {
242 it("should handle non-enumerable string properties correctly", () => {
243 const obj1 = { a: 1 }
244 const obj2 = { a: 1 }
245
246 Object.defineProperty(obj1, "hidden", { value: "secret", enumerable: false })
247 Object.defineProperty(obj2, "hidden", { value: "secret", enumerable: false })
248
249 expect(Equal.equals(obj1, obj2)).toBe(true)
250 })
251
252 it("should consider non-enumerable string properties even when they differ", () => {
253 const obj1 = { a: 1 }
254 const obj2 = { a: 1 }
255
256 Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false })
257 Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false })
258
259 expect(Equal.equals(obj1, obj2)).toBe(false)
260 })
261
262 it("should consider enumerable properties and non-enumerable ones", () => {
263 const obj1: any = { a: 1 }
264 const obj2: any = { a: 1 }
265
266 // Add same enumerable property
267 obj1.b = 2
268 obj2.b = 2
269
270 // Add different non-enumerable properties (should be considered)
271 Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false })
272 Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false })
273
274 expect(Equal.equals(obj1, obj2)).toBe(false)
275 })
276 })
277
278 describe("hash consistency with symbol properties", () => {
279 it("should produce same hash for objects with identical symbol keys and values", () => {
280 const sym1 = Symbol("test")
281 const sym2 = Symbol("other")
282 const obj1 = { [sym1]: 1, [sym2]: 2 }
283 const obj2 = { [sym1]: 1, [sym2]: 2 }
284
285 expect(Hash.hash(obj1)).toBe(Hash.hash(obj2))
286 expect(Equal.equals(obj1, obj2)).toBe(true)
287 })
288
289 it("should return false for equality even if symbols have same hash", () => {
290 const sym1 = Symbol("test")
291 const sym2 = Symbol("test") // Different symbol with same description
292 const obj1 = { [sym1]: 1 }
293 const obj2 = { [sym2]: 1 }
294
295 // Note: symbols with same description may have same hash (acceptable hash collision)
296 // but equality should still work correctly
297 expect(Equal.equals(obj1, obj2)).toBe(false)
298 })
299
300 it("should produce different hash for symbols with different descriptions", () => {
301 const sym1 = Symbol("first")
302 const sym2 = Symbol("second")
303 const obj1 = { [sym1]: 1 }
304 const obj2 = { [sym2]: 1 }
305
306 expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
307 expect(Equal.equals(obj1, obj2)).toBe(false)
308 })
309
310 it("should produce different hash for objects with different symbol values", () => {
311 const sym = Symbol("test")
312 const obj1 = { [sym]: 1 }
313 const obj2 = { [sym]: 2 }
314
315 expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
316 expect(Equal.equals(obj1, obj2)).toBe(false)
317 })
318
319 it("should produce same hash for objects with mixed string and symbol keys", () => {
320 const sym = Symbol("test")
321 const obj1 = { a: 1, [sym]: 2 }
322 const obj2 = { a: 1, [sym]: 2 }
323
324 expect(Hash.hash(obj1)).toBe(Hash.hash(obj2))
325 expect(Equal.equals(obj1, obj2)).toBe(true)
326 })
327
328 it("should consider non-enumerable symbol properties in hash calculation", () => {
329 const sym1 = Symbol("enumerable")
330 const sym2 = Symbol("non-enumerable")
331 const obj1: any = { a: 1 }
332 const obj2: any = { a: 1 }
333
334 // Add same enumerable symbol property
335 obj1[sym1] = "visible"
336 obj2[sym1] = "visible"
337
338 // Add different non-enumerable symbol properties
339 Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false })
340 Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false })
341
342 expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
343 expect(Equal.equals(obj1, obj2)).toBe(false)
344 })
345 })
346 })
347
348 describe("plain arrays", () => {
349 it("should return true for structurally identical arrays (structural equality)", () => {
350 const arr1 = [1, 2, 3]
351 const arr2 = [1, 2, 3]
352 expect(Equal.equals(arr1, arr2)).toBe(true)
353 })
354
355 it("should return true for same reference", () => {
356 const arr = [1, 2, 3]
357 expect(Equal.equals(arr, arr)).toBe(true)
358 })
359
360 it("should return true for empty arrays", () => {
361 expect(Equal.equals([], [])).toBe(true)
362 })
363
364 it("should return false for arrays with different elements", () => {
365 const arr1 = [1, 2, 3]
366 const arr2 = [1, 2, 4]
367 expect(Equal.equals(arr1, arr2)).toBe(false)
368 })
369
370 it("should return false for arrays with different lengths", () => {
371 const arr1 = [1, 2, 3]
372 const arr2 = [1, 2]
373 expect(Equal.equals(arr1, arr2)).toBe(false)
374 })
375 })
376
377 describe("primitives", () => {
378 it("should work correctly for numbers", () => {
379 expect(Equal.equals(42, 42)).toBe(true)
380 expect(Equal.equals(42, 43)).toBe(false)
381 })
382
383 it("should work correctly for strings", () => {
384 expect(Equal.equals("hello", "hello")).toBe(true)
385 expect(Equal.equals("hello", "world")).toBe(false)
386 })
387
388 it("should work correctly for booleans", () => {
389 expect(Equal.equals(true, true)).toBe(true)
390 expect(Equal.equals(false, false)).toBe(true)
391 expect(Equal.equals(true, false)).toBe(false)
392 })
393
394 it("should work correctly for null and undefined", () => {
395 expect(Equal.equals(null, null)).toBe(true)
396 expect(Equal.equals(undefined, undefined)).toBe(true)
397 expect(Equal.equals(null, undefined)).toBe(false)
398 })
399 })
400
401 describe("Date objects", () => {
402 it("should compare dates by ISO string", () => {
403 const date1 = new Date("2023-01-01T00:00:00.000Z")
404 const date2 = new Date("2023-01-01T00:00:00.000Z")
405 expect(Equal.equals(date1, date2)).toBe(true)
406 })
407
408 it("should return false for different dates", () => {
409 const date1 = new Date("2023-01-01T00:00:00.000Z")
410 const date2 = new Date("2023-01-02T00:00:00.000Z")
411 expect(Equal.equals(date1, date2)).toBe(false)
412 })
413
414 it("should compare invalid dates without throwing", () => {
415 expect(Equal.equals(new Date(NaN), new Date(NaN))).toBe(true)
416 })
417 })
418
419 describe("DataView objects", () => {
420 it("should compare viewed bytes", () => {
421 const self = new DataView(Uint8Array.of(1).buffer)
422 const that = new DataView(Uint8Array.of(2).buffer)
423 expect(Equal.equals(self, that)).toBe(false)
424 })
425 })
426
427 describe("Effect data structures", () => {
428 it("should work with HashMap (implements Equal interface)", () => {
429 const map1 = HashMap.make(["a", 1], ["b", 2])
430 const map2 = HashMap.make(["a", 1], ["b", 2])
431 expect(Equal.equals(map1, map2)).toBe(true)
432 })
433
434 it("should work with Option (implements Equal interface)", () => {
435 const opt1 = Option.some(42)
436 const opt2 = Option.some(42)
437 expect(Equal.equals(opt1, opt2)).toBe(true)
438 })
439
440 it("HashMap should not equal plain objects", () => {
441 const map = HashMap.make(["a", 1], ["b", 2])
442 const obj = { a: 1, b: 2 }
443 expect(Equal.equals(map, obj)).toBe(false)
444 })
445 })
446
447 describe("custom Equal implementations", () => {
448 class CustomPoint implements Equal.Equal {
449 constructor(readonly x: number, readonly y: number) {}
450
451 [Equal.symbol](that: Equal.Equal): boolean {
452 return that instanceof CustomPoint &&
453 this.x === that.x &&
454 this.y === that.y
455 }
456
457 [Hash.symbol](): number {
458 return 0
459 }
460 }
461
462 it("should work with custom Equal implementation", () => {
463 const point1 = new CustomPoint(1, 2)
464 const point2 = new CustomPoint(1, 2)
465 expect(Equal.equals(point1, point2)).toBe(true)
466 })
467
468 it("clears visited state when a custom Equal throws", () => {
469 let throws = true
470 class Flaky implements Equal.Equal {
471 constructor(readonly id: number) {}
472
473 [Equal.symbol](that: Equal.Equal): boolean {
474 if (throws) throw new Error("boom")
475 return that instanceof Flaky && this.id === that.id
476 }
477
478 [Hash.symbol](): number {
479 return 0
480 }
481 }
482
483 const x = { k: new Flaky(1) }
484 const y = { k: new Flaky(2) }
485 assert.throws(() => Equal.equals(x, y))
486 throws = false
487 assert.isFalse(Equal.equals(x, y))
488 assert.isTrue(Equal.equals(x, { k: new Flaky(1) }))
489 })
490 })
491
492 describe("nested structures", () => {
493 it("should return true for nested objects (deep structural equality)", () => {
494 const obj1 = { a: { b: 1 }, c: [1, 2] }
495 const obj2 = { a: { b: 1 }, c: [1, 2] }
496 expect(Equal.equals(obj1, obj2)).toBe(true)
497 })
498
499 it("should return true for nested arrays (deep structural equality)", () => {
500 const arr1 = [[1, 2], [3, 4]]
501 const arr2 = [[1, 2], [3, 4]]
502 expect(Equal.equals(arr1, arr2)).toBe(true)
503 })
504
505 it("should return false for nested objects with different values", () => {
506 const obj1 = { a: { b: 1 }, c: [1, 2] }
507 const obj2 = { a: { b: 2 }, c: [1, 2] }
508 expect(Equal.equals(obj1, obj2)).toBe(false)
509 })
510
511 it("should return false for nested arrays with different values", () => {
512 const arr1 = [[1, 2], [3, 4]]
513 const arr2 = [[1, 3], [3, 4]]
514 expect(Equal.equals(arr1, arr2)).toBe(false)
515 })
516 })
517
518 describe("special values", () => {
519 describe("NaN", () => {
520 it("should handle NaN correctly (structural equality - NaN equals NaN)", () => {
521 expect(Equal.equals(NaN, NaN)).toBe(true)
522 })
523
524 it("should return false when comparing NaN with regular numbers", () => {
525 expect(Equal.equals(NaN, 0)).toBe(false)
526 expect(Equal.equals(NaN, 42)).toBe(false)
527 expect(Equal.equals(42, NaN)).toBe(false)
528 })
529
530 it("should return false when comparing NaN with Infinity", () => {
531 expect(Equal.equals(NaN, Infinity)).toBe(false)
532 expect(Equal.equals(NaN, -Infinity)).toBe(false)
533 expect(Equal.equals(Infinity, NaN)).toBe(false)
534 expect(Equal.equals(-Infinity, NaN)).toBe(false)
535 })
536
537 it("should handle NaN in arrays", () => {
538 expect(Equal.equals([NaN], [NaN])).toBe(true)
539 expect(Equal.equals([NaN, 1], [NaN, 1])).toBe(true)
540 expect(Equal.equals([NaN, 1], [NaN, 2])).toBe(false)
541 expect(Equal.equals([NaN], [Infinity])).toBe(false)
542 })
543
544 it("should handle NaN in objects", () => {
545 expect(Equal.equals({ a: NaN }, { a: NaN })).toBe(true)
546 expect(Equal.equals({ a: NaN, b: 1 }, { a: NaN, b: 1 })).toBe(true)
547 expect(Equal.equals({ a: NaN }, { a: Infinity })).toBe(false)
548 expect(Equal.equals({ a: NaN }, { a: 42 })).toBe(false)
549 })
550
551 it("should handle NaN in nested structures", () => {
552 expect(Equal.equals({ arr: [NaN] }, { arr: [NaN] })).toBe(true)
553 expect(Equal.equals([{ val: NaN }], [{ val: NaN }])).toBe(true)
554 expect(Equal.equals({ nested: { deep: NaN } }, { nested: { deep: NaN } })).toBe(true)
555 expect(Equal.equals({ arr: [NaN] }, { arr: [Infinity] })).toBe(false)
556 })
557 })
558
559 describe("Infinity", () => {
560 it("should handle Infinity correctly (structural equality)", () => {
561 expect(Equal.equals(Infinity, Infinity)).toBe(true)
562 })
563
564 it("should return false when comparing Infinity with regular numbers", () => {
565 expect(Equal.equals(Infinity, 0)).toBe(false)
566 expect(Equal.equals(Infinity, 42)).toBe(false)
567 expect(Equal.equals(42, Infinity)).toBe(false)
568 expect(Equal.equals(Infinity, Number.MAX_VALUE)).toBe(false)
569 })
570
571 it("should return false when comparing Infinity with -Infinity", () => {
572 expect(Equal.equals(Infinity, -Infinity)).toBe(false)
573 expect(Equal.equals(-Infinity, Infinity)).toBe(false)
574 })
575
576 it("should handle Infinity in arrays", () => {
577 expect(Equal.equals([Infinity], [Infinity])).toBe(true)
578 expect(Equal.equals([Infinity, 1], [Infinity, 1])).toBe(true)
579 expect(Equal.equals([Infinity, 1], [Infinity, 2])).toBe(false)
580 expect(Equal.equals([Infinity], [-Infinity])).toBe(false)
581 })
582
583 it("should handle Infinity in objects", () => {
584 expect(Equal.equals({ a: Infinity }, { a: Infinity })).toBe(true)
585 expect(Equal.equals({ a: Infinity, b: 1 }, { a: Infinity, b: 1 })).toBe(true)
586 expect(Equal.equals({ a: Infinity }, { a: -Infinity })).toBe(false)
587 expect(Equal.equals({ a: Infinity }, { a: 42 })).toBe(false)
588 })
589
590 it("should handle Infinity in nested structures", () => {
591 expect(Equal.equals({ arr: [Infinity] }, { arr: [Infinity] })).toBe(true)
592 expect(Equal.equals([{ val: Infinity }], [{ val: Infinity }])).toBe(true)
593 expect(Equal.equals({ nested: { deep: Infinity } }, { nested: { deep: Infinity } })).toBe(true)
594 expect(Equal.equals({ arr: [Infinity] }, { arr: [-Infinity] })).toBe(false)
595 })
596 })
597
598 describe("-Infinity", () => {
599 it("should handle -Infinity correctly (structural equality)", () => {
600 expect(Equal.equals(-Infinity, -Infinity)).toBe(true)
601 })
602
603 it("should return false when comparing -Infinity with regular numbers", () => {
604 expect(Equal.equals(-Infinity, 0)).toBe(false)
605 expect(Equal.equals(-Infinity, -42)).toBe(false)
606 expect(Equal.equals(-42, -Infinity)).toBe(false)
607 expect(Equal.equals(-Infinity, Number.MIN_VALUE)).toBe(false)
608 })
609
610 it("should handle -Infinity in arrays", () => {
611 expect(Equal.equals([-Infinity], [-Infinity])).toBe(true)
612 expect(Equal.equals([-Infinity, 1], [-Infinity, 1])).toBe(true)
613 expect(Equal.equals([-Infinity, 1], [-Infinity, 2])).toBe(false)
614 expect(Equal.equals([-Infinity], [Infinity])).toBe(false)
615 })
616
617 it("should handle -Infinity in objects", () => {
618 expect(Equal.equals({ a: -Infinity }, { a: -Infinity })).toBe(true)
619 expect(Equal.equals({ a: -Infinity, b: 1 }, { a: -Infinity, b: 1 })).toBe(true)
620 expect(Equal.equals({ a: -Infinity }, { a: Infinity })).toBe(false)
621 expect(Equal.equals({ a: -Infinity }, { a: -42 })).toBe(false)
622 })
623
624 it("should handle -Infinity in nested structures", () => {
625 expect(Equal.equals({ arr: [-Infinity] }, { arr: [-Infinity] })).toBe(true)
626 expect(Equal.equals([{ val: -Infinity }], [{ val: -Infinity }])).toBe(true)
627 expect(Equal.equals({ nested: { deep: -Infinity } }, { nested: { deep: -Infinity } })).toBe(true)
628 expect(Equal.equals({ arr: [-Infinity] }, { arr: [Infinity] })).toBe(false)
629 })
630 })
631
632 describe("mixed special values", () => {
633 it("should handle arrays with multiple special values", () => {
634 expect(Equal.equals([NaN, Infinity, -Infinity], [NaN, Infinity, -Infinity])).toBe(true)
635 expect(Equal.equals([NaN, Infinity], [Infinity, NaN])).toBe(false) // order matters
636 expect(Equal.equals([NaN, Infinity, 0], [NaN, Infinity, 0])).toBe(true)
637 })
638
639 it("should handle objects with multiple special values", () => {
640 expect(Equal.equals(
641 { nan: NaN, inf: Infinity, negInf: -Infinity },
642 { nan: NaN, inf: Infinity, negInf: -Infinity }
643 )).toBe(true)
644 expect(Equal.equals(
645 { nan: NaN, inf: Infinity },
646 { nan: NaN, inf: -Infinity }
647 )).toBe(false)
648 })
649
650 it("should handle complex nested structures with special values", () => {
651 const obj1 = {
652 data: [
653 { value: NaN, meta: { type: "special" } },
654 { value: Infinity, meta: { type: "infinite" } },
655 { value: -Infinity, meta: { type: "negative-infinite" } }
656 ]
657 }
658 const obj2 = {
659 data: [
660 { value: NaN, meta: { type: "special" } },
661 { value: Infinity, meta: { type: "infinite" } },
662 { value: -Infinity, meta: { type: "negative-infinite" } }
663 ]
664 }
665 const obj3 = {
666 data: [
667 { value: NaN, meta: { type: "special" } },
668 { value: -Infinity, meta: { type: "infinite" } }, // swapped values
669 { value: Infinity, meta: { type: "negative-infinite" } }
670 ]
671 }
672
673 expect(Equal.equals(obj1, obj2)).toBe(true)
674 expect(Equal.equals(obj1, obj3)).toBe(false)
675 })
676 })
677
678 describe("zero values", () => {
679 it("should handle -0 and +0 correctly", () => {
680 expect(Equal.equals(-0, +0)).toBe(true)
681 expect(Equal.equals(0, -0)).toBe(true)
682 expect(Equal.equals(+0, -0)).toBe(true)
683 })
684
685 it("should handle zero values in arrays", () => {
686 expect(Equal.equals([-0], [+0])).toBe(true)
687 expect(Equal.equals([0], [-0])).toBe(true)
688 expect(Equal.equals([-0, 1], [+0, 1])).toBe(true)
689 })
690
691 it("should handle zero values in objects", () => {
692 expect(Equal.equals({ a: -0 }, { a: +0 })).toBe(true)
693 expect(Equal.equals({ a: 0 }, { a: -0 })).toBe(true)
694 })
695 })
696 })
697
698 describe("JavaScript Map", () => {
699 it("should return true for structurally identical maps", () => {
700 const map1 = new Map([["a", 1], ["b", 2]])
701 const map2 = new Map([["a", 1], ["b", 2]])
702 const map3 = new Map([["b", 2], ["a", 1]])
703 expect(Equal.equals(map1, map2)).toBe(true)
704 expect(Equal.equals(map1, map3)).toBe(true)
705 })
706
707 it("should return true for same reference", () => {
708 const map = new Map([["a", 1]])
709 expect(Equal.equals(map, map)).toBe(true)
710 })
711
712 it("should return true for empty maps", () => {
713 expect(Equal.equals(new Map(), new Map())).toBe(true)
714 })
715
716 it("should return false for maps with different values", () => {
717 const map1 = new Map([["a", 1], ["b", 2]])
718 const map2 = new Map([["a", 1], ["b", 3]])
719 expect(Equal.equals(map1, map2)).toBe(false)
720 })
721
722 it("should return false for maps with different keys", () => {
723 const map1 = new Map([["a", 1], ["b", 2]])
724 const map2 = new Map([["a", 1], ["c", 2]])
725 expect(Equal.equals(map1, map2)).toBe(false)
726 })
727
728 it("should return false for maps with different sizes", () => {
729 const map1 = new Map([["a", 1], ["b", 2]])
730 const map2 = new Map([["a", 1]])
731 expect(Equal.equals(map1, map2)).toBe(false)
732 })
733
734 it("should handle maps with object keys", () => {
735 const key1 = { id: 1 }
736 const key2 = { id: 1 }
737 const map1 = new Map()
738 map1.set(key1, "value1")
739 map1.set({ nested: { x: 1 } }, "value2")
740
741 const map2 = new Map()
742 map2.set(key2, "value1")
743 map2.set({ nested: { x: 1 } }, "value2")
744
745 expect(Equal.equals(map1, map2)).toBe(true)
746 })
747
748 it("should handle maps with object values", () => {
749 const map1 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]])
750 const map2 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]])
751 expect(Equal.equals(map1, map2)).toBe(true)
752 })
753
754 it("should handle nested maps", () => {
755 const inner1 = new Map([["x", 1]])
756 const inner2 = new Map([["x", 1]])
757 const map1 = new Map([["nested", inner1]])
758 const map2 = new Map([["nested", inner2]])
759 expect(Equal.equals(map1, map2)).toBe(true)
760 })
761
762 it("should handle maps with special values", () => {
763 const map1 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]])
764 const map2 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]])
765 expect(Equal.equals(map1, map2)).toBe(true)
766 })
767 })
768
769 describe("JavaScript Set", () => {
770 it("should return true for structurally identical sets", () => {
771 const set1 = new Set([1, 2, 3])
772 const set2 = new Set([1, 2, 3])
773 expect(Equal.equals(set1, set2)).toBe(true)
774 })
775
776 it("should return true for same reference", () => {
777 const set = new Set([1, 2, 3])
778 expect(Equal.equals(set, set)).toBe(true)
779 })
780
781 it("should return true for empty sets", () => {
782 expect(Equal.equals(new Set(), new Set())).toBe(true)
783 })
784
785 it("should return false for sets with different elements", () => {
786 const set1 = new Set([1, 2, 3])
787 const set2 = new Set([1, 2, 4])
788 expect(Equal.equals(set1, set2)).toBe(false)
789 })
790
791 it("should return false for sets with different sizes", () => {
792 const set1 = new Set([1, 2, 3])
793 const set2 = new Set([1, 2])
794 expect(Equal.equals(set1, set2)).toBe(false)
795 })
796
797 it("should return true for sets with same elements in different insertion order", () => {
798 const set1 = new Set([1, 2, 3])
799 const set2 = new Set([3, 1, 2])
800 expect(Equal.equals(set1, set2)).toBe(true)
801 })
802
803 it("should handle sets with object elements", () => {
804 const set1 = new Set([{ x: 1 }, { y: 2 }])
805 const set2 = new Set([{ x: 1 }, { y: 2 }])
806 expect(Equal.equals(set1, set2)).toBe(true)
807 })
808
809 it("should handle nested sets", () => {
810 const inner1 = new Set([1, 2])
811 const inner2 = new Set([1, 2])
812 const set1 = new Set([inner1, "other"])
813 const set2 = new Set([inner2, "other"])
814 expect(Equal.equals(set1, set2)).toBe(true)
815 })
816
817 it("should handle sets with special values", () => {
818 const set1 = new Set([NaN, Infinity, -Infinity])
819 const set2 = new Set([NaN, Infinity, -Infinity])
820 expect(Equal.equals(set1, set2)).toBe(true)
821 })
822
823 it("should handle sets with array elements", () => {
824 const set1 = new Set([[1, 2], [3, 4]])
825 const set2 = new Set([[1, 2], [3, 4]])
826 expect(Equal.equals(set1, set2)).toBe(true)
827 })
828 })
829
830 describe("Map and Set mixed", () => {
831 it("matches Map and Set entries one-to-one", () => {
832 const setResult = Equal.equals(
833 new Set([new Key("x"), new Key("x")]),
834 new Set([new Key("x"), new Key("y")])
835 )
836 const mapResult = Equal.equals(
837 new Map([[new Key("x"), 1], [new Key("x"), 1]]),
838 new Map([[new Key("x"), 1], [new Key("y"), 1]])
839 )
840
841 assert.deepStrictEqual([setResult, mapResult], [false, false])
842 })
843
844 it("matches colliding Map and Set entries independent of insertion order", () => {
845 expect(Equal.equals(
846 new Set([new Key("x"), new Key("y")]),
847 new Set([new Key("y"), new Key("x")])
848 )).toBe(true)
849 expect(Equal.equals(
850 new Map([[new Key("x"), 1], [new Key("y"), 2]]),
851 new Map([[new Key("y"), 2], [new Key("x"), 1]])
852 )).toBe(true)
853 })
854
855 it("matches cyclic values in colliding Map entries", () => {
856 const left: { self?: unknown } = {}
857 const right: { self?: unknown } = {}
858 left.self = left
859 right.self = right
860
861 expect(Equal.equals(
862 new Map<Key, unknown>([[new Key("x"), left], [new Key("y"), 1]]),
863 new Map<Key, unknown>([[new Key("y"), 1], [new Key("x"), right]])
864 )).toBe(true)
865 })
866
867 it("should handle objects containing maps and sets", () => {
868 const obj1 = {
869 map: new Map([["a", 1], ["b", 2]]),
870 set: new Set([1, 2, 3]),
871 array: [new Map([["x", 1]]), new Set([4, 5])]
872 }
873 const obj2 = {
874 map: new Map([["a", 1], ["b", 2]]),
875 set: new Set([1, 2, 3]),
876 array: [new Map([["x", 1]]), new Set([4, 5])]
877 }
878 expect(Equal.equals(obj1, obj2)).toBe(true)
879 })
880
881 it("should return false when Map/Set differ in nested structures", () => {
882 const obj1 = {
883 map: new Map([["a", 1], ["b", 2]]),
884 set: new Set([1, 2, 3])
885 }
886 const obj2 = {
887 map: new Map([["a", 1], ["b", 3]]), // different value
888 set: new Set([1, 2, 3])
889 }
890 expect(Equal.equals(obj1, obj2)).toBe(false)
891 })
892 })
893
894 describe("byReference", () => {
895 it("should allow objects to opt out of structural equality", () => {
896 const obj1 = { a: 1, b: 2 }
897 const obj2 = { a: 1, b: 2 }
898
899 // Normal structural equality
900 expect(Equal.equals(obj1, obj2)).toBe(true)
901
902 // Create instance equality version of obj1
903 const obj1Instance = Equal.byReference(obj1)
904 expect(Equal.equals(obj1Instance, obj2)).toBe(false)
905 expect(Equal.equals(obj1Instance, obj1Instance)).toBe(true)
906 })
907
908 it("should work with arrays", () => {
909 const arr1 = [1, 2, 3]
910 const arr2 = [1, 2, 3]
911
912 // Normal structural equality
913 expect(Equal.equals(arr1, arr2)).toBe(true)
914
915 // Create instance equality version of arr1
916 const arr1Instance = Equal.byReference(arr1)
917 expect(Equal.equals(arr1Instance, arr2)).toBe(false)
918 expect(Equal.equals(arr1Instance, arr1Instance)).toBe(true)
919 })
920
921 it("should work with Maps", () => {
922 const map1 = new Map([["a", 1]])
923 const map2 = new Map([["a", 1]])
924
925 // Normal structural equality
926 expect(Equal.equals(map1, map2)).toBe(true)
927
928 // Create instance equality version of map1
929 const map1Instance = Equal.byReference(map1)
930 expect(Equal.equals(map1Instance, map2)).toBe(false)
931 expect(Equal.equals(map1Instance, map1Instance)).toBe(true)
932 })
933
934 it("should work with Sets", () => {
935 const set1 = new Set([1, 2])
936 const set2 = new Set([1, 2])
937
938 // Normal structural equality
939 expect(Equal.equals(set1, set2)).toBe(true)
940
941 // Create instance equality version of set1
942 const set1Instance = Equal.byReference(set1)
943 expect(Equal.equals(set1Instance, set2)).toBe(false)
944 expect(Equal.equals(set1Instance, set1Instance)).toBe(true)
945 })
946
947 it("should return a proxy that behaves like the original object", () => {
948 const obj = { a: 1 }
949 const result = Equal.byReference(obj)
950
951 // The result should not be the same reference (it's a proxy)
952 expect(result).not.toBe(obj)
953
954 // But it should behave the same way
955 expect(result.a).toBe(1)
956
957 // And should support property access
958 expect(result.a).toBe(obj.a)
959 })
960
961 it("should work when either object is marked for instance equality", () => {
962 const obj1 = { a: 1, b: 2 }
963 const obj2 = { a: 1, b: 2 }
964
965 // Create instance equality version of obj2
966 const obj2Instance = Equal.byReference(obj2)
967 expect(Equal.equals(obj1, obj2Instance)).toBe(false)
968 expect(Equal.equals(obj2Instance, obj1)).toBe(false)
969 })
970
971 it("should work with nested structures", () => {
972 const obj1 = { a: { b: 1 }, c: [1, 2] }
973 const obj2 = { a: { b: 1 }, c: [1, 2] }
974
975 // Normal structural equality
976 expect(Equal.equals(obj1, obj2)).toBe(true)
977
978 // Create instance equality version of obj1
979 const obj1Instance = Equal.byReference(obj1)
980 expect(Equal.equals(obj1Instance, obj2)).toBe(false)
981 })
982 })
983
984 describe("byReferenceUnsafe", () => {
985 it("uses reference hashing after a structural hash was cached", () => {
986 const obj = { value: 1 }
987 const structuralHash = Hash.structure(obj)
988 assert.notStrictEqual(structuralHash, 0)
989 assert.strictEqual(Hash.hash(obj), structuralHash)
990
991 const random = vi.spyOn(Math, "random").mockReturnValue(0)
992 try {
993 Equal.byReferenceUnsafe(obj)
994 assert.strictEqual(Hash.hash(obj), 0)
995 } finally {
996 random.mockRestore()
997 }
998 })
999
1000 it("should allow objects to opt out of structural equality without proxy", () => {
1001 const obj1 = { a: 1, b: 2 }
1002 const obj2 = { a: 1, b: 2 }
1003
1004 // Mark obj1 for reference equality (modifies obj1 directly)
1005 const obj1ByRef = Equal.byReferenceUnsafe(obj1)
1006 expect(obj1ByRef).toBe(obj1) // Same object, no proxy created
1007 expect(Equal.equals(obj1ByRef, obj2)).toBe(false) // uses reference equality
1008 expect(Equal.equals(obj1ByRef, obj1ByRef)).toBe(true) // same reference
1009
1010 // The original obj1 now uses reference equality
1011 expect(Equal.equals(obj1, obj2)).toBe(false) // obj1 uses reference equality
1012 })
1013
1014 it("should work with arrays", () => {
1015 const arr1 = [1, 2, 3]
1016 const arr2 = [1, 2, 3]
1017
1018 // Mark arr1 for reference equality
1019 const arr1ByRef = Equal.byReferenceUnsafe(arr1)
1020 expect(arr1ByRef).toBe(arr1) // Same array, no proxy
1021 expect(Equal.equals(arr1ByRef, arr2)).toBe(false)
1022 expect(Equal.equals(arr1ByRef, arr1ByRef)).toBe(true)
1023 })
1024
1025 it("should work with Maps", () => {
1026 const map1 = new Map([["a", 1]])
1027 const map2 = new Map([["a", 1]])
1028
1029 // Mark map1 for reference equality
1030 const map1ByRef = Equal.byReferenceUnsafe(map1)
1031 expect(map1ByRef).toBe(map1) // Same map, no proxy
1032 expect(Equal.equals(map1ByRef, map2)).toBe(false)
1033 expect(Equal.equals(map1ByRef, map1ByRef)).toBe(true)
1034 })
1035
1036 it("should work with Sets", () => {
1037 const set1 = new Set([1, 2])
1038 const set2 = new Set([1, 2])
1039
1040 // Mark set1 for reference equality
1041 const set1ByRef = Equal.byReferenceUnsafe(set1)
1042 expect(set1ByRef).toBe(set1) // Same set, no proxy
1043 expect(Equal.equals(set1ByRef, set2)).toBe(false)
1044 expect(Equal.equals(set1ByRef, set1ByRef)).toBe(true)
1045 })
1046
1047 it("should permanently change object equality behavior", () => {
1048 const obj1 = { a: 1, b: 2 }
1049 const obj2 = { a: 1, b: 2 }
1050
1051 // Mark obj1 for reference equality
1052 Equal.byReferenceUnsafe(obj1)
1053
1054 // Now obj1 permanently uses reference equality
1055 expect(Equal.equals(obj1, obj2)).toBe(false)
1056 expect(Equal.equals(obj1, obj1)).toBe(true)
1057 })
1058
1059 it("should work when either object is marked for reference equality", () => {
1060 const obj1 = { a: 1, b: 2 }
1061 const obj2 = { a: 1, b: 2 }
1062
1063 // Mark obj2 for reference equality
1064 Equal.byReferenceUnsafe(obj2)
1065 expect(Equal.equals(obj1, obj2)).toBe(false)
1066 expect(Equal.equals(obj2, obj1)).toBe(false)
1067 })
1068
1069 it("should work with nested structures", () => {
1070 const obj1 = { a: { b: 1 }, c: [1, 2] }
1071 const obj2 = { a: { b: 1 }, c: [1, 2] }
1072
1073 // Mark obj1 for reference equality
1074 Equal.byReferenceUnsafe(obj1)
1075 expect(Equal.equals(obj1, obj2)).toBe(false)
1076 })
1077 })
1078
1079 describe("Error objects", () => {
1080 it("should return true for Error objects with same message and additional properties", () => {
1081 const error1 = new Error("test message")
1082 ;(error1 as any).code = "TEST_CODE"
1083 ;(error1 as any).details = { info: "additional details" }
1084
1085 const error2 = new Error("test message")
1086 ;(error2 as any).code = "TEST_CODE"
1087 ;(error2 as any).details = { info: "additional details" }
1088
1089 // Error objects are now structurally comparable (stack is excluded)
1090 expect(Equal.equals(error1, error2)).toBe(true)
1091 })
1092
1093 it("should return false for Error objects with different messages", () => {
1094 const error1 = new Error("message 1")
1095 ;(error1 as any).code = "TEST_CODE"
1096
1097 const error2 = new Error("message 2")
1098 ;(error2 as any).code = "TEST_CODE"
1099
1100 expect(Equal.equals(error1, error2)).toBe(false)
1101 })
1102
1103 it("should work with custom Error classes", () => {
1104 class CustomError extends Error {
1105 readonly code: string
1106 constructor(message: string, code: string) {
1107 super(message)
1108 this.name = "CustomError"
1109 this.code = code
1110 }
1111 }
1112
1113 const error1 = new CustomError("test", "CUSTOM")
1114 const error2 = new CustomError("test", "CUSTOM")
1115
1116 // Custom Error objects are now structurally comparable (stack is excluded)
1117 expect(Equal.equals(error1, error2)).toBe(true)
1118 })
1119
1120 it("should ignore stack traces when comparing Error objects", () => {
1121 function createErrorAtDifferentLocation1() {
1122 return new Error("same message")
1123 }
1124
1125 function createErrorAtDifferentLocation2() {
1126 return new Error("same message")
1127 }
1128
1129 const error1 = createErrorAtDifferentLocation1()
1130 const error2 = createErrorAtDifferentLocation2()
1131
1132 // Even though stack traces are different, errors should be equal
1133 expect(error1.stack).not.toBe(error2.stack)
1134 expect(Equal.equals(error1, error2)).toBe(true)
1135 })
1136 })
1137
1138 describe("objects with different constructors", () => {
1139 it("should return true for objects with different constructors but same shape", () => {
1140 class CustomClass {
1141 readonly a: number
1142 readonly b: number
1143 constructor() {
1144 this.a = 1
1145 this.b = 2
1146 }
1147 }
1148
1149 const plainObject = { a: 1, b: 2 }
1150 const customObject = new CustomClass()
1151 expect(Equal.equals(plainObject, customObject)).toBe(true)
1152 })
1153
1154 it("should return true for objects with null prototype", () => {
1155 const nullProtoObject: any = Object.create(null)
1156 nullProtoObject.a = 1
1157 nullProtoObject.b = 2
1158
1159 const plainObject = { a: 1, b: 2 }
1160 expect(Equal.equals(nullProtoObject, plainObject)).toBe(true)
1161 })
1162
1163 it("should return true for objects with different function constructors", () => {
1164 function Constructor1(this: any) {
1165 this.a = 1
1166 this.b = 2
1167 }
1168
1169 function Constructor2(this: any) {
1170 this.a = 1
1171 this.b = 2
1172 }
1173
1174 const obj1 = new (Constructor1 as any)()
1175 const obj2 = new (Constructor2 as any)()
1176 expect(Equal.equals(obj1, obj2)).toBe(true)
1177 })
1178
1179 it("should return false when properties differ regardless of constructor", () => {
1180 class CustomClass1 {
1181 readonly a: number
1182 readonly b: number
1183 constructor() {
1184 this.a = 1
1185 this.b = 2
1186 }
1187 }
1188
1189 class CustomClass2 {
1190 readonly a: number
1191 readonly b: number
1192 constructor() {
1193 this.a = 1
1194 this.b = 3 // Different value
1195 }
1196 }
1197
1198 const obj1 = new CustomClass1()
1199 const obj2 = new CustomClass2()
1200 expect(Equal.equals(obj1, obj2)).toBe(false)
1201 })
1202
1203 it("should handle mixed constructor types in nested structures", () => {
1204 class CustomClass {
1205 readonly inner: { x: number }
1206 constructor() {
1207 this.inner = { x: 1 }
1208 }
1209 }
1210
1211 const obj1 = { data: new CustomClass() }
1212 const obj2 = { data: { inner: { x: 1 } } }
1213 expect(Equal.equals(obj1, obj2)).toBe(true)
1214 })
1215
1216 it("should include constructor property when it has a meaningful user-defined value", () => {
1217 const obj1 = { constructor: 10, a: 1 }
1218 const obj2 = { constructor: 10, a: 1 }
1219 const obj3 = { constructor: 20, a: 1 }
1220
1221 // Objects with same constructor value should be equal
1222 expect(Equal.equals(obj1, obj2)).toBe(true)
1223
1224 // Objects with different constructor values should not be equal
1225 expect(Equal.equals(obj1, obj3)).toBe(false)
1226 expect(Equal.equals(obj2, obj3)).toBe(false)
1227 })
1228
1229 it("should include constructor property when it has a meaningful user-defined value from a prototype", () => {
1230 const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 })
1231 const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 })
1232 const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: 20 })
1233
1234 // Objects with same constructor value should be equal
1235 expect(Equal.equals(obj1, obj2)).toBe(true)
1236
1237 // Objects with different constructor values should not be equal
1238 expect(Equal.equals(obj1, obj3)).toBe(false)
1239 expect(Equal.equals(obj2, obj3)).toBe(false)
1240 })
1241
1242 it("should include constructor property when it has a meaningful user-defined value from a prototype when it is a function", () => {
1243 const f = () => 10
1244 const g = () => 10
1245 const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: f })
1246 const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: f })
1247 const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: g })
1248
1249 // Objects with same constructor value should be equal
1250 expect(Equal.equals(obj1, obj2)).toBe(true)
1251
1252 // Objects with different constructor values should not be equal
1253 expect(Equal.equals(obj1, obj3)).toBe(false)
1254 expect(Equal.equals(obj2, obj3)).toBe(false)
1255 })
1256
1257 it("should still ignore default constructor property", () => {
1258 const obj1 = { a: 1, b: 2 }
1259 const obj2 = { a: 1, b: 2 }
1260
1261 // Normal objects should still be equal (constructor property ignored when it's the default)
1262 expect(Equal.equals(obj1, obj2)).toBe(true)
1263
1264 // Verify that obj1.constructor and obj2.constructor are the default Object constructor
1265 expect(obj1.constructor).toBe(Object)
1266 expect(obj2.constructor).toBe(Object)
1267 })
1268 })
1269
1270 describe("recursive objects", () => {
1271 it("should handle circular references in Equal implementations without infinite recursion", () => {
1272 class CircularEqualTest implements Equal.Equal {
1273 constructor(readonly value: string, public child: CircularEqualTest | null = null) {}
1274
1275 [Equal.symbol](that: Equal.Equal): boolean {
1276 if (!(that instanceof CircularEqualTest)) {
1277 return false
1278 }
1279
1280 // Circular references are handled automatically by the system
1281 // This implementation can safely call Equal.equals without manual tracking
1282 return this.value === that.value && Equal.equals(this.child, that.child)
1283 }
1284
1285 [Hash.symbol](): number {
1286 return Hash.string(this.value)
1287 }
1288 }
1289
1290 // Test case 1: Self-referencing objects
1291 const obj1 = new CircularEqualTest("test")
1292 obj1.child = obj1
1293
1294 const obj2 = new CircularEqualTest("test")
1295 obj2.child = obj2
1296
1297 // This should not cause infinite recursion
1298 expect(Equal.equals(obj1, obj2)).toBe(true)
1299
1300 // Test case 2: Different values should not be equal even with same structure
1301 const obj3 = new CircularEqualTest("different")
1302 obj3.child = obj3
1303
1304 expect(Equal.equals(obj1, obj3)).toBe(false)
1305 })
1306
1307 it("should handle complex circular references in Equal implementations", () => {
1308 class ComplexEqualTest implements Equal.Equal {
1309 constructor(readonly id: number, public ref: ComplexEqualTest | null = null) {}
1310
1311 [Equal.symbol](that: Equal.Equal): boolean {
1312 if (!(that instanceof ComplexEqualTest)) {
1313 return false
1314 }
1315
1316 // Circular references are handled automatically by the system
1317 return this.id === that.id && Equal.equals(this.ref, that.ref)
1318 }
1319
1320 [Hash.symbol](): number {
1321 return Hash.number(this.id)
1322 }
1323 }
1324
1325 // Create cycle: obj1 -> obj2 -> obj1
1326 const obj1 = new ComplexEqualTest(1)
1327 const obj2 = new ComplexEqualTest(2)
1328 obj1.ref = obj2
1329 obj2.ref = obj1
1330
1331 // Create identical cycle: obj3 -> obj4 -> obj3
1332 const obj3 = new ComplexEqualTest(1)
1333 const obj4 = new ComplexEqualTest(2)
1334 obj3.ref = obj4
1335 obj4.ref = obj3
1336
1337 // Should not cause infinite recursion and should be equal
1338 expect(Equal.equals(obj1, obj3)).toBe(true)
1339 expect(Equal.equals(obj2, obj4)).toBe(true)
1340 })
1341
1342 it("should expose visited sets for advanced use cases", () => {
1343 // Most implementations don't need manual tracking, but the sets are exposed
1344 // for advanced scenarios where integration with the tracking system is needed
1345
1346 class SimpleContainer implements Equal.Equal {
1347 constructor(readonly name: string, readonly items: Array<SimpleContainer> = []) {}
1348
1349 [Equal.symbol](that: Equal.Equal): boolean {
1350 if (!(that instanceof SimpleContainer)) {
1351 return false
1352 }
1353
1354 // Simple implementation - no manual tracking needed
1355 // Circular references are handled automatically
1356 return this.name === that.name &&
1357 this.items.length === that.items.length &&
1358 this.items.every((item, i) => Equal.equals(item, that.items[i]))
1359 }
1360
1361 [Hash.symbol](): number {
1362 // Simple hash implementation - circular references are handled automatically
1363 let result = Hash.string(this.name)
1364 for (const item of this.items) {
1365 result = Hash.combine(result, Hash.hash(item))
1366 }
1367 return result
1368 }
1369 }
1370
1371 // Test that automatic circular reference handling works
1372 const container1 = new SimpleContainer("A")
1373 container1.items.push(container1) // Self-reference
1374
1375 const container2 = new SimpleContainer("A")
1376 container2.items.push(container2) // Self-reference
1377
1378 // Should not cause infinite recursion due to automatic tracking
1379 expect(Equal.equals(container1, container2)).toBe(true)
1380 })
1381
1382 it("should handle objects with circular references without infinite recursion", () => {
1383 // Create objects that reference themselves
1384 const obj1: any = { a: 1, b: 2 }
1385 obj1.self = obj1
1386
1387 const obj2: any = { a: 1, b: 2 }
1388 obj2.self = obj2
1389
1390 // This should not throw due to stack overflow
1391 expect(() => Equal.equals(obj1, obj2)).not.toThrow()
1392 expect(Equal.equals(obj1, obj2)).toBe(true)
1393 })
1394
1395 it("should handle arrays with circular references without infinite recursion", () => {
1396 // Create arrays that reference themselves
1397 const arr1: any = [1, 2, 3]
1398 arr1.push(arr1)
1399
1400 const arr2: any = [1, 2, 3]
1401 arr2.push(arr2)
1402
1403 // This should not throw due to stack overflow
1404 expect(() => Equal.equals(arr1, arr2)).not.toThrow()
1405 expect(Equal.equals(arr1, arr2)).toBe(true)
1406 })
1407
1408 it("should handle Uint8Array", () => {
1409 const arr1: Uint8Array = new Uint8Array([1, 2, 3])
1410 const arr2: Uint8Array = new Uint8Array([1, 2, 3])
1411 expect(Equal.equals(arr1, arr2)).toBe(true)
1412 })
1413
1414 it("should handle mixed circular references between objects and arrays", () => {
1415 // Create complex circular structure
1416 const obj1: any = { type: "object", items: [] }
1417 const arr1: any = [1, 2, obj1]
1418 obj1.items = arr1
1419
1420 const obj2: any = { type: "object", items: [] }
1421 const arr2: any = [1, 2, obj2]
1422 obj2.items = arr2
1423
1424 // This should not throw due to stack overflow
1425 expect(() => Equal.equals(obj1, obj2)).not.toThrow()
1426 expect(Equal.equals(obj1, obj2)).toBe(true)
1427 })
1428
1429 it("should handle hashing objects with circular references without infinite recursion", () => {
1430 // Create object that references itself
1431 const obj: any = { a: 1, b: 2 }
1432 obj.self = obj
1433
1434 // This should not throw due to stack overflow
1435 expect(() => Hash.hash(obj)).not.toThrow()
1436
1437 // Hash should be consistent
1438 const hash1 = Hash.hash(obj)
1439 const hash2 = Hash.hash(obj)
1440 expect(hash1).toBe(hash2)
1441 })
1442
1443 it("should handle hashing arrays with circular references without infinite recursion", () => {
1444 // Create array that references itself
1445 const arr: any = [1, 2, 3]
1446 arr.push(arr)
1447
1448 // This should not throw due to stack overflow
1449 expect(() => Hash.hash(arr)).not.toThrow()
1450
1451 // Hash should be consistent
1452 const hash1 = Hash.hash(arr)
1453 const hash2 = Hash.hash(arr)
1454 expect(hash1).toBe(hash2)
1455 })
1456
1457 it("should produce different hashes for objects with different circular structures", () => {
1458 const obj1: any = { a: 1, b: 2 }
1459 obj1.self = obj1
1460
1461 const obj2: any = { a: 1, b: 3 } // Different value
1462 obj2.self = obj2
1463
1464 // Should not throw and should produce different hashes
1465 expect(() => Hash.hash(obj1)).not.toThrow()
1466 expect(() => Hash.hash(obj2)).not.toThrow()
1467 expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
1468 })
1469
1470 it("should return false for objects with different circular reference patterns", () => {
1471 // Object that references itself
1472 const obj1: any = { a: 1, b: 2 }
1473 obj1.self = obj1
1474
1475 // Object that references a different object
1476 const obj2: any = { a: 1, b: 2 }
1477 const obj3: any = { a: 1, b: 2 }
1478 obj2.self = obj3
1479
1480 // These should be considered different
1481 expect(() => Equal.equals(obj1, obj2)).not.toThrow()
1482 expect(Equal.equals(obj1, obj2)).toBe(false)
1483 })
1484 })
1485})