cljs-dev

DrLjótsson 2026-01-28T19:01:47.892989Z

Am I mad or is this a bug in Clojurescript?

(do
    (def foo nil)
    (set! foo "value")
    [foo
     (str foo) 
     (subs foo 0)]
    )
;=> ["value" "" "value"]
(str foo) returns empty string!

borkdude 2026-01-28T19:04:44.761079Z

hmm

cljs.user=> (str (fn [] (str foo)))
"function (){\nreturn (\"\");\n}"

👍 1
borkdude 2026-01-28T19:05:05.399589Z

I'll have a look shortly

👍 1
schadocalex 2026-01-28T20:40:58.334229Z

I got the right value, both in REPL and after compilation

🤯 1
DrLjótsson 2026-01-28T20:42:09.168109Z

Tried it again - same result

borkdude 2026-01-28T20:43:48.899289Z

it might have to do with a string optimization: when (str ..) is compiled, it looks at the type of foo and if it's nil, you get an empty string - @schad.alexis did you try the newest CLJS?

borkdude 2026-01-28T20:44:07.622939Z

but it should probably not make this assumption for global vars

schadocalex 2026-01-28T20:49:10.107309Z

yes ok nevermind, latest doesn't work

schadocalex 2026-01-28T20:49:17.660789Z

[foo,(""),cljs.core.subs.call(null,foo,(0))] got compiled to this

borkdude 2026-01-28T21:56:19.765459Z

