cljs-dev

Roman Liutikov 2025-10-16T10:46:08.790999Z

@dnolen just noticed you commented about some concerns in https://clojure.atlassian.net/browse/CLJS-3456 but then deleted the comment, is that message still relevant?

dnolen 2025-10-16T11:00:40.639549Z

@roman01la no because I realized I was looking at the patches in the wrong order - I think we should think about this a bit more, there is no small advantage in following the established perf patterns that Clojure already has.

dnolen 2025-10-16T11:01:54.573109Z

What you've done is a kind of manual inlining - which is always a little irritating - I was looking at the patch, what is currently being done, and what Clojure does. I was thinking that I was surprised that the existing pattern couldn't be further optimized by the JS engines.

dnolen 2025-10-16T11:02:03.534499Z

Was also an interesting reminder how much faster JSC is

πŸ‘ 1
Roman Liutikov 2025-10-16T11:04:06.112399Z

for ChunkedSeq specifically I think -equiv is going to be significantly faster just with ICounted implemented https://clojure.atlassian.net/browse/CLJS-2471

dnolen 2025-10-16T11:06:29.029759Z

yeah that one's obvious - will test that one out shortly

πŸ‘ 1
borkdude 2025-10-16T11:36:14.655489Z

Does CLJS optimize this stuff by hoisting the constant collection out of the function?

(defn skip-truth? [tag]
  (contains? #{'boolean 'string} tag))

πŸ‘€ 1
dnolen 2025-10-16T12:39:56.359589Z

I think there's a ticket for that, but no we don't currently do that

borkdude 2025-10-16T11:53:02.333329Z

@dnolen I don't know if it exists, but I think = can be optimized when one of the operands is known to be a primitive, we can just use === directly instead of going through cljs.core/= (it's an optimization I did in squint) e.g.

(str (fn [x] (= 1 x)))
"function (x){\nreturn cljs.core._EQ_.call(null,(1),x);\n}"
This could just become:
return 1 === x

borkdude 2025-10-16T11:53:28.130289Z

Happy to submit a patch for it

p-himik 2025-10-16T12:20:39.808009Z

> when one of the operands is known to be a primitive But only the first one, right?

βž• 1
dnolen 2025-10-16T12:43:07.918329Z

ok, but I think stuff like this is better as compiler passes now - the macro approach just isn't that great in general

borkdude 2025-10-16T14:20:26.909169Z

@p-himik > But only the first one, right? What's the catch here?

borkdude 2025-10-16T14:21:01.289699Z

@dnolen could do a pass but I wonder what is the trade-off here: macro hardly causes longer compilation time, pass might be more expensive macro would be a quickwin in idioms that are very common like (= 1 x)

dnolen 2025-10-16T15:01:44.381929Z

passes are not slow, macros tends to compose badly and you simply don't have enough information - type inference stuff doesn't flow well in that context

πŸ‘ 1
borkdude 2025-10-16T15:02:40.910769Z

ok, let me do some benchmarks first how much can be gained from this and I'll write a pass if appropriate

dnolen 2025-10-16T15:04:53.144239Z

the and/or fix is a good example of this stuff as macro just not being any good - it composed badly w/ core.async

Roman Liutikov 2025-10-16T15:05:05.753319Z

re doing stuff in macros I've been looking into folding predicates into constants for non-nil cases (`some?`, nil?, etc), for now I'm not sure how this can be done in the compiler itself since nil? for example is a macro that calls into coercive-= which is a macro as well that expands into (js* "(~{} == ~{})" x y), leaving no info about semantics of the expression.

Roman Liutikov 2025-10-16T15:06:05.036339Z

but doing (ana/infer-tag &env (resolve-var x)) in the macro worked, although I'm not happy with this approach

dnolen 2025-10-16T15:07:34.055769Z

this probably means we could do w/ some prep work - why not include the original form w/ js*

πŸ’‘ 1
dnolen 2025-10-16T15:08:00.636749Z

and make sure in the js* branch of the the analyzer we parse the original form as well

dnolen 2025-10-16T15:08:18.270359Z

js* is generally pure optimizing - i.e. inlining macro

dnolen 2025-10-16T15:08:34.937799Z

semantically the original form is equivalent, and we're using js* for juice

borkdude 2025-10-16T15:09:29.687449Z

here's a simple benchmark:

(def iters 1000000000)

(defn str-test []
  (let [f1 (fn [x] (= 1 x))
        f2 (fn [x] (identical? 1 x))]
    (js/console.log "f1" (str f1))
    (js/console.log "f1" (str (fn []
                                (f1 1))))

    (js/console.log "f2" (str f2))
    (js/console.log "f2" (str (fn []
                                (f1 1))))
    (simple-benchmark [] (f1 2) iters)
    (simple-benchmark [] (f2 2) iters))))
f1 function a(c){return R.h(1,c)}
f1 function(){return a(1)}
f2 function(c){return 1===c}
f2 function(){return a(1)}
[], (f1 2), 1000000000 runs, 4076 msecs
[], (f2 2), 1000000000 runs, 271 msecs
20x speedup seems worth it.

dnolen 2025-10-16T15:09:59.108259Z

this optimization is actually more useful than it might appear

dnolen 2025-10-16T15:10:34.663679Z

