Statistics made simple

I have a weird relationship with statistics: on one hand, I try not to look at it too often. Maybe once or twice a year. It’s because analytics is not actionable: what difference does it make if a thousand people saw my article or ten thousand?

I mean, sure, you might try to guess people’s tastes and only write about what’s popular, but that will destroy your soul pretty quickly.

On the other hand, I feel nervous when something is not accounted for, recorded, or saved for future reference. I might not need it now, but what if ten years later I change my mind?

Seeing your readers also helps to know you are not writing into the void. So I really don’t need much, something very basic: the number of readers per day/per article, maybe, would be enough.

Final piece of the puzzle: I self-host my web projects, and I use an old-fashioned web server instead of delegating that task to Nginx.

Static sites are popular and for a good reason: they are fast, lightweight, and fulfil their function. I, on the other hand, might have an unfinished gestalt or two: I want to feel the full power of the computer when serving my web pages, to be able to do fun stuff that is beyond static pages. I need that freedom that comes with a full programming language at your disposal. I want to program my own web server (in Clojure, sorry everybody else).

Existing options

All this led me on a quest for a statistics solution that would uniquely fit my needs. Google Analytics was out: bloated, not privacy-friendly, terrible UX, Google is evil, etc.

What is going on?

Some other JS solution might’ve been possible, but still questionable: SaaS? Paid? Will they be around in 10 years? Self-host? Are their cookies GDPR-compliant? How to count RSS feeds?

Nginx has access logs, so I tried server-side statistics that feed off those (namely, Goatcounter). Easy to set up, but then I needed to create domains for them, manage accounts, monitor the process, and it wasn’t even performant enough on my server/request volume!

My solution

So I ended up building my own. You are welcome to join, if your constraints are similar to mine. This is how it looks:

It’s pretty basic, but does a few things that were important to me.

Setup

Extremely easy to set up. And I mean it as a feature.

Just add our middleware to your Ring stack and get everything automatically: collecting and reporting.

(def app
  (-> routes
    ...
    (ring.middleware.params/wrap-params)
    (ring.middleware.cookies/wrap-cookies)
    ...
    (clj-simple-stats.core/wrap-stats))) ;; <-- just add this

It’s zero setup in the best sense: nothing to configure, nothing to monitor, minimal dependency. It starts to work immediately and doesn’t ask anything from you, ever.

See, you already have your web server, why not reuse all the setup you did for it anyway?

Request types

We distinguish between request types. In my case, I am only interested in live people, so I count them separately from RSS feed requests, favicon requests, redirects, wrong URLs, and bots. Bots are particularly active these days. Gotta get that AI training data from somewhere.

RSS feeds are live people in a sense, so extra work was done to count them properly. Same reader requesting feed.xml 100 times in a day will only count as one request.

Hosted RSS readers often report user count in User-Agent, like this:

