cljs-dev 2025-10-16

@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?

@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.

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.

Was also an interesting reminder how much faster JSC is

πŸ‘ 1

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

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

πŸ‘ 1

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

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

πŸ‘€ 1

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

@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

Happy to submit a patch for it

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

βž• 1

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

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

@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)

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

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

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

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.

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

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

πŸ’‘ 1

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

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

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

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.

this optimization is actually more useful than it might appear

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

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 :)

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

will do some pass work then

@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

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

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

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

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

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

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

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

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

πŸ€·β€β™‚οΈ 1

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.

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

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

Non commutative = should be forbidden

no I'm suggesting they did that work

then inlining would break their work

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?

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

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

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

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

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

Yes:

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

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)))

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

@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.

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

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