squint

borkdude 2025-10-06T16:27:38.524769Z

I did some extra digging for a deepEquals library and found dequal. Compared to es-toolkit/isEqual it yields an even smaller build (1kb vs 3kb before gzip, after the ratio is similar-ish). ๐Ÿงต

borkdude 2025-10-06T16:28:04.865469Z

borkdude@m1-5 /tmp $ npx esbuild --bundle --minify --format=esm --target=es2020 --outfile=out.js <<< "import { isEqual } from 'es-toolkit'; console.log(isEqual({}, {}))";
gzip -c out.js | wc -c

  out.js  3.1kb

โšก Done in 25ms
    1259
borkdude@m1-5 /tmp $ npm install dequal

added 1 package, and audited 3 packages in 504ms

found 0 vulnerabilities
borkdude@m1-5 /tmp $ npx esbuild --bundle --minify --format=esm --target=es2020 --outfile=out.js <<< "import { dequal } from 'dequal'; console.log(dequal({}, {}))";
gzip -c out.js | wc -c

  out.js  1.0kb

โšก Done in 11ms
     503
I also did a benchmark for performance:
import { dequal } from 'dequal';
import { isEqual } from 'es-toolkit';
import Benchmark from 'benchmark';

// Test data
const obj1 = {
  name: 'Alice',
  age: 30,
  hobbies: ['reading', 'cycling', 'hiking'],
  meta: { active: true, score: 42 },
  nested: Array.from({ length: 1000 }, (_, i) => ({ id: i, v: Math.random() })),
};
const obj2 = structuredClone(obj1);

const suite = new Benchmark.Suite();

suite
  .add('dequal', () => {
    dequal(obj1, obj2);
  })
  .add('es-toolkit/isEqual', () => {
    isEqual(obj1, obj2);
  })
  .on('cycle', (event) => {
    console.log(String(event.target));
  })
  .on('complete', function () {
    console.log('\nFastest is ' + this.filter('fastest').map('name'));
  })
  .run({ async: true });
$ node bench.mjs
dequal x 13,219 ops/sec ยฑ0.20% (95 runs sampled)
es-toolkit/isEqual x 3,045 ops/sec ยฑ0.32% (98 runs sampled)

Fastest is dequal
My thoughts: perhaps we can switch eucalypt to dequal. And if it works well, we could maybe even adopt dequal for = in squint for structural equality... cc @chris358

๐Ÿ’ฏ 2
๐Ÿ˜ 3
Chris McCormick 2025-10-07T11:00:56.916199Z

All for it!

Chris McCormick 2025-10-07T14:12:05.153099Z

isEqual:

../../docs/index.html  42.78 kB โ”‚ gzip: 15.16 kB
../../docs/small.html  27.97 kB โ”‚ gzip: 9.76 kB
../../docs/games.html  34.64 kB โ”‚ gzip: 12.06 kB
dequal:
../../docs/index.html  40.59 kB โ”‚ gzip: 14.43 kB
../../docs/small.html  25.78 kB โ”‚ gzip: 9.01 kB
../../docs/games.html  35.70 kB โ”‚ gzip: 12.43 kB
Works as a drop-in replacement. All tests pass.

Chris McCormick 2025-10-07T14:12:56.254089Z

Since dequal says it can compare functions there is a complicated bit of the code that may be able to be replaced where it has to assign functions IDs to see if they changed. I wonder if I can just get rid of that now. ๐Ÿค”

Chris McCormick 2025-10-07T14:21:59.224189Z

Once I replaced the isEquals in the games.html demos:

../../docs/games.html  32.45 kB โ”‚ gzip: 11.33 kB

borkdude 2025-10-07T14:45:12.282599Z

ok, a minor decrease

borkdude 2025-10-07T14:45:31.223209Z

function comparison: that's weird, I don't think it should work outside of just object identity

borkdude 2025-10-07T14:49:42.000159Z

yeah, it's just doing object identity: https://github.com/lukeed/dequal/blob/37c21f675c1f538f2d4b63ebf19a161411e7a5fd/test/index.js#L171-L173

๐Ÿ‘ 1
Chris McCormick 2025-10-07T14:49:44.355819Z

