Bayes’ Theorem, Revisited: Three Interactive Simulations
Rebuilding three earlier Bayesian simulations with Kindly, Scittle, Reagent, and accessible SVG.
Rebuilding three earlier Bayesian simulations with Kindly, Scittle, Reagent, and accessible SVG.
Two gigs in one night: not quite a first (the same happened in Kathmandu many years ago), but perhaps the first representing two completely different projects on the same stage. First up is Khyal Geometries with Shama Rahman on sitar, second is The Printer Jam with Evan Raskob on printer, both sets as part of ACM Creativity and Cognition 2026. The former will be live-coded (Max, Node.js, ClojureScript); the latter will be controllerism.
It’s only now that I realise that my carefully crafted blog taxonomy cannot deal with a single blog post that spans more than one project.
Hi friends,
This newsletter we’re streamlining the process of creating circuit boards and revisiting the DIY electronic calipers project from two years ago.
I first started playing around with electronics via Arduino back in 2010, and I designed my first circuit board back in 2015 for my walnut and leather cell phone. Since then, I’ve made around a dozen PCBs, with a notable spike during the pandemic making weird mechanical keyboards.
Averaging roughly one PCB/year is a maximally frustrating frequency: I have 100’s of hours of cumulative experience, but it’s sufficiently spread across the forgetting curve that every time I start something the details are only vaguely familiar and I’ve got to re-orient myself again. It reminds me of filling out my taxes, where I also furiously consult my notes and attempt to interpret them against new versions of the UI where input boxes have been hidden across other forms and new sub-menus.
Anyway, armed with a coding agent, I externalized everything I kept forgetting how to do in a pile of scripts: Kevin’s KiCad Helpers.
This is tailored to my needs — designing PCBs in KiCad 10 to be manufactured by JLCPCB — but if yours are similar you might find ‘em helpful. Even if you don’t make PCBs, it might be good inspiration for how to apply LLMs to sand down rough edges of whatever convoluted infrastructure is required for your projects.
A quick tour of the tools:
I specify all of my mechanical stuff — board outlines, mounting hole positions, etc. — in Autodesk Inventor since it has a constraint solver and allows me to directly reference complex geometry driven by other objects.
KiCad’s GUI has a DXF import tool, but it doesn’t have a mechanism to replace already imported geometry, which makes it tedious to iterate.
This script:
panel_Edge.Cuts.dxf imports a group ID’d “panel” to the Edge Cuts layer)I combine this with a similar poll + export-to-file script in my mechanical CAD tool (shown above) to get live syncing to KiCad (shown below):
DXFs map into KiCad’s origin at top-left of the page outline and the DXF positive Y direction points up, so most of my PCBs end up drawn outside of the page ¯\_(ツ)_/¯.
I also need the reverse direction: Import a 3D model of a circuit board and its components back into my mechanical CAD program so I can verify clearances, etc. KiCad has native STEP export, but unfortunately some of the parts have extremely detailed 3D models. My M1 Macbook Air running Windows CAD software in a virtual machine does not do well when every pin of every chip is a separate solid body.
This script generates a STEP export where all parts have been replaced with axis aligned bounding boxes:

I design all of my boards to be assembled by JLCPCB, so it’s extremely helpful to have a low-latency way to query their parts. This script downloads CDFER’s daily JLCPCB parts sqlite database and consolidates everything into a single table with a numeric price column and lots of indexes so that searching is fast:
I use DB Browser for SQLite but you can of course use whatever interface you like.
It’s also extremely useful to point LLM agents at this database:
My dude, I need an H-bridge that can drive +/- 30 Volts, please query ~/foo/bar/parts.db and give me a table with 5 options for integrated ones showing price / stock / description. Please also make a table showing options for drivers with external transistors. Include links to the datasheets.
Shout out again to CDFER for their JLCPCB KiCad Library, which has all of the “basic” jellybean parts and some of the extended ones as well.
For the parts that are not already available in here, I need to import them. Rather than draw them entirely from scratch, I use uPesy’s easyeda2kicad.py.
However I don’t always want to create an entirely new symbol and footprint if the part actually corresponds to something that’s in the KiCad standard library. So my import tool tries to match existing footprints (including 90 degree rotations) and spawns a terminal UI so you can interactively compare potential matches with the EasyEDA footprint:
The most interesting helper I’ve created so far is a general schematic analysis framework, which imports the KiCad netlist and schematic instance properties into a DataScript graph database to run various queries/checks.
For example, this lil’ function calculates the capacitance (of all the explicit capacitors, anyway) on a given net:
(defn net-capacitance
[db net-name]
(some->> (d/q '{:find [?ref ?v]
:in [$ ?net]
:where [[?n :net/name ?net]
[?n :net/nodes ?node]
[?node :node/pin ?pin]
[?i :instance/pins ?pin]
[?i :instance/ref ?ref]
[(clojure.string/starts-with? ?ref "C")]
[?i :instance/value ?v]]}
db net-name)
(keep (comp parse-capacitance second))
seq
(reduce + 0.0)))
This function can then be used to check the total capacitance on, e.g., the power nets, and throw an error if it exceeds, e.g., the maximum 10uF allowed by the USB specification.
(defn check-total-capacitance!
[db]
(let [rows (->> ["VCC" "VBUS"] ;;TODO: make this configurable
(keep (fn [net]
(when-let [c (net-capacitance db net)]
{:net net :total-uF (format "%.2f" (* c 1e6))}))))]
(when (seq rows)
(print "total capacitance:")
(clojure.pprint/print-table rows)
(doseq [{:keys [net total-uF]} rows]
(assert (< (Double/parseDouble total-uF) 10) (str "Net " net " exceeds USB spec 10uF capacitance"))))))
(It’s easy to accidentally exceed this limit if you keep incrementally adding ICs and their recommended bypass capacitors.)
Since KiCad allows one to add arbitrary key/value pairs to schematic instances, it’s easy to check and print other data as well.
For example, I record the i2c address(es) of each chip this way (note i2c and max_mA fields in property inspector on left):

