shadow-cljs 2026-03-20

Should tagged literals work the same in shadow-cljs? I'm in the process of migrating malli's test suite from cljs-test-runner to shadow, and I'm getting a cryptic error from a tagged literal. The same file compiles fine under cljs-test-runner (which invokes the cljs compiler directly). Details in ๐Ÿงต

Multiple files failed to compile.
------ ERROR -------------------------------------------------------------------
 File: /home/joel/work/metosin/malli/test/malli/parser_test.cljc:135:25
--------------------------------------------------------------------------------
 132 | (deftest and-complex-parser-test
 133 |   (is (= {} (m/parse [:and :map [:fn map?]] {})))
 134 |   (is (= {} (m/parse [:and [:fn map?] :map] {})))
 135 |   (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1)))
-------------------------------^------------------------------------------------
malli.core.Tag

--------------------------------------------------------------------------------
 136 |   (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1)))
 137 |   (is (= 1 (m/parse [:and {:parse/transforming-child :none} [:fn number?] [:orn [:left :int] [:right :int]]] 1)))
 138 |   (is (= 1 (m/parse [:and :int [:or :int :boolean]] 1)))
 139 |   (is (= 1 (m/parse [:and [:or :int :boolean] :int] 1)))
--------------------------------------------------------------------------------
------ ERROR -------------------------------------------------------------------
 File: /home/joel/work/metosin/malli/test/malli/core_test.cljc:3614:26
--------------------------------------------------------------------------------
3611 |             :message nil}]}
3612 |          (with-schema-forms
3613 |            (m/explain [:andn [:m :map] [:v [:vector :any]]] {}))))
3614 |   (is (= #malli.core.Tags{:values {:m {} :f {}}}
--------------------------------^-----------------------------------------------
malli.core.Tags

--------------------------------------------------------------------------------
3615 |          (m/parse [:andn [:m :map] [:f [:fn map?]]] {})))
3616 |   (let [s [:andn [:m :map] [:f [:fn map?]]]]
3617 |     (is (= {} (->> {} (m/parse s) (m/unparse s)))))
3618 |   (is (= #malli.core.Tags{:values {:o #malli.core.Tag{:key :left, :value 1}, :f 1}}
--------------------------------------------------------------------------------

where are they defined?

malli.core has

(defrecord Tag [key value])

there's no explicit definition of the tagged literal

then its not a tagged literal is it? ๐Ÿ˜›

record literal? honestly, I never use this syntax myself so I don't know what to call it

I honestly don't know what it works. doesn't seem like it should

Yeah I'm a bit surprised but it seems to work in CLJ and under cljs-test-runner!

can't say to be honest. not quite sure what the point of using reader literals here is either. I mean its code, just use the records directly ๐Ÿคท

if you make a smaller repro to show that is works in cljs.main compile but not shadow-cljs I can take a look

too much going on in the malli repo to easily reproduce

Yeah for sure, I wasn't expecting you to debug it for me! I asked because I wondered if there's some known gotcha that I'm missing.

Switching to record constructors instead of # is a good idea. I'll also try the minimal repro.

#malli.core.Tags{:values {:m {} :f {}}} is just (core/->Tags {:m {} :f {}}) or whatever the structure is

fyi: the clojure reference has a couple of references to that reader notation > deftype/defrecord can be written with a special reader syntax #my.thing[1 2 3] where: ... > defrecord supports an additional reader form of #my.record{:a 1, :b 2} taking a map that initializes a defrecord according to:... etc

yeah, not saying that this isn't a bug in shadow-cljs, but it is kind of complicated given how that whole business is implemented

fairly certain it only works because its .cljc and won't work in plain .cljs, but could be entirely wrong on that one too

my rule of thumb is to not use record literals in code ever. tagged literals are great for data, but just complicated to no end in code

I've been avoiding them as well due to getting bitten too many times

thanks for the quote @lasse.olavi.maatta, I was trying to find them described in https://clojure.org/reference/reader

Interesting, trying to reproduce this on cljs.main gives me "class not found: record.Foo". So now the mystery is why is this working in malli!

there is some special REPL behavior where it works differently there. maybe something to do with that

or as I said cljc vs cljs

repro was cljc as well

maybe forgot the (:require-macros [repro]) for cljs part? as in making the clj side never loaded?

ah yes malli.core has :require-macros

thanks for confirming my suspicion ๐Ÿ˜‰

still didn't get the repro to work

shadow-cljs uses the same reader impl ๐Ÿคท

Yeah! So I guess the difference between malli & my repro is that the ns defining the record hasn't been loaded in clj mode, even though I have :require-macros.

if you did it correctly that means it will be loaded

but it might be a loading order issue. meaning if its in the same file then it might only apply to files read/compiled after that

eg. your malli.core gets loaded before malli.core-test. so core-test can use the literal but core can't

just guessing blindly really

I know I wrote some custom code for literals but can't find it

I'll keep trying to repro, loading order sounds like a potential culprit. I still haven't gotten any version of this to compile in CLJS.

found it ๐Ÿ˜›

FWIW this is my current status

% cat src/record.cljc
(ns record
  #?(:cljs (:require-macros [record])))

(defrecord Foo [field])
% cat src/repro.cljc
(ns repro
  #?(:cljs (:require-macros [record]))
  (:require [record]))

(prn #record.Foo{:field 1})
% clj -M --main cljs.main --compile repro
WARNING: record is a single segment namespace at line 1 /home/joel/tmp/shadow-record-literal-repro/src/record.cljc
WARNING: repro is a single segment namespace at line 1 /home/joel/tmp/shadow-record-literal-repro/src/repro.cljc
Unexpected error (ClassNotFoundException) compiling at (REPL:1).
record.Foo

Full report at:
/tmp/clojure-824673515660939121.edn

gotta have some meetings now, will get back to this later, thanks

#?(:cljs (:require-macros [record])) that isn't needed in ns repro since record already did it

yeah I was trying everything

various permutations of requires

well the result is what I would have expected, so not sure what special magic cljs-test-runner is doing to make it work

its different from clojure given how cljs works

I have a hypothesis that it might actually be doing a clj-side :require of malli.core before invoking the cljs compiler

but yeah .. without fail ... record literals in code = headache

๐ŸŽฏ 1