for example I think we might have special cases in case - where if (== ...) worked this way - those special cases would go away

borkdude 2025-10-16T15:10:55.153559Z

sorry, made a mistake, the second f2 line should be:

(js/console.log "f2" (str (fn []
                                (f2 1))))
closure make of this:
f2 function(){return!0}
which is very optimal :)

borkdude 2025-10-16T15:11:05.513829Z

nice

dnolen 2025-10-16T15:11:28.399729Z

right anytime you can lower to primitives, Closure can do more work for us

borkdude 2025-10-16T15:11:35.852639Z

cool

borkdude 2025-10-16T15:11:43.690689Z

will do some pass work then

dnolen 2025-10-16T15:12:38.281149Z

@roman01la if you have cycles, I would definitely take a patch that makes js* not throw away information, I think that opens up some interesting doors later.

πŸ‘ 1
borkdude 2025-10-16T15:13:29.800049Z

often macros preserve js* info by adding metadata to the list right?

dnolen 2025-10-16T15:14:30.178179Z

yeah but it might be cool to always parse this extra data in the analyzer - and make sure we doing this uniformly

dnolen 2025-10-16T15:14:43.288779Z

so that we have an established pattern - it's probably a bit ad-hoc right now

p-himik 2025-10-16T15:32:46.536629Z

@borkdude > What's the catch here? If a particular type specifies its own -equiv, you cannot assume that it can be replaced with ===.

borkdude 2025-10-16T15:33:34.640419Z

you mean that you can have a custom type that says: I'm equal to the primitive 1. good point

borkdude 2025-10-16T15:34:23.026979Z

but how does order affect that? this could both be true? (= 1 custom) vs (= custom 1) ?

borkdude 2025-10-16T15:35:37.320749Z

hmm, only in the second case .equiv is called on custom, I see

borkdude 2025-10-16T15:36:15.983369Z

seems very brittle for some equiv impl to rely on the order of args in =. who on earth would do this

πŸ€·β€β™‚οΈ 1
dnolen 2025-10-16T15:42:48.340899Z

it is true however good or bad it is - that would be a breaking change for libs / apps that rely on this behavior w/ JS primitives.

dnolen 2025-10-16T15:44:41.747059Z

that is they already redefined equiv for numbers for some reason - an inlining thing would break that so that is a good point @p-himik

dnolen 2025-10-16T15:45:49.577329Z

but we are in a position to detect that case actually ... since ClojureScript is whole program

borkdude 2025-10-16T15:46:43.706099Z

Non commutative = should be forbidden

dnolen 2025-10-16T15:47:01.901529Z

no I'm suggesting they did that work

dnolen 2025-10-16T15:47:13.008199Z

then inlining would break their work

borkdude 2025-10-16T15:48:36.918749Z

I know. We should not break. But it’s a very bad idea. Are you suggesting we can detect equiv impls that return true for primitives?

dnolen 2025-10-16T15:49:29.750859Z

it could be done but it would be warping the compilation model for a very small benefit

borkdude 2025-10-16T15:49:38.242469Z

I have a clj-kondo linter for writing the expected value first. We could just optimize only that case

dnolen 2025-10-16T15:51:09.459299Z

optimizing only the expected value first is still a breaking change if they overwrote (extend-type number IEquiv ...) with their own thing

borkdude 2025-10-16T15:53:11.587509Z

Ok I guess I could just write a clj-kondo linter that’s going to suggest rewriting to identical? When a primitive is used and be done with it

borkdude 2025-10-16T15:55:28.916449Z

Or does CLJS have == for triple equals maybe (not at kbd now)

p-himik 2025-10-16T15:56:16.999579Z

Yes:

(core/defmacro ^::ana/numeric ==
  ([x] true)
  ([x y] (bool-expr (core/list 'js* "(~{} === ~{})" x y)))
  ([x y & more] `(and (== ~x ~y) (== ~y ~@more))))

p-himik 2025-10-16T15:57:16.430389Z

But there's still a non-macro variant that respects -equiv. But it does say that it should be used only for numbers.

(defn ^boolean ==
  "Returns non-nil if nums all have the equivalent
  value, otherwise false. Behavior on non nums is
  undefined."
  ([x] true)
  ([x y] (-equiv x y))
  ([x y & more]
   (if (== x y)
     (if (next more)
       (recur y (first more) (next more))
       (== y (first more)))
     false)))

borkdude 2025-10-16T16:01:41.082429Z

Right I guess that locks the door for the optimization then, equiv can be overridden for primitives.

Roman Liutikov 2025-10-16T17:47:03.696229Z

@dnolen > this probably means we could do w/ some prep work - why not include the original form w/ js* let's say we store :js-op-form (cljs.core/coercive-= x nil) in ast node of the :js op, how we can make use of this info later and at what stage (analyzer or compiler)? I'm trying to think of a concrete example, like folding (nil? x) into a constant boolean. btw we already store :js-op cljs.core/coercive-= in ast.

Roman Liutikov 2025-10-16T17:50:12.753699Z

I can think of having a set of predefined "evaluators", (eval-form 'cljs.core/coercive-= ...args)

Roman Liutikov 2025-10-16T17:55:53.186099Z

ideally this would be a generic mechanism, but we'd have to parse JS string then