@dnolen I transferred the (typed-expr? &env x '#{clj-nil}) from the old implementation. I think for global vars, no tag should be inferred since they can change? (see above). https://github.com/clojure/clojurescript/commit/6cb2c4dfa971abd56b2b1acee5f1eb97fda9bac1#diff-0c7256dd86be433b0c77bfba825436d345fdbfc8cb975ff8d8a84d85d256c73bR844

dnolen 2026-01-28T22:02:20.957349Z

can't load this up right now, but yes make a ticket and please collected relevant info there

borkdude 2026-01-28T22:13:19.911879Z

I'll file a ticket tomorrow

borkdude 2026-02-03T11:55:25.317759Z

I've been thinking about this one @dnolen. If you mark the var as dynamic, it will actually be correct.

cljs.user=> (def ^:dynamic foo nil)
#'cljs.user/foo
cljs.user=> (do (set! foo "foo") (str foo))
"foo"

borkdude 2026-02-03T11:56:34.716489Z

since dynamic vars gets analyzed with tag :any since they are expected to change. but I guess in a REPL situation you can change vars with (def ..) as well and this actually also works correctly:

cljs.user=> (do (def foo "foo") (str foo))
"foo"

borkdude 2026-02-03T11:57:01.083019Z

so maybe it's just best practice to mark vars that you want to modify with set! as dynamic?

borkdude 2026-02-03T11:57:33.272419Z

(similar to JVM Clojure)

DrLjótsson 2026-02-03T11:59:18.811829Z

I never use dynamic vars in Cljs since dynamic binding doesn't work well with async code. I use set! to for one-time initialization of global variables. Although this is the first report, I think this is a pretty big BC break.

borkdude 2026-02-03T12:00:22.371139Z

I was actually working on a fix, while this popped into my mind. In JVM Clojure you can't use set! on vars, unless they are marked dynamic. The whole CLJS type inference machinery assumes vars aren't mutated with set! unless they are marked with :dynamic

borkdude 2026-02-03T12:01:08.356209Z

let me give another example that would fail horribly right now.. (I expect it to but I haven't tested it...)

borkdude 2026-02-03T12:04:54.958979Z

cljs.user=> (def x true)
#'cljs.user/x
cljs.user=> ((fn [] (set! x 0) (if x 1 2)))
2

borkdude 2026-02-03T12:05:08.665589Z

cljs.user=> (def ^:dynamic x true)
#'cljs.user/x
cljs.user=> ((fn [] (set! x 0) (if x 1 2)))
1

borkdude 2026-02-03T12:08:16.744229Z

So for consistency I think there are two directions: • Turn off optimizations based on var tags everywhere in CLJS • Warn on set! on var that is not marked dynamic

borkdude 2026-02-03T12:17:13.654039Z

Compromise third solution: consider cljs.core stable (people won't mutate it with set! ) This is the approach I took when I was writing the fix for the above issue. But even that won't hold when you use with-redefs of course

borkdude 2026-02-03T12:23:50.764439Z

My in progress fix looked like this, but I think it's better wait for @dnolen to chime in on this first.

(core/let [ast (cljs.analyzer/no-warn (cljs.analyzer/analyze &env x))
           op (:op ast)
           tag (cljs.analyzer/infer-tag &env ast)
           cljs? (core/when (= :var op)
                   (core/= 'cljs.core (:ns (:info ast))))
           optimization-safe? (core/or (= :local op)
                                       (= :const op)
                                       cljs?)]
  (core/cond
    (core/and (core/= 'clj-nil tag)
              optimization-safe?)
    nil
    (core/and (compatible? tag '#{boolean number
                                  cljs.core/Keyword
                                  cljs.core/Symbol})
              optimization-safe?)
    ["+~{}" x]
    :else
    ["+cljs.core.str.cljs$core$IFn$_invoke$arity$1(~{})" x]))

borkdude 2026-02-03T13:09:38.529549Z

Oh I guess we can throw local optimization out of the window too if we can't rely on type inference of user vars.

cljs.user=> (def foo nil)
#'cljs.user/foo
cljs.user=> (do (set! foo "foo") (let [foo foo] (str foo)))
:tag clj-nil
:opt-safe true
:compat false
""

dnolen 2026-02-03T13:34:41.171389Z

did you consider just throwing away the nil fast path? I don't know that just doesn't seem worth the trouble to dovetail into generalization work unless people are complaining about this is many contexts.

borkdude 2026-02-03T13:35:25.626979Z

@dnolen sure, but that doesn't solve the fundamental problem that CLJS relies on tags inferred from vars in other places too

dnolen 2026-02-03T13:37:56.367899Z

the whole feels very minor is all, not enough to get into a design discussion to address the immediate problem.

dnolen 2026-02-03T13:38:04.256369Z

just drop the nil case, not that useful.

dnolen 2026-02-03T13:38:18.721089Z

then a separate ticket about the more general issue.

borkdude 2026-02-03T13:39:03.855349Z

The outcome of the more general issue affects the solution to this issue. But sure, I can do that.

dnolen 2026-02-03T13:39:42.508789Z

the set! problem has existed forever yes, but it's not clear that many people are hitting it.

dnolen 2026-02-03T13:39:53.668049Z

this one was reported because (as usual) a recent change

dnolen 2026-02-03T13:40:02.464169Z

fix the recent change first, then worry about other stuff

borkdude 2026-02-03T13:40:08.912949Z

👍

dnolen 2026-02-03T13:40:51.679859Z

re: planning for next release probably only async/await

dnolen 2026-02-03T13:41:00.991829Z

but we should resolve stuff like this before

borkdude 2026-02-03T13:41:18.137909Z

(as for the more general problem: perhaps it's a good heuristic to only infer tags from defn, not from def which should be always any unless explicit tag is provided)

borkdude 2026-02-03T13:41:40.769559Z

yeah, seems good

dnolen 2026-02-03T13:42:23.314759Z

thanks!

dnolen 2026-02-03T13:42:51.224729Z

but also please open a ticket about the more general problem - I am not disinterested, just don't want to queue that up right now

borkdude 2026-02-03T13:42:59.383349Z

makes sense

borkdude 2026-02-03T13:50:02.041089Z

@dnolen btw, you can close https://clojure.atlassian.net/browse/CLJS-890#icft=CLJS-890 and https://clojure.atlassian.net/browse/CLJS-847 probably too. t's already tested in test-cljs-3452

borkdude 2026-02-03T14:00:09.862769Z

@dnolen fixed: https://clojure.atlassian.net/browse/CLJS-3472

borkdude 2026-02-03T16:18:37.721609Z

general issue: https://clojure.atlassian.net/browse/CLJS-3474