cljs-dev

Roman Liutikov 2025-10-14T10:40:59.074679Z

@dnolen is there a reason ICounted is not implemented for ChunkedSeq? The perf difference is massive, constant time vs linear time basically (there's a ticket with a patch from 2018 https://clojure.atlassian.net/browse/CLJS-2471)

(def v4 (seq (vec (range 1e5))))

(simple-benchmark [] (count v4) 1e4)

== master

Node/V8
[], (count v4), 10000 runs, 5097 msecs

Bun/JSC
[], (count v4), 10000 runs, 6764 msecs

== with ICounted implemented

Node/V8
[], (count v4), 10000 runs, 1 msecs

Bun/JSC
[], (count v4), 10000 runs, 0 msecs

dnolen 2025-10-14T10:49:18.544899Z

@roman01la did you check to see if is implemented in Clojure, and if not if there's a discussion about it?

dnolen 2025-10-14T10:53:20.485839Z

nice! go for it!

dnolen 2025-10-14T10:53:42.982539Z

no reason it wasn't done far as I know

Roman Liutikov 2025-10-14T10:54:02.849919Z

cool! the patch is already there πŸ™‚ https://clojure.atlassian.net/browse/CLJS-2471

Roman Liutikov 2025-10-14T10:54:33.001539Z

I can update it against master if you want

dnolen 2025-10-14T10:58:11.425499Z

that would be great

πŸ‘ 1
quoll 2025-10-14T11:22:00.857819Z

I just looked through both codebases. Clojure implements chunked seqs as classes in 4 places, with 2 of them being counted (`PersistentVector$ChunkedSeq` and LongRange), while the other 2 are not (`ChunkedCons` and Range). Meanwhile ClojureScript has ChunkedCons, ChunkedSeq, IntegerRange, Range. The IntegerRange type is already counted. I'm speculating that ChunkedSeq was overlooked because it's not embedded inside PersistentVector like it is on the JVM, and also because it's hard to copy everything exactly the same way πŸ™‚

quoll 2025-10-14T10:53:41.184789Z

I recently hit a difference between Clojure and ClojureScript: if you conj an existing value into a set in Clojure, then it always tests if the value is already present, and returns this if it is. However, ClojureScript explicitly creates a new object every time.

(let [s #{1}] (identical? s (conj s 1)))
;; Clojure => true
;; ClojureScript => false
This happens for each implementation of Set, with the ClojureScript implementations looking like:
(-conj [coll o]
    (PersistentHashSet. meta (assoc hash-map o nil) nil))
  (-conj [coll o]
    (PersistentTreeSet. meta (assoc tree-map o nil) nil))
Is this difference intentional? Fortunately, it doesn't happen for maps, where ClojureScript and Clojure will both return the original object when adding a redundant key/value. This would allow for a small change if we want parity:
(-conj [coll o]
  (let [m (assoc hash-map o nil)]
     (if (identical? m hash-map)
       coll
       (PersistentHashSet. meta m nil))))

(-conj [coll o]
  (let [m (assoc tree-map o nil)]
     (if (identical? m tree-map)
       coll
       (PersistentTreeSet. meta m nil))))

πŸ‘€ 1
borkdude 2025-10-16T09:06:14.718769Z

@quoll This behavior bit me a few times in the past:

(def x1 (with-meta 'x {:tag 'boolean}))
(def m (assoc {x1 x1} 'x 'x))
(meta (get (conj #{x1} 'x) 'x))
=> {:tag boolean}
I guess it's just the way it works on Clojure/ClojureScript, so just a note.

quoll 2025-10-16T12:14:59.690999Z

The final line makes sense to me. The thing inside the set has metadata. We add something that is evaluated as equal, and therefore it doesn't get added. So when we look up that same object in the set via an "equivalent" key, we get the original object, which is the one with metadata.

quoll 2025-10-16T12:15:50.306279Z

It's also why (meta (ffirst m)) => {:tag boolean} and (meta (second (first m))) => nil

quoll 2025-10-16T12:16:33.244249Z

It's the consequence of allowing equality between things that have differing metadata.

quoll 2025-10-16T12:17:07.079479Z

I actually reported a bug around similar behavior some years ago: https://clojure.atlassian.net/browse/CLJS-2736

quoll 2025-10-16T12:20:10.139419Z

Looking a value up in a set used to return the thing that had been the lookup argument, rather than the thing that was inside the set.

πŸ‘ 1
quoll 2025-10-16T12:20:50.348319Z

The behavior differed between Clojure and ClojureScript. At the time, I got around it by replacing the set with a map of values to themselves.

dnolen 2025-10-14T10:55:07.541259Z

@quoll just an oversight, patch welcome

πŸ‘ 2
quoll 2025-10-14T13:49:49.863489Z

https://clojure.atlassian.net/browse/CLJS-3454

πŸ™πŸ½ 1
quoll 2025-10-15T15:34:53.208439Z

Hi David. I've a question about your comment on that ticket: > it triggers lite mode test failures because we had missing coverage I'm not sure which tests you run for this, sorry. I mostly stick to script/test* but also tried clj -M:lite.test.build. But you seem to be mentioning another test. If you can point me at it, then I could take a look?

dnolen 2025-10-15T17:51:53.951929Z

oh ha, it's pretty hard to run all the tests locally - there's just too many. One moment I can show you

dnolen 2025-10-15T17:53:09.029369Z

you'll see there's a lite test run - which uses different compiler flags.

dnolen 2025-10-15T17:54:14.510299Z

I always set up a PR for patches, yours is here - https://github.com/clojure/clojurescript/pull/272

quoll 2025-10-15T18:03:42.255799Z

Thanks for this. I was just keeping it to a few of the JS engines (GraalVM, V8, Javascript Core), and only trying to run the tests I thought I needed. At this point though, I just want to find whatever it is that failed for you πŸ™‚

dnolen 2025-10-15T18:55:55.577369Z

you didn't break anything, it's just that your tests showed that there was too few test covering this

dnolen 2025-10-15T18:56:12.845849Z

in :lite-mode we swap in different data structures and they don't implement this behavior

quoll 2025-10-15T19:23:13.486779Z

I did add unnecessary tests, but that’s because it just felt like the existing tests were too few πŸ™‚

Roman Liutikov 2025-10-14T11:00:34.721929Z

I'm also looking into improving equiv perf for persistent vector when compared with a chunked seq. Specialized code path is already faster but with ICounted for ChunkedSeq the gains are more significant, 3-5x

(def v1 (vec (range 1e2)))
(def v2 (seq (vec (range 1e2))))

(def v3 (vec (range 1e5)))
(def v4 (seq (vec (range 1e5))))

(simple-benchmark [] (= v1 v2) 1e6)
(simple-benchmark [] (= v3 v4) 1e4)

== master

Node/V8
[], (= v1 v2), 1000000 runs, 1795 msecs
[], (= v3 v4), 10000 runs, 13003 msecs

Bun/JSC
[], (= v1 v2), 1000000 runs, 1351 msecs
[], (= v3 v4), 10000 runs, 13377 msecs

== equiv speciailized on ChunkedSeq + ICounted

Node/V8
[], (= v1 v2), 1000000 runs, 573 msecs
[], (= v3 v4), 10000 runs, 5492 msecs

Bun/JSC
[], (= v1 v2), 1000000 runs, 234 msecs
[], (= v3 v4), 10000 runs, 2499 msecs

πŸ‘€ 1
dnolen 2025-10-14T12:30:42.183859Z

@roman01la sure - noting the order of the eq check matters here, (= chunked vec) vs. (= vec chunked)

Roman Liutikov 2025-10-14T12:36:51.240039Z

right, I'll extend ChunkedSeq as well will create a ticket for that

πŸ‘πŸ½ 1
Roman Liutikov 2025-10-14T15:01:59.077659Z

Done https://clojure.atlassian.net/browse/CLJS-3456

Roman Liutikov 2025-10-14T17:48:33.676259Z

@dnolen do you think it makes sense to specialise -equiv for other data types: PersistentVector <> LazySeq <> Cons <> ChunkedCons? I think those are the most commonly used ones. On the other hand this is going to complicate -equiv implementation for all of them.

dnolen 2025-10-14T18:10:10.716169Z

@roman01la seems less impactful?

πŸ‘ 1