shadow-cljs

opqdonut 2026-03-20T07:02:19.088869Z

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 ๐Ÿงต

opqdonut 2026-03-20T07:02:38.228979Z

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

opqdonut 2026-03-20T07:03:33.392959Z

https://github.com/metosin/malli/pull/1268

thheller 2026-03-20T07:05:15.271459Z

where are they defined?

opqdonut 2026-03-20T07:05:42.973979Z

malli.core has

(defrecord Tag [key value])

opqdonut 2026-03-20T07:05:53.625009Z

there's no explicit definition of the tagged literal

thheller 2026-03-20T07:06:21.824509Z

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

opqdonut 2026-03-20T07:06:57.840979Z

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

thheller 2026-03-20T07:07:03.790409Z

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

opqdonut 2026-03-20T07:07:50.423279Z

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

thheller 2026-03-20T07:10:50.723269Z

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 ๐Ÿคท

thheller 2026-03-20T07:11:18.984069Z

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

thheller 2026-03-20T07:11:28.075289Z

too much going on in the malli repo to easily reproduce

opqdonut 2026-03-20T07:12:05.194909Z

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.

opqdonut 2026-03-20T07:12:29.102399Z

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

thheller 2026-03-20T07:12:29.730829Z

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

opqdonut 2026-03-20T07:12:35.826019Z

yep

lassemaatta 2026-03-20T07:13:00.856459Z

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

thheller 2026-03-20T07:14:32.158889Z

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

thheller 2026-03-20T07:14:52.339619Z

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

thheller 2026-03-20T07:15:44.525399Z

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

opqdonut 2026-03-20T07:17:21.879869Z

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

opqdonut 2026-03-20T07:17:49.211649Z

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

opqdonut 2026-03-20T07:20:50.108139Z

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!

thheller 2026-03-20T07:21:47.677009Z

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

thheller 2026-03-20T07:22:03.337139Z

or as I said cljc vs cljs

opqdonut 2026-03-20T07:22:18.186699Z

repro was cljc as well

thheller 2026-03-20T07:23:18.784569Z

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

opqdonut 2026-03-20T07:24:04.142689Z

ah yes malli.core has :require-macros

thheller 2026-03-20T07:25:22.398609Z

thanks for confirming my suspicion ๐Ÿ˜‰

opqdonut 2026-03-20T07:25:45.643209Z

still didn't get the repro to work

opqdonut 2026-03-20T07:53:22.367719Z

procrastinating by reading the jvm clj impl, here's the relevant branch: https://github.com/clojure/clojure/blob/8ae9e4f95e2fbbd4ee4ee3c627088c45ab44fa68/src/jvm/clojure/lang/LispReader.java#L1430

thheller 2026-03-20T07:57:47.786009Z

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

opqdonut 2026-03-20T07:58:12.833409Z

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.

thheller 2026-03-20T07:58:36.634439Z

if you did it correctly that means it will be loaded

thheller 2026-03-20T07:59:04.496349Z

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

thheller 2026-03-20T07:59:26.555729Z

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

thheller 2026-03-20T07:59:38.270839Z

just guessing blindly really

thheller 2026-03-20T08:00:29.918359Z

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

opqdonut 2026-03-20T08:00:34.873089Z

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.

thheller 2026-03-20T08:01:26.667189Z

found it ๐Ÿ˜›

opqdonut 2026-03-20T08:01:38.867369Z

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

opqdonut 2026-03-20T08:02:16.651849Z

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

thheller 2026-03-20T08:02:44.989009Z

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

opqdonut 2026-03-20T08:03:11.007239Z

yeah I was trying everything

opqdonut 2026-03-20T08:03:22.892229Z

various permutations of requires

thheller 2026-03-20T08:04:11.641759Z

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

thheller 2026-03-20T08:04:29.571519Z

its different from clojure given how cljs works

opqdonut 2026-03-20T08:04:49.388199Z

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

thheller 2026-03-20T08:04:52.829429Z

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

๐ŸŽฏ 1