In this post I'll give updates about open source I worked on during March and April 2025.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors that make this work possible. Without you the below projects would not be as mature or wouldn't exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.
Current top tier sponsors:
Open the details section for more info about sponsoring.
Sponsor info
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!
On to the projects that I've been working on!
I blogged about an important improvement in babashka regarding type hints here.
Also I did an interview with Jiri from Clojure Corner by Flexiana, viewable here.
Here are updates about the projects/libraries I've worked on in the last two months.
ThreadBuilder
interopjava.util.concurrent.ThreadLocalRandom
java.util.concurrent.locks.ReentrantLock
java.time.chrono.ChronoLocalDate
java.time.temporal.TemporalUnit
java.time.chrono.ChronoLocalDateTime
java.time.chrono.ChronoZonedDateTime
java.time.chrono.Chronology
cheshire.factory
namespace (@lread)24
0.9.45
1.4.28
java.util.regex.PatternSyntaxException
1.8.735
6.0.0
0.8.65
:context expr
in compile-string
:context expr
in set!
expressionreturn
:require
+ :rename
+ allow renamed value to be used in other :require clausenull
when written in else branch of if
#jsx
and #html
range
fixesrun!
defclass
: elide constructor when not provideddefclass
:config-in-ns
on :missing-protocol-method
:redundant-ignore
on :missing-protocol-method
:missing-protocol-method
. See docs.
, e.g. py.
according to clojure analyzer--repro
flag to ignore home configurationdeftype
form results in NPE
(alias)
bug (@Noahtheduke)sci.async/eval-string+
should return promise with :val nil
for ns form rather than :val <Promise>
volatile?
to core vars1.4.28
<sub>
deps.edn
2025.04.07
org.babashka/cli
dependencycljs.pprint/pprint
add-tap
#html
id and class shortcuts + additional features and optimizations, such as an optimization for aset
jsr:
dependency support, stay tuned.fs/match
doesn't match when root dir contains glob or regex characters in pathfs/update-file
to support paths (@rfhayashi)These are (some of the) other projects I'm involved with but little to no activity happened in the past month.
Click for more details
cljs.spec.alpha
and related namespacesPublished: 2025-05-02
Tagged: clojure oss updates
Consider the following Clojure code:
(def thread-pool (java.util.concurrent.Executors/newCachedThreadPool))
(def fut (.submit thread-pool (fn [] 3)))
@fut ;;=> ?
I didn't use any type hints, but the names of the vars should give you an idea what's happening:
What result would you expect this program to give? My initial guess would be 3
.
However, in babashka the result would sometimes be nil
(version 1.12.199
on macOS Apple silicon does for example). I've seen this happen in JVM Clojure too in CI (given no type hints, and thus relying on Clojure reflection). I discovered this problem when trying to make bb run the fusebox library and executing its test suite in CI using babashka.
What's the mechanism behind this flakiness? The .submit
method in the snippet above is overloaded. When Clojure is doing reflection, it finds the most suitable method for .submit
given the instance object and the arguments. In this case the type of the instance object (the thread-pool
value) is of type java.util.concurrent.ThreadPoolExecutor
, which has three overloads (inherited from java.util.concurrent.AbstractExecutorService
):
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Runnable task, T result)
public <T> Future<T> submit(Callable<T> task)
Only two of those match the number of arguments we used in the snippet, so we are left with:
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Callable<T> task)
Clojure's reflector will try to see if the argument type to .submit
matches any of these methods. Since we called it with a function, in this case it matches both. If multiple methods match, it will try to pick the most specific method using clojure.lang.Compiler/subsumes
. Since Runnable
and Callable
are two distinct types (one doesn't inherit from the other), Clojure's reflector just returns the first method it deemed most suitable. So it could be either method. The java documentation mentions the submit
method with the Runnable
argument first, but this isn't necessarily the order in which Java reflection will list those methods. I have found out that the order may even be indeterministic over multiple CI runs and JVM implementations or versions. What the exact cause of this indeterminism is, I don't know, but I found out the hard way that it exists 😅.
So when does the above snippet return nil
? When the Runnable
overload is chosen, since Runnable
is an interface with run
method that returns void
(which is nil
in Clojure). Let me show this using a snippet that uses JVM Clojure type hinting, where we don't rely on reflection:
(set! *warn-on-reflection* true)
(def ^java.util.concurrent.ThreadPoolExecutor thread-pool
(java.util.concurrent.Executors/newCachedThreadPool))
(def fut (.submit thread-pool ^Runnable (fn [] 3)))
@fut ;; => nil
Now let's do the same with Callable
:
(set! *warn-on-reflection* true)
(def ^java.util.concurrent.ThreadPoolExecutor thread-pool
(java.util.concurrent.Executors/newCachedThreadPool))
(def fut (.submit thread-pool ^Callable (fn [] 3)))
@fut ;; => 3
So we see the issue: depending on the overload of .submit
we get a different value when we dereference the future, either nil
or the value returned from the function.
So far, babashka has relied exclusively on runtime reflection to implement Java interop. So we do get into a problem with the above snippet, unless we add type hints. But so far babashka, or more specifically, SCI hasn't made use of type hints to determine the most suitable method. The Clojure compiler does this, but as you know, SCI interprets code and doesn't make use of the clojure Compiler. It does use a forked version of clojure.lang.Reflector
, clojure's runtime reflection code, aptly renamed to sci.impl.Reflector
. So far, pretty much the only change to that code was making some methods public that were used internally by SCI. To fix the above problem, SCI now actually makes use of type hints and passes these to sci.impl.Reflector
. So in the newly published version of babashka, this code:
(def thread-pool (java.util.concurrent.Executors/newCachedThreadPool))
(def fut (.submit thread-pool ^Callable (fn [] 3)))
@fut ;;=> 3
will consistently return 3
and when changing Callable
to Runnable
, you'll get nil
. Note that the above snippet will still have the ambiguous behavior in JVM Clojure, since it doesn't have enough type hints to figure out the right method at compile time and clojure.lang.Reflector
does nothing with type hints. Once the Clojure compiler finds out that you're calling .submit
on a java.util.concurrent.ExecutorService
though, the code will run with the expected method.
Can you think of more methods in the Java standard library which have this ambiguity when relying solely on Clojure reflection? I'd love to hear about those to see if they work reliably in babashka given some extra help through type hints.
Thanks to Tim Pote for thinking along when fixing the issues in babashka and reading a preview of this blog post.
Published: 2025-04-26
In this post I'll give updates about open source I worked on during January and February 2025.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn't exist or be maintained at all.
Current top tier sponsors:
Open the details section for more info about sponsoring.
Sponsor info
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!
On to the projects that I've been working on!
As I'm writing this I'm still recovering from a flu that has kept me bedridden for a good few days, but I'm starting to feel better now.
Here are updates about the projects/libraries I've worked on in the last two months.
:unknown-ns-options
will warn on malformed (ns)
calls. The linter is {:level :warning}
by default. (@Noahtheduke):do-template
linter to check args & values counts (@imrekoszo):discouraged-var
linter for fixed arities(Class/forName ...)
in defprotocol
and extend-type
:min-clj-kondo-version
results in incorrect warning (@imrekoszo):min-clj-kondo-version
warning/error should have a location in config.edn
(@imrekoszo)api/resolve
should return nil
for unresolved symbols and localsapi/env
to determine if symbol is localapi/quote-node
and api/quote-node?
to hooks API (@camsaul)remove
return a transducer when no collection is providedset?
function (@jonasseglare)aset
take-last
(zero? "0")
should return false
deftype
field name munging problemfn
args don't get munged (@grayrest)|
and if
clojure.string
functions lower-case
, upper-case
, capitalize
(@plexus)--paths
with squint.edn
config properlymapcat
return a transducer if no collections are provided (@jonasseglare)Throwable
to have instance methods invoked (@bobisageek)rewrite-clj
to 1.1.49
(fixes parsing of foo//
among other things)deps.clj
fs
nil
XDG_DATA_HOME
environment variable according to XDG Base Directory Specificationassoc
/update
now handles map keys that have no indent at all (@lread)assoc
/update
now handles map keys that have no indent at all (@lread)assoc
/update
now aligns indent to comment if that's all that is in the map (@lread)update
now indents new entries in same way as assoc
(@lread)These are (some of the) other projects I'm involved with but little to no activity happened in the past month.
Click for more details
cljs.spec.alpha
and related namespaces:require-cljs
which allows you to use .cljs
files for render functionsPublished: 2025-02-28
Tagged: clojure oss updates