babashka 2025-09-15

follow up thread on clj-reload and clojure+ bb support

@tonsky with bb 1.12.209 out, I think https://github.com/tonsky/clojure-plus/pull/20 is ready for review. It adds bb support to all ns except print.

think I found another place where clj-reload works oddly with bb, test/clojure+/walk_test.cljc has a (defrecord Foo [a b c]), and when reloading it on bb I see this error

Reloading 13 namespaces...
  failed to load clojure+.walk-test #error {
 :cause Could not resolve symbol: clojure_PLUS_.walk_test.Foo
 :data {:type :sci/error, :line 84, :column 1, :file clojure+/walk_test.cljc, :phase analysis}
 :via
 [{:type clojure.lang.ExceptionInfo
   :message Could not resolve symbol: clojure_PLUS_.walk_test.Foo
   :data {:type :sci/error, :line 84, :column 1, :file clojure+/walk_test.cljc, :phase analysis}
   :at [sci.impl.utils$throw_error_with_location invokeStatic utils.cljc 49]}]
 :trace
 [[sci.impl.utils$throw_error_with_location invokeStatic utils.cljc 49]
  [sci.impl.resolve$throw_error_with_location invokeStatic resolve.cljc 11]
  [sci.impl.resolve$resolve_symbol invokeStatic resolve.cljc 281]

I think that issue only happened because of the ^:clj-reload/keep metadata, but let me double check

with "that issue" do you mean the issue you posted or 1839?

the underlying issue is the same. it is because of this:

(ns foo)
(defrecord Dude [])
(ns bar)
(remove-ns 'foo)
foo.Dude ;; still exists
in SCI foo.Dude no longer exists after remove-ns but clj-reload relies on this stuff still existing

uhm I think there's something else at play here though... look:

user=> (ns foo+)
nil
foo+=> (defrecord Dude [])
Could not resolve symbol: foo_PLUS_.Dude [at <repl>:2:1]
foo+=>

lol. who uses a plus in their ns anyway :P but yes, this is a nice isolated repro worthy of its own github issue, thank you

I can also confirm that if I go into the clj-reload tests and add a defrecord with ^:clj-reload/keep it will fail to reload, but if I remove the metadata it will not

will make issue

ok here's a new wip for clojure+ bb support: https://github.com/tonsky/clojure-plus/compare/main...filipesilva:clojure-plus:support-bb β€’ print has a lot of java imports that bb doesn't have, unclear if it's worth even trying that one β€’ print, error, and test need (.addMethod ^MultiFn ...) β€’ walk I think is working but the test fails to load because of 1868 β€’ that leaves hashp and core working and tested

why does it need addMethod instead of just calling defmethod

we should also add clojure.lang.ITransientCollection" . the string solution isn't a nice one

please make an issue for that

I'm willing to change the message "Could not" to "Unable" in Unable to resolve symbol please also an issue for that

I think it uses .addMethod because the code keeps the replacement and original fns, then the uninstall restores the original one

(defonce ^:private clojure-print-method
  (get-method print-method Throwable))

(defn- patched-print-method [^Throwable t ^Writer w]
  (let [t (if (:root-cause-only? config)
            (root-cause t)
            t)]
    (if *print-readably*
      (if (:reverse? config)
        (print-readably-reverse w t)
        (print-readably w t))
      (if (:reverse? config)
        (print-humanly-reverse w t)
        (print-humanly w t)))))

(defn install!
  "Improves the way exceptions are printed, including print*, pr*,
   and clojure.pprint/pprint.

   Possible options:

     :clean?           <bool> :: Converts Clojure-specific stack trace elements
                                 to be more clojure-like. True by default.
     :collapse-common? <bool> :: With chained exceptions, skips common part of
                                 stack traces. True by default.
     :trace-transform  <fn>   :: A fn accepting and returning trace--a sequence
                                 of maps {:keys [element file line ns method]}
                                 where element is original StackTraceElement.
     :color?           <bool> :: Whether to use color output. Autodetect by default.
     :reverse?         <bool> :: Whether to print stack trace and cause chain
                                 inner-to-outer (Java default) or outer-to-inner.
                                 Useful for REPL and small terminals as class,
                                 message and source will always be at the bottom.
                                 False by default.
     :root-cause-only? <bool> :: Only print root cause. False by default.
     :indent           <int> | <string> :: how many spaces to use to indent
                                 stack trace elements. Java uses \"\\t\", we use
                                 2 spaces by default."
  ([]
   (install! {}))
  ([opts]
   (alter-var-root #'config (constantly (merge (default-config) opts)))
   (.addMethod ^MultiFn print-method Throwable patched-print-method)))

(defn uninstall!
  "Restore default Clojure printer for Throwable"
  []
  (.addMethod ^MultiFn print-method Throwable clojure-print-method))

in error there's just the single one, but in print and test there's more

print also does it dynamically

(defn install-printers!
  "Install printers for most of Clojure built-in data structures.
   
   After running this, things like atoms and transients will print like this:
   
     (atom 123)          ; => #atom 123
     (transient [1 2 3]) ; => #transient [1 2 3]
   
   Possible opts:
   
     :include :: [sym ...] - list of tags to include (white list)
     :exclude :: [sym ...] - list of tags to exclude (black list)"
  ([]
   (install-printers! {}))
  ([opts]
   (let [catalogue (catalogue opts)]
     (doseq [{:keys [class print]} catalogue]
       (.addMethod ^MultiFn print-method class print)
       (.addMethod ^MultiFn print-dup class print)
       (.addMethod ^MultiFn pprint/simple-dispatch class #(print % *out*))))))

I think you can just use defmethod for this really.

user=> (macroexpand '(defmethod foo bar baz))
(. foo clojure.core/addMethod bar (clojure.core/fn baz))

but maybe it's slightly more annoying

ok, issue for .addMethod welcome too

I think the harder bit is getting the arg list from the fn, whereas with .addMethod you just provide the fn instead of args+fn

making issues then

sounds good!

adding classes: same file. Please one PR per issue and add a test

tests can be added in babashka.interop-test

(of course: patches are optional)

awesome, I was going to ask if I could these myself lol

always been curious about contributing to bb but it seemed a bit daunting

contributing to classes.clj is the easiest for getting started :)

add ITransientCollection to :instance-checks - this will have the least impact on binary size

πŸ‘ 1

no need to write a test for that if it's only about (instance? ... x)

but .addMethod does need a test

ah I forgot to mention why I removed some assertions from the hashp test:

(core/if-clojure-version-gte "1.12.0"
        (do
          (is (= "#p String/1 [<pos>]\njava.lang.String/1\n"                              (:out (eval "#p String/1"))))
          (when-not util/bb?
            (is (= "#p StringWriter/1 [<pos>]\njava.io.StringWriter/1\n"                  (:out (eval "#p StringWriter/1")))))))
I got a bit of a weird error here, I think this wants to test the new clj 1.12 stuff, and the second one failed but I couldn't quite understand if it was testing something different from the first
ERROR in (interop-test) (clojure+/hashp_test.clj:117)
classes
expected: (= "#p StringWriter/1 [<pos>]\njava.io.StringWriter/1\n" (:out (eval "#p StringWriter/1")))
  actual: org.graalvm.nativeimage.MissingReflectionRegistrationError: The program tried to reflectively instantiate the array class

   java.io.StringWriter[]

without it being registered for runtime reflection. Add java.io.StringWriter[] to the reflection metadata to solve this problem. Note: Add "unsafeAllocated" to the array class registration to enable runtime instantiation. See  for help.
 at com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.errorForArray (MissingReflectionRegistrationUtils.java:121)
And the other is:
(when-not util/bb?
      (testing "instance fields"
        (is (= {:res 1 :out "#p (.-x (java.awt.Point. 1 2)) [<pos>]\n1\n"}  (eval "#p (.-x (java.awt.Point. 1 2))")))
        (is (= {:res 1 :out "#p (.x (java.awt.Point. 1 2)) [<pos>]\n1\n"}   (eval "#p (.x (java.awt.Point. 1 2))")))
        (is (= {:res 1 :out "#p (. (java.awt.Point. 1 2) -x) [<pos>]\n1\n"} (eval "#p (. (java.awt.Point. 1 2) -x)")))
        (is (= {:res 1 :out "#p (. (java.awt.Point. 1 2) x) [<pos>]\n1\n"}  (eval "#p (. (java.awt.Point. 1 2) x)")))))
I tried for a good 20m to find a class in bb with instance fields but couldn't, I even enlisted Claude's help over the list of classes in bb

I'll take a stab and making patches for the other issues a bit later, thanks for taking the time to give me directions there

Heya, about the test in https://github.com/babashka/babashka/pull/1873... I'm not sure if it's in the right place, but there were similar tests in that ns so I did it there. If I should put the test somewhere else please let me know.

looks great to me

πŸ‘ 1

basically everything except print works

walk works, except the test needs that defrecord

test works, but test is excluded from the normal tests, so I tested it manually

now error works as well, which it didn't before

I'm gonna take a stab at getting print to work with a lot of conditionals for all the types that it tries to import but aren't in bb

here's a super weird one that came up when I got print working https://github.com/babashka/babashka/issues/1874

welp, the more I drill into print, the more issues I get... I don't think this one is worth it. latest issue is Method getName on class sci.lang.Namespace not allowed! coming up on loads of tests

Just use ns-name

Why use all this JAva interop when there’s core functions for them

dunno, I'm mostly trying to change as little as possible

Well that’s a change that would be fine I think. I’ll get back to the rest, afk at the moment

πŸ‘ 1

put up a draft PR in https://github.com/tonsky/clojure-plus/pull/19 with base support, which looks pretty solid, and a separate commit with clojure+.print support, which I think isn't very solid

Given a bb.edn with :tasks {:init (clojure "-X:deps prep") where https://clojure.org/reference/clojure_cli#deps_prep refers to a sibling project within a monorepo, how can I force performing a prep again if the sibling's src/java dir has had any change in .java files? (I reckon that I can solve this step by step, but perhaps someone around here has precisely fixed this very problem, or an equivalent one)

You can perhaps use fs/modified-since for this

You give it one or a coll of files to compare to and then a directory and it will return the newer files

Last modified

Or you can give it a collection of files with fs/glob to find only the Java files

Thanks! Yes, fs/modified-since had come to mind. Was just wondering if anyone has the perfect snippet at hand. Otherwise yes, using bb.fs should give plenty of options

I kinda regret making fs/modified-since lazy so you have to call seq on it to get nil if nothing has changed, but whatever

and here's an example:

$ bb -e '(fs/modified-since "target" (fs/glob "." "**.java"))'
()
$ touch impl-java/src-java/babashka/impl/URLClassLoader.java
$ bb -e '(fs/modified-since "target" (fs/glob "." "**.java"))'
(#object[sun.nio.fs.UnixPath 0x77e29668 "impl-java/src-java/babashka/impl/URLClassLoader.java"])

Nice. So the final invocation may be as simple as approximately:

:init 
(do
  (when (seq (fs/modified-since "target" "../foo/java/src/**.java"))
    ;; delete target/ so that prepping will be forcibly run...
    )
  (clojure "-X:deps prep"))

but this will always invoke clojure -X:deps prep, maybe you can prevent that when nothing has changed?

anyway, that's for you to decide

Although yeah, in a monorepo there can be N other siblings, so better not to optimize

is this for prepping the current project btw? then you might need :current true

but if you use it as a dep, then not

It's prepping siblings

note that this will need a glob call:

(fs/modified-since "target" "../foo/java/src/**.java")
so:
(fs/modified-since "target" (fs/glob "../foo/src" "**.java"))

1