Yeah the function comparison looks like a dead end. I think the minor decrease is worth switching to dequals. I also like that it is a standalone library not part of some larger thing.

borkdude 2025-10-07T14:50:17.162029Z

so if I would inline dequal into squint and switch = to it, you could then just use = from squint. I wonder if this is a good idea.

๐Ÿ‘€ 1
borkdude 2025-10-07T14:50:59.425429Z

my brain then went to the next step: I could make a HashMap type in Squint that derives from Map and uses hashing + collision solution using dequal

borkdude 2025-10-07T14:51:04.663629Z

also Set

borkdude 2025-10-07T14:51:54.110109Z

I will make a PR to squint just to see how it feels. It would bloat "minimal" apps with this impl though.

borkdude 2025-10-07T14:52:03.318069Z

unless you avoid = with identical?

Chris McCormick 2025-10-07T14:52:41.534349Z

You mean update Eucalypt to use identical?

borkdude 2025-10-07T14:52:47.150299Z

no!

borkdude 2025-10-07T14:52:57.526229Z

I meant "unless one avoided"

borkdude 2025-10-07T14:53:14.809819Z

for small stuff that doesn't need deep equals

๐Ÿ‘ 1
borkdude 2025-10-07T14:54:59.850829Z

oh here is a gotcha:

- keys within Maps use value equality

borkdude 2025-10-07T14:55:19.340529Z

(I don't think it would matter for eucalypt, just thinking more broadly)

borkdude 2025-10-07T14:57:35.678239Z

Oh wait, "value" is good, he means value vs reference equals

borkdude 2025-10-07T14:57:36.557159Z

perfect

borkdude 2025-10-07T15:28:12.567049Z

ok...

$ ./node_cli.js --repl --show -e "(= {:a 1} {:a 1} {:a 1})"
(async function() { var squint_core = await import('squint-cljs/core.js');
globalThis.user = globalThis.user || {};
return squint_core._EQ_(({ "a": 1 }), ({ "a": 1 }), ({ "a": 1 })) })()
true

borkdude 2025-10-07T15:55:38.347999Z

https://github.com/squint-cljs/squint/pull/716 I'm still pondering about the consequences. Being able to use this in case would be nice too but I'd need to switch up case to use _EQ_ etc as well. But I guess I can do this step by step...

borkdude 2025-10-07T15:57:27.568309Z

$ ./node_cli.js --show -e "(= {:a 1} {:a 1} {:a 1})" | esbuild --minify --bundle | gzip |  wc -c
     836

borkdude 2025-10-07T15:58:30.075559Z

433 bytes difference gzipped when you don't use =

borkdude 2025-10-07T15:58:59.033529Z

1135 bytes ungzipped

borkdude 2025-10-07T15:59:48.651569Z

all tests are passing which is promising

borkdude 2025-10-07T16:00:28.139319Z

need to fix stuff like this too:

$ ./node_cli.js --repl --show -e "(= [1 2 3] '(1 2 3))"
(async function() { var squint_core = await import('squint-cljs/core.js');
globalThis.user = globalThis.user || {};
return squint_core._EQ_([1, 2, 3], squint_core.list(1, 2, 3)) })()
false

borkdude 2025-10-07T16:22:15.728409Z

I think I can reduce the size of dequals a bit by ripping out regexp, arraybuffer, etc which aren't supported in CLJS as equal anyway

borkdude 2025-10-07T16:22:40.320209Z

also Date isn't supported in CLJS as equals

borkdude 2025-10-07T18:38:14.375409Z

Implemented a test that also tests eucalypt for breakage locally and switched over eucalypt to squint = : https://github.com/squint-cljs/squint/pull/716

borkdude 2025-10-07T18:58:42.757909Z

merged. https://github.com/squint-cljs/squint/releases/tag/v0.8.156 I'll bump eucalypt

๐Ÿ†’ 1
๐Ÿ™ 1
borkdude 2025-10-07T19:02:22.992129Z

https://github.com/chr15m/eucalypt/pull/10

Chris McCormick 2025-10-11T01:25:18.785329Z

Merged, thanks!