Within the schematic text labels I reference using the KiCad text variable format.
(E.g., the above Addr: 0x49 label is defined as Addr: ${U1:i2c}.)
The analysis script throws an error if an address maps to multiple chips:
(defn i2c-addresses
[db]
(d/q '{:find [?hex-addr (distinct ?ref)]
:where [[?instance :instance/ref ?ref]
[?instance :instance/attributes ?attribute]
[?attribute :attribute/name "i2c"]
[?attribute :attribute/value ?addrs]
[(clojure.core/identity ?addrs) [?addr ...]]
[(clojure.core/format "0x%x" ?addr) ?hex-addr]]}
db))
(defn check-i2c!
[db]
(let [refs-by-addr (i2c-addresses db)]
(when (seq refs-by-addr)
(print "i2c addresses")
(clojure.pprint/print-table (sort-by :addr (for [[addr refs] refs-by-addr]
{:addr addr :refs (clojure.string/join " " (sort refs))})))
(doseq [[addr refs] refs-by-addr
:when (< 1 (count refs))]
(throw (ex-info (str "Addr " addr " matches multiple refs: " refs)
{:addr addr :refs refs})))
(println ""))))
This also prints out a helpful table of everything on the bus every time I build the project:
| :addr | :refs |
|-------+-------|
| 0x20 | U3 |
| 0x49 | U1 |
| 0x60 | U2 |
| 0x61 | U2 |
| 0x62 | U2 |
| 0x63 | U2 |
| 0x64 | U2 |
| 0x65 | U2 |
| 0x66 | U2 |
| 0x67 | U2 |
| 0x68 | U2 |
| 0x69 | U2 |
| 0x6a | U2 |
| 0x6b | U2 |
| 0x6c | U2 |
| 0x6d | U2 |
| 0x6e | U2 |
(In this example, U2 is an LED driver and exposes each channel on its own i2c address, so its i2c property is specified as 0x60..0x6F.)
Speaking of builds, most of the functionality described above is packaged up as a script, so you just run kkh build in a folder and it’ll create an output directory next to every *.kicad_pro it finds in any subfolder.
The outputs are named with the date, git revision, and also indicate whether there are unstaged changes in the repository working tree:
2026-07-07-73c5c1
├── bom.csv
├── designators.csv
├── netlist.ipc
├── positions.csv
├── receiver-gerbers-2026-07-07-73c5c1.zip
├── receiver.full.step
├── receiver.simplified.step
└── schematics
├── receiver-pcb-back.pdf
├── receiver-pcb-front.pdf
└── receiver-sch.pdf
So one command runs DRC, ERC, and custom analysis checks and then builds all of the output files required to place a JLCPCB assembly order.
The build script also exposes the version string as a KiCad variable, so if you add ${KKH_VERSION_DATE} to your PCB silkscreen, the actual version will appear in the output Gerber files.
I hope by making a proper build script I will never again relive the shame of forgetting to run ERC and having a PCB manufactured where I literally forgot to connect some IC pins entirely…
About two years ago I had my first foray into digital signal processing and made some electronic calipers.
I haven’t touched the project since I published that post, but a few folks recently asked me about it, and since I had some spare LLM credits I figured I’d spend five minutes sending off the little dude at the problem.
And I literally mean five minutes — these are very rough, rambly dictated prompts that I didn’t even bother to edit. I’m sharing here to remind everyone (especially myself!) that not everything needs to be A Big New Project and sometimes you can have great success from a quick, low-effort attempt.
I spawned a Claude session in the project repo, dictated the following prompt, and went to brush my teeth:
This is a project that I worked on a while ago and the accuracy that came out was alright but I’m wondering if there’s anything I can do to improve the accuracy purely from a computational way rather than making new hardware. Be sure to read the blog post mentioned at the top of the read me to get a background
After I brushed my teeth, it had come up with a few plausible sounding ideas, so I replied with the following and went to sleep:
I’d like you to set this up and come up with a few different ideas. These are pretty good ones and what I want you to do is Wire them up so that I can test them out Individually as a series of experiments with a given protocol and I want you to set everything up as much as possible So I can do it Quickly on my end without having to get in and change the code or anything like that so what you can do is you can make a branch and then Have maybe different entry points or something for each of these different improvements and then Write like an overall program or something like that Which I can just run to test through it and then make it interactive I guess so that I can You know start it up in the hardware Let it sit still for a certain amount of time Move it a fixed amount and back and then You know or some protocol like that and then I’ll tell you what I’m done and then we can just run through that program To test out all of the different ideas and potentially combinations of ideas in like a 10 or 15 minute setting and then We’ll figure out from that which ones are working most effectively
Again, I’m really not trying that hard here.
I woke up the next morning and spent about 20 minutes setting up the caliper PCBs and then running the programs it generated to collect new measurement data. It tried the following improvement hypotheses (LLM-generated text):
| knob | idea it tests |
|---|---|
| window size | longer coherent integration (noise ∝ 1/√N), incl. one 50 Hz line period |
| mean_sub | remove DC so ADC offset drift can’t leak into the phase |
| hann | suppress spectral leakage from non-integer-cycle windows |
| smoothing | averaging in I/Q (phasor) space instead of phase space; EMA vs block |
| hysteresis | the current 0.1 rad dead-band vs smaller vs none |
and based on the initial results and a bit more chatting, the agent proposed an improvement that is, of course, completely obvious in hindsight. My initial parameter sweep (two years ago) used a fixed 2kHz spacing. However, the actual signal and sampling timings generated by the microcontroller are driven by integer divisions of a fixed clock frequency — so most of these sampled timings don’t “line up” nicely in terms of an integer number of signal periods.
Thus, there’s always a bit of DC bias in the signal, which causes undesired noise.
With this change, the LLM-generated experimental code reports a noise floor of around 50um, which is about 10x better than what I was getting before. I’m currently on holiday away from my lab, but I expect it’ll take 30 minutes once I get back to code it up myself and validate.
Anyway, the main takeaways for me are:
Accidental anonymity “Sorry to say it but if you need your work to be polished and beyond reproach, that’s a determination and character problem, not a skill problem.”
High Performance Motor Control From the Ground Up || Field Oriented Control (FOC)
Bio-Rad Laboratories Helios® Gene Gun sounds like a joke, but it’s absolutely real and I desperately want to use it.
I didn’t build a Full Body Ultrasound… but I know the people that did
AgentsView is a very nice local web UI from my friend Wes McKinney that makes it easy to review transcripts and token usage across Claude Code, Codex, etc. I used it to quote some of my LLM sessions above.
A learning-in-public first pass at estimating receptive vocabulary from stratified responses with a Beta–binomial model.
Code:
Greetings Clojurists!
Please take a moment to complete the Q3 2026 Funding Survey which helps inform our Q3 project awards. It is not a heavy lift - maybe 5 minutes of your time. Your input is invaluable! A link to the survey was sent to your email in the last few weeks - and just in case it made its way to spam, you can look for “We Need Your Input - Q3 2026 Funding”. The survey closes midnight PST on July 15, 2026.
Thanks as always for your support of Clojurists Together and for being a part of this awesome community.
Any questions, please email me at kdavis@clojuriststogether.org
Kathy Davis Program Manager Clojurists Together Foundation
In Clojure you can destructure a map using an arbitrary expression as the key.
For example, here kw is a local binding.
(let [kw :key
{a kw} {:key 1}]
a)
;=> 1
Usually this syntax is demonstrated as {sym0 :kw0 sym1 :kw1 ...},
which doesn’t reveal that the keywords are actually in expression position,
or an evaluation context.
The reason why this more recognizable syntax works is because keyword literals
are self-evaluating.
(let [{a :key} {:key 1}]
a)
;=> 1
The basic rule for expanding these expressions is:
(let [{binding expression} map])
=>
(let [binding (get map expression)])
So that code is equivalent to:
(let [a (get {:key 1} :key)]
a)
Symbols are not self-evaluating syntax in Clojure, so they must be quoted:
(let [{a 'key} {'key 1}]
a)
;=> 1
Applying the rule makes the need more obvious:
(let [a (get {'key 1} 'key)]
a)
Destructuring is pleasingly compositional. This ability to drop down to computed keys makes destructuring available in many more situations than if all keys were required to be statically declared. I found a few examples in my own code and other libraries where this flexibility has been useful.
An example that dereferences the var u/expr-type to compute the key:
(let [{cargs :args
res u/expr-type} (-> expr-noinline
ana2/unmark-top-level
ana2/unmark-eval-top-level
(check-expr expected opts))]
Another example that uses three class literals as computed keys:
(let [r (reflect-u/reflect cls)
{methods clojure.reflect.Method
fields clojure.reflect.Field
ctors clojure.reflect.Constructor
:as members}
(group-by
class
(filter (fn [{:keys [name] :as m}]
(if constructor-call
(instance? clojure.reflect.Constructor m)
(= m-or-f name)))
(:members r)))]
A snippet of code that destructures nested maps using a mix of keywords, quoted symbols and computed vectors-of-locals as keys. Notice that the vectors are in binding position sometimes to introduce names, then in expression position to perform lookups.
(let [...
{{[x1 x2] 'x} :fv
{[y1] 'y [z1 z2] 'z} :idx} remap
{{{[y1_x1 y1_x2] 'x
[y1_y1 y1_y2 y1_y3 y1_y4] 'y} [y1]
{[z1_x1] 'x
[z1_y1] 'y} [y1 z1]
{[z2_x1] 'x
[z2_y1] 'y} [y1 z2]} :idx-context} remap]
(is (= {:fv {'x [x1 x2]}
:idx {'y [y1]
'z [z1 z2]}
:idx-context {[y1] {'x [y1_x1 y1_x2]
'y [y1_y1 y1_y2 y1_y3 y1_y4]}
[y1 z1] {'x [z1_x1]
'y [z1_y1]}
[y1 z2] {'x [z2_x1]
'y [z2_y1]}}}
remap)))
You’ve probably seen code like this
that destructures booleans from a group-by:
(let [...
{anns false inits true} (group-by list? normalised-bindings)]
This Malli snippet nests :keys destructuring under a local binding key, method:
(-value-transformer [_ schema method options]
(reduce
(fn [acc {{:keys [name qname default transformers]} method}]
And this example elegantly destructures a nested map using keywords and locals, supporting the common pattern of updating a nested value in an atom then destructuring the swapped-in value’s relevant parts.
(defn remove-stale-cache-entries
[nsym ns-form-str sforms slurped opts]
{:pre [(simple-symbol? nsym)]}
(when ns-form-str
(let [{{{forms-cache ns-form-str} nsym} ::check-form-cache}
(env/swap-checker!
(env/checker opts)
update-in
[::check-form-cache nsym]
(fn [m]
(some-> m
(select-keys [ns-form-str])
not-empty
(update ns-form-str select-keys sforms))))]
The next Biff 2 library is ready to go: biff.graph
biff.graph makes your codebase more understandable/maintainable by helping you split up your code for reading/deriving data into small independent chunks. "Data-oriented dependency injection" is a term I've used to describe the approach.
biff.graph is a lightweight clone/variant of Pathom. I'm a huge fan of Pathom and wanted to include it in Biff by default, however it is a bit of a heavy abstraction and I was concerned if the benefits would be worth the learning effort for people working on small projects.
So biff.graph is an attempt to provide something similar to Pathom in as few lines of code as possible (~600 to be specific, with ~200 for the query engine). You still have to learn the same conceptual model, but learning what exactly it's doing under the hood should be easier. The tradeoff is that biff.graph has less functionality--most significantly there's no query planner, so biff.graph isn't as intelligent about how it executes your queries as Pathom is.
Notes
Clojure 1.13.0-alpha3 is now available! Find download and usage information on the Downloads page.
RT.map, and thus reader, tracks new PAM thresholds
CLJ-1789 select-keys - improve performance (transients, etc)
CLJ-2958 ILookup on sets
CLJ-2902 pprint - prints arbitary objects in unreadable form
CLJ-2801 TaggedLiteral - doesn’t define print-dup
CLJ-2269 definterface - does not resolve parameter type hints
CLJ-2781 clojure.test/report - docstring has broken references
CLJ-2929 zipper - docstring typo
CLJ-2901 bytes, shorts, chars - docstring typos
CLJ-2811 scalb - docstring links to the documentation for nextDown
CLJ-2809 clojure.math/floor - docstring has line that should be on ceil docstring
In this post I&aposll give updates about open source I worked on during May and June 2026.
To see previous OSS updates, go here.
I&aposd 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&apost 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.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!
A lot happened in the past two months! Not just coding but also...
Three years after the initial installment, Babashka Conf 2026 happened on May 8th at the OBA Oosterdok library in Amsterdam, with David Nolen, primary maintainer of ClojureScript, as our keynote speaker. Thanks to our sponsors Nubank, Exoscale, Bob, Flexiana and Itonomi, to Wendy Randolph for hosting, and to all the speakers, volunteers and attendees who made it such an inspiring day. You can watch all the videos here. Thanks to Ray for recording! The day after, Dutch Clojure Days 2026 rounded out a full weekend of Clojure in Amsterdam, where I did a presentation about ClojureScript and async/await. The video of that is hopefully coming soon.

Babashka Conf 2026. From left to right: David Nolen, Jen Myers, Adrian Smith, Josh Glover, Rahul Dé, Arne Brasseur, Christoph Neumann, Timo Kramer, Jynn Nelson, Wendy Randolph.
I&aposm pleased to announce that Rahul Dé and I will be hosting a babashka workshop at the Clojure Conj 2026. The workshop will showcase various use cases of babashka. This hands-on workshop covers the whole lifecycle of a babashka tool, from a quick script to a published, installable CLI app. We assume you know the basics of Clojure and won&apost explain the language itself. Topics include:
bb.edn)bbinEvery concept comes with an exercise, building toward one culminating CLI app. There will be lots of interaction and fun!
Besides this update I published two blog posts in the past two months:
and a ClojureScript reference on async functions:
Babashka CLI got the most attention this cycle. I added automatic --help generation for dispatch-based CLIs and shell tab completion for bash, zsh, fish, PowerShell and Nushell. There&aposs a dedicated post with a "build your own git" walkthrough linked above. I also made Babashka CLI Squint compatible, so CLIs built with it run on Node.js and in the browser, published as the @babashka/cli npm package. Also ClojureDart support for Babashka CLI got added.
Squint saw a large amount of work that kept going right into early July: a browser nREPL, dynamic vars and binding that survive across separately-compiled ESM modules, an EDN reader, cached lazy seqs, defrecord and a wide set of core protocols, and a big compatibility push to make it pass jank&aposs clojure-test-suite. Replicant now runs on Squint too. I added key diffing to Reagami and did some benchmarks, showing that Reagami on squint performs in the ballpark of React. The benchmark also shows that Replicant on Squint performs even a tad better than on ClojureScript. Not that this makes a huge difference in practice, but it&aposs nice to validate the idea that Squint, for typical apps, can be a valid CLJS replacement while not giving up that much in terms of Clojure features.
A security issue in SCI deserves a callout. A string type-hint could bypass the :classes allowlist and statically initialize any class on the classpath at analysis time. If you sandbox untrusted code with SCI, upgrade to 0.13.53. ClojureDart support and fine-grained interop control (which was needed for cljd support since it has no reflection) also got added. You can now make REPLs for your mobile apps!
Since porting was a theme these past months, I&aposll mention another one: babashka.fs now runs on Node.js via ClojureScript and squint, published as the @babashka/fs npm package.
Here are some highlights per project. See each project&aposs CHANGELOG.md for the full list.
babashka CLI: Turn Clojure functions into CLIs!
--help generation for dispatch CLIs, plus shell completions for bash, zsh, fish, PowerShell and Nushell (#112, #24, #95). I wrote a full post on it with a "write your own git" walkthrough: babashka CLI: automatic --help and shell completionsparse-opts*, coerce-opts, validate-opts, apply-defaults, table->treedispatch now accepts a tree directly (as returned by table->tree), and subcommand order is preserved in printed help and completions@babashka/cli npm packageopts->table accepts :columns to override the auto-detected columns (#148, thanks Jan Seeger)--no-foo on a non-boolean option errors instead of silently coercing, and :edn :coerce now requires an explicit value (#166, #174)Squint: CLJS syntax to JS compiler
str wrapping tripping esbuild), #819 (macro changes not picked up in watch mode), #820 (:macros option ignored from JS callers) and #832 (nREPL server hanging on advertised-but-unimplemented ops)--help, usage and error handling from babashka.cli&aposs dispatch, plus shell tab completionbinding now work via a mutable box, safe across separately-compiled ESM modules; syntax-quote resolves symbols through the current namespace and aliases like Clojure. defprotocol got :extend-via-metadata support.reify addedclojure.walk addedsquint.edn/clojure.edn with a ~300-line EDN reader*print-fn*, print, pr and with-out-str, like CLJSsorted-map, hash-map, subvec, pop, merge, keys/vals, peek, transducers, = on dates/regexes/lazy seqs, and more) now throw or behave exactly like CLJS instead of the old loose JS semantics, alongside full built-in cljs.test support... spreaddefrecord, record? and the IRecord marker protocol. Records store their fields as own string-keyed properties and implement the map-facing protocols, so keyword lookup, keys, seq, assoc, conj and = all work through the regular core functions; the generated implementations are shared runtime functions imported only by files that use defrecordILookup, IAssociative, IMap, ICounted, ICollection, IEquiv, ISet, the transient protocols, and IAtom/IDeref/IReset/ISwap/IWatchable (so a reagent-style reactive atom can be a plain deftype)cljs.analyzer.api/resolve now sees vars of built-in library namespaces like clojure.string, plus :squint/compile-time forms and fixes for macro self-useclj-kondo: static analyzer and linter for Clojure code that sparks joy.
defmacro (plus any supporting defn/defn-/def) tagged with {:clj-kondo/macroexpand-hook true} is automatically extracted into .clj-kondo/ and registered as a :macroexpand hook on the next run. See doc/hooks.mdasync/await in ClojureScript: bumped built-in CLJS analysis to 1.12.145 and added the :await-without-async-fn and :misplaced-async-metadata linters:alias-same-as-ns, warns when an alias equals the namespace it aliases (default :off) (@tomdl89):conditional-build-up, warns on successive (if pred (assoc m ...) m) rebinding and suggests cond-> (default :off) (@walber-araujo):if-x-x-y, suggests (or x y) instead of (if x x y) (default :off) (@jramosg):redefined-var false positive across files declaring the same namespace:protocol-method-arity-mismatch false positive for definterface declaring the same method with multiple arities (@jramosg)recur inside a vector, map or set literal, since recur is never in tail position there:invalid-arity false positive when an inner binding or fn param shadows a local function name (@yuhan0)get-in/select-keys, faster sexpr, leaner node allocation (@alexander-yakushev):keys!/:syms!/:strs!), including inferring required keys and reporting them at call sites (#2870)SCI: Configurable Clojure/Script interpreter suitable for scripting
:closed allowlist for :classes, giving fine-grained control over host interop; see the interop control docs. Also 1.6x faster instance-method interop on babashka^"some.Class" x) bypassed the :classes allowlist, loading and static-initializing any class on the classpath at analysis time. Only affects sandboxing of untrusted code via :classes; upgrade to 0.13.53:interrupt-fn option: a zero-arg function called on every interpreted fn entry, so host code can interrupt or cancel a running SCI eval (thanks @whilo)sci.interrupt/interrupt! to throw an interrupt that sandboxed try/catch cannot catch, and gate finally and the regex functions (re-matches/re-find/re-seq, JVM) through :interrupt-fn too, closing off ways to mask an interrupt and escape the sandbox #1044copy-var incorrectly marking a function as inlined when its unqualified name collided with a clojure.core/cljs.core inlined var (e.g. a custom get), silently breaking with-redefs (@verberktstan)defrecord/deftype type symbol resolution via alias (e.g. (instance? r/Foo x)), fixing nbb#410fs: file system utility library for Clojure
@babashka/fs npm package. Most functions are supported. The JVM behavior is the reference implementation so all operations are synchronous, and the glob syntax is reimplemented from scratch to match the JVM. File times are BigInt nanoseconds to preserve sub-millisecond precision. zip is left out since Node.js has no native support for itspit and slurp on both the JVM and Node.jsexec-paths returns [] when PATH is unset or blank instead of throwingcopy, copy-tree, delete-tree, zip/unzip, gunzip and the setters explicit and documented/tested (#197)Babashka: native, fast starting Clojure interpreter for scripting.
bb.edn.with-redefs on copied vars (e.g. org.httpkit.client/get) incorrectly treated as inlinedorg.jline.keymap.BindingReader for reading key bindings in terminal applications, completing the input side of the bundled JLine APIclojure.lang.ChunkedCons, clojure.lang.APersistentVector$SubVector, clojure.lang.ArraySeq, clojure.lang.PersistentVector$ChunkedSeq, java.util.AbstractCollection and java.util.Queue to :instance-checks (@paintparty)examples/tetris.clj) built on JLine&aposs Display and AttributedString, showing off the new terminal APIsReagami: A minimal zero-deps Reagent-like for Squint and CLJS
:key on children for stable node identity, so diffing reuses nodes instead of recreating them:lite-mode compatibility and added it to CI (#41)Cream: Clojure + GraalVM Crema native binary
html: Html generation library inspired by squint&aposs html tag
style maps emitting a literal \n between declarations via pr-str, which produced invalid CSS and dropped every declaration after the first (@cycl1st)style; other map-like values (e.g. records) now render via str (@telekid)Edamame: configurable EDN and Clojure parser with location metadata and more
:auto-resolve-ns, bare syntax-quoted symbols now resolve to the current namespace, matching Clojure&aposs behaviorNeil: A CLI to add common aliases and features to deps.edn-based projects
neil dep upgrade now upgrades unstable deps (e.g. release candidates) to a newer unstable version when no newer stable version existsbrew trust for users who installed neil before Homebrew introduced tap trustNbb: Scripting in Clojure on Node.js using SCI
deps.clj: a faithful port of the clojure CLI bash script to Clojure
Pod-babashka-gozxing: a babashka pod for QR code and barcode decoding/encoding, backed by gozxing
Graal-build-time: initialize Clojure classes at build time for GraalVM native-image
Contributions to third party projects:
async/await support from last cycle on the ClojureScript site, including an enhanced reference (#423, #424)dom.cljs to .cljc, adjusting core.cljc for portability), added babashka/squint test runners and wired them into CI, and fixed a multi-root render bug under squint by switching DOM state tracking to a node-map (#71, #72, merged)These are (some of the) other projects I&aposm involved with but little to no activity happened in the past two months.
Some of you might know that Clojurists Together are supporting my work on nREPL, CIDER and friends this year. Normally I send them a bi-monthly progress report, but I saw some other people who got funding for their OSS work publish those reports as blog posts for the broader public and I thought to try this for a change.
The past two months were super productive. I had a lot of inspiration during this period and I managed to tackle a lot of long-standing ideas and issues across the entire nREPL/CIDER ecosystem. Funnily enough, I also managed to grow the ecosystem with a couple of brand new projects, but more about those later.
The big highlights from my perspective:
Below you’ll find more details about the work I did, project by project.
CIDER 1.22 (“São Miguel”) landed in mid-June, wrapping up the 1.x series. Its main features:
cider-jack-inIt also fixed a long list of small annoyances: severe editor lag in unlinked buffers, several TRAMP and SSH tunnel problems, request id leaks, and a bunch of broken menu entries.
Right after that I switched the development version to 2.0 and most of the planned work is already done. The headline items so far:
tap> buffer and a dedicated trace bufferThat last one deserves a special mention: evaluation
results that are images now render inline out of the box, and file/URL results
offer their content on demand, six years after the feature had to be disabled
over its safety problems. There was also a big cleanup pass: consolidated
configuration options, the REPL history browser renamed to cider-history to
end a long-standing naming clash, theme-aware faces instead of hardcoded colors,
refreshed docs and a regenerated refcard. CIDER 2.0 is available from MELPA
snapshots and I’d love for more people to take it for a spin before the final
release.
Lots of cider-nrepl releases, driving the CIDER work above:
cider/who-implements, cider/type-protocols, cider/protocols-with-method).pprint backed by orchard.pp.Along the way the project’s build was migrated from Leiningen to tools.deps, which required a new MrAnderson release (see the blog posts below).
Orchard, the library that powers much of cider-nrepl’s functionality, kept pace:
orchard.meta, a programmatic listener API for the tracer, and protocol/multimethod introspection in orchard.xref. The project also moved to tools.deps and its CI now covers JDK 26.Sayid, the omniscient Clojure debugger, had been dormant for years and I finally gave it the revival it deserved:
mx.cider/sayid coordinates, a documented nREPL middleware API, a consolidated op surface (37 ops down to 26) and fixes for the most annoying Emacs client breakages.port is a brand new project I started in May: a minimalist Clojure interactive programming environment for Emacs, built on prepl instead of nREPL. It went from nothing to three releases in the course of the month:
I don’t have any particular plans for the future of this project - it was just something that I wanted to experiment with for a while.
I see it as an interesting option for people looking for some middle ground between inf-clojure and CIDER.
neat is the other new arrival: a small, language-agnostic nREPL client for Emacs. neat 0.1.0 has the essentials in place: a pure-elisp bencode codec, a comint-based REPL, and a source-buffer minor mode with eval, completion, eldoc, xref and doc lookup, tested against Clojure, Babashka and Basilisp. It’s early days, but it’s a nice testbed for exercising the nREPL protocol outside CIDER.
This project also means I’ve dropped any plans to try to make CIDER a language-agnostic development environment. Going forward CIDER will focus only on
Clojure-like languages, and everything else will be covered by neat.
The nREPL org saw some ClojureScript-flavored action:
load-file evaluate the editor’s buffer contents instead of re-reading from disk, tears down ClojureScript REPLs when their sessions close (no more leaked Node processes) and surfaces ClojureScript status in the describe response.WebSocket, so it runs in any modern JavaScript runtime (browsers, Node 22+, Deno, Bun, workers), and the minimum requirements moved to Clojure/ClojureScript 1.12.I also backfilled proper GitHub releases for the historic tags of both projects, so their release history is finally browsable.
Improving the ClojureScript support in CIDER has long been a major objective for me, and these small changes were some initial steps in that direction.
refactor-nrepl got three releases: 3.12.0, 3.13.0 and 3.14.0, the last one making the AST-based indexing much faster and more reliable. clj-refactor.el received a round of maintenance on master as well, and will get a new release after I wrap up the work on CIDER 2.0.
I’m still pondering the future of both projects, as I plan to move the most useful refactor-nrepl features (those that don’t carry a lot of complexity) to CIDER
and cider-nrepl eventually, and I’m not sure that the flagship AST-powered refactorings are very competitive these days (compared to clojure-lsp and static project-wide analysis a la clj-kondo in general).
I’ll write a bit more about this and I’d certainly appreciate more feedback from the users of clj-refactor on the subject. It’s funny that I’ve been maintaining the project for ages, but I’ve never really used it (mostly due to its brittleness in the past). I think I managed to address some of the biggest problems recently, but perhaps this happened too late and the project has lost its relevance by now.
I wrote a few articles related to the work above:
Big thanks to Clojurists Together, Nubank and the other organizations and people supporting my Clojure OSS work! I love you and none of this would have happened without you. Sadly, the amount of financial support my projects receive has eroded massively over the past 4 years and I’ve kind of lost hope that this negative trend will eventually be reversed. It was never easy to maintain many popular OSS projects, but the job certainly hasn’t got any easier or more rewarding in recent years…
Overall, a super productive two months. Hopefully the next two are going to be just as productive, although I have to admit I’ve plucked most of the low-hanging fruit already. Then again, I’ve said this many times in the past, so one never knows…
Clojure 1.13.0-alpha2 is now available! Find download and usage information on the Downloads page.
Fixes a build problem from new Maven plugins by reverting the change
I have a soft spot for Sayid - it’s one of the most ingenious Clojure tools ever built, and also one of the most neglected. It’s an omniscient debugger: instead of stopping your program at a breakpoint, it quietly records every call to the functions you’ve traced and lets you rummage through the recording afterwards. It’s the kind of thing you demo to people and watch their jaw drop. And it had been sitting practically unmaintained for the better part of a decade.
Here’s the awkward part: that neglect is largely on me. Bill Piel, Sayid’s original author, handed me the keys ages ago, and I’ve been… let’s say a less than exemplary steward. I’d merge the occasional patch to keep the lights on, but until very recently I’d done precious little to actually move the project forward.
The best time to maintain your open-source project was six years ago. The second best time is now.
– Ancient proverb, lightly adapted
So what finally lit a fire under me? Blame CIDER 2.0. I’ve been reworking CIDER’s debugging and tracing story, and at some point I sat down to make the built-in tracer smarter. A few hours in it hit me: nothing I could realistically bolt onto the built-in tracing would come anywhere close to what Sayid already does. So instead of building a worse Sayid, I dusted off the real one, gave it a good scrub, and here we are - Sayid 0.4.
For the uninitiated: Sayid records the arguments, return value, timing and full
call tree of the functions you trace, so you can go back and inspect exactly what
happened - no breakpoints, no println, no re-running the thing five times.
While we’re on the subject of embarrassing confessions: I’ve been the maintainer of this thing for years and I still have no idea what “Sayid” actually means or what it’s a reference to. If you happen to know, please, put me out of my misery - I’d love to finally get the joke.
Trace a namespace, run your code, and pop open the workspace with C-c s w:
▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
(demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45
can-afford? says true, but four coins worth 41 cents shouldn’t cover a
45-cent tab. Something’s off, and the bug lives inside total-cents. This is
where Sayid really shines - flip on an inner trace and it records every
expression inside the function:
▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
▾ (demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45
▾ (apply + (map coin-values coins)) => 45
(map coin-values coins) => (25 10 5 5)
There it is, staring right at us: (25 10 5 5), when the last value should be
1. A penny is worth five cents in our coin map. We never wrote a single
println, and we never had to guess where to put a breakpoint - we just looked
at what actually happened.
That’s the pitch, and it’s a great one. So why was such a cool tool gathering dust?
Honestly, part of it is that Clojure folks are spoiled. We have the REPL, so the
reflex for most of us is to just re-run a form with some tap> or println
sprinkled in and eyeball the output. That works right up until it doesn’t -
until the bug is three layers deep in a map over a lazy seq, or only shows up
on the 400th call, or lives in code you didn’t write and don’t feel like
instrumenting by hand.
A traditional stepping debugger stops the world and makes you drive. Sayid does
the opposite: it lets the program run to completion and then hands you the entire
execution history as something you can navigate at your own pace.
tools.trace is in the same spirit, but it dumps text to
stdout - Sayid keeps structured data and gives you a query DSL to slice it. That’s a much better fit for how we
actually work in Clojure: run it, capture everything, explore the data.
And here’s the thing that made me want to revive it rather than reinvent it - “capture everything as data and explore it later” is exactly the workflow that’s becoming more relevant, not less. Structured execution traces are gold, whether the thing doing the exploring is you, a data-inspection tool like Portal, or an AI assistant trying to understand why your code misbehaved.
First order of business was dragging the project into the present.
Sayid used to live under com.billpiel/sayid on Clojars, with namespaces like
com.billpiel.sayid.core. The new home is clojure-emacs, so the artifact is
now published as mx.cider/sayid:
{:user {:plugins [[mx.cider/sayid "0.4.0"]]}}
I also dropped the personal-domain prefix from every namespace - it’s plain
sayid.core, sayid.trace, sayid.nrepl-middleware and so on now. The old
com.billpiel/sayid coordinates still get the same releases for the time being,
so nobody’s dependency breaks overnight, but the future is mx.cider.
Here’s the change I’m most excited about. Sayid’s nREPL middleware used to hand the Emacs client a pre-rendered blob of text plus a pile of text properties for colouring. In other words, the server did all the rendering and the client was a dumb terminal. That single decision is a big part of why there was exactly one Sayid client.
So the middleware now speaks data. There’s a family of new ops -
sayid-get-workspace-data, sayid-query-data and friends - that return the
recorded call tree as honest, navigable data instead of a wall of text:
{"id" "4793"
"name" "demo.coins/can-afford?"
"args" ["[:quarter :dime :nickel :penny]" "45"]
"return" "true"
"file" "demo/coins.clj"
"line" 12
"children" [...]}
The structural bits (ids, names, timings, source location, the tree shape) come
across as real nested maps and lists you can walk by key. The captured values are
pr-str‘d, since an arbitrary Clojure value can’t always round-trip over the
wire - but that’s the only place strings sneak in, and it’s exactly where you’d
expect them.
The upshot: any editor or tool that speaks nREPL can now fetch a workspace and render it however it likes, and a REPL one-liner or a Portal tap gets you the same data with zero Sayid-specific machinery. The whole thing is written up in doc/nrepl-api.md, so you don’t have to reverse-engineer the wire format from the Emacs client the way you would have before.
With the data ops in place, I rebuilt the Emacs UI on top of them. The workspace
and the “what’s traced” views are now proper foldable trees, built on CIDER’s
new cider-tree-view. You get real folding (TAB), navigation (n/p),
jump-to-source (RET), and - my favourite - c i hands the actual captured
value straight to CIDER’s inspector, so you can drill into an argument or a
return value as a live, navigable object rather than squinting at its printed
form.
There’s also a query layer wired into the tree: f narrows to every recorded
call of the function at point, i focuses a single call and its subtree. On a
big trace that’s the difference between “wall of text” and “actually finding the
thing”.
The best part is that the client is now smaller, not bigger - all the tree rendering, folding and value inspection are handled by mature components instead of bespoke code painting text properties by hand. That’s the payoff of moving rendering to the client: the server ships data, and the client is free to be as fancy or as plain as it wants.
Backwards compatibility is a promise you make to the users you have. Sayid had exactly one, and reader, I am that user.
– Me, rationalising
Yeah, I did. I went pretty wild with the breaking changes this time around - new artifact coordinates, new namespaces, a reworked nREPL API, a bumped minimum CIDER version. Normally I’d bend over backwards to keep old clients working, but here I made a deliberate call: as far as I can tell, the bundled Emacs client was the only client Sayid ever had. Dancing around imaginary third parties to preserve compatibility nobody was relying on would have just made the project harder to adopt and harder to maintain.
So I opted for sweeping changes that leave Sayid in a much better place to build on, rather than a museum of backwards-compatible cruft. If it turns out I was wrong and you were quietly depending on the old coordinates - I’m sorry, and do let me know, because that’s genuinely useful information.
That’s the gist of it. Sayid is alive again, it’s leaner, it speaks data, and it has a UI I’m not embarrassed to demo. What I’d love now is for more people to actually use it and tell me whether the new direction resonates.
So please - [mx.cider/sayid "0.4.0"], trace something gnarly, and pop open the
tree. Then head over to the issue tracker and tell me what you think:
what feels great, what feels rough, what’s missing. I have ideas for where to
take it next (bounding the recording so you can safely trace a whole namespace
under a test suite is high on the list), but I’d rather steer by what people
actually want out of it.
Big thanks to Bill Piel for building such a wonderful tool in the first place - I’m merely standing on the shoulders of a giant here. And thanks in advance to everyone willing to kick the tyres on the revival!
That’s all from me for now. Keep hacking!
Well, that didn’t take long. Remember that “high on the list” bit a few paragraphs up - bounding the recording so you can safely trace a whole namespace under a test suite? Turns out I couldn’t leave it alone. A short burst of small improvements after this post went up, and it’s done: Sayid 0.5.
That was the thing that made Sayid feel like a toy - point it at a real workload and it would cheerfully eat your whole heap and fall over. The recording now has a set of tunable bounds instead: a cap on how many top-level calls it keeps, a depth limit, 1-in-N sampling for hot paths, a per-function cap, and a keep-only-the-last-N mode for when what you care about is whatever happened right before things went sideways. Fat and infinite values no longer hang the data ops either, and the traced-functions view got its enable/disable/remove actions back.
The upshot for you: you can finally point Sayid at real code under real load without babysitting it. Same ask as before - give it a spin and tell me how it feels.