Feedly/1.0 (+http://www.feedly.com/fetcher.html; 457 subscribers; like FeedFetcher-Google)

Mozilla/5.0 (compatible; BazQux/2.4; +https://bazqux.com/fetcher; 6 subscribers)

Feedbin feed-id:1373711 - 142 subscribers

My personal respect and thank you to everybody on this list. I see you.

Graphs

Visualization is important, and so is choosing the correct graph type. This is wrong:

Continuous line suggests interpolation. It reads like between 1 visit at 5am and 11 visits at 6am there were points with 2, 3, 5, 9 visits in between. Maybe 5.5 visits even! That is not the case.

This is how a semantically correct version of that graph should look:

Some attention was also paid to having reasonable labels on axes. You won’t see something like 117, 234, 10875. We always choose round numbers appropriate to the scale: 100, 200, 500, 1K etc.

Goes without saying that all graphs have the same vertical scale and syncrhonized horizontal scroll.

Insights

We don’t offer much (as I don’t need much), but you can narrow reports down by page, query, referrer, user agent, and any date slice.

Not implemented (yet)

It would be nice to have some insights into “What was this spike caused by?”

Some basic breakdown by country would be nice. I do have IP addresses (for what they are worth), but I need a way to package GeoIP into some reasonable size (under 1 Mb, preferably; some loss of resolution is okay).

Finally, one thing I am really interested in is “Who wrote about me?” I do have referrers, only question is how to separate signal from noise.

Performance. DuckDB is a sport: it compresses data and runs column queries, so storing extra columns per row doesn’t affect query performance. Still, each dashboard hit is a query across the entire database, which at this moment (~3 years of data) sits around 600 MiB. I definitely need to look into building some pre-calculated aggregates.

One day.

How to get

Head to github.com/tonsky/clj-simple-stats and follow the instructions:

Let me know what you think! Is it usable to you? What could be improved?

Permalink

clj-suitable 0.8.0: Closing the Gap with Compliment

You had me at js/.

– Jerry Maguire, on ClojureScript interop completion

No sufficiently useful system can be both complete and consistent.

– Kurt Gödel, subtweeting every autocomplete ever

clj-suitable 0.8.0 is out! If the name doesn’t ring a bell, that’s rather the point - clj-suitable is the small library that quietly powers ClojureScript code completion in CIDER, Calva, and pretty much anything else that talks to a cljs REPL over nREPL. Think of it as the ClojureScript counterpart to what compliment1 does for Clojure. For years it lagged well behind its Clojure sibling, and this release is my attempt to finally close that gap - to make cljs completion, if you’ll forgive me, a touch more suitable.

This is also a direct follow-up to the Piggieback work I wrote about a couple of weeks ago. Once I had the cljs REPL plumbing back in decent shape, fixing up the completion story sitting on top of it was the obvious next move.2

The mission: catch up with compliment

Clojure programmers have had really good completion for years, thanks to compliment. ClojureScript programmers got a paler version of the same idea - the basics worked, but all the little touches that make completion feel smart were missing. My goal for this cycle was simple to state and less simple to deliver: teach clj-suitable the tricks compliment has had all along. I think we’re kind of there now.

Here’s what that means in practice, mostly borrowed straight from compliment’s playbook:

  • Fuzzy matching. You no longer have to type a prefix - cs now completes to clojure.string and rkv to reduce-kv, the same subsequence matching you’re used to on the Clojure side.
  • Smarter ranking. Candidates are ordered the way compliment orders them - vars from the current namespace first, then cljs.core, then everything else. The thing you actually want tends to be at the top instead of buried alphabetically.
  • Local bindings. Completion now sees the bindings from the surrounding form - let, loop, fn, for, doseq and friends - including destructured ones. Type first| inside (let [{:keys [first-name]} m] ...) and you’ll get first-name, which previously you would not.
  • Referred vars. Inside a (:require [clojure.string :refer [jo|]]) clause you now get join, scoped to that one namespace rather than the whole world.
  • Context awareness. Special forms are only offered at the head of a list, so if, let and company stop showing up as candidates in argument position where they make no sense.

None of these are revolutionary on their own, but together they’re the difference between completion that feels like an afterthought and completion that feels like it belongs.

A bit of backstory

There’s a nice irony in chasing compliment, because for a while ClojureScript completion actually lived inside it. Back in 2019 Andrea Richiardi ported the cljs-tooling completion machinery - the same code CIDER used for cljs at the time - straight into compliment, and there was even a follow-up attempt to pull clj-suitable’s JavaScript interop completions in alongside it. (You can still spot the heritage: a few functions in clj-suitable’s current source are marked “Ported from compliment.”)

In the end we went the other way around: instead of growing compliment to cover ClojureScript, we consolidated the ClojureScript side in clj-suitable and reverted the port. That sounds like wasted effort, but it wasn’t - compliment’s pluggable custom source architecture is exactly what made the split clean. clj-suitable just registers itself as another source, so tools get Clojure and ClojureScript completion side by side without compliment having to know a thing about cljs.

Robert Krahn had started clj-suitable earlier that year for the dynamic, runtime-introspection side, and the static ClojureScript completion found its permanent home there too. In hindsight it was clearly the right call: cljs completion gets to grow (and break, and get fixed) on its own schedule, and compliment stays focused and lean. A good architecture is the kind that makes the split you didn’t plan for feel obvious after the fact.

Why ClojureScript makes this harder

Completion for Clojure is almost unfairly simple. Your code runs on the same JVM as the nREPL server, so compliment can just reflect on the live thing - real vars, real namespaces, real Java classes, all sitting in the same process. Ask a question, get an answer.

ClojureScript doesn’t get to be that lucky, because it lives in two worlds at once. The compiler is a Clojure program running on the JVM, and it’s the source of truth for namespaces, vars and their metadata - so static completion reads the ClojureScript compiler state, not your running program. But your actual program runs somewhere else entirely: a Node process, a browser tab, maybe a React Native app on a phone, reachable only across a REPL bridge. When you want to complete JavaScript interop - the methods on js/console, say - there’s nothing on the JVM to reflect on. You have to ship a bit of code across that bridge, run it in the JS runtime, and read back what a live object actually exposes.

That one fact is where all the complexity comes from. The bridge isn’t even a single thing - a piggieback-driven cljs.repl runtime evaluates differently from shadow-cljs, and clj-suitable has to speak both. The runtime can vanish under you - refresh a browser tab and the namespace you loaded is gone. And poking at a JS object to list its properties can have side effects, because a property getter is just code that runs. (If you’ve ever wondered why clj-suitable is so careful to only evaluate things that genuinely look like interop, that’s why.)

So a completion request that looks like one operation from the editor is really two very different machines under the hood - one reading compiler state on the JVM, one evaluating code in a JS runtime you don’t control. Here’s the whole picture:

  editor (CIDER / Calva)
     |   complete: prefix + context
     v
  nREPL server (one JVM) - cider-nrepl + clj-suitable
     |
     +-- static ---> compliment + clj-suitable's cljs source
     |               reads the ClojureScript compiler state
     |               (namespaces, vars, locals, keywords) - stays on the JVM
     |
     +-- dynamic --> only for JS interop forms
                        |   eval introspection code across the REPL bridge
                        v
              piggieback (cljs.repl)   or   shadow-cljs
                        |
                        v
              Node / browser / React Native  (the JS runtime)
              suitable.js-introspection reads a live object's
              properties and methods, and sends them back
     |
     v
  candidates from both paths, merged and returned to the editor

Two paths, one answer. Clojure completion has only ever needed the top half of that diagram.

Dynamic completion, tightened up

That dynamic path is the fiddly one, and it’s where this release did most of its sanding. The interop completion already worked - (.| js/console) would offer you log, warn and the rest - but it had some rough edges:

  • Completing interop no longer clobbers your REPL history. Poking at (.| js/some-obj) used to quietly overwrite *1/*2/*3 with the introspection result; now your last real value stays put where it belongs.
  • The introspection namespace is loaded once per session instead of on every single completion request. On a Node REPL that’s a needless round-trip gone from every keystroke.
  • The browser-runtime path got hardened. I chased down a couple of long-standing “no completions in the browser” reports, stood up a real headless-Chrome integration test to reproduce them, and fixed a lurking crash along the way. (The short version of the investigation: the old failures came from an inlined build that current CIDER no longer produces, so most of you were never affected - but now there are tests making sure it stays that way.)

The full changelog has everything that didn’t make the highlights.

What came before

0.8.0 gets the headline, but it stands on the 0.7.0 release from a week earlier, which did the unglamorous groundwork: modern dependencies (ClojureScript 1.12, compliment 0.8.0, shadow-cljs 3.x), a move from CircleCI to GitHub Actions, a tools.build-based build, and - crucially for my sanity - actual integration tests that drive real Node and browser runtimes instead of trusting things to work.

Coming to CIDER

If you use CIDER, you don’t have to do anything to get any of this - it’ll ship to you as part of CIDER 2.1. Calva and other nREPL-based tools that depend on clj-suitable will pick it up on their own schedule.

As always, this stands on the shoulders of others. Huge thanks to Alex Yakushev, whose compliment is both the benchmark I was chasing and the source of a good chunk of these ideas; to Andrea Richiardi, who did much of the early work bridging ClojureScript completion and compliment; and to Robert Krahn for creating clj-suitable in the first place and giving me such a solid foundation to build on.

Is any of this complete? Of course not - completeness is a horizon, not a destination, and of all people a completion library should be the first to admit it (see the gentleman up top). But clj-suitable suits ClojureScript a good deal better than it did a month ago, and that was rather the point.

Keep hacking!

  1. The completion library, spelled with an i. Not the nice thing you say to someone, and - I really cannot stress this enough - not complement with an e

  2. One thing invariably leads to another with this stuff. You set out to fix a REPL env wrapper and three weeks later you’re writing a headless-Chrome test harness. No regrets. 

Permalink

Looking for work

Hey, Niki here. This is a bit unusual. My sabbatical is coming to an end, and I am looking for a new opportunity. Full-time or contract, startup or research, remote or Berlin, individual contributor, ideally—tight team, ambitious product.

I am a software engineer first and foremost with 20+ years of experience. I work on technically challenging products, foundational technology, dev tools. I’ve been doing Clojure and web recently, but I'm also very excited to explore closer-to-the-metal programming.

I have an eye for design, user interfaces, UX, DX. I would love to work with a team that takes interface quality seriously. Or to work with graphics!

I am pretty sure I am good at explaining stuff, including what we are building, why, why this way, why is it important, etc. For example.

The overarching theme is to understand computers deeply, and then use that to make better and simpler software. If you care about that too, we might be a great match!

Recent work

Instant DB is a US startup building a modern Firebase. I worked on the sync algorithm, performance, DX. A summary of my commit log.

Roam Research is an OG personal knowledge manager. I worked on database optimization and a plugin system.

At JetBrains, I developed a new Skia renderer for Fleet and Jetpack Compose Desktop.

I’ve built many open-source libraries, including a database, a GUI toolkit, a Clojure dev environment, a React wrapper, a well-known font... More recently, Clojure+ gives you a taste of my approach to DX, and Fast EDN—to performance.

I maintain several active projects — AlleKinos.de, Grumpy Website, this site.

If you want to dive deeper, here’s the usual stuff: Projects / Talks / LinkedIn / GitHub

Why this post?

It’s an attempt to reach beyond my immediate network. I’ve been doing Clojure for a long time, and now want to explore.

If you are working on a compiler, a database, an IDE, a programming language or another technically ambitious product, touching graphics, typography, algorithms, low-level programming, and you think my experience can help, let’s talk: niki@tonsky.me.

Permalink

Maybe not microservice: The Case for Pipes, Pipelines, and Functional Isolation

1. Subsystem Decomposition

1.1 The Decomposition Problem

A subsystem decomposes a codebase into smaller, cohesive units. Two primary axes of decomposition exist:

  • Technical axis: grouping by component type (controller, service, model, view)
  • Functional axis: grouping by business capability (cataloguing, circulation, etc.)

1.2 Tension Between Framework Prescriptions and Decomposition Strategy

Organizing top-level subsystems functionally may create friction with frameworks that prescribe a technical-first structure. Concrete examples:

  • Rails enforces model, view, and controller directories at the root level, making functional decomposition awkward without additional mechanisms like Rails Engines
  • Sinatra (a microframework) imposes minimal structure, leaving architectural decisions entirely to the team

Frameworks with rigid prescriptions constrain architectural choices. Frameworks with no structure shift the entire burden onto the team with no guidance. This second approach might be fine for teams that know what they are doing and how to shape the architecture properly. Not everyone needs guidance from the framework.

1.3 Contexts as a Middle Ground

Phoenix provides contexts as a compromise:

  • Explicit, guideline-oriented subsystems that enable functional decomposition without rigid enforcement
  • Contexts define functional boundaries while allowing technical organization to remain nested within them
  • Functional blocks may later evolve into microservices, but this is optional
  • The same decomposition serves equally well in a modular monolith or a distributed architecture
  • The choice depends on team needs, scaling requirements, and operational maturity, not on the decomposition strategy itself

2. Pipeline Topology and Data Flow

2.1 The Unix Pipeline Model

Unix pipelines model data flow through a single stream connecting stdout to stdin. This forms a linear chain where each stage's output becomes the next stage's input. Key characteristics:

  • Each stage has exactly one input and one output
  • Cognitive overhead is minimized because the topology is trivial to trace
  • The linear, single-stream characteristic is not mandatory for a pipeline, but it reduces complexity significantly

2.2 Arbitrary DAG Topologies

Orchestrators like Airflow allow arbitrary DAG topologies with fan-in and fan-out edges. Tradeoffs:

  • Powerful for expressing complex dependencies
  • DAGs with dense interconnections tend to become hard to read even with visual rendering
  • Complex function-call topologies with many parameters outside Airflow and Unix pipelines also produce unreadable code

2.3 Byte Streams and Opaque Containers

Unix-like pipelines connect programs by passing data through unidirectional byte-streams:

  • Programs at each end agree on a structure such as JSON, CSV, or tar archives
  • The pipe mechanism itself transports only raw bytes
  • This has a direct analogue in dynamically typed languages: in Lisp and Clojure, collections (lists, maps, vectors) serve as opaque containers that can hold almost arbitrary data
  • The consumer interprets the contents rather than the container dictating them

3. Typing Heterogeneous Pipeline Data

3.1 The Problem

Strict type systems introduce complications when handling heterogeneous data flowing through a pipeline where each stage transforms the shape slightly.

3.2 Failed Approaches

Single large type with many optional fields:

  • Creates dependencies between all pipeline steps
  • Loses the ability to reject illegal data
  • Makes reuse difficult
  • Changes to one field propagate everywhere

Many separate types for each step:

  • Exhaustive and adds noise to the program
  • Structures may not be mutually exclusive yet are treated as such
  • Maintenance burden grows with every new stage

Both approaches fail because each pipeline stage depends on more than it needs.

3.3 Partial Fixes From Functional Programming

Two techniques alleviate but do not fully resolve the problem:

  • Functional record update: enables creating modified copies without mutation, reducing coupling related to state changes
  • Sum types: restore the ability to discriminate valid from invalid data and support exhaustiveness checking

Remaining limitation: every step that pattern-matches on a sum type must know about all variants. Adding a new case still propagates changes through the pipeline.

3.4 Structural Type Compatibility

Structural type compatibility offers a complementary solution:

  • Independently defined types become compatible based on shape alone without requiring any inheritance relationship
  • A consumer can specify only the subset of fields it needs via a structural interface
  • Each step depends on a minimal projection of the data rather than the full type
  • This decouples pipeline stages more effectively than either naive approach or sum types alone

3.5 Python Implementation

Python implements several of these patterns:

  • dataclasses.replace(): supports immutable record updates
  • The | union operator: simplifies union type expressions
  • Protocol classes: enable structural subtyping, allowing independent types to satisfy contracts based on method and attribute signatures
  • Tagged unions: modeled using Literal discriminator fields on dataclasses or TypedDicts
  • typing.assert_never with mypy: enforces exhaustiveness checking on pattern matching or if chains, providing compile-time guarantees similar to sum types in functional languages

Combined approach:

  • Protocols decouple steps through structural conformance
  • Tagged unions enable variant discrimination with exhaustiveness checking
  • Functional record updates reduce mutation-related coupling

4. Microservices, Processes, and Isolation Patterns

4.1 The Shared Principle: Isolated State by Default

A microservice and a Unix process share architectural similarities:

  • Microservices: in well-designed architectures, a service does not share variables or databases with other services. Communication happens through well-defined interfaces. This is a best practice, not a hard technical constraint.
  • Unix processes: each process has its own virtual address space and does not share memory directly with other processes. Explicit sharing is possible through mechanisms such as shm_open (POSIX shared memory) or mmap.

4.2 Historical Lineage

Microservices on GNU/Linux are literally processes communicating via HTTP over TCP/IP. The historical chain:

  • TCP/IP: first implemented in 4.2BSD Unix in 1983
  • HTTP: developed at CERN in 1989 to 1990, building upon these networking foundations
  • Tim Berners-Lee wrote the first HTTP server and web browser on a NeXT workstation running NeXTSTEP in fall 1990
  • NeXTSTEP was heavily influenced by BSD Unix
  • GNU/Linux copied many initial ideas from Unix while remaining free and open

The modern distributed system traces an unbroken lineage back to Unix.

4.3 Pipes as an Alternative to Microservices

Piping via stdin-stdout chains is another mode of interprocess communication:

  • Not as powerful or generic as TCP/IP or HTTP
  • Easy to use and reason about
  • Naturally fits data pipelines
  • A data pipeline can be built using command-line tools piped together, running as processes on GNU/Linux instead of using microservices and a full orchestration system
  • Scalability can be achieved by SSH and distribution through GNU Parallel, which launches jobs across multiple machines accessed over the network

4.4 Erlang and Clojure as Additional Isolation Models

Erlang processes:

  • Lightweight alternative to Unix processes
  • Rich high-level interprocess communication via mailbox message passing
  • The Erlang VM enforces process isolation as a runtime guarantee

Clojure and other functional runtimes:

  • Do not have the same process isolation constraints as the Erlang VM
  • Provide lightweight isolation via persistent data structures and Software Transactional Memory (STM)
  • STM allows memory sharing while preventing conflicts even when multiple functions run in parallel or concurrently

Bottom Line: Think Twice Before Going Micro

Here is the real talk. You might want to pause before spinning up your first microservice. Ask yourself these questions:

  • Do I actually need physical isolation, or will logical separation suffice?
  • Can a simple pipe between processes do the job just as well?
  • Am I solving a scaling problem that does not exist yet?
  • Do I have the ops maturity to handle distributed tracing, service meshes, and deployment pipelines?
  • Will my team understand this architecture six months from now?

The truth is, Unix pipes have been doing data transformation reliably since 1973. Erlang processes have handled millions of concurrent connections since the 1980s. Functional isolation with STM has been working since Clojure showed up in 2009. None of these require Kubernetes. None of them need a dedicated platform team. And none of them will haunt you with debugging nightmares at 3am.

Microservices are not evil. They are just heavy. They are the nuclear option for isolation. Use them when the problem demands the weight. Otherwise, reach for the lighter tool. A pipe, a context boundary, a protocol type. Try the easy solution first. If it breaks, then scale up. Most teams never get to that point. And their systems stay simpler, cheaper, and easier to maintain because of it.

So yeah, think twice. Maybe thrice. Then build the smallest thing that could possibly work.

Permalink

Sayid 0.8

Sayid 0.8 is out! It’s the third release since I brought Sayid back from the dead a couple of weeks ago, and it has a clear theme: making the tool easy to pick up. The revival releases were mostly about the engine - bounding the recording, consolidating the API, getting the data out. This one is about the experience. If you’ve ever bounced off Sayid because you couldn’t figure out what to press, or what it was trying to tell you, 0.8 is for you.

It started with a bug report

Shortly after the revival post, someone reported that pressing c i in the workspace view - “inspect this captured value” - printed Def'd as $s/* and then… nothing. The fix turned out to be a one-liner: Sayid was calling a CIDER function whose signature changed years ago, and nobody had noticed since. Which tells you everything about how many people were actually using that command.1

The one-liner was easy, but the report got under my skin. If the inspector integration could sit broken for years, what else about Sayid was quietly hostile to anyone trying it for the first time? So I sat down and did a proper UX audit of the Emacs client, wrote down everything that made me wince, and 0.8 is the result.

Here’s the current state of affairs in one take - trace, run, explore:

The Sayid workflow - trace some namespaces, run your code, explore the recording

Catching up: 0.6 and 0.7

Before we get to the UX work, a quick recap of the two releases I never got around to announcing (the revival post covered things up to 0.5).

Sayid 0.6 rebuilt inner tracing - the mode that records every intermediate expression inside a function - on top of tools.analyzer.jvm. The old implementation re-read your source and rewrote raw forms, with special cases for individual macros; the new one works off the analyzed AST. That killed the long-standing bug where an inner-traced try/catch would swallow exceptions, along with the per-macro special-casing that made the old instrumenter so fragile.

Sayid 0.7 made the trace itself data. sayid.data/trace-data returns the recorded call tree as plain Clojure data with the live captured values, and tap-trace! sends it to tap>, so you can explore a recording in Portal or your data tool of choice. It also added sayid.golden - capture a run’s call tree as a baseline, then assert future runs still match it, which is a surprisingly pleasant way to pin down the behavior of gnarly legacy code before refactoring it.

The highlights

Now, the 0.8 goodies. The full list is in the changelog, but here’s what I’m most excited about:

  • There’s a proper entry point now: C-c s pops up a transient menu (sayid-menu) that groups the commands along Sayid’s core loop - trace something, run your code, explore the recording - and shows you how much is traced and recorded right now, or what’s missing (a REPL, the middleware) when you’re not connected. The menu uses the same key sequences as the classic prefix map, so your muscle memory keeps working, and sayid-use-menu brings the plain keymap back if popups aren’t your thing.

The Sayid menu

  • sayid-trace-fn (C-c s t t) is the new “just trace this” command. You no longer need to know what inner and outer traces are before you can trace your first function - the default does the right thing, and you can graduate to the fancier variants later.
  • Empty views teach instead of scolding. Opening the workspace before anything was recorded used to greet you with an error; now you get the buffer anyway, with a short walkthrough of how to get data into it. The first five minutes with Sayid should no longer require reading the manual.
  • The commands talk back like a human. Tracing a function now tells you what happened and what to do next (“Outer-traced acme.checkout/subtotal - run some code, then C-c s w shows what was recorded”), and trying to enable a trace that doesn’t exist tells you the actual problem instead of pretending everything went fine.
  • The workspace tree is now the one view to rule them all: on any call you can inspect a captured value in CIDER’s inspector (c i - yes, it works now), def it to a var for REPL poking (c d), pretty-print it (c p), or copy an expression that reproduces the call (c r). C-c s f - show the recorded calls of the form at point - renders there too.
  • A pile of small courtesies that add up: tracing a function no longer steals your window with a popup, g refreshes the Sayid buffers like every other Emacs buffer, and resetting the workspace asks before irreversibly dropping your traces and recording.
  • The client and the middleware now tolerate version skew - if your deps.edn pins an older Sayid jar than your Emacs package expects, things degrade politely instead of erroring in strange ways.
  • The Emacs client now requires CIDER 2.0, which let me drop a bunch of compatibility shims and lean on the new cider-tree-view throughout.
  • And the README finally shows the tool instead of just describing it - screenshots and the GIF above included. A picture of a call tree is worth a thousand words about one.

Upgrading notes

Nothing here should break a working setup, but two things are worth knowing. The Emacs package now requires CIDER 2.0 (released earlier this week), so they’ll need to be upgraded together. And C-c s now opens the menu instead of acting as a bare prefix - every old key sequence still works exactly as typed, but if you prefer the old silent prefix, set sayid-use-menu to nil. A few commands got more consistent names (sayid-trace-fn-outer and friends); the old names live on as obsolete aliases.

Onward

When I wrote the revival post I asked people to kick the tyres and tell me what feels rough. This release is what acting on that feedback looks like, and it’s exactly the kind of contribution I need more of - a two-line bug report turned into the biggest usability overhaul in the project’s history. So thanks to everyone who has been trying out the revived Sayid, and please keep the reports coming on the issue tracker.

[mx.cider/sayid "0.8.0"] is on Clojars, the Emacs package is on MELPA, and tracing your first function is now a single C-c s t t away. Give it a spin!

Keep hacking!

  1. In fairness, it also tells you something about me - the bug shipped with my own resurrection releases. Reviving a decade-old codebase means inheriting a decade of API drift, and some of it only surfaces when a real user presses a real key. 

Permalink

Why Aussom?

You have a Java application, and now you need it to do something Java is awkward at. Maybe you want to let users script your app without recompiling it. Maybe you want to change a piece of business logic without a full redeploy. Maybe you just want to run a quick, throwaway script and not stand up a whole build. Java is a phenomenal language and runtime, but these are the edges where it starts to feel heavy. That is exactly the space Aussom is built for.

I started using Java almost 20 years ago and have loved every bit of it. I'm proud of all the recent momentum in the Java space and I hope it continues. Java does so much so well. But every great tool has an edge where it stops being the right one, and reaching for another language there isn't a betrayal of Java. It's how the best ecosystems work.

A useful comparison: C and Python

Look at C. C is clearly important. Even after a lifetime of use it's still the foundation for new projects today, and new competitors such as Rust haven't been able to meaningfully displace it. C is fast and powerful on its own, but it isn't great for simple tasks. It's poor for throwaway code and quick scripts, it isn't very portable, and it hands you plenty of footguns. It's also a poor choice when you want to offer a scripting interface.

Enter Python. Python is everywhere today because the barrier to entry is so low and it's genuinely useful. But Python leans on C. It's written in C, and much of its power comes from existing C libraries, whether they're UI frameworks, AI inference engines, or anything in between. C is efficient but poor at simple dynamic work; Python is dynamic and simple but poor at raw power and efficiency. Neither replaced the other. They endure together because they complement each other's weaknesses.

That is the case I make for Aussom. Aussom is to Java as Python is to C. It doesn't compete with Java, it complements it.

What makes Aussom different from other JVM languages

This is where Aussom parts ways with most of its neighbors. Kotlin, Scala, Groovy, Clojure, and friends compile to JVM bytecode. However different their syntax, they ultimately produce bytecode that's handed to the JVM to run, so they live inside the JVM's rules.

Aussom does not compile to bytecode. The Aussom runtime is compiled and starts up on the JVM, but your Aussom program is never compiled. The runtime parses it on the fly and interprets it directly. There's no compile step, and the JVM's rules don't apply to your code. Because a program is just text until the moment it runs, Aussom can load and unload code at runtime, in real time.

For a Java developer, that means you can hand Aussom a string, run it, throw it away, and hand it another one, all inside a running JVM process.

What it looks like

For quick work, an Aussom script is just a .auss file of top-level statements. No class, no main, no ceremony. Read some input, print some output, and you're done:

include os;

name = os.readLine("Your name: ");
os.printf("Hello, {}!\n", name);

// Uppercase whatever is piped in: echo hi | ./shout.auss
text = os.readAll();
os.out(text.toUpper());

The language will still feel familiar. Lists and maps have first-class literals, try/catch works the way you'd expect, and the standard library covers the everyday scripting jobs:

include os;
include http;

res = new Http().get("https://api.example.com/status");
if (!res.info.isSuccessful) {
    os.outErr("request failed: " + res.info.responseCode);
    os.exit(1);
}

data = json.parse(res.body);
os.printf("state: {}\n", data.get("state"));

When a script grows up into something bigger, the same language scales into classes, methods, and modules, so nothing you learn in script mode is thrown away.

Embedding it in your Java app

Here's the part that matters most for a Java shop. You embed the interpreter directly:

Engine eng = new Engine(new SecurityManagerImpl());
eng.parseFile("script.aus");
int result = eng.run();

That's the whole integration. The script is data, so it can come from a file, a database row, a text field in your admin UI, or a request body. You decide where the code lives and when it runs.

The security model

Letting arbitrary code run inside your process should make you nervous, and that's precisely why Aussom is built around a security manager. The Engine takes a SecurityManagerInt, and sensitive actions are gated by named properties such as reflect.eval.string (run code built at runtime), current.path.view (see the working directory), or os.info.view (read details about the host). The built-in SecurityManagerImpl denies these by default, so you opt in to exactly what you're comfortable granting, and you can supply your own implementation for finer control. That makes Aussom a real option for a user-facing scripting interface, not just internal glue.

Why not just use X?

A fair question. JShell and scripting engines such as Nashorn or GraalVM's JavaScript can evaluate code at runtime too, and Groovy has long been the go-to for JVM scripting. Aussom's distinction is the combination: pure interpretation with no compile step, true load-and-unload of code at runtime, a small and familiar syntax, and a built-in, property-based security model designed for handing scripting to your users. It's aiming to be the simple, dynamic complement to Java, not another way to write Java.

Try it

Java isn't bad at these tasks; it's just built for something else. When you need something simple, dynamic, and safely embeddable, that's where Aussom shines.

The easiest way to get a feel for it is the online playground. No install, no setup, just write a script and run it right in your browser:

https://playground.aussom-lang.com/

Try a few lines, then imagine that same interpreter embedded in your own Java app. I think you'll see the fit.

Permalink

Arbitrary Update: The Next One

Just a minor updates post. There are three tiny things.

Firstly, I now have a Printables account. I already have a Thingiverse profile that I haven't touched in a fair while. Not really sure why. If I had to speculate, I'd say that it has less to do with the MyMiniFactory buy-out and more to do with the fact that some of the creators I follow are more active on the Prusa site than Thingiverse. This makes me mildly sad because, as a Cory Doctorow fan, I'm more positively disposed towards the scrappy maker ethos exemplified by Thingiverse.

Secondly, the logo bar now includes the OpenSCAD logo. Given how much work I've been doing in it, and given the fact that PHP and Rebol are still up there, it was about time.

Thirdly, this blog is now written in Python. I've been holding on to Clojure for probably longer than was sensible. And in particular, given what this blog is, it was getting harder and harder to justify running a full JVM on my server for it. Deployment was kind of a pain and involved screen, and doing magic to make sure the blog came back up when I restarted the server. There is now a docker-compose.yml over in the repo, which should tell you exactly how I plan to use this. As per the usual, there was heavy LLM assistance here. I should note; I started this port sometime last year, and didn't get annoyed enough by server restarts to finish it until last week. The difference is staggering. Originally, I was doing the function-by-function thing, occasionally rewriting the output from ChatGPT entirely in order to make the Clojure to Python translation at all sensical. It kept trying to do weird things like reimplement the atom system, even where it was literally being used for plain mutable state (which Python has by default). That didn't happen this time; I fed the remaining files in, told it what I wanted and where the cleavage points were, and it spat out a 98% good blog server, along with deployment workflows and setup. As someone who always used programming as an instrumental skill in service of acts of creation, I'm pretty damn happy about this.

That's it for now. I'm working on a few things in the background. One of them might even eventually involve Clojure (or at least Clojurescript) in some capacity. But I wanted to get the update out before it got stale.

As always, I'll let you know how it goes.

Permalink

July 2026 Short Term Project Updates

Here are July’s updates for short term projects funded in Q2 2026. You can find overviews of these projects and the two others which will be reporting on a slightly different schedule in the original funding announcement. Thanks everyone!

Clojure LLM: Dragan Djuric
Malli: Ambrose Bonnaire-Sergeant
PluMCP: Shantanu Kumar

Clojure LLM: Dragan Djuric

Q2 2026 Report 2. Published June 30, 2026

The proposal was (in short):

The goal is to provide a high performance local LLM (large language model) AI solution, that supports mainstream open models, freely available at Hugging Face and elsewhere. Something like llama.cpp (https://llama-cpp.com/), but (hopefully!) simpler and faster, with both GPU and CPU support baked-in from the start.

I even have a catchy name for the library: iLLaManati :)

iLLaManati should:

  • work :)
  • be very fast,
  • have a very simple API (possibly even a NO-API if you use the default configuration),
  • have a fairly elegant implementation with not many lines of code, which will be a great showcase for Clojure as an enabling technology, and a good learning source for Clojurians.
  • integrate into the Clojure ecosystem naturally and seamlessly,
  • NOT require Clojurists to know anything about CUDA, ONNX, tensors, or linear algebra, to be able to use it (will require some of that if you want to extend it, though!),
  • run on your laptop, server, or cloud; wherever Clojure runs. It’s your choice.
  • be a great low-effort gateway for Clojurists to peek, as users, into high-performance and GPU computing,
  • be a very attractive topic to tell the world about!

Progress so far:

In the second month, main focus was on the hammock, but I also accomplished plenty of implementation. The initial prototype is almost there. When it start working correctly, I will be able to polish it a bit and try to squeeze as much performance is available in ONNX Runtime (not that much!) However, these challenges help me forge a better overall framework for more serious engines in the future.

The hammock

Lots of reading and thinking. And again.

Tokenizer

I polished the tokenizer a bit and integrated it with LLM. It works, and works well!

The original superfast token sampler

I polished this sampler and integrated it into the LLM loop. I fixed some correctness bugs and also supported float16 data (without losing perofmance). I didn’t have time to write up a scientific article, so that’s pushed into July (I hope!) so I didn’t publish the source yet.

The heart: LLM runner

This is still WIP, but i made lots of progress still. I implemented universal Clojure types that can cover both CPU and GPU implementations of prefill and decode. I connected that with the tokenizer and sampler, and got a consistent and meaningful stream of tokens out of it. So the first milestone for the functional part is there.

The KV manager also works well, having in mind that it has to cater to the ONNX Runtime constraint.

However, lots of challenges, especially with the CUDA EP. The ONNX Runtime has its own quirks, and of course the documentation is scarce and examples non-existing when it comes to integrating this with other CUDA code. I spent many hours debugging heisenbugs and trying to make it fits nicely. I made huge progress, but still have some quirks to solve before I get it to the same level of correctness that the CPU has. ONNX Runtime seems to have mind of its own with CUDA, cuBLAS, and cuDNN contexts and streams, and, of course it can’t be controlled fully from the outside, and of course it sometimes work this way, and sometimes that way…

So, the correctness part is not fully there, but I expect to solve it soon.

I’m less optimistic about the performance part. Of course it’s not expected of the default execution provider, but plenty of powerful providers are there: CUDA, TensorRT, OpenVINO, DNNL. Alas, none of these providers supports ALL Gemma3 operations, and this seriously sabotages the performance. Some less advanced models might be better supported (and even Gemma3 is last years news though) but overall it seems that ONNX Runtime struggles with up-to-date support for diverse LLM model architectures.

But does that make me pessimistic? On the contrary! This struggle gave me great insights into the challenges of running diverse LLMs (not only for text generation), and I have some concrete ideas about a solution with many backends, not unlike how Neanderthal and Deep Diamond solved this for matrices and tensors. The backend based on ONNX Runtime will be a good multiplatform baseline and the initial prototype, and then I can create backends based on industry heavyweights such as TensorRT-LLM for Nvidia GPUs, OpenVINO for Intel CPUs, MLX for MacOS, and, why not, even integrate Llama.cpp as an all-rounder.

Miscellaneous

To accommodate the requirements of iLLaManati, I worked on assorted improvements and upgrades in Uncomplicate libraries. I also spent a lot of time compiling upstream C++ code and dealing with cryptic C++ compiler shenanigans, that I am constantly reminded why Clojure is so great to work with :)

Of great importance is that I added support for Float16 to both Neanderthal and Deep Diamond!

I haven’t had time to make official releases, nor I committed all code to GitHub. I’m still in the middle of the battle.


Malli: Ambrose Bonnaire-Sergeant

Q2 2026 Report 2. Published July 17, 2026

In this project, I am tackling exponential growth related to Malli refs.

There has been a lot of progress to report in this second month of work. As before, I have been iterating on an implementation in this pull request, and I think I have carved out a design and implementation that addresses the main goals of the project, while streamlining and simplifying both the current and future design of Malli schemas.

If we view Malli schemas as a graph where nodes are schemas and edges point to their child schemas, then this graph is acyclic in Malli’s current implementation. Not only that, nodes that represent the exact same schema but are merely occurrences naming the same schema are not consolidated. This has caused many performance and usability issues with Malli that we have historically tackled by trying to consolidate this graph within Malli’s operations.

For example, when converting Malli schemas to value generators we add extra checks to essentially detect cycles in the graph of schemas in order to avoid generating unusably large values. We then solved the same problem separately for validators, explainers and transformers to fix memory leaks caused by recursing down large values, with each implementation being distict. The same problem would have to be solved for each current and future operation that we’d like to purge these issues of.

One particular symptom of this surprising duplication of effort is worth mentioning. Schema instances (nodes in the graph described earlier) each carry an internal cache for caching results. Notably absent is any use of this cache in the algorithms that exploit cycles in generators, validators or any other operation. Keeping the visualization of schemas as a graph in mind, each schema (node), and thus each schema’s cache, is self contained. Since there is no deduplication of schemas in the graph, even semantically identical schemas do not share a cache. This points to an elegant solution: upgrading Malli’s internal representation of schemas to reliably deduplicate schema instances such that the same schemas share the same cache.

My previous report explained a solution to this which still seems effective. I speculated that we could undo the duplication of effort in schema operations, and this month I’m happy to report exactly that: the new design since then reverts the custom ref validator implementation back to its original implementation while still tying the knot and thus avoiding memory leaks:

           (-validator [_]
             (let [validator (-memoize #(validator (rf)))]
               (fn [x] ((validator) x))))

Well, there is one subtle difference: we are memoizing a call to validator instead of -validator. The former caches the validator in the schema’s internal cache, which is now effective because we have deduplicated the graph of schemas and thus the same cache is used for semantically identical schemas.

In this new design, a schema like:

(m/schema
 [:schema {:registry {::list-of [:seqable ::element],
                      ::element :string}}
  [:tuple ::list-of
          ::list-of
          ::list-of]])

deduplicates the three ref occurrences of ::list-of in the :tuple to all point to the same :seqable schema instance, and thus the same cache. This means only the first ::list-of occurrence actually creates a validator, the second and third merely pull it from the shared cache. In contrast, the old implementation would create (at least) three distinct schema instances, and then the validator algorithm would manually deduplicate validators via a subtle algorithm. In this design, Malli’s own schema parsing logic performs deduplication, making the efficient implementation of operations much easier.

There are a few unknowns to resolve. Malli’s maintainers previously rejected this approach of caching recursive calls to validator, but have expressed interest in reconsidering the decision. The crux of the concern is that excessive caching of internal results will interfere with exotic registry implementations, such as those based on dynamic vars, and my stance is that Malli already uses caches too extensively for these kinds of registries to be reliable in these scenarios.

Also, I expected this new design to simplify the implementation of ref generators, but it caused some tests to fail and I reverted the change (you can see that here).

I suspect it may be hard to beat the current implementation mapping recursive refs to gen/recursive-gen using this new design, but I would like to at least know if the test failures are pointing to a problem in the schema deduplication algorithm itself. I’m curious if it will reveal differences between pointers and refs—or recursive and non-recursive refs—that I have missed.


PluMCP: Shantanu Kumar

Q2 2026 Report 1. Published July 1, 2026

I am grateful to Clojurists Together for sponsoring PluMCP during the 2026 Q2 cycle. The planned scope of work for this sponsorship is:

  • MCP spec 2025-11-25 implementation
  • PluMCP Usage documentation enhancement

Early in the sponsorship period I had to deal with unavoidable commitments outside the project, which delayed my progress. Kathy kindly granted me an extension, so this report is being submitted about two weeks later than originally planned.

MCP 2025-11-25 implementation

At the beginning of the sponsorship cycle, PluMCP v0.2.x supported the following MCP specification versions:

MCP spec version PluMCP implementation status
2025-11-25 (TODO) To be implemented during the sponsorship cycle
2025-06-18 (Done) Supported as the main spec version
2025-03-26 (Done) Supported in compatibility mode
2024-11-05 Not supported, no plan to support

At the start of the cycle, PluMCP advertised support for the 2025-11-25 specification during the MCP handshake but the implemented feature set corresponded entirely to the 2025-06-18 specification. This was functionally correct because the differences between the two specification versions were limited to optional features.

This sponsorship cycle closes that gap. PluMCP is implementing features listed in the MCP 2025-11-25 spec. The changes between 2025-06-18 and 2025-11-25 are captured here.

Progress so far

The larger part of the work is the MCP 2025-11-25 implementation, which can be tracked in PluMCP pull request #6.

This list does not include the schema updates required by the new MCP specification or the corresponding entity generator functions. Although implementing these required considerable groundwork, they provide a reliable foundation for the remaining implementation.

At this point, 5 of the 9 major feature changes and 5 of the 10 minor feature changes are complete. The completed work includes:

Major Changes completed

  • Allow servers to expose icons as additional metadata for tools, resources, resource templates, and prompts
  • Validate tool names as per the new spec
  • Update ElicitResult and EnumSchema to use a more standards-based approach and support titled, untitled, single-select, and multi-select enums
  • Add support for URL mode elicitation
  • Add tool calling support to sampling via tools and toolChoice parameters

Minor changes completed

  • Add utility function(s) to let servers using STDIO transport use STDERR for all types of logging, not just error messages
  • Add optional description field to Implementation (schema) interface to align with MCP registry server.json format and provide human-readable context during initialization
  • Have the servers respond with HTTP 403 Forbidden for invalid Origin headers in Streamable HTTP transport
  • Add support for default values in all primitive types (string, number, enum) for elicitation schemas
  • Establish JSON Schema 2020-12 as the default dialect for MCP schema definitions (2020-12 is the ONLY supported dialect for now)

Current status

10 of the 19 planned specification changes are complete, with Task orchestration currently under active development.

Remaining work

MCP 2025-11-25 introduced an experimental feature for task orchestration, which is also one of the largest additions in this release. This is the feature I am currently working on, and I hope to release a 0.3.0 alpha in about a week.

More than half of the specification’s feature set has now been implemented. What remains is to implement the rest of the specification along with the planned documentation improvements and example code.

I look forward to completing the remaining implementation and documentation work during the second half of the sponsorship period.

Permalink

Annually-Funded Developers' Update: May & June 2026

Hello Fellow Clojurists!

This is the third of six reports from the developers who are receiving annual funding for 2026. Thanks to everyone for supporting their work and these important contributions to the Clojure community. Their previous reports can be found here:
January/February 2026
March/April 2026


Bozhidar Batsov: cider-nREPL, neat, Sayid, port, Orchard, CIDER, and more
Clojure Camp: datascript playground, fill in the blank excercises
Eric Dallo: eca, eca clients, clojure-lsp
Jeaye Wilkerson: Jank optimization and beta release prep
Michiel Borkent: babashka, SCI, fs, squint, cream, and much more

Bozhidar Batsov

2026 Annual Funding Report 3. Published July 10, 2026.

The past two months were some of the most productive I’ve had in a long while. I had a lot of inspiration during this period and managed to tackle plenty of long-standing ideas and issues across the entire nREPL/CIDER ecosystem. I even grew the ecosystem with a couple of brand new projects. The highlights:

  • CIDER 1.22 is out
  • CIDER 2.0 is essentially ready and needs more user testing
  • Sayid is reborn
  • Two brand new projects saw the light of day: port and neat
  • Piggieback 0.7.0 is out (and Weasel got modernized while I was in the area)
  • clj-refactor and refactor-nrepl got some love as well

Below are the details, project by project.

CIDER

CIDER 1.22 (“São Miguel”) landed in mid-June, wrapping up the 1.x series. Its main features:

  • a registry for jack-in tools, so third parties can plug new build tools and Clojure dialects into cider-jack-in
  • a “default session” escape hatch from sesman’s project-based dispatch
  • keyword-argument versions of the low-level request APIs, alongside a proper decoupling of the nREPL client layer from CIDER’s UI

It 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:

That 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.

cider-nrepl

Lots of cider-nrepl releases, driving the CIDER work above:

  • 0.60.0 added the ops backing the new protocol exploration commands (cider/who-implements, cider/type-protocols, cider/protocols-with-method).
  • 0.61.0 brought ClojureScript test support, a ClojureScript macroexpansion fix, formatting that honors the project’s cljfmt configuration, and a pprint backed by orchard.pp.
  • 0.62.0-alpha1 and 0.62.0-alpha2 hardened the content-type and slurp middleware (URL scheme allowlist, size caps, graceful fetch errors) and cleaned up the response protocol, which is what made it safe to turn rich content on by default in CIDER 2.0.

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

Orchard, the library that powers much of cider-nrepl’s functionality, kept pace:

  • 0.42.0 and 0.43.0 continued the inspector polish, added symbol classification to 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

Sayid, the omniscient Clojure debugger, had been dormant for years and I finally gave it the revival it deserved:

  • 0.2.0 was the big modernization pass: new 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.
  • 0.3.0 followed with usability work: no more frozen Emacs during the reload workflow, simpler query commands and help buffers generated from the keymaps.

port

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:

  • 0.1.0
  • 0.2.0
  • 0.3.0, which added eldoc with active argument highlighting, a wire-level message log for debugging and a roughly 10x speedup in handling large prepl responses.

I don’t have any particular plans for the future of this project - it was just something I’d 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

neat is the other new arrival: a small, language-agnostic nREPL client for Emacs. 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.

Piggieback and Weasel

The nREPL org saw some ClojureScript-flavored action:

  • Piggieback 0.6.2 and Piggieback 0.7.0. The 0.7.0 release makes 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.
  • Weasel 0.8.0 modernized the WebSocket REPL: the client now uses the platform’s native 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 and clj-refactor

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 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’d certainly appreciate more feedback from the users of clj-refactor on the subject.

Blog posts

I wrote a few articles related to the work above:

What’s next

Getting CIDER 2.0 across the finish line is the main priority, followed by a clj-refactor release once that settles. I’ve plucked most of the low-hanging fruit by now, but there’s always more to do.

Thanks to Clojurists Together for the continued support of my Clojure OSS work! You rock!


Clojure Camp

2026 Annual Funding Report 3. Published July 12, 2026.

What happened:

  • Released: datascript playground https://clojure-camp.github.io/datascript-playground/
  • Released: better “fill-in-the-blank” exercises, drag-and-drop https://exercises.clojure.camp/
  • Mobs - regular mobs are back
  • Book Club - going well, this kind of group likely to become an ongoing camp offering
  • Conj Code Jam - proposal for 2x 3 hour pairing/mobbing sessions at conj (accepted)
  • Badges - technical side pretty much done, now for the content

Plans:

  • announcing Conj bursary soon
  • prep for Conj Code Jams
  • build event management into our tooling (Discord’s events are mediocre)
  • work on Badges content

Eric Dallo

2026 Annual Funding Report 3. Published July 11, 2026.

Another 2 months of intense work! The focus this time was on stability, performance and polish: ECA got smarter context management, a new provider, and much more robust MCP/OAuth support, while clojure-lsp finally received the long-awaited memory and startup performance overhaul I planned in the last report. Thanks ClojuristsTogether and everyone from the community helping with issues, feedback and contributions! :heart:

ECA

ECA keeps growing steadily, now with more than 900 stars and contributions coming every week. As usual the changelog is huge, so here are the main highlights:

eca-context Dallo MayJune 2026 smaller

0.132.0 - 0.144.0

  • /context command + live context-usage bar: shows how full the context window is, broken down by category (system prompt, rules, skills, tools, conversation), with the same data sent to all clients to render a live usage bar with the auto-compaction marker.
  • AWS Bedrock provider: using the native Converse/ConverseStream APIs, supporting models like Claude inference profiles.
  • Durable chat persistence: chats are saved after every step with atomic writes plus several /resume fixes, so long chats are never lost.
  • Chat pagination: new chat/history method so clients open long chats instantly and load older messages on demand.
  • Chat /export and /import: transfer a chat between machines as a resumable file.
  • Hooks expansion: new preCompact, postCompact, subagentStart and chatStatusChanged hooks, a /hooks command and richer contracts.
  • MCP robustness: proactive OAuth token refresh, per-workspace tokens, and recovery of stale/broken connections with keep-alive pings and auto re-initialization.
  • Live-reload config: config file edits now reconcile MCP servers and refresh tools without restarting the server.
  • Remote improvements: ask_user reaches editor and web clients simultaneously, pending approvals exposed in the REST API, and runtime-fetched TLS certs.
  • Per-chat scoping: model/agent/variant changes are now scoped per chat, fixing selections leaking between chats.
  • Custom models QoL: per-model limit/cost overrides enabling the usage bar and auto-compaction for local models, new extraConfigs, and custom commands with named arguments.
  • Prompt-cache friendliness: reworked system prompt and per-turn cursor context so providers reuse cached prompts (big win for llama.cpp).
  • More providers and models: Claude Opus 4.8, Sonnet 5, Fable 5 and Mythos 5, gpt-5.5, glm-5.2 and deepseek-v4-pro variants, Claude console subscription auth, and swapping between Anthropic and non-Anthropic models mid-chat.

ECA clients

All clients received improvements (resume-chat picker, MCP servers management from settings, per-chat model/agent scoping, context-usage bar, light theme fixes) plus client specific work:

  • eca-desktop: big robustness and security pass: resolve the user’s login shell env before spawning the server (fixing the classic “GUI app can’t find my tools” issue), Electron 33 -> 41 with 0 npm audit vulnerabilities, and server lifecycle fixes with verified downloads.
  • eca-emacs: big performance overhaul backed by a new benchmark harness making long chats fast again (streaming rendering up to ~4000x cheaper), context-usage bar in the mode-line, paginated chat history, inline image rendering and a new eca-doctor command for bug reports.
  • eca-vscode (0.48.6 - 0.49.4): shared webview improvements and several UI fixes.
  • eca-intellij (0.26.6 - 0.27.3): CI publishing to the JetBrains marketplace with plugin zips on every release, blank tool window fix, lots of Light theme fixes and Emacs keymap support in the prompt.
  • eca-web: per-chat close and clear-history actions in the sidebar.

We also started two new experiments in the org: eca-cli, an ECA client for terminals, and eca-ios, an iOS app to remotely connect to a running ECA server.

clojure-lsp

These 2 months were all about the plan mentioned in the last report: memory management and startup performance! The results are really exciting for large projects: warm initialize dropped from ~73s to ~8s on a large monorepo, and memory usage was drastically reduced for projects with big dependency sets. All of this is guarded by the new performance integration tests so we don’t regress, thanks for the community sharing their kind words with good results of the improvements, that means a lot to me!

Details about the release below:

2026.07.06-14.34.19

  • Skip re-analysis of unchanged source paths on warm startup by persisting the internal analysis, dep-graph, documents and clj-kondo findings in the db cache, only re-analyzing files whose checksum changed. #2316
  • Publish startup diagnostics off the initialize critical path, so large projects become interactive much sooner (warm initialize from ~73s to ~8s on a large monorepo). #2326
  • Analyze external java member definitions lazily on first navigation/hover/completion instead of all up front, drastically reducing memory usage. #2313
  • Reduce memory usage of java class and member definitions analysis, and shrink the db cache considerably by not serializing redundant analysis uris. #2314 #2315
  • Scale the JVM server heap with -XX:MaxRAMPercentage instead of a fixed -Xmx, matching the native image and avoiding OOMs on very large projects. #2313
  • Run the db cache write on a dedicated thread, write it atomically, and sanitize clj-kondo findings before caching so custom hooks can’t break the cache. #2318 #2313
  • Optimize clj-kondo analysis ingestion with single-pass normalization using transients and caches. #2317
  • Added performance integration tests for server initialization measuring cold and warm start.
  • Auto generate clojure-lsp nightly builds from clj-kondo master commits.
  • Add missing namespace form, guessing the name when outside project sources, when adding a missing :require or :import via code action. #1734
  • Group comments and clj-kondo directives along with namespaces when sorting or removing :require/:import during ns organization. #1237
  • Remove restriction on renaming unqualified keywords. #2139
  • Fix cyclic-dependencies linter falsely reporting cycles for :as-alias requires. #2108
  • Fix crash when using :exclude-when-defined-by as a vector. #2292
  • Lots of bumps: Clojure 1.12.5, core.async 1.9.865, promesa 12.0.0, nrepl 1.7.0, cljfmt 0.16.4, sci 0.13.52, rewrite-clj 1.2.55, opentelemetry 1.63.0 and more.

Jeaye Wilkerson

2026 Annual Funding Report 3. Published July 11, 2026.

Hi everyone. :) Thank you so much for the sponsorship this year. Last update, I covered how I had just added a new intermediate representation (IR) for jank, as well as some other optimization work. The last two months of jank development have been broken into two parts:

Further optimization work
Beta release preparation

Optimization work

I wrote a blog post about the optimizations I did to jank in order to run a naive ray tracer faster than Clojure JVM. This involved more than tripling jank’s overall speed at the benchmark by improving the low-level representation of our numbers, improving compiler inlining, optimizing the machinery behind function calls, and minimizing the generated code size by 30%. These benefits will carry to many other benchmarks, and real applications, going forward.

Beta release

Starting in June, I switched my focus away from optimization and toward checking all of the boxes needed to get jank into people’s hands as quickly as possible. To do this, I want to ensure there is an impressive vertical slice of jank for everyone to use.

A big part of that vertical slice is related to build systems, AOT compilation, and native packages. I have been working, with Kyle Cesare, on jank’s native build setup. At this point, we have an incredible UX for the lein-jank plugin, thanks to all of Kyle’s work, and it plays into all of the things I’ve been building on the jank side. AOT compilation in jank is very fast, compared to Graal native images, and the binary sizes are even smaller. Startup time is instantaneous.

To play into our new build system, I have created the jank commons, which is a set of packages for consuming native libraries from jank. This builds on all of the tooling Kyle has created. To start with, we have libraries focused on graphics programming (OpenGL, GLFW, Raylib, Dear ImGUI) and TUI programming (ftxui). We’ll be building on this to incorporate more and more of the native world, making the consumption of native libs in jank just a one line change in your project.clj.

I’ve also been working a lot on jank’s distribution and packaging. There are new continuous Arch builds, which make the jank-bin AUR package much more reliable. I’ve also fixed some LLVM 23 related issues to make the jank-git AUR package work better.

On top of all of this has been countless small fixes for jank’s behavior. Right now is a great time to jump in and try out jank. More documentation is on the way, along with more fixes and stability.

Thanks, again, for the support as I develop jank! Stay tuned for my next blog post, covering the state of jank and what’s to come.


Michiel Borkent

2026 Annual Funding Report 3. Published July 8, 2026.

In this post I’ll give updates about open source I worked on during May and June 2026.

To see previous OSS updates, go here.

Sponsors

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.

gratitude

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. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!

Updates

A lot happened in the past two months! Not just coding but also…

Babashka Conf 2026 and Dutch Clojure Days

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.

image

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.

Upcoming: babashka workshop at the Clojure/conj

I’m 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’t explain the language itself. Topics include:

  • Setting up your dev environment
  • Managing projects with babashka tasks (bb.edn)
  • A tour of built-in libraries (fs, process, http-client, and more)
  • Writing and running tests
  • Building a CLI with subcommands and automatic help
  • Programming a terminal UI (TUI)
  • Producing a small web app
  • Publishing via GitHub or as an installable tool with bbin

Every concept comes with an exercise, building toward one culminating CLI app. There will be lots of interaction and fun!

Blog posts

Besides this update I published two blog posts in the past two months:

and a ClojureScript reference on async functions:

Projects

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’s 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’s 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’s 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’ll 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’s CHANGELOG.md for the full list.

  • babashka CLI: Turn Clojure functions into CLIs!

    • Automatic --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 completions
    • Exposed the underlying building blocks so you can roll your own custom CLI parsing: parse-opts*, coerce-opts, validate-opts, apply-defaults, table->tree
    • dispatch now accepts a tree directly (as returned by table->tree), and subcommand order is preserved in printed help and completions
    • Squint support and a new @babashka/cli npm package
    • ClojureDart support (#182)
    • opts->table accepts :columns to override the auto-detected columns (#148, thanks Jan Seeger)
    • Better error messages: negation errors now name the base option, --no-foo on a non-boolean option errors instead of silently coercing, and :edn :coerce now requires an explicit value (#166, #174)
    • Thanks to @lread for a lot of documentation review and general maintenance during this cycle
    • Full changelog
  • Squint: CLJS syntax to JS compiler

    • Browser nREPL support landed, followed by a number of REPL/nREPL fixes: #815 (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)
    • The CLI now gets its --help, usage and error handling from babashka.cli’s dispatch, plus shell tab completion
    • Dynamic vars and binding 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 added
    • clojure.walk added
    • Added squint.edn/clojure.edn with a ~300-line EDN reader
    • Printing is now done through *print-fn*, print, pr and with-out-str, like CLJS
    • Lazy seqs are now cached instead of recomputed on every consumption, matching CLJS’s chunked-seq behavior
    • A big push for compatibility with jank’s clojure-test-suite: dozens of core functions (sorted-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
    • #771: dead-code elimination for varargs/multi-arity functions, now emitted via ... spread
    • Replicant support landed, with an example
    • Added defrecord, 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 defrecord
    • Added a large set of core protocols so custom types participate in the standard functions: ILookup, 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)
    • Compile-time namespace resolution: 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-use
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • NEW: macros from source. A 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.md
    • Support for async/await in ClojureScript: bumped built-in CLJS analysis to 1.12.145 and added the :await-without-async-fn and :misplaced-async-metadata linters
    • #2822: NEW linter :alias-same-as-ns, warns when an alias equals the namespace it aliases (default :off) (@tomdl89)
    • #2807: NEW linter :conditional-build-up, warns on successive (if pred (assoc m ...) m) rebinding and suggests cond-> (default :off) (@walber-araujo)
    • #2062: NEW linter :if-x-x-y, suggests (or x y) instead of (if x x y) (default :off) (@jramosg)
    • #2818: fix :redefined-var false positive across files declaring the same namespace
    • #2814: fix :protocol-method-arity-mismatch false positive for definterface declaring the same method with multiple arities (@jramosg)
    • #2817: warn on recur inside a vector, map or set literal, since recur is never in tail position there
    • #2854: fix :invalid-arity false positive when an inner binding or fn param shadows a local function name (@yuhan0)
    • Performance work on the rewrite-clj parser and analysis internals: efficient get-in/select-keys, faster sexpr, leaner node allocation (@alexander-yakushev)
    • Deprecation notice: 2026.05.25 is the last release to include the clj-kondo LSP server and VS Code extension; use clojure-lsp instead, which embeds clj-kondo
    • Queued for the next release: early support for the Clojure 1.13 map destructuring keys (:keys!/:syms!/:strs!), including inferring required keys and reporting them at call sites (#2870)
    • Full changelog
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • ClojureDart support, with a Flutter REPL example
    • Instance/static method and field overrides plus a :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
    • Security fix (sandbox escape): a string type-hint (e.g. ^"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
    • Add an :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)
    • Add 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 #1044
    • Fix copy-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)
    • Fix cross-namespace defrecord/deftype type symbol resolution via alias (e.g. (instance? r/Foo x)), fixing nbb#410
    • Fix a self-require (a namespace requiring itself) being reported as a cyclic load dependency
    • Full changelog
  • fs: file system utility library for Clojure

    • Released 0.5.34 with Node.js support (#265): fs now runs on Node.js via ClojureScript and Squint / JavaScript, published as the @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 it
    • Added spit and slurp on both the JVM and Node.js
    • exec-paths returns [] when PATH is unset or blank instead of throwing
    • @lread did a thorough review pass making the return values of copy, copy-tree, delete-tree, zip/unzip, gunzip and the setters explicit and documented/tested (#197)
  • Babashka: native, fast starting Clojure interpreter for scripting.

    • Working towards a new release integrating all the newest updates in Babashka CLI and babashka.fs. Most importantly I’m working on autocompletions added for tasks defined in bb.edn.
    • #1979: fix with-redefs on copied vars (e.g. org.httpkit.client/get) incorrectly treated as inlined
    • Add org.jline.keymap.BindingReader for reading key bindings in terminal applications, completing the input side of the bundled JLine API
    • #1982: add clojure.lang.ChunkedCons, clojure.lang.APersistentVector$SubVector, clojure.lang.ArraySeq, clojure.lang.PersistentVector$ChunkedSeq, java.util.AbstractCollection and java.util.Queue to :instance-checks (@paintparty)
    • Added a terminal tetris example (examples/tetris.clj) built on JLine’s Display and AttributedString, showing off the new terminal APIs
    • Full changelog
  • Reagami: A minimal zero-deps Reagent-like for Squint and CLJS

    • Added keyed reconciliation (#40): support :key on children for stable node identity, so diffing reuses nodes instead of recreating them
    • Fixed CLJS :lite-mode compatibility and added it to CI (#41)
    • Added a benchmarks page comparing reagami against CLJS React wrappers and React-free solutions, with mermaid charts to visualize the results (#42, #43)
    • Expanded the README with an ADR on the unkeyed reconciliation algorithm
  • Cream: Clojure + GraalVM Crema native binary

    • I was finally able to reproduce an issue with core.async and filed this upstream
    • Once this is fixed I’m going to consider crema more seriously and play with the thought that this could be a substrate for “Babashka next”.
  • html: Html generation library inspired by squint’s html tag

    • Fixed inline style maps emitting a literal \n between declarations via pr-str, which produced invalid CSS and dropped every declaration after the first (@cycl1st)
    • Only render a map attribute value as CSS when the key is style; other map-like values (e.g. records) now render via str (@telekid)
    • Fixed a symbol-valued attribute resolving to its runtime value instead of its literal name
  • Edamame: configurable EDN and Clojure parser with location metadata and more

    • Added ClojureDart support (non-indexing plain readers matching tools.reader, zero-literal parsing fix, and more)
    • With :auto-resolve-ns, bare syntax-quoted symbols now resolve to the current namespace, matching Clojure’s behavior
  • Neil: A CLI to add common aliases and features to deps.edn-based projects

    • #261: neil dep upgrade now upgrades unstable deps (e.g. release candidates) to a newer unstable version when no newer stable version exists
    • Added a README note on brew trust for users who installed neil before Homebrew introduced tap trust
    • The next neil version will make use of the new Babashka CLI features which is already prepared in a PR
  • Nbb: Scripting in Clojure on Node.js using SCI

    • #410: fixed a regression, introduced by the async/await work in #408, where a defrecord/deftype type symbol referenced through a namespace alias (e.g. (instance? r/Foo x)) failed to resolve
  • deps.clj: a faithful port of the clojure CLI bash script to Clojure

    • As always, catching up with the most recent Clojure CLI versions
  • Pod-babashka-gozxing: a babashka pod for QR code and barcode decoding/encoding, backed by gozxing

    • Initial release 0.0.1, installable via the pod registry
  • Graal-build-time: initialize Clojure classes at build time for GraalVM native-image

    • #55: munge package names for namespaces with special characters

Contributions to third party projects:

  • ClojureScript: documented the async/await support from last cycle on the ClojureScript site, including an enhanced reference (#423, #424)
  • Nexus: a data-driven state management library by Christian Johansen. I ported the core engine and test suite to run under squint and added a cljs test runner alongside the existing kaocha setup, so both babashka and squint stay covered in CI (#15, #16, merged)
  • Replicant: a data-driven DOM rendering library by Christian Johansen. I made Replicant itself run under Squint (converting 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)

Other projects

These are (some of the) other projects I’m involved with but little to no activity happened in the past two months.

Click for more details

Permalink

Clojure 1.13.0-alpha4

Clojure 1.13.0-alpha4 is now available! Find download and usage information on the Downloads page.

Destructuring changes and additions

Idents after & in :keys!/syms!/strs!/:keys/syms/strs must now be actual keys, not binding symbols. This is a change in syntax since alpha3. Note that symbol keys should be quoted as unadorned symbols are binding symbols.

:or now accepts key→val mappings in addition to binding→val.

Added a new :defaults name directive at top level to bind name to a map of defaults, key→val. Binding symbols in the :or map are transformed to the key value in the :defaults map. :defaults without :or is an error.

:select name, introduced in alpha3, now selects deeply, through nested maps, and fills in values for missing keys from :or. The :select map contains all keys mentioned anywhere in the binding form.

  • CLJ-2964 :select directive in map destructuring

  • CLJ-2966 :defaults directive in map destructuring

  • CLJ-2967 tests for nested destructuring

Other changes since Clojure 1.13.0-alpha3

  • CLJ-2870 Exception phase during top-level eval is miscategorized

Try it out

Update your deps.edn :deps with:

org.clojure/clojure {:mvn/version "1.13.0-alpha4"}

Start a REPL with the Clojure CLI (any version) with:

clj -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.13.0-alpha4"}}}

Permalink

Dual Duet

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.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.