Decoupling from the Data POV: Stop-and-Start Boundaries, Independent Pointers, and Why Your Code (and AI) Need It

The Blind Spot in Modern Architecture Debates

Ask five engineers what "decoupling" means, and you will get five abstract answers about SOLID principles, hexagonal layers, microservice boundaries, or dependency inversion interfaces.

Almost nobody talks about decoupling from the point of view of the data itself.

The Fundamental Law:

If you decouple the data, the logic decouples automatically.

If you only decouple the logic while sharing mutable data, you haven't decoupled anything.

What does data actually experience as it moves through a running system? Is it continuously tethered across shared memory, or does it move across clean, discrete boundaries?

Understanding the physics of data decoupling—specifically independent memory pointers and stop-and-start boundaries—not only transforms how you structure production software, but also unlocks how we solve the two biggest bottlenecks in modern engineering: Human Snippet Tunnel Vision and AI Context Amnesia.

1. The Physics of Coupling: The Shared Pointer Trap

In a tightly coupled codebase, modules don't just depend on each other conceptually—they are physically tethered in RAM.

❌ COUPLED DATA (Continuous Live Tether / Shared Mutable Pointer):

   Pointer A (package auth) ────┐
                                ├──► [ RAM Memory Slot: 0x7FFE4A20 ]
   Pointer B (package payment) ─┘    Data: { UserID: 42, Status: "Active", Balance: 100 }

   * Danger: If auth.go mutates the status or alters the memory layout, 
     payment.go reads corrupted state or fails at runtime without warning.

When multiple packages hold pointers to the same mutable memory block:

  1. Temporal Coupling: Package A and Package B must execute in lockstep. You cannot delay, retry, or parallelize one without coordinating locks.
  2. Invisible Side Effects: Changes made inside auth.go propagate silently across the heap into payment.go.
  3. The "Decoupling Illusion": Even if you wrap both packages in clean interfaces, if they are still passing shared mutable pointers underneath, they are not decoupled.

2. The Decoupled Mental Model: Stop-and-Start Boundaries

True data decoupling happens when data moves in discrete "stops and starts" across explicit boundaries.

Instead of sharing a live pointer, each system holds an independent pointer pointing to its own isolated memory allocation:

✅ DECOUPLED DATA (Independent Pointers & Stop-and-Start Handoff):

   [ Stage 1: Auth Engine ]
      Pointer A ──► [ Local Buffer 1: { UserID: 42, Status: "Active" } ]
                           │
                           ▼ (Serialization / Value Handoff)
                    [ Boundary / SQLite / Queue / Channel ]  <── "STOPS" (Data at rest)
                           ▲
                           │ (Deserialization / Local Allocation)
   [ Stage 2: Payment Engine ]
      Pointer B ──► [ Local Buffer 2: { UserID: 42, Status: "Active" } ]

Why Independent Pointers Win:

  • Spatial Isolation: Pointer A lives only in Auth's scope; Pointer B lives only in Payment's scope. If Pointer A is mutated or garbage collected, Pointer B remains 100% intact.
  • Temporal Independence: The handoff boundary acts as a temporal air gap. Auth can run at 10:00:01 AM, write to the boundary, and shutdown. Payment can wake up at 10:00:05 AM and process the payload.
  • Reference by Identity (IDs), Not RAM Addresses: Instead of passing raw RAM addresses (0x7FFE4A20), decoupled systems pass IDs (UserID: 42 or node_id: "ast_func_402"). Each component queries or constructs what it needs.

3. Where Does the Data "Stop"? (RAM vs. Disk)

You can place your stop-and-start boundaries in two places depending on your performance and persistence needs:

┌──────────────────────────────────────┬──────────────────────────────────────┐
│       IN-MEMORY RAM BOUNDARIES       │       PERSISTENT DISK BOUNDARIES     │
├──────────────────────────────────────┼──────────────────────────────────────┤
│ • In-memory SQLite (`:memory:`)      │ • Local SQLite files (`synapse.db`)  │
│ • In-RAM JSON / DTO string snapshots │ • File system snapshots (.json/.pb)  │
│ • In-memory Go Channels (`ch <- v`)  │ • Message queues (Kafka/RabbitMQ)    │
│ • Deep clones & move transfers       │ • Write-Ahead Logs (WAL / Append)    │
│ • Pass-by-value stack copies         │ • Embedded KV stores (RocksDB/Pebble)│
│                                      │                                      │
│ Speed: Microseconds to Nanoseconds   │ Speed: 1–2ms (via OS page cache)     │
│ Scope: Same process / local runtime  │ Scope: Cross-process / Air-gapped    │
└──────────────────────────────────────┴──────────────────────────────────────┘

4. The Double Crisis in Modern Codebases

As codebases scale past tens of thousands of lines, this data flow problem triggers two simultaneous breakdowns:

+-------------------------------------------------------------------------+
|                         YOUR ENTIRE REPOSITORY                          |
|    [auth.go]      [payment.go]      [user.go]      [db.go]   [queue.go] |
|                                                                         |
|            +---------------------------------------+                    |
|            | YOUR IDE VIEWPORT (30-50 lines)       |                    |
|            | Editing line 42 in auth.go...         |                    |
|            +---------------------------------------+                    |
|                                                                         |
|    * Blind to cross-package blast radius & contract mutations! *        |
+-------------------------------------------------------------------------+

1. The Human Problem: Snippet Tunnel Vision (The Straw Problem)

Standard IDEs (VS Code, JetBrains) show 30 to 50 lines of code at a time. Trying to comprehend complex data flows through a 50-line viewport is like peering into a skyscraper through a drinking straw. You cannot see the blast radius of your changes.

2. The AI Problem: Context Window Amnesia (The Overflow Problem)

Autonomous AI coding agents (Claude Code, Cursor, Windsurf) struggle when developers dump 50 raw source files into the prompt window:

  • Token Inflation: $20+/hr in API fees burning context on boilerplate.
  • Mental Pointer Tracking: The LLM is forced to mentally simulate live data pointers across 50 text files, leading directly to hallucinations and broken imports.

5. The Solution: Treating Code Itself as Decoupled Relational Data

When I ran into these two friction points on large projects, I realized the answer wasn't to write another static linter or dump more raw text into an LLM prompt.

The answer was to apply data decoupling principles to the codebase itself:

  1. The Stop-and-Start Boundary: Parse the repository's Abstract Syntax Tree (AST) and LSP symbols into a local, relational SQLite database (synapse.db), and let the parser terminate.
  2. Independent AI Pointers via MCP: Instead of forcing an AI agent to read 50 raw text files, expose the SQLite database via a local Model Context Protocol (MCP) server. The agent runs recursive SQL queries in 2 milliseconds, retrieving exact dependency graphs without swamping its context window.
  3. Independent Spatial Canvas: Wire the relational tables into a local 2D visual canvas (http://127.0.0.1:8080). When an engineer or AI agent refactors a module, the canvas lights up the blast radius and traces data taint flows in real time.

6. How Different Languages Decouple Data

Every major language runtime has wrestled with this problem, producing some ingenious data-decoupling mechanics:

1. JavaScript: structuredClone() & Transferable Objects

Many developers still use JSON.parse(JSON.stringify(obj)) for deep copies, which silently strips functions, undefined, Date objects, and crashes on circular references. Modern JS includes structuredClone(), which creates a 100% isolated heap allocation and correctly clones circular graphs, Map, Set, ArrayBuffer, and Blob (though functions and DOM nodes still throw a DataCloneError). Even faster: Transferable Objects (postMessage(buffer, [buffer])) completely transfer memory ownership from the main thread to a Web Worker, instantly zeroing out the sender's pointer for zero-copy concurrency.

2. Erlang & Elixir (BEAM): The Zero-Shared-Heap Actor Model

Unlike Java, Node, or Go (where threads share a single global heap), every single Erlang/Elixir process has its own private heap and private garbage collector (with off-heap reference counting for large binaries >64 bytes). When one process sends a message to another, the BEAM VM physically copies the bytes across process heaps. There is literally no shared mutable memory in the entire VM—making deadlocks and race conditions structurally impossible.

3. Clojure: Persistent Data Structures (HAMT)

How do you update an immutable collection with 1,000,000 items without copying the entire array every time? Clojure uses Hash Array Mapped Tries (HAMT). When you "modify" an immutable map, Clojure shares 99.9% of the existing tree nodes (structural sharing) and only allocates a tiny new path of 3–4 nodes. Because of its 32-way branching factor (M=32), you get a brand-new, decoupled immutable snapshot in Olog₃₂n (effectively bounded O(1)) time with minimal memory overhead.

4. Rust: Compile-Time Move Semantics

Rust takes a different route: instead of copying memory or running a garbage collector, it enforces single ownership at compile time. When you pass data to a new function, Rust moves ownership and marks the original pointer as invalid in the compiler. If you try to read from the old pointer on the next line, the code won't even compile—giving you zero-cost pointer isolation with zero runtime overhead.

5. Go: Channels & Value Receivers

In Go, structs are value types by default (b := a copies top-level fields, though beware that inner slices, maps, or pointers still share underlying backing storage). When pairing goroutines, Go favors channels (ch <- msg) to transfer data across memory boundaries without shared locks: "Do not communicate by sharing memory; instead, share memory by communicating."

6. SQLite as the Universal Polyglot Air Gap

When you need to decouple across completely different languages (e.g. a Go compiler engine, a Python AI model, and a TypeScript web browser), in-memory pointers are impossible. Storing state in a local SQLite database acts as a universal relational boundary. It utilizes the OS page cache for sub-2ms reads, ensures ACID serialization, and lets any tool or language query the state with independent pointers and zero runtime coupling.

Conclusion: Drawing the Line in the Sand

Decoupling isn't about design patterns or complex class hierarchies—it’s about drawing a line in the sand for your data.

  • On one side of the line: Your memory, your pointers, and your execution scope.
  • On the other side of the line: Their memory, their pointers, and their execution scope.
  • At the line itself: Clean, stop-and-start data boundaries.

When you draw clear lines in the sand, you eliminate invisible regressions, free your systems to scale independently, and give both yourself and your AI agents the clarity to build with confidence.

I’ve been exploring these mechanics while building Go-Synapse—a local, 2D AST canvas and SQLite MCP engine. How do you handle data boundaries and pointer ownership in your own architecture? Drop your thoughts in the comments below!

Permalink

vim-slime

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> vim-slime </h1> <p> Mar 26, 2012 </p> </div> </div> <div id="post_content"> <html><body><p>Today I found the awesomeness that is <a href="https://github.com/jpalardy/vim-slime">vim-slime</a>, it's been an exciting day for me. <a href="http://common-lisp.net/project/slime/">Slime</a> is the "The Superior Lisp Interaction Mode for Emacs", I can almost hear the emacs crowd laughing.</p> <p>For those that use vim and haven't used Slime, vim-slime or <a href="https://github.com/vim-scripts/VimClojure">something similar</a>, this is why it's awesome:</p> <p><strong>Text can be sent from any process to the stdin of a <a href="http://www.gnu.org/software/screen/">gnu screen</a> or <a href="http://tmux.sourceforge.net/">tmux</a> session. The process in this case is vim and the screen/tmux session is a terminal</strong>.</p> <p>Screen is a <a href="/journal/oh-screen-where-have-you-been">really neat</a> terminal multiplexer (you can run multiple terminals in a terminal window). The multiplexed shell processes are children of the screen process, which itself is not a child of the terminal window process. This means a screen process and its child processes keep running if you close the terminal window. Later you can re-connect to it, this is what makes vim-slime possible.</p> <p>Here is an screen shot, on the left is me in gVim writing some awful Clojure <a href="#footnote-1">[1]</a>. On the right is a screen buffer in which I started a Clojure REPL. When I want to try run some code I can send any vim text selection to the REPL in a keystroke (or two).</p> <p><img alt="vim slime screenshot" src="screenshot.jpg"/></p> <p>It doesn't have to be a Clojure REPL either, we can send anything to a screen shell. We could run git commands, find, grep, sed, etc. Like with the Clojure REPL we can even interact with any terminal programs that use STDIN.</p> <p>This concept can be taken even further, You can even connect to a tmux session over SSH and share a terminal or a <a href="http://remotepairprogramming.com/remote-pair-programming-with-tmux-and-vim-the">terminal program like vim to do remote pairing</a>!</p> <p>Hopefully remote pairing is the topic of my next post as there are a couple geographically distant people I know who are keen to do some pair hacking. I stand to learn a lot!</p> <p><a name="footnote-1">[1]</a> I learnt almost everything I know about Lisp from <a href="http://www.ccs.neu.edu/home/matthias/BTLS/">The Little Schemer</a>. Great book.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> </body> </html>

Permalink

Swipe Keyboard

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> <style> #swipe-canvas { position: relative; width: 900px; height: 300px; } #swipe-results { font-size: 30px; padding-left: 50px; padding-left: 50px; } #swipe-results ul { margin: 0px; padding: 0px; } #swipe-results li { float: left; background-color: #DDDDDD; list-style-type: none; padding: 10px; margin: 5px; border-radius: 5px; } #swipe { position: relative; } #swipe-loading { position: absolute; height: 50px; width: 300px; top: 85px; left: 300px; background-color: darkgray; border-radius: 10px; text-align: center; padding-top: 20px; border: black; border-width: 5px; } </style> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> Swipe Keyboard </h1> <p> Apr 06, 2014 </p> </div> </div> <div id="post_content"> <html><body><p>When I first tried a <a href="http://www.swype.com/">Swype</a> keyboard I was impressed how effective it was. Even though I don't use the feature on my phone I was interested in how it could be built, so I <a href="https://github.com/tarnacious/swipe-keyboard">implemented this otherwise useless swipe-able keyboard</a> below. It probably doesn't work on mobile devices, but works on modern browsers with mouse pointers (although I've only really tried Chrome and Firefox).</p> <div id="swipe"> <canvas height="300px" id="swipe-canvas" width="900px"></canvas> <div id="swipe-results"></div> <div style="clear: both"></div> <h2 id="swipe-loading">Loading<noscript>Javascript is Required</noscript></h2> </div> <p>I initially tried to solve this using the technique Peter Norvig famously uses in his <a href="http://norvig.com/spell-correct.html]">spell checker</a>. He takes a sequence of characters and generates a set of word candidates by adding, removing and swapping characters in the original sequence, the generated candidates are removed if they are not found a dictionary. This can work but to be effective too many combinations need to be generated.</p> <p>If the dictionary is indexed into a <a href="http://en.wikipedia.org/wiki/Trie">trie</a> the number of combinations generated can be reduced significantly by traversing the trie and only generating valid letter combinations. This is a pretty bare implementation of that, it requires: </p> <ul> <li>The first and last characters of the initial sequence are used </li> <li>Intermediate characters in the initial sequence can be repeated or discarded </li> <li>No characters are added or swapped</li> </ul> <p>Basically, if you swipe through all the characters in a word in order, then the word will be found if it is in the index regardless how many characters are swiped in between. It is surprisingly quick and effective.</p> <p>This implementation uses <a href="https://raw.github.com/first20hours/google-10000-english/master/google-10000-english.txt">these 10000 words</a>, I intended to use digital books but never got around to it as these words demonstrate the concept well enough.</p> <p>This is the first thing I've written in <a href="https://github.com/clojure/clojurescript">ClojureScript</a> or <a href="https://github.com/clojure/clojurescript">Clojure</a>, so my code my vary from non-idiomatic to shamblolic. I initially used a <a href="http://clojuredocs.org/clojure_core/clojure.zip/zipper">zipper</a> to build the trie with immutable data structures, but found the indexing took to long with my zipper implementation so I <a href="https://github.com/tarnacious/swipe-keyboard/commit/6edd7b26e78121fbe8586b3f0ef54ca8277d9e32">switched to using native Javascript maps</a>.</p> <p>I found that <a href="https://github.com/clojure/core.async">core.async</a> library is really awesome, the <a href="http://docs.closure-library.googlecode.com/git/index.html">Google closure library</a> and <a href="https://developers.google.com/closure/compiler/">compiler</a> integration with <a href="http://leiningen.org/">Leiningen</a> the <a href="https://github.com/emezeske/lein-cljsbuild">cljsbuild plug-in</a> to be impressive. My main pains were the slow JVM start-up time, the advanced closure compiler build of the web worker script fails silently when run (but the main script works fine when compiled with the advanced compiler), and at times I felt some compile time type checking would be nice.</p> <p>I would like to extend this experiment to index the word occurrence counts and proceeding word counts in original text and rank the found words as most likely. Support casing, umlauts, special characters, spelling correction and compound words in the indexing and lookup. I think a live lookup while swiping would also be possible.</p> <p>Overall this was fun, turned out OK I think, and was a great learning experience.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> <script src="swipe.js" type="text/javascript"></script> </body> </html>

Permalink

Smarter Form Targeting Is Not Coming to CIDER

A couple of days ago I wrote that smarter form targeting was coming to CIDER, and I ended that post by asking whether I’d got the resolution rules right and whether anything still surprised people. I got an answer. CIDER 2.1 will ship with the classic behaviour intact.

This is not a sad story, though. The detour turned up a bug that was quietly mangling people’s comments, and CIDER came out of it better than it went in.

What I was actually after

The targeting change wasn’t really about cursor positions. What I wanted was for every CIDER command that operates on a form to behave the same way, without adding yet another command to get there. CIDER has an enormous surface of evaluation commands, and every “at point” variant I could have added would have made that worse. If the existing commands simply resolved the form you meant, newcomers would have had fewer commands to learn, not more.

That was the bet: consistency by redefinition rather than by addition. In hindsight it was the wrong bet for a project this old. Fifteen years in, the existing behaviour isn’t an implementation detail I get to tidy up - it’s the contract. And as it turned out, the redefinition wasn’t nearly as transparent as I’d convinced myself it was.

The feedback

Several users, including CIDER’s co-maintainer Sashko Yakushev, voiced concerns and flagged issues I’d overlooked while playing with this initially. The most important one: inspection is just another flavour of evaluation, and people inspect bare symbols constantly. If it got the same treatment, the disruption would be real enough to run a fork over.

My instinct was that this was an edge case, so I measured it instead of arguing. Across every cursor position in a buffer, only three rules actually differ, and the flagship flow - type a form, hit C-x C-e - is identical under both. Two of those three were fine. The third was this one:

Cursor on a closing paren: the classic rules evaluate the last form inside, smart targeting evaluates the whole enclosing call

The cursor doesn’t move between those two evaluations. That’s the same position, twice, and the answers differ - "b" under the classic rules, "ab" under the new ones.

I’d filed “cursor on a closing paren” as an oddity nobody hits deliberately. But think about when you land there: you finish typing the last thing inside a form, and the cursor is now sitting on the ). For someone inspecting symbols all day, that fires constantly. And the classic answer isn’t arbitrary either - as Sashko put it, the rule of thumb is “whatever paredit-backward jumps back to”, which is a better description of the tradition than anything I’d written down.

Why the tradition exists in the first place

Here’s the part I under-weighted, and it’s worth spelling out for anyone who finds “the form before the cursor” arbitrary.

Emacs form navigation overwhelmingly leaves the cursor after a form. C-M-f (forward-sexp) moves over the next form and stops just past its closing delimiter. C-M-e (end-of-defun) leaves you after the whole top-level form. C-M-n (forward-list) does the same for the enclosing list. Paredit’s paredit-forward behaves the same way, and so does typing: finish a form and the cursor is, by definition, right after it.

So “evaluate the preceding form” isn’t a quirk - it composes with how you already move around. Navigate forward over a form, evaluate it. Type a form, evaluate it. The cursor is already in the right place, every time.

Getting onto a form instead takes deliberate effort: C-M-b (backward-sexp), C-M-a (beginning-of-defun), paredit-backward, or a jump package like avy. All perfectly good tools, but you have to reach for them - unless you’re clicking around with a mouse, in which case the cursor lands wherever you pointed and “the form before the cursor” genuinely is useless. Which, I suspect, is exactly the workflow difference behind this whole argument.

What CIDER got instead

The half of the idea that was never controversial is still there, as commands you opt into rather than a new meaning for keys you already use. Every operation now has an “at point” variant, not just eval and tap: cider-inspect-sexp-at-point, cider-pprint-eval-sexp-at-point, cider-macroexpand-1-at-point, cider-macroexpand-all-at-point, cider-format-edn-sexp-at-point, cider-insert-sexp-at-point-in-repl.

Three ways to say which form you mean, and now every command supports all of them:

The same expression evaluated three ways: the preceding form, the form at the cursor, and the enclosing top-level form

The at-point commands fall back to the preceding form when there’s nothing to point at, so they’re drop-in replacements rather than a separate mode of working. Which means anyone who wanted smart targeting can simply have it:

(with-eval-after-load 'cider-mode
  (define-key cider-mode-map (kbd "C-x C-e") #'cider-eval-sexp-at-point)
  (define-key cider-mode-map (kbd "C-c C-e") #'cider-eval-sexp-at-point))

Two lines, no hidden mode, and everyone else’s fingers keep working. This is what I should have shipped in the first place, and it’s what the manual now recommends. Yes, it’s more commands than I wanted. It’s also the version that doesn’t break anyone.

Macroexpansion is the one place a bit of cleverness survived on its own merits, because an expansion needs a call form. Stand on a bare symbol and cider-macroexpand-1-at-point widens to the call around it, since expanding a lone symbol is never what anyone meant.

The bug at the bottom of the hole

While unifying the plumbing I found this, which is much worse than anything form targeting was ever guilty of. Put the cursor at the end of a comment:

(defn foo [])
;; a comment|

CIDER answered comment. Not the (comment ...) form - the word, lifted out of your prose, because sexp motion has no notion of comments once the cursor is inside one and happily reads the words as symbols.

For evaluation that produced a puzzling error. For the in-place macroexpansion commands, which replace the region they resolved, it did this:

;; a comment          ->   ;; a EXPANDED<comment>
(+ 1 2) ; hey         ->   (+ 1 2)          ; EXPANDED<hey>

It rewrote the comment. That bug has been in CIDER for years, and I only found it because I went looking at the primitives while cleaning up after myself.

Then I checked the neighbours, and this is my favourite part of the whole episode: SLIME, SLY and Emacs Lisp itself all still do this. Both Lisp environments use a bare backward-sexp, and if you put the cursor after (+ 1 2) ; hey in any Emacs Lisp buffer and ask for the preceding sexp, you get hey. CIDER now steps out of the comment first, which as far as I can tell makes it the only one of the family that gets this right.1

One idea worth stealing

The same survey turned up something CIDER was missing. SLY briefly flashes the region it compiled, so you see what it acted on. That’s a direct answer to the confusion this whole saga was about - “which form did it just take?” - and it changes nothing about what gets taken.

(setq cider-flash-evaluated-region t)

Off by default, because I’ve learned my lesson about switching things on for everyone. But if the targeting rules ever puzzle you, turn it on for a day.

The moral

In the original post I described cider-form-targeting, the escape hatch back to the classic rules, as “living on borrowed time” - clutter left over from a plan I’d since reversed, which I was itching to delete.

That option is the only reason the conversation stayed a conversation. Its existence made the objection “please don’t remove the fallback” rather than “I’m forking CIDER”, and I very nearly removed it before anyone had tried the change.

Every mistake is a learning experience for me, and this one was cheap: nothing had shipped, so the whole thing cost a few days and some rewriting. The testing and feedback cycle worked exactly as it should have - people tried something on master, told me plainly what was wrong with it, and the result is better than either what I proposed or what we had before. Everyone gets the behaviour they want, and CIDER lost a text-eating bug on the way.

Thanks to everyone who took the time to tell me I was wrong.

That’s all I have for you today. Keep hacking!

  1. If you’re an Emacs maintainer reading this: elisp--preceding-sexp has the same behaviour, and I’d be happy to be told why it’s intentional. 

Permalink

Wrapping GTK4 in 800 lines of Clojure with Jolt

Native toolkits are not terribly ergonomic, and are a lot more painful to use compared with web dev in many ways. Building a UI with them is an imperative exercise where you construct widgets one call at a time, pack them into containers, and wire each event to its handler by hand. What's worse is that the structure of the interface often ends up living outside your language altogether forcing you to use tools like GtkBuilder XML or Xcode storyboards, where none of your usual tools for composing and refactoring code can reach. Layout is governed by box packing rules and constraint systems that are easy to describe but hard to predict. And a common task such as turning a list of items into a list of widgets that stay in sync with the data results in a ton of boilerplate that you have to repeat over and over.

The pain of working with native toolkits gave rise to things like Electron which simply package a browser engine as a frontend for the application. While that technically works, it's a clunky and inefficient hack around the problem. Every app ends up having to ship its own copy of the browser along with a JavaScript runtime, and the result never quite feels native.

However, even doing that still doesn't address the biggest frustration about having a compile cycle that breaks your development flow. And that's especially problematic when building a UI where you can't easily automate testing. You have to load up the app, click through its menus, and get it to a particular state so that you can visually inspect whether a change works as you intended and has decent UX.

Working with Reagent and other Clojure UI toolkits in ClojureScript is an enjoyable experience precisely because you can build up the UI gradually, and keep a running state as you add more components to it. You make a change, look at the app, see it instantly, and then iterate on it.

I've always been a big fan of the Reagent reactive model which I find to be intuitive. You treat your UI state as a data structure, and have UI components subscribe to paths within it. Whenever an element at a particular path changes, the UI component associated with it gets updated. And that's really all there is to it. While Reagent is built on top of React, the latter isn't actually needed in this model. React uses a VDOM that gets diffed and then rendered to the actual DOM, and the reason it needs a VDOM is due to the fact that React is agnostic regarding what triggers a component change. That's why you need the whole React lifecycle with checks for componentDidMount, componentWillUnmount, and so on. In Reagent a component collapses to a single render function, and the reactive tracking decides which components need to be re-run, so the lifecycle is derived from the state of the data. And because the reactive model already knows exactly what changed, it makes it possible to use it to drive the DOM directly without needing React as the mr-clean library illustrates.

Since this model works well with a browser DOM, then why not apply it to a native toolkit? All that's needed is to create wrappers to render native widgets by passing them values from the reactive atom, and then provide a callback for the widgets to trigger on user input. We don't need an equivalent of a VDOM because all the changes are driven by the state of the reactive atoms, and can be rendered directly to the UI. This can even be done with a batching layer to control the rate of UI updates if needed. And this is how glimmer works, providing a reactive core that can be hooked up to a particular UI toolkit. Then, there are glimmer-gtk, glimmer-uikit, and glimmer-tui to provide concrete bindings for different types of UI widgets.

The canonical counter with glimmer-gtk looks pretty much exactly like its Reagent counterpart:

(ns counter
  (:require [glimmer.ratom :as r :refer [atom]]
            [glimmer.core :as ui]
            [glimmer-gtk.core])) ; installs the GTK4 backend

(defn counter []
  (let [count (atom 0)]
    (fn []
      [:vbox {:spacing 12}
       [:label {:label (str "Count: " @count)}]
       [:hbox {:spacing 8}
        [:button {:label "- 1" :on-click #(swap! count dec)}]
        [:button {:label "+ 1" :on-click #(swap! count inc)}]
        [:button {:label "reset" :on-click #(reset! count 0)}]]])))

(defn -main [& _]
  (ui/run counter :title "counter" :width 320 :height 160))

The outer function runs once to create the local state, and the inner one re-runs whenever @count changes, patching the live widgets in place instead of rebuilding the tree. If you've used Reagent before, this should all look very familiar with the only difference being that the elements are GTK4 widgets instead of DOM nodes.

What it takes to wrap a widget

It turns out that there is surprisingly little code involved in hooking a C toolkit up to a reactive Clojure core. The entire glimmer-gtk backend sits under 800 lines of Clojure split across four namespaces, and none of it involves anything exotic.

Jolt's FFI lets you promote a C function to the Clojure layer by naming the symbol it points at along with its argument and return types. We can see a few examples of what bindings from the GTK backend look like below.

(ns glimmer-gtk.ffi
  (:require [jolt.ffi :as ffi]))

(ffi/defcfn gtk-button-new-with-label "gtk_button_new_with_label" [:string]
  :pointer)
(ffi/defcfn gtk-button-set-label "gtk_button_set_label" [:pointer :string]
  :void)
(ffi/defcfn gtk-box-new "gtk_box_new" [:int :int]
  :pointer)
(ffi/defcfn gtk-box-append "gtk_box_append" [:pointer :pointer]
  :void)

Pointers are plain machine addresses represented as numbers, strings are marshalled to and from C strings automatically, and GTK booleans are ints that are handled by a one line ->bool helper. There is no C shim to compile or bindings generator to run, and no interface DSL to learn. The shared libraries are declared in deps.edn under :jolt/native, and Jolt loads them before the namespaces are required.

One thing to note here is that the main loop binding needs a bit of special treatment.

(ffi/defcfn g-application-run "g_application_run" [:pointer :int :pointer]
  :int :blocking)

The :blocking flag tells the runtime that the call parks the thread for the lifetime of the app, so it shouldn't pin the garbage collector while GTK owns the main loop.

GTK's API is also full of enums like GTK_ALIGN_START and GTK_ORIENTATION_VERTICAL, so a common approach is to maintain a table of constants mirroring the C headers. But glimmer-gtk has no need for such tables since every GObject enum registers its members with a lowercase nick which maps to a Clojure keyword. So, three extra bindings are all it takes to resolve a nick to its integer value at runtime through the GObject type registry.

(ffi/defcfn g-type-from-name
  "g_type_from_name" [:string] 
  :size_t)
(ffi/defcfn g-type-class-ref         
  "g_type_class_ref" [:size_t] 
  :pointer)
(ffi/defcfn g-enum-get-value-by-nick 
  "g_enum_get_value_by_nick" [:pointer :string]
  :pointer)

When you write [:label {:halign :start}], the backend looks up the GtkAlign type to get its class struct, asks it for the member with the start nick, and reads the integer out of the struct it gets back. Successful lookups are then memoized, and a raw integer can still be used as an escape hatch. This trick is the reason why there isn't a single GTK_* constant needed anywhere in the library.

So that's all the boilerplate that's needed to expose the needed GTK components. With that in place, each hiccup tag can map to a widget spec in a registry. These specs are represented as small maps describing how to construct the widget, apply props to it, and what kind of container it is. Here is what the code for declaring a button looks like.

(defn- ->bool [x] (if x 1 0))

(defn- button-spec []
  {:ctor    (fn [p]
              (if (:label p)
                (g/gtk-button-new-with-label (:label p))
                (g/gtk-button-new)))
   :apply   (fn [w p]
              (when (contains? p :label)
                (g/gtk-button-set-label w (:label p)))
              (when (:tooltip p)
                (g/gtk-widget-set-tooltip-text w (:tooltip p)))
              (when (contains? p :sensitive)
                (g/gtk-widget-set-sensitive w (->bool (:sensitive p)))))
   :container :none})

The reconciler drives these through a fixed lifecycle where create! runs the constructor, applies the props, and wires up the event handlers, while apply-props! re-runs the prop application against the existing widget on each re-render, patching it in place as needed. Handlers are connected once when the widget mounts, and they're expected to close over reactive cells, so the closure captured on the first render stays correct for the life of the widget, just like it does in Reagent.

Event props are :on-* keys looked up in a signal table.

(def signals
  (atom {:on-click    "clicked"
         :on-change   "changed"
         :on-activate "activate"
         :on-toggled  "toggled"}))

Each handler gets wrapped in a foreign-callable and connected with g_signal_connect_data.

(doseq [[event handler] props]
  (when-let [signal (@signals event)]
    (let [cb (ffi/foreign-callable
              (fn [widget _data] (handler))
              [:pointer :pointer] :void :collect-safe)]
      (retain-callable! cb)
      (g/g-signal-connect-data widget signal cb ffi/null ffi/null g/CONNECT-DEFAULT))))

The :collect-safe flag is important here because GTK calls the handler from inside the blocking main loop, and the callable also has to be retained on the Clojure side, since C holds it as a raw pointer that's opaque to the garbage collector.

Another detail worth noting is that GTK emits signals synchronously from its own setters, so when a re-render programmatically sets an entry's text, GTK fires changed on the spot which triggers the handler. Since the handler causes the atom to reset, you end up in a render loop. The fix is to bracket programmatic setters with a suppression set so that their emissions are ignored.

(defn- set-entry-text! [widget text]
  (when (and (some? text) (not= text (g/gtk-editable-get-text widget)))
    (swap! suppressing conj widget)
    (g/gtk-editable-set-text widget text)
    (swap! suppressing disj widget)))

Both the widget and the signal registries are open, so adding a widget the library doesn't know about is simply a matter of writing its spec and registering it.

Finally, glimmer itself doesn't need to see any of this because a backend simply has to provide a map of eight functions handed to the reconciler at registration time.

(def backend
  {:name           :gtk4
   :create!        w/create!
   :apply-props!   w/apply-props!
   :append-child!  w/append-child!
   :remove-child!  w/remove-child!
   :replace-child! w/replace-child!
   :reorder-child! w/reorder-child!
   :schedule       post-to-gui
   :run            run!})

That's the entire contract between the reactive core and the platform, and that's why writing a backend for a different toolkit is simply a matter of wiring up the widgets into the lifecycle. It's even possible to have a stable subset of common widgets across platforms as long as you keep the naming consistent.

Bring your own toolkit

Another notable approach is seen with glitter and its AppKit renderer glitter-uikit, which are based on Replicant. While glimmer is driven by a reactive atom where you build a tree of stateful components, Replicant rejects having local state entirely, and models the entire user interface as a single pure function which turns your application data into hiccup. While Reagent couples your rendering logic with a reactive state graph to optimize updates behind the scenes, Replicant acts as a remarkably strict unidirectional renderer where data goes in and hiccup comes out.

The same counter in glitter-uikit looks like this:

(require '[glitter-uikit.app :as app]
         '[glitter-uikit.appkit :as appkit]
         '[glitter.core :as core])

(defonce state (atom {:count 0}))

(defn view [{:keys [count]}]
  [:vbox {:spacing 12}
   [:label {:label (str "Count: " count)}]
   [:hbox {:spacing 8}
    [:button {:label "+ 1" :on {:click [[:action/inc]]}}]]])

(defn execute-actions [_event actions]
  (doseq [[kind] actions]
    (case kind
      :action/inc (swap! state update :count inc)
      nil)))

(core/set-dispatch! execute-actions)

(defn -main [& _]
  (app/run (fn [window] (appkit/mount! window view state))))

Here the leaves are AppKit views instead of GTK widgets, and notice that the button no longer closes over the atom. It simply declares what it wants done as data, and a dispatch function interprets those actions against the application state. The hiccup stays the same, and the only difference is where the state lives and who is responsible for updating it.

Conclusion

Bringing web-style development to native widgets isn't a new idea, of course. React Native popularised the approach letting you write React components, and render to the platform's native widgets. The catch there is that the app still runs inside a JavaScript runtime that talks to the platform through a bridge, while the development loop revolves around bundling JavaScript and hot-reloading it into a running app. Flutter sidesteps the bridge by bringing its own rendering engine and drawing every control itself, which means you're no longer using native widgets. Another approach is what Tauri does keeping the web frontend backed by the OS webview. This approach is lighter than Electron but still renders HTML rather than native controls, and is subject to the quirks of the webview implementation on each platform. Meanwhile, the native world has been converging on the same idea from the other side, with SwiftUI and Jetpack Compose offering declarative UIs, but those put you right back in a compiled language with a rebuild cycle and no REPL. Each of these approaches ends up being a compromise involving either having a heavyweight runtime or giving up ergonomics.

With Jolt, we can finally have the best of both worlds using native widgets without having to bundle a whole browser engine just to render the UI, have a clean Hiccup based API that lets you arrange components just like you would with HTML elements in the DOM, and have an interactive development environment where you can see the application evolve as you make changes to it. Since Jolt is a Clojure dialect that compiles to native code, there's no JVM or JavaScript runtime in the way, and the app compiles into a lean binary. You get the same feedback loop that makes web development pleasant, while driving real platform widgets in a native application.

Permalink

Previewing the Model Hardware Standard

One of my summer jobs in university was working in an analytical chemistry lab. I spent countless hours pipetting liquids from tube to tube, scanning labels, manually entering data into spreadsheets, and operating fancy machines. Even way back then (please don’t ask exactly how long ago 🙈) I couldn’t help but think “we must be close to automating most of this work”. Turns out we were not, but with this announcement I think its closer than ever.

MHS enables AI agents to operate multiple lab and manufacturing instruments, such as microscopes, liquid handlers, and robotic arms, in parallel, and perform intricate tasks ranging from routine drug discovery experiments to laser calibration on a quantum computer.

This is a really cool development and one way I could imagine AI having some genuinely beneficial impact on society. I think we’re arguably living in the bad timeline right now and unless public opinion shifts pretty dramatically somehow it will be difficult to get to a place where AI does more good than harm. Part of getting there is delivering some credible public benefit for all the cost and risk the public is expected to bear as a result of its development and deployment. I think meaningful progress in drug discovery is a potential area where that could happen.

Permalink

Macro Macros

This is a follow up to my recent posts on Domain Specific Languages (DSLs) for database queries in Clojure, and different quoting forms in Clojure to enable this.

I had originally thought that I would write this post in a "Tutorial" style. But then it occurred to me that I used to get the best responses from people when I just documented what I learned as I learned it. I think it's a more "open" and personable style of writing, which may be what people liked. This approach might have extra appeal in the current era of AI slop, since people ought to see that I've written it myself. Unless I usually sound like an AI 🙃

I'm also hoping that this approach will be easier to write, since I don't need to restructure my exploration as an instructional post.

On the other hand, it may be a terrible idea. But I won't know unless I try…

Graph Queries

SQL is very structured around column selection, so it does not usually need anything like variables inside a query. This allows HoneySQL to build most of its structures using functions that take keywords and values as arguments.

On the other hand, graph query languages like Datomic, SPARQL, and GQL typically use pattern matching on the graph, where parts of the pattern contain a variable that will be "bound" to associated values when a pattern matches. For instance:

?person :hasFriend ?friend

… is a pattern for matching edges in a graph where something is connected by a property labelled :hasFriend to another node. Every edge in the graph that matches this pattern, leads to a pair of nodes that the variables ?person and ?friend get bound to.

GQL would do something similar with a MATCH clause of:

(person:Person)-[:HAS_FRIEND]->(friend:Person)

Like the SPARQL form, this binds the first node (which must be of type Person) to the variable person, and the second node (which is also a Person) to the variable friend.

I'm focused on Datomic and SPARQL, which are more closely related to each other, so I won't address GQL here.

The issue with variables is that keywords are already being used in queries (such as for the :hasFriend property), so we need something else. The obvious candidate is the Symbol, and this is the route that Datomic took.

However, Clojure uses symbols to refer to values, meaning that they need to be inserted in queries without being evaluated. That was why I did that post on quoting symbols: quoting is a mechanism to create a symbol as a part of a structure without asking Clojure to instead insert the value the symbol references. i.e. if I say:

(let [?person "bad data"]
  [?person :hasName "Fred"])

;; returns=> ["bad data" :hasName "Fred"]

This isn't what I wanted. It is even worse if I don't have ?person predefined (as the let expression does), since the code can't even run.

Instead, I want the symbol in the first position:

(let [?person "bad data"]
  ['?person :hasName "Fred"])

;; returns=> [?person :hasName "Fred"]

This doesn't care if the symbol is defined or not. We get back the structure that we wanted with a symbol in it.

Flint

A colleague has been using Flint to build SPARQL queries programmatically. I like Flint, because it uses Datomic-style queries to express SPARQL. Datomic has been designed to use from Clojure, with a query language based in Clojure data structures. So Flint should make it just as easy to query SPARQL.

However, because the queries are being built programmatically, we run the risk of reusing a variable name each time a new expression is added into the query. Reusing a variable for a different purpose will generally break a query, so he was generating new variable names whenever he was generating a new query clause.

There are a couple of ways to generate new variables for a query. One way is to generate a new name, build a symbol with that name, and then insert it without quoting the name being used to carry that symbol:

(let [?container (gensym "?container")]
  [[:my-object :contains ?container]
   [?container :value "hello"]])

;; result=> [[:my-object :contains ?container180]
;;           [?container180 :value "hello"]]

Another approach is to use an auto gensym:

`[[:my-object :contains ?container#]
  [?container# :value "hello"]]

;; result=> [[:my-object :contains ?container__2__auto__]
;;           [?container__2__auto__ :value "hello"]]

Both of these seem fine, but it gets harder to read and write when new constraints are added. For instance, I may be looking for a :value that is a string containing the substring "lo":

(let [?container (gensym "?container")
      ?value (gensym "?value")]
  [[:my-object :contains ?container]
   [?container :value ?value]
   [:filter '(contains ~?value "lo")]])

;; result=> [[:my-object :contains ?container140]
;;           [?container140 :value ?value141]
;;           [:filter (contains (clojure.core/unquote ?value) "lo")]]

Which doesn't work. We need syntax quoting instead:

(let [?container (gensym "?container")
      ?value (gensym "?value")]
  [[:my-object :contains ?container]
   [?container :value ?value]
   [:filter `(contains ~?value "lo")]])

;; result=> [[:my-object :contains ?container144]
;;           [?container144 :value ?value145]
;;           [:filter (user/contains ?value145 "lo")]]

Again… the quoting didn't work:

(let [?container (gensym "?container")
      ?value (gensym "?value")]
  [[:my-object :contains ?container]
   [?container :value ?value]
   [:filter `(~'contains ~?value "lo")]])

;; result=> [[:my-object :contains ?container148]
;;           [?container148 :value ?value149]
;;           [:filter (contains ?value149 "lo")]]

To be fair, I did know how to quote that all along, but I wanted to demonstrate that it can catch people out.

Another Step

This is all very contrived. But what about if I want to construct something more complex. Say, I want a general way to select an entity and its contained value, and then I decide that I want to filter that by containing "lo". The first part would be in a function, but I will need to provide the binding ?value variable to filter on it:

(defn select-entity-value [?entity ?value]
  (let [?container (gensym "?container")]
    [[?entity :contains ?container]
     [?container :value ?value]]))

(let [?e (gensym "?e")
      ?v (gensym "?v")]
  `[~@(select-entity-value ?e ?v)
    [:filter (~'contains ~?v "lo")]])

;; result=> [[?e178 :contains ?container180]
;;           [?container180 :value ?v179]
;;           [:filter (contains ?v179 "lo")]]

Splice-unquoting like this (the ~@ syntax) isn't really necessary, though quoting everything means that the expression inside the :filter doesn't need its own quoting (since it is a list, and we have to quote lists, or else they get executed).

We could even avoid almost quotes with:

(let [?e (gensym "?e")
      ?v (gensym "?v")]
  (conj (select-entity-value ?e ?v)
        [:filter (list 'contains ?v "lo")]))

But now we have conj and list in the expression, we're still quoting contains, and we have the messy gensym declarations at the top. This DSL doesn't look all that easy to use.

Filters and Bindings

Until now, I've been rehashing what I've talked about already, and some of the things my colleague was building. It all worked, but it just looked… clunky. I found myself thinking that surely we could do better, right?

Patterns are not too complex to work with, since they are just short vectors, containing keywords, symbols, or simple values like strings and numbers. The mess really showed up when we tried to introduce a filter.

The filter we used here was the contains function. There are a lot of other functions in SPARQL, so there isn't anything particularly special about that function. Hopefully, whatever we do to address one function could be applied to all of them.

It would be nice to just insert the contains expression directly into the query. Something like:

[:filter (contains ?v "lo")]

Bindings work similarly, except instead of evaluating an expression and passing through everything that returns a true result, they evaluate an expression and save it in a variable. For instance, to save a the lower-case form of the string ?v, you can bind it:

[:bind (lcase ?v) ?lowv]

The problems here are that contains and + looks like a function. Even if we created a function for it, the ?v is not bound to anything, so the function would fail. However, Clojure macros can accept any symbol as an argument. Can we use them somehow?

Macros are Clojure code that generates Clojure data. The trick is that the macro is called during compilation, and the resulting data is inserted into the source code before it gets compiled. This lets you generate code using code.

Clojure itself uses macros everywhere. Most of the "built in" syntax is actually just macros that write out other code. At the end of the day, Clojure only contains a few "special forms" that get presented to the compiler. But don't let that list of special forms fool you: many of those (e.g. defn, fn, and let) are actually macros as well, with simpler special forms underlying them.

Many of these macros accept symbols that are not bound, safely generating code that uses those symbols. For instance, consider an expression for an identity function: (fn [x] x)

We can see what the fn macro expands to by using macroexpand:

=> (macroexpand '(fn [x] x))
(fn* ([x] x))

This is just a rewrite to use fn* instead, which is much simpler: fn* does not do argument destructuring, it does not handle pre nor post conditions, it does not attach metadata, and it always expects parentheses around a function argument list and body even when there is only a single arity.

But importantly, notice how we can provide fn with an expression that includes x and it returns a new expression that also includes x? We don't need x to exist before doing this. That may be what we need.

Let's try it out. Can we create a contains macro that accepts a symbol argument and returns a list with appropriate symbols in it?

My first attempt was laughable:

(defmacro contains [a b] `(~'contains ~a ~b))

So when I call (contains "foot" "foo") it should return a list containing the symbol contains, the string "foot" and the string "foo". That was what was returned, but then that goes to the compiler, and the symbol contains doesn't exist, so it fails. Doh.

I needed an actual symbol object in the position of contains:

(defmacro contains [a b]
 (let [c# (symbol "contains")]
  `(~c# ~a ~b)))

But I didn't need to evaluate that to know what it would do… calling (contains "foot" "foo") would generate the list (contains "foot" "foo") and that would be evaluated, which then repeats the process until the macro evaluation overflows the stack. I had forgotten that I'm not returning the data structure anymore. Now I'm returning code that evaluates to the required data structure.

Aside from quoting a list (which gets tricky, because macro expansion essentially unquotes what you've quoted, so you need to double-quote), you can create a list with the list function:

(defmacro contains [a b]
 (let [c# (symbol "contains")]
  `(list ~c# ~a ~b)))

How does this look?

=> (macroexpand '(contains "foot" "foo"))
(clojure.core/list contains "foot" "foo")

Oh. That was sort of obvious in hindsight.

Let's forget inserting the symbol and just put it in place:

(defmacro contains [a b]
 `(list (symbol "contains") ~a ~b))
=> (macroexpand '(contains "foot" "foo"))
(clojure.core/list (clojure.core/symbol "contains") "foot" "foo")
=> (contains "foot" "foo")
(contains "foot" "foo")

See why I don't usually like writing this way? Showing how long it takes to get to something that should be easy is embarrassing.

But I'm really not happy about embedding the symbol by generating one based on a string. Is there another way? What if I quote it?

(defmacro contains [a b] `(list 'contains ~a ~b))
=> (macroexpand '(contains "foot" "foo"))
(clojure.core/list (quote user/contains) "foot" "foo")

That got me part of the way there, but I forgot that I'm in a syntax quote, so contains is resolved with its full namespace. I can write out (quote ~'contains), but I'm curious… can I just chain the quote/unquote syntax here?

(defmacro contains [a b] `(list '~'contains ~a ~b))
=> (macroexpand '(contains "foot" "foo"))
(clojure.core/list (quote contains) "foot" "foo")
=> (contains "foot" "foo")
(contains "foot" "foo")

I might stick to the (quote …) form though, since cascading quote/unquote could get hard to read. It turns out that this mattered later on.

Arguments

That did work, but what about the arguments? Right now they are being passed through, and they just appear verbatim in the final form. That doesn't work for variables like ?value:

=> (contains ?value "foo")
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: ?value in this context

We need to quote the arguments too.

(defmacro contains [a b] `(list (quote ~'contains) (quote ~a) (quote ~b)))
=> (macroexpand '(contains ?v "foo"))
(clojure.core/list (quote contains) (quote ?v) (quote "foo"))
=> (contains ?v "foo")
(contains ?v "foo")

This looked great! Until my colleague pointed out that he couldn't pass non-literal values into the macro. Of course he couldn't 🤦‍♀️

In this case, I need to check if the argument is a symbol, and if it is, then look for the form ?name or $name (SPARQL allows either ? or $ to indicate a variable). I'll create a helper function that tests if an argument is a symbol that starts with one of these characters, and only quote the ones that match. However, a function like that can't just return (quote a), since that just evaluates to a and won't get inserted into the output correctly. Instead, I need to return a list of the form (quote a):

(defmacro contains [a b]
 (let [vquote (fn [s]
               (if (and (symbol? s) (#{\? \$} (first (name s))))
                 (list 'quote s)
                 s))
       a# (vquote a)
       b# (vquote b)]
   `(list (quote ~'contains) ~a# ~b#)))

user=> (macroexpand '(contains ?v "foo"))
(clojure.core/list (quote contains) (quote ?v) "foo")
user=> (macroexpand '(contains v "foo"))
(clojure.core/list (quote contains) v "foo")

OK, I'll admit it… it took me a few iterations to get that right. Quoting quote was not something I was expecting.

Macro Macros

This works for contains, but what about all the other SPARQL functions? Do I have to write this out for all of them as well? Then there are functions like regex that can take 2 or 3 arguments.

It would be nice if I could take a function name, and generate the appropriate macro.

Except, macros are just code, and macros generate code. So I could always try generating a macro with a macro, right?

Well, sort of. It turned out to be harder than I thought.

Let's try creating a macro that generates a macro for a single-argument function, like lcase. I decided to skip the argument handling to start with, just to get the basic structure down. So my first attempt was:

(defmacro sparql-fn1 [s]
 `(defmacro ~s [~'a] `(list (quote (unquote ~s)) ~'a)))

This made it clear that I wasn't really sure of how to perform the (quote ~'contains) when I didn't have a literal value to put into the quoted position.
I thought I'd try it anyway, and it complained about "No such var: user/s", which didn't really surprise me. So let's see, what did it look like? I'll reformat the output so it's not all on a single line:

=> (macroexpand '(sparql-fn1 lcase))
(do (clojure.core/defn lcase
      ([&form &env a]
       (clojure.core/seq
         (clojure.core/concat
           (clojure.core/list (quote clojure.core/list))
           (clojure.core/list
             (clojure.core/seq
               (clojure.core/concat
                  (clojure.core/list (quote quote))
                  (clojure.core/list
                    (clojure.core/seq
                      (clojure.core/concat
                        (clojure.core/list (quote clojure.core/unquote))
                        (clojure.core/list user/s)))))))
           (clojure.core/list (quote user/a))))))
    (. (var lcase) (setMacro))
    (var lcase))

Let's remove the clojure.core namespaces, the redundant seq calls, and use some quoting syntax:

(do (defn lcase
     ([&form &env a]
      (concat
        (list 'list)
        (list (concat
                (list 'quote)
                (list (concat
                        (list 'unquote)
                        (list user/s)))))
        (list 'user/a))))
    (. (var lcase) (setMacro))
    (var lcase))

This helped me figure out where I'm passing things in poorly, but there was something much more important going on here.

The returned data structure was not the code to call defmacro but instead was a do block with 3 steps:

  • Define a function called lcase. This takes 3 arguments, prepending &form and &env before the argument I had declared.
  • Defining a function creates a var for it. The second step calls setMacro on that var.
  • Returns the lcase var.

The arguments &form and &env are explained in the Clojure docs as special variables that are available inside macros without having to declare them. We are seeing this being set up.

Also, rather than creating a "macro", a function is being created that is then converted to a macro. I'd seen elements of this before, but I'd never really dug into it. This prompted me to look at the defmacro source (by typing (source defmacro) a the repl), and… yes, this is what it's doing.

Most surprisingly, the code of the macro is not in the function, but rather the list structure of the macro is being constructed and returned. Reading the body, it looks like the final structure evaluates to:

(list 'list (list 'quote (list 'unquote user/s)) 'user/a)

The user/s part came about due to my unfortunate quote/unquote structure, and the user/a is because of how I'm calling macroexpand, but the rest of it actually evaluates to what I was expecting, to wit:

(list (quote (unquote user/s)) user/a)

And that structure would evaluate to what gets inserted into source code.

So the function here generates a structure that evals to what the macro returns, which then evals a second time to what gets inserted into the source.

So I can manually create a macro by creating a function that needs to evaluate twice to create the final code structure. That's going to be confusing.

But then I realized that I don't have to worry about the quote/unquote mess anymore. If I want to quote something, then using quote does not hide the quoted value, because now I am creating a list where quote is the first element, and the thing to be quoted is exactly what I want. So it's more complex, but more flexible.

Let's try then. To start with, I decided on a form that just takes all of its arguments, and wraps each one in the helper function that quotes it if the argument is a symbol starting with $ or ?.

(defmacro sparql-fn
  [s]
  (let [argq# (fn [a]
                (list 'if (list 'and (list 'symbol? 'a) (list #{\? \$} (list 'first (list 'name 'a))))
                      (list 'list (list 'quote 'quote) a)
                      a))]
    (list 'do
          (list 'defn s
                '[&form &env & args]
                (list 'list
                      (list 'quote 'cons)
                      (list 'list (list 'quote 'quote) (list 'quote s))
                      (list 'concat
                            (list 'list (list 'quote 'list))
                            (list 'map
                                  (list 'fn '[a] (argq# 'a))
                                  'args))))
          (list '. (list 'var s) '(setMacro))
          (list 'var s))))

This took me a few attempts, but it worked out pretty well:

=> (sparql-fn contains)
#'user/contains
=> (sparql-fn lcase))
#'user/lcase
=> (contains ?x "lo")
(contains ?x "lo")
=> (lcase "Hello")
(lcase "Hello")
=> (lcase a)
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: a in this context

Let's see what the generated code looks like:

=> (macroexpand '(sparql-fn lcase))
(do
  (defn lcase
   [&form &env & args]
   (list (quote cons)
         (list (quote quote) (quote lcase))
         (concat
           (list (quote list))
           (map (fn [a]
                 (if (and (symbol? a) (#{\? \$} (first (name a))))
                   (list (quote quote) a)
                   a))
                args))))
  (. (var lcase) (setMacro))
  (var lcase))

How does the returned list structure eval then? Working it manually, I came up with:

(list 'cons (list 'quote 'lcase) (concat '(list) (map (fn [a] ) args))))

The function here was a bit long, so I elided it. It's looking good, but evaluating it one more time, gets us to:

(cons (quote lcase) (list (map (fn [a] ) args)))

Which, when called as (lcase "Hello") should return a list containing 'lcase followed by the function mapped over all arguments. The function returns its input for a string, so the final list would be (lcase "Hello").

Similarly, for (lcase ?value) it's the same kind of thing, but now the function will return a list of (quote ?value), so the final output will be (lcase (quote ?value)).

There is no checking on the arguments at all, but I know that Fink is already doing that kind of work, so I thought this was where I could stop.

Small Issue

At this point I decided to try it with all of the SPARQL functions, and immediately hit a wall with concat. This is a function that appears in both SPARQL and in the above macro. As soon as concat is declared as a SPARQL macro, then the next thing I try to declare as a SPARQL macro will use the new concat macro instead of clojure.core/concat. Fink also tries to map and to SPARQL's &&, so that should be addressed as well:

(defmacro sparql-fn
  [s]
  (let [argq# (fn [a]
                (list 'if (list 'clojure.core/and (list 'symbol? 'a) (list #{\? \$} (list 'first (list 'name 'a))))
                      (list 'list (list 'quote 'quote) a)
                      a))]
    (list 'do
          (list 'defn s
                '[&form &env & args]
                (list 'list
                      (list 'quote 'cons)
                      (list 'list (list 'quote 'quote) (list 'quote s))
                      (list 'clojure.core/concat
                            (list 'list (list 'quote 'list))
                            (list 'map
                                  (list 'fn '[a] (argq# 'a))
                                  'args))))
          (list '. (list 'var s) '(setMacro))
          (list 'var s))))

Finally, I can write SPARQL queries using Fink with clean filters:

['[?entity :contains ?c]
 '[?c :name ?name]
 [:filter (contains ?name "lo")]]

Wrap Up

Well, this wasn't a tutorial. It was more like a litany of my mistakes as I stumbled towards understanding. I'm sure an LLM could have told me how to do this immediately, but then I wouldn't have learned anything.

I have no idea if anyone else will ever read this, but if you did, then thanks. If not, then that's OK. Writing these things down helps my clarify and consolidate what I learned. This is something I used to do regularly a long time ago, and I should do more of it.

As per my AI policy, I'm going to leave the typos alone so you know I really did type this. But let me know if you see any sentences that make no sense, and I'll clean them up.

Permalink

Smarter Form Targeting Is Coming to CIDER

If I had a dollar for every time someone asked on the Clojurians Slack in #cider why C-x C-e evaluated “the wrong thing”, I’d probably be writing this post from a yacht. The answer was always the same: the cursor wasn’t where CIDER expected it to be. The upcoming CIDER 2.1 release changes that - the evaluation commands now figure out which form you mean from where your cursor actually is.

NOTE: Update: this didn’t survive contact with its users, and CIDER 2.1 ships with the classic behaviour after all. See /posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html for what the feedback was, what replaced it, and the rather nasty bug the detour turned up.

A bit of history

Emacs has a very particular tradition when it comes to evaluating code: eval-last-sexp (the venerable C-x C-e) acts on the expression before the cursor. Not the one you’re looking at, not the one you’re inside of - the one that ends exactly where your cursor stands. SLIME follows this tradition, Emacs Lisp itself follows it, and for the past 15+ years CIDER has followed it too.1 If you grew up in Emacs, this rule is in your fingers and you’ve never once thought about it.

Here’s the thing, though - most people using CIDER didn’t grow up in Emacs. And many of them never programmed in Emacs Lisp and Common Lisp with SLIME. They came to Emacs because of CIDER (or Clojure in general), and for them the rule is invisible, arbitrary and mildly hostile. You put your cursor on a form, you press the eval key, and CIDER cheerfully evaluates… something else. Meanwhile every other modern Clojure environment - Calva, Conjure, the various vim plugins - resolves the form from the cursor position and just does what you meant.2

For a long time I resisted changing this, mostly out of respect for the Emacs tradition (and my own muscle memory). But at some point I had to admit that I was optimizing for the wrong audience. CIDER’s users are mostly casual Clojure hackers who happen to use Emacs, not Emacs experts who happen to write Clojure. The tradition was serving me, and confusing them.

What’s actually changing

The evaluation commands (and their macroexpansion, inspection and tapping siblings) now resolve “the form the cursor indicates”. Concretely, with | marking the cursor:

(map inc |(range 10))

Pressing C-c C-e here used to evaluate inc - the form before the cursor, which is almost never what you wanted. Now it evaluates (range 10) - the form your cursor is pointing at.

(str "hello" " " "world"|)

This one used to evaluate "world" (really!), because the last complete expression before a cursor sitting on the closing paren is the final string. Now it evaluates the whole (str ...) call.

Macroexpansion benefits too:

(when tru|e (launch-missiles))

C-c C-m here used to complain that true is not a macro. Now it expands the enclosing (when ...) call, because expanding a bare symbol is never what anyone means.

And my favorite one - the rich comment workflow is now consistent everywhere:

(comment
  (calculate-all-the-things|))

Every defun-level command - eval, pretty-print, inspect, debug - now treats the form inside the (comment ...) as the top-level one. Evaluating a whole comment form returns nil by definition, which has exactly zero uses, so CIDER no longer does that no matter which command you reach for.

Why you probably won’t notice

Here’s the part I’m most pleased with: the new behavior agrees with the old one at every position where “the form before the cursor” made sense. Cursor right after a form? Same result as always. Cursor in the whitespace after a form? Same. Cursor in the middle of a symbol? Same. The two behaviors only diverge where the classic answer was something nobody ever wanted - a previous sibling, a lone trailing atom.

So if your muscle memory follows the Emacs tradition, nothing changes for you. If it doesn’t - CIDER stops punishing you for it. That’s the whole change.

Reverting to the classic behavior (for now)

If you do want the traditional rules - maybe you genuinely use “evaluate the previous sibling while standing on an opening paren” - one setting restores them exactly:

(setq cider-form-targeting 'preceding)

There’s also a per-session toggle in the eval menu (C-c C-v T) that shows the active mode in the mode line while you experiment.

This option is probably living on borrowed time, though. I added it back when I planned to keep the classic behavior as the default and offer smart targeting as an opt-in. Now that the roles are reversed, an option whose only job is restoring rules almost nobody deliberately relied on doesn’t really make much sense, and lately I’ve been trying to trim that kind of clutter from CIDER, not add to it. Don’t be surprised if the option quietly disappears - possibly even before the release ships. Which is one more reason to speak up now if the classic behavior genuinely matters to you.

Farewell, “last sexp”

This change forced my hand on something I’d been putting off for years - the command names. cider-eval-last-sexp is a fine name for a command that evaluates the last sexp. It’s a lie for a command that evaluates the form your cursor indicates. So the commands got honest names:

  • cider-eval-last-sexp is now cider-eval-form
  • cider-eval-defun-at-point is now cider-eval-defun (the -at-point never carried information)
  • likewise for the pprint/tap/inspect/insert variants

Every old name keeps working as an alias, so your config and your M-x habits are safe. But why “form” and not “sexp”? Beyond the targeting change, there’s a Clojure-specific reason: in Clojure a form isn’t always a single sexp. ^:private x is two sexps but one form; so is #inst "2024-01-01". The commands operate on forms - the reader’s unit of evaluation - and now they say so.3 The manual’s evaluation docs got a proper glossary explaining all of this.

Closing thoughts

All of this is on master and in the MELPA snapshots today, ahead of the next stable release. I’d really love for people to play with it before the release ships - especially if you’re an Emacs veteran whose fingers disagree with my reasoning, or a newcomer for whom this was supposed to just work. Did we get the resolution rules right? Does anything still surprise you?

Share your feedback on the CIDER discussions board, in #cider on the Clojurians Slack, or just file an issue. This is exactly the kind of change that’s easy to adjust before a release and painful after - and the fate of the compatibility option depends on what I hear.

That’s all I have for you today. Keep hacking!

  1. CIDER started its life as a SLIME “clone” for Clojure, after all - the tradition runs deep. 

  2. Interestingly, Cursive is the only major non-Emacs Clojure environment that kept the classic “form before the caret” model. 

  3. This also explains a subtlety Emacs veterans might appreciate: plain forward-sexp movement doesn’t know that Clojure metadata belongs to the form it annotates, which is why clojure-mode has always needed its own “logical sexp” movement functions. The new targeting is built on those, so metadata is never silently dropped from what you evaluate. 

Permalink

Clojure 1.12.6-alpha1

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

  • CLJ-2794 - gen-class - incorrectly treats interface default methods as abstract, throwing when the var delegate is unbound

Permalink

Making Magic stable

Rationale

MAGIC (Morgan And Grand Iron Clojure) compiles Clojure to .NET so we can run it in Unity, including on iOS. When its creator Ramsey Nasser no longer had time to maintain it, I consolidated his six repositories into one monorepo under our Flybot org. I then improved the tooling around the compiler, which helped me fix bugs faster, and improved the integration in Unity, which was the whole point of the compiler in the first place. This article is about the decisions behind all that, not how the compiler works. The docs cover the how.

ClojureCLR vs MAGIC

The first question is always why not just use ClojureCLR, David Miller's mature Clojure-to-.NET port, which runs well on desktop. Its dynamic dispatch goes through the DLR (Dynamic Language Runtime), which builds each call site by emitting IL at runtime through System.Reflection.Emit. IL is the bytecode the .NET runtime executes, so this is a form of JIT (Just-In-Time) compilation: new executable code is produced while the program runs.

The problem is that Unity's IL2CPP backend compiles everything to C++ ahead of time, so there is no runtime left to execute IL that was generated on the fly. iOS forces IL2CPP, because Apple forbids any third-party JIT, and Android forces it too, not through a JIT ban but because Google mandates 64-bit and Unity's Mono has no ARM64 build. Consoles have the same constraints. So ClojureCLR is just not an option if you want to build your Unity app for anything other than desktop, which is pretty much everybody really. That constraint is what pushed Ramsey to create his own compiler that writes all IL at build time, so the IL2CPP transpiler has everything it needs to generate its C++.

How we use it at Flybot

At Flybot we helped port a client's old Java game libraries to Clojure. Then, because we knew MAGIC already existed, we took on the harder task of making those Clojure libraries run as .NET DLLs inside Unity. The payoff is that the same game APIs run in both the server backend and the Unity frontend, written once. I worked closely with Ramsey across two stretches, first on performance and then on stability (the earlier story), until those games shipped in production. I was doing the bug reporting, he was fixing the compiler.

The compiler worked fine for the most part, but the toolchain around it was painful. Six repositories, each with its own version and no shared release. Ramsey's time for it had become limited, so bugs could sit a while. And the internals were undocumented, with no public dev workflow, so contributing meant first reverse-engineering how the pieces fit. Our gaming platform frontend team actually came up with quite a few workarounds over the years. By the time I took over the compiler, their repos still carried patches just to get MAGIC to compile and integrate with Unity, both in the Clojure libs (ported to the CLR) and on the Unity side. I wanted to get rid of these patches by improving MAGIC directly, so everybody could benefit from it in the future.

1. Gather the six repos into one

The first step was to gather everything in one place so I could add proper project tasks, proper CI, and therefore a more convenient dev workflow.

A monorepo was the obvious choice here because these six repos always worked as one system. One version instead of six, one place to file bugs, and the freedom to land a compiler change, the runtime tweak it needs, and a stdlib fix in a single PR, instead of coordinating three separate repos. The diagram below shows the merge:

flowchart LR
    m1["magic"] --> gfr
    m2["mage"] --> gfr
    cr["Clojure.Runtime"] --> gfr
    mr["Magic.Runtime"] --> gfr
    no["nostrand"] --> gfr
    mu["Magic.Unity"] --> gfr
    gfr{{"git-filter-repo<br/>(full history kept)"}} --> mono["flybot-sg/magic<br/>one repo · one version · one CI"]

I used git-filter-repo to merge the six trees while keeping every author and commit date, going all the way back to 2009 since the runtime carries David Miller's history from its ClojureCLR fork. So the history itself credits the extensive work of Ramsey, of David Miller, and of everyone who contributed.

It also lets anyone trace a bug back to the commit that introduced it. A human or an LLM can bisect far faster when the entire history of every piece sits in one place.

2. Build tooling instead of becoming a compiler expert

I am not a compiler expert but I still had a plan. The best move was to make the compiler understandable by anyone. Everything runs as a Babashka (bb) task. I really like Babashka and it works very well for monorepos (see Clojure Monorepo with Babashka).

In order to understand a bit more what is going on when I compile something, I created two tasks:

  • bb pipeline walks a form through macroexpansion, the AST, and the symbolic IL
  • bb prepl-eval runs a form against a live MAGIC runtime.

Between them, that is usually enough to see where something goes wrong without reading the compiler internals.

For example, for (+ 1 2), bb pipeline shows how it compiles, walking the form through macroexpansion, the AST and the type stages down to the symbolic IL the emitter produces:

$ bb pipeline '(+ 1 2)'

================================================================
FORM   (+ 1 2)
================================================================

================================================================
MACROEXPAND
================================================================
(. clojure.lang.Numbers (add 1 2))

================================================================
AST (skeleton)
================================================================
{:args ...
 :method #object[RuntimeMethodInfo 0x6ab6fcd0 "Int64 add(Int64, Int64)"],
 :original ...
 :type System.Int64,
 :op :intrinsic,
 :il-fn #object[<magic>magic_intrinsics$add-mul-compiler__0 ...],
 :form (. clojure.lang.Numbers (add 1 2)),
 :target ...}

================================================================
TYPES (4 typed nodes)
================================================================
  :intrinsic (. clojure.lang.Numbers (add 1 2)) :: System.Int64
  :const 1 :: System.Int64
  :const 2 :: System.Int64
  :const clojure.lang.Numbers :: :class

================================================================
SYMBOLIC IL (3 instructions)
================================================================
  ldc.i8 1
  ldc.i8 2
  add.ovf

There is no Var lookup and no IFn.invoke: + inlines to the static Numbers.add call, which the compiler recognises as an intrinsic, a call it knows how to emit directly, so the whole form lowers to three CLR instructions. Very cool.

bb prepl-eval is the other half, running the same form on a live MAGIC runtime and handing back a structured reply:

$ bb prepl-eval '(+ 1 2)'

{:tag :ret, :val "3", :ns "user", :ms 2.1492, :form "(+ 1 2)"}

With these two tasks, we can see both what the compiler emits, as pure data, and what it actually does, which is most of what I need to localise a bug. It also pays off with LLMs: given these two tasks, Claude Code finds the origin of a bug way faster than by digging through the compiler code. It is truly impressive.

There are quite a few other tasks, used in CI among other places; you can find more about them in docs/development.

3. Drift check the bootstrapping

MAGIC is bootstrapped, which means it uses a previous version of itself to compile its next version (see docs/bootstrap). MAGIC commits the emitted DLLs alongside the Clojure source code and the C# runtime, so it can build the next version from these assemblies. Committing a bug fix therefore requires two things:

  • the Clojure or C# source change
  • the new DLLs that contain the fix

So I have a bb check-drift task that checks that nothing is stale (including that the DLLs were regenerated, among other things). I just needed to be sure the new DLLs were committed alongside the source change.

However, I could not easily know which DLLs were really affected by a source change, since they all came out different on every rebuild: the compilation was not deterministic. So I worked on making it deterministic (see the repo doc Deterministic compilation and the drift check). This allowed me to byte-diff the DLLs! This is really valuable, because it means I can see exactly which DLLs are impacted by any source change.

More details on how I made the compiler deterministic and its caveats in the article: Drift Checks for a Self-Hosting Compiler.

4. Catching IL2CPP bugs

Ramsey had told me that the IL2CPP documentation is sometimes incomplete and even wrong, so a lot of the behavior has to be inferred by testing and disassembling what it produces. The consequence: some code runs totally fine on Mono and fails only when we build with IL2CPP.

Rather than keep rediscovering those failures inside our large game projects, I built a standalone Unity project that collects a minimal repro of every IL2CPP edge case we have hit so far: nine suites, 90 checks, all green on Mono and on a Standalone Mac IL2CPP build. Every time a fix is suspected to behave differently under IL2CPP, its repro lands in the suite in the same commit.

Example of divergence: overriding ToString on an anonymous object like so:

(.ToString
 (reify System.Object
   (ToString [_] "from reify")))
;; Mono: "from reify"
;; IL2CPP: the build itself dies

MAGIC emitted the reify class with System.Object listed both as its base type and in its interface list, which is invalid metadata, since Object is a class, not an interface. Mono loads the class without a word and runs it correctly. UnityLinker, the code stripper Unity runs during every IL2CPP build, walks that interface list to mark methods, meets the class's own base type there, and recurses until it overflows its stack. The build dies before producing a player, so no test that runs on Mono can ever see this bug: only an actual IL2CPP build surfaces it.

The suite is the one piece that does not run in CI, because the IL2CPP build needs a machine with Unity installed. So I run it by hand after any suspect fix.

5. Foundation first, then the backlog

Only with the monorepo, tooling, CI, and smoke suite in place did I start on the bugs that had been open on Ramsey's repos for years. Each commit references the issue it closes, including the original nasser/* numbers, and the conventions in CONTRIBUTING.md mean a human or an LLM can file and fix without re-asking how we work.

The releases came fast once the base held:

timeline
    title MAGIC release arc (May to August 2026)
    v0.1.0 May 22 : Monorepo, bb tooling, CI, IL2CPP smoke
    v0.2.0 May 23 : Compiler and stdlib bug fixes
    v0.3.0 Jun 01 : Clojure 1.10 stdlib, magic.flags
    v0.4.0 Jun 04 : Native deps.edn in Nostrand
    v0.5.0 Jun 04 : Consumer quality-of-life
    v0.6.0 Jun 07 : Unity editor/player coexistence
    v0.7.0 Jun 09 : Dual Unity package
    v0.8.0 Jun 24 : Compiler fixes, bootstrap drift guard
    v0.9.0 Jul 08 : deps-clr.edn and magic.edn, by-ref fix
    v0.10.0 Jul 14 : Deterministic compilation, byte-diff drift
    v0.11.0 Jul 24 : Constant and integer-promotion fixes, per-test skip
    v0.12.0 Aug 18 : One Unity package, editor runtime by define

Versioning is one version.edn, and bb tag creates the tag that a CI job turns into a published release tarball on GitHub. One command, and a release builds and ships itself with nothing done by hand. That predictable, hands-off release path is what the single shared repo finally makes possible. Per-release detail is in the CHANGELOG.

I was happy to see that for the first time, I was able to use David Miller's clr.test.check as is with MAGIC! Before, I had to comment out its clojure.core require and rewrite every core/let to its fully qualified form, just to dodge a MAGIC bug. After the v0.2.0 fixes, his port compiled under MAGIC with zero source patches, sooner than I expected. Then, testing against our own libraries, I found that some workarounds were still necessary, because MAGIC had never been fully ported to Clojure 1.10. So v0.3.0 filled that gap and put every compiler option behind one magic.flags namespace.

So latent bugs were fixed and Clojure 1.10 fully ported: good progress. And yes, Claude Code clearly helped me find bug sources and suggest fixes, using the bb tasks I made it write when I took over the repo.

6. Managing dependencies

MAGIC is one of these old projects that predate deps.edn! So Ramsey made his own resolver that reads a project.edn. Nostrand is the runtime environment that loads MAGIC and executes tasks (via nos), including the deps resolver. Since MAGIC was more stable and on par with Clojure 1.10, it was the right time to modernise its dependency handling: get rid of the dedicated project.edn deps files and support deps.edn.

The obvious first task was to adopt David Miller's CLR port of tools.deps (clr.tools.deps), but it did not load as-is on MAGIC's Clojure 1.10 base: .cljr files were not recognized yet, and it calls a few stdlib functions newer than 1.10. Adopting it meant maintaining a compat fork and re-applying the patches on every upstream sync, which was not worth it.

Our need was narrow anyway: resolve git and local coordinates transitively, skip Maven, and authenticate through the developer's own git and SSH config. So I wrote my own resolver first, then aligned it with the ClojureCLR conventions.

A native deps.edn resolver (v0.4.0, v0.5.0)

I added native deps.edn resolution, one file for both JVM and CLR runtimes, with a :clr alias that swaps a JVM library for its CLR fork via :override-deps. It worked well, and I found it quite clean to have a dedicated alias carry the JVM-only / CLR-only mapping. However, that was not how the existing ClojureCLR community did it. David Miller's convention is a dedicated deps-clr.edn file that is read in place of deps.edn. It is a bit more verbose, but it is convenient for loading different paths per platform, notably a precompiled-assembly loader namespace that the CLR must load and the JVM must ignore.

deps-clr.edn, the file the CLR community already writes (v0.9.0)

David Miller's cljr, the ClojureCLR CLI, reads a deps-clr.edn in place of deps.edn when it is present, and that is where the CLR community already writes its CLR-specific dependencies. So I made nos prefer it the same way. Now both the cljr and nos CLIs resolve deps-clr.edn, so a library already ported to the CLR for ClojureCLR builds the same with nos (assuming no core functions above 1.10). This was a necessary milestone to unify the effort of porting libraries to the CLR. I recently ported robertluo/fun-map to the CLR: it carries a deps-clr.edn and its CI runs the tests with ClojureCLR, matching the existing convention. And fun-map also builds with MAGIC as is, which is really nice.

The CLR dependency flow is documented in docs/clr-dependency-files.

magic.edn, build and test config (v0.9.0)

However, for the test runner, I could not follow the ClojureCLR way. We could not use David Miller's CLR port of Cognitect's test-runner because its dependency chain bottoms out in clr.tools.reader, which reads record literals through runtime reflection (ClojureCLR's Reflector class), and MAGIC deliberately ships no runtime reflection since that is exactly what IL2CPP forbids.

The other MAGIC-only file was the dotnet.clj build helper. So nos build and nos test became built-in tasks that read an optional magic.edn, a small map where a project states only what differs from the defaults. A library that needs no tweaks omits the file; the hand-written dotnet.clj is gone.

So a lib still specifies the io.github.dmiller/test-runner port in its test deps to run tests with cljr, and adds a small magic.edn file at its root to run them with nos.

The full guide is in docs/porting-libraries-to-magic.md.

7. The right runtime per phase, in Unity

With consumers able to build and depend on CLR libraries cleanly, the last piece left was the one we actually ship into. The right arrangement was not my idea: Hong, an engineer on our client's Unity team, had arrived at it out of necessity: Run ClojureCLR in the editor, where it compiles Clojure from source in memory so hot reload works, and run MAGIC only in the player build, where its static IL is what IL2CPP needs. I wanted this setup in a UPM package that ships both runtimes with the proper defineConstraints in their .meta files, to avoid conflicts in the Unity editor.

This took some time and was actually only available in version 0.12.0. My first draft was one package that ships MAGIC only, meant to be used in both the editor and the player build. The downside of course was that it was slow, because changing a Clojure source file required a full AOT compilation and reset the scene on every change.

So then, since the Unity team was using a fork of ClojureCLR 1.11 in the editor, I generated a second package variant whose MAGIC DLLs carry a !UNITY_EDITOR constraint, so the editor never loads them and their ClojureCLR DLLs work as usual.

This was not ideal of course, so a colleague of mine looked into packaging both runtimes while letting the Unity consumer project choose which one the editor loads. The solution was a single scripting define symbol, MAGIC_RUNTIME_IN_EDITOR, whose constraint applies to the MAGIC DLLs, the ClojureCLR DLLs, and, through a reconcile pass after each domain reload, to any ported Clojure libs present under Assets/Plugins.

We actually had to fork ClojureCLR to fix a few bugs, for reasons I detail further down this article.

The consumer setup is documented in docs/unity-integration.md.

8. Test each version of MAGIC on 30+ repos

My goal was to be able to compile with MAGIC all the libs our Unity gaming platform depends on, without custom forks carrying workarounds just to make them compile. I wanted to be able to use David Miller's CLR ports right away (as long as they use no features above Clojure 1.10), and to run rich comment tests (RCT) on the CLR, since most of our recent internal libraries use them for unit tests.

rct-clr: rich comment tests on the CLR

rich-comment-tests puts a function's example calls and their expected results in a (comment ...) block and runs them as assertions, keeping the documentation and the tests as one thing. The problem is that the library relies heavily on the JVM, so it is not easy to port with just interop. So my colleague Parth had the idea to extract the assertions on the JVM and emit a plain .cljc file of ordinary deftests, which nostrand can run. That became rct-clr: the (comment ...) blocks stay the single source of truth, and the CLR runs the very same assertions as the JVM, just in a generated file of deftests instead of via the RCT runner.

magic-conformance: does it still build under MAGIC?

While working on MAGIC, I want to be sure that all the libs our gaming platform depends on compile and test OK on the latest MAGIC release. So I wanted an easy way to rebuild and re-run the tests of each of our libs in these two scenarios:

  • the MAGIC version got bumped: I want to be sure there is no regression
  • a lib SHA moved: I want to be sure it still compiles with MAGIC

magic-conformance is a runner that reads a manifest of libraries and, for each, clones it and runs its nos build and nos test on the ci-clj-clr image, which carries the JVM, MAGIC, and ClojureCLR toolchains. A manifest entry can also carry an inline magic.edn or deps-clr.edn, which the runner writes into the clone when the library ships none of its own.

flowchart TD
    L["manifest<br/>(the libraries)"] --> R["conformance run"]
    R -->|per library| S{"commit + MAGIC version<br/>+ config unchanged?"}
    S -->|yes| K["reuse cached result"]
    S -->|no| A["clone, inject config"] --> B["nos build + nos test<br/>+ cljr -X:test when declared"]
    B --> W[("results")]
    K --> W

The public repo ships the runner with a small green example manifest, a few public libraries such as fun-map that build under MAGIC straight from upstream. Internally, we run magic-conformance on around 30 repos to be sure they compile with both MAGIC and our fork of ClojureCLR 1.11.

It is not every open source library being recompiled, like Rust does with its Crater, but that is a good start!

Our fork of clojure-clr

To recap our setup, we actually need three compilers:

  • JVM Clojure on the server
  • ClojureCLR in the Unity editor
  • MAGIC in the Unity player build

In one MAGIC release, I shipped a fix for a bug where datafied class names came out in their short form (String) where the JVM uses the fully qualified name (System.String). JVM and MAGIC tests were green, so a consumer lib deleted its own datafy workaround. Then a Unity dev reported that their editor broke: ClojureCLR has the same datafy bug, and ClojureCLR is what runs in the editor, so the editor hit the bug the workaround had been hiding, while player builds stayed green. That is when I realised that for this dual CLR compile workflow to work in Unity, I needed to guarantee similar behaviour from both compilers.

So I forked ClojureCLR 1.11 (the version the client was using) into flybot-sg/clojure-clr, and with my colleague we added a few things:

  • the post-1.11 backports the cljr CLI needs to run
  • later fixes David Miller shipped upstream, backported to 1.11
  • our own fixes (including the datafy fix mentioned above), found while running ClojureCLR next to MAGIC

I also added ClojureCLR to our ci-clj-clr image, so clients can run their backend libs with cljr and my conformance check can run all our libs with cljr as well.

9. Documentation

I tried to have an LLM generate the docs for me, and it was bad. So I did almost all of it manually first, and let the LLM fix the usual typos and generate the Mermaid diagrams, because diagrams help me understand. So the doc is written for humans and understood by LLMs.

Document What it covers
docs/ why MAGIC exists, porting a library, cross-platform .cljc, and the Unity integration
Component READMEs what each piece is, with the Clojure version, runtimes, and Unity version it is tested against
CHANGELOG one entry per release, every issue it closes (including the upstream nasser/* numbers)

Internally, I made a Claude Code plugin with skills to port a lib to the CLR with both MAGIC and ClojureCLR. I have not made it public yet, because I am aware of the skepticism of some in the Clojure community about LLMs, and I am still polishing it anyway. The skills mainly refer to the docs of the magic repo, so it should be easy for anybody to write their own. ClojureCLR is the reference compiler and the most up to date with upstream Clojure, so when I port an open source lib to the CLR, I use cljr and not nos. For people who want to run their Clojure lib in Unity, I advise using our ci-clj-clr image and running the tests on both compilers.

What is next

The next real effort is dropping Mono for CoreCLR, which Unity is moving to and which Nostrand still predates. We also plan on porting Clojure 1.11.

Permalink

Drift Checks for a Self-Hosting Compiler

Why bother making it deterministic

MAGIC is a compiler. It turns Clojure into .NET so Clojure can run in Unity, even on iOS. To do its job, it commits some of its build outputs straight into the repo, including the compiler's own compiled files. You can read more about it here Making Magic stable. At first the compilation was not deterministic, so a small change to the compiler would generate different DLLs during bootstrap which made the byte diff impossible. Technically it is fine, but having the compiler deterministic unlocks some interesting features such as doing the drift check via a byte diff directly and being able to see at a glance which DLLs are impacted by a change.

What led to that decision

I wanted to always have a pair of commits: the source change and the bootstrap containing only the DLLs impacted. But since the compiler was not deterministic and all DLLs had moved, I was handpicking the ones I thought were impacted based on what the source change was. That works fine when it is, say, a Clojure fix in the Clojure compiler, but it is very hard to "guess" when it is a runtime change that could subtly impact a lot of DLLs.

In bc629a67 I changed how the C# runtime hashes strings and maps to match JVM Clojure. The change reached binaries I never considered, 17 stdlib ones affected indirectly through hashes baked into their compiled bytes, and since their .clj sources never moved, nothing flagged them. The code compiled, the tests passed, the review was green. Seven weeks later every clojure.spec.alpha regex op threw No matching clause (#40), and the trail led back to that hash change; b70ac965 is the patch that refreshed the binaries my selection had missed.

As I was explaining this to my colleague, he mentioned very casually "why not make the compiler deterministic then". And this is how I decided to look into it.

Why the compiler commits its own output

A compiler turns source code into something else, and MAGIC turns Clojure source into compiled .dll files (a .dll is the .NET unit of compiled code). Other compilers rebuild those files on every build, so they always match the source. MAGIC cannot do that, because the compiler is built from its own previous output. It compiles itself, which means its self-hosted .clj.dlls have to be committed and then reused to build the next version (the C# parts rebuild like any project though). It is called bootstrapping.

A committed file is a snapshot of its source at one moment in time. Edit the source without rebuilding, and that snapshot quietly becomes a lie, a drift. And catching this drift is almost trivial with determinism: just a byte diff.

Drift check attempt before determinism

Before determinism, comparing the binaries was off the table: every rebuild changed every DLL's bytes, so a diff against the committed ones would fail on every commit and prove nothing. What I had instead were two workarounds, one for picking the DLLs to commit and one for catching a forgotten rebuild. Let's look at these workarounds first.

Claude Code picked the DLLs to commit

Since every rebuild left every committed DLL modified, choosing which ones carried a real change was a judgment call, and I delegated the judgment to Claude Code: given the source change, reason about which binaries it can reach, commit only those, revert the rest. It was impressively good at this. For the :extend-via-metadata port (2af529c5), it refreshed core_deftype, clojure.core.protocols and the C# Clojure.dll in one paired commit, three artifacts for one logical change. Even the stdlib miss from earlier was very likely my fault, not its analysis: I did not suspect that the stdlib DLLs could be affected by the C# change, so my prompt never put them in scope.

But I could not trust the LLM to always be right. There was no tool that could have caught a wrong pick. Plus, it is bad software engineering practice to rely on an LLM for a check that must be deterministic. An LLM should help us write the deterministic tooling, not be the tooling. In our example, we should use Claude Code to help us find how to make the compilation deterministic, so drift is caught by a predictable CI pipeline instead of a judgment call.

A manifest of source fingerprints

The second workaround guarded against forgetting the rebuild entirely. A committed manifest maps each binary to a fingerprint of its .clj source (a SHA hash, a short string that changes whenever the file changes). Back then it was two files, stdlib-manifest.edn and bootstrap-manifest.edn; determinism later merged them into one dll-sources.edn. The entry for clojure.core looked like this:

clojure.core {:source "magic-compiler/src/stdlib/clojure/core.clj"
              :sha256 "33435bc12bcc893ebe91319b5cefa21aa6cfb31254b5269c4cc687feedebb1d2"}

I had a refresh task that rewrote the manifest, in the same run that rebuilt the DLLs. CI then re-hashed every source and compared against it. It answers only one question: does this source change come with its updated DLL? It is not much, but at least it forces the developer to run the bootstrap and commit its output.

What neither workaround could prove

Neither workaround ever looked at the committed bytes. The hasheq incident is the proof: a case over keywords bakes each key's hash into a jump table inside the DLL, so a C# change to hashing invalidates binaries whose .clj sources never moved, and no source hash can see it. The diagram below shows the blind spot:

flowchart TD
    SRC["Source .clj"] --> BIN["Compiled binary"]
    RT["Runtime hashing, in C#"] --> BIN
    SRC -->|watched| FP["Fingerprint says: in sync ✅"]
    RT -->|not watched| GAP["Blind spot ❌<br/>runtime changed, source did not"]

The honest fix is a byte diff of the binaries themselves, and that is impossible while the compiler is non-deterministic. So the real work became removing the non-determinism.

Making the build deterministic

Deterministic here means one thing: identical inputs produce identical bytes. Five things stood in the way, each found the same way, by compiling the same namespace twice (and later, on two different machines) and diffing the disassembled output. The full site-by-site inventory is in docs/deterministic-compilation.md; here is what each one was.

Class member order

Take one small form:

(reify
  System.IDisposable (Dispose [_] ...)
  Object             (ToString [_] ...))

Compile it twice in a row and diff the disassembly of the generated class:

 .class private '<magic>user$reify__0'
-  .method public Dispose  ...   ; body at IL offset 0x2050
-  .method public ToString ...   ; body at 0x20c4
+  .method public ToString ...   ; body at 0x2050
+  .method public Dispose  ...   ; body at 0x20c4

Both DLLs hold the same two methods, but the order the class members are emitted in decides where each body lands in the file, so one swapped pair shifts every byte after it and the two files diff from that point on.

What happens is that each method in a reify implements a method that already exists on an interface or base class, so the compiler pairs every body with the host method it implements, in a map keyed by that method's MethodInfo object.

A MethodInfo has no value to hash by, so its hash code is just an arbitrary number the runtime assigns to that object per process; same method, new process, new number, so the map iterates in a different order in every process. Maps keyed by Type or Var objects have the same problem, and this was not one bug but a pattern: five places in the emitter iterated collections that way. Each now sorts its entries by a key built from the content itself. For a method it hashes the full signature string, so every process emits in the same order. The doc linked above lists all five.

String sort order

The CLR's default string compare does not behave the same across OSes. String.CompareTo does not compare character codes: it asks the platform's collation library how the user's language orders these words, Windows' NLS or ICU on macOS and Linux, each machine shipping its own version of the rules. Collation is linguistic (it files "a" before "B", where a code-unit compare puts every uppercase letter first), and which rules run, in which version, depends on the machine, so the same sort could emit different bytes on macOS and Linux. Every sort in the emitter now compares ordinally, raw UTF-16 units via String.CompareOrdinal, which is arithmetic and therefore the same everywhere.

The CLR does offer the ordinal compare, but only as an explicit opt-in at each call site, and that is the trap: the bare String.CompareTo is what IComparable binds, so every generic path, a default comparer, a plain sort, Clojure's own compare, silently gets the culture-sensitive one. There is not even a parameter to forget, because generic code offers no place to pass it. Most languages put the defaults the other way around, the JVM included: Java's String.compareTo is defined as code-unit comparison, and locale-aware sorting is the explicit opt-in. So the emitter carries its own ordinal comparator, and the fix simply mirrors the JVM compare behavior.

Source paths

Every def bakes its source's :file path into metadata, and it used to be the absolute path, so the bytes depended on where the repo was cloned. It is now the load-relative path, clojure/zip.clj instead of /home/me/.../clojure/zip.clj for instance, matching JVM Clojure. Old committed DLLs could be dated by this alone: some still carried Ramsey's /home/nasser/... paths in their bytes from years ago.

Generated names

Every anonymous fn compiles to a generated type, and those types are numbered by process-global counters, gensym included. Everything nos does before compiling your file consumes them: booting compiles nostrand's own Clojure in memory, resolving dependencies runs more. So any edit to that prelude shifted every number that every later file baked.

For example, one edit to nostrand/core.clj renumbered every committed stdlib DLL, the only difference in each being __49 becoming __50. The counters now reset at each file-writing compile. So an emitted name is a function of the namespace and the toolchain alone; REPL evals keep the process-global counter and its uniqueness guarantee. The diagram below shows both regimes:

%% mermaid lays disconnected subgraphs out perpendicular to the parent
%% direction, so "after" is declared first to render "before" on the left
flowchart TD
    subgraph after["after"]
        direction TB
        a1["one fn added early"] --> a2["every file starts numbering<br/>from the same reset value"]
        a2 --> a3["only DLLs whose own<br/>source changed differ"]
    end
    subgraph before["before"]
        direction TB
        b1["one fn added early"] --> b2["the shared counter is one<br/>ahead from there on"]
        b2 --> b3["every DLL compiled after it<br/>changes: __49 becomes __50"]
    end

When I described the reset to Ramsey Nasser, he immediately said it could collide, without looking at the code. He was right: a process-global counter guarantees a gensym name is fresh for the whole process, and resetting per file lets two files mint the same one. Equal names are harmless where they are scoped, and locals and type names both are, but a namespace split across files whose macro defs a gensym-named var is not:

;; main.clj
(defmacro defslot [reader v]
  (let [g (gensym "slot")]
    `(do (def ~g ~v) (defn ~reader [] ~g))))

(load "part_a")   ;; (defslot read-a :from-a)
(load "part_b")   ;; (defslot read-b :from-b)

Each sub-file compiles as its own unit, so both reset to the same value, both mint slot10001, and part_b's def silently shadows part_a's: (read-a) returns :from-b. No stdlib or compiler macro writes that pattern, so nothing in the tree hits it, but MAGIC compiles anyone's code, and the trigger is ordinary: a plain nos build of a namespace split across (load ...) files is enough, no special driver. The possible fix would be to seed the counter per file, or to fail the build when a gensym-shaped var is redefined across files of one namespace. It's a niche case but I will address it.

Assembly ID and timestamp

Two values in the emitted file say nothing about the code:

  • the build timestamp
  • MVID, a GUID identifying the module that gets randomized on every run.

Reflection.Emit offers no option to control either, where the C# compiler has -deterministic, so the only way is to patch the bytes ourselves once the assembly is saved. The timestamp is easy, a fixed offset in the PE header, the container format .NET assemblies use. The MVID is not in that header at all: it sits in the metadata #GUID heap, so reaching it means walking the section table and the stream headers by hand.

Then the trick, in the arrows below: a file cannot contain the hash of itself, so both fields are cleared first, and the hash of that cleared file becomes the MVID.

flowchart LR
    A["the .clj.dll as saved"]
    B["timestamp and MVID zeroed"]
    C["the .clj.dll rewritten in place"]
    A -->|"clear both fields"| B
    B -->|"SHA256 the whole file,<br/>write it into the MVID slot"| C

Together, the bytes are a function of the source and the toolchain, nothing else. A Linux container running mono 6.12 rebuilds the 73 committed compiler and stdlib binaries byte-for-byte from a tree committed on macOS with mono 6.14.

The first real audit of the now-comparable bytes also settled how much the old checks had missed: it found seven committed DLLs that no build flow ever rebuilt, the oldest untouched since 2020, one of them from a source file that could no longer compile at all (#45). Five years of green checks, and no judgment, no review, and no hash had any way to see it. The byte-diff did, on its first run.

With that, the special case for compiled binaries disappears: the check rebuilds everything and byte-compares the lot, binaries included.

What the check does today

All of it runs from a single command, bb check-drift, on every change in GitHub CI, right after a fresh build. It regenerates the C# call sites MAGIC pre-generates for IL2CPP (committed .g.cs files, plain text, so these were always byte-comparable), recompiles and redeploys the stdlib binaries, syncs the Unity version, re-authors the Unity package's runtime-selection constraints, then asks git one simple question: did any tracked file change?

Because the build is deterministic, that question now covers the committed binaries too, byte for byte (the two runtime DLLs that embed a git-derived version stamp are restored from the commit instead, since their bytes change with every commit by design).

If the answer is yes, something was not refreshed, and the build fails, listing exactly what drifted.

flowchart TD
    Start["bb check-drift<br/>(after a fresh bb build)"] --> S1["Regenerate the C# from templates"]
    S1 --> S2["Recompile + redeploy the stdlib binaries"]
    S2 --> S3["Sync the Unity version number"]
    S3 --> S4["Re-author the Unity DLLs'<br/>runtime-selection constraints"]
    S4 --> Q{"Did any tracked<br/>file change?<br/>(binaries byte-compared)"}

The fix and its binary travel together

The convention I used from the start is that for changes in the monorepo requiring new DLLs, I push a pair of commits: one for the source change and one for the bootstrap.

* 8c9a0d7e - fix(compiler): resolve inherited interface properties via interface walk
* 5da7deff - chore(bootstrap): refresh analyze-host-forms DLL for inherited interface property fix
* 7b639c7b - fix(compiler): resolve proxy-super base type from enclosing proxy
* 8ae40888 - chore(bootstrap): refresh typed-passes DLL for proxy-super shadowed-this fix

It is just a convention, both could go in the same commit really, but some commits do not affect the DLLs at all (updating the docs, a bb task, and so on) so I find it clearer to have two separate commits when a dll moves. Sometimes a third one follows, adding the smoke test that guarantees the fix under IL2CPP.

The payoff

The result is that we can see exactly which DLLs a source change affects, whether it came from the Clojure compiler or the C# runtime.

On top of that, a byte diff is enough as a drift check in CI to catch any missing bootstrap.

And we get usable versioning on the DLLs themselves. Since only the DLLs a change really touched ever move, git log on one of them lists the commits that actually changed it. So when a DLL misbehaves, the last commit that touched it, and the source commit paired with it, is the change that caused it.

Permalink

August 2026 Short Term Project Updates

Here are August’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 for the awesome work!

Clojure LLM: Dragan Djuric
Gloat and Glojure: Ingy dot Net
PluMCP: Shantanu Kumar


Clojure LLM: Dragan Djuric

Q2 2026 Final Report 3. Published August 1, 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!

TLDR results after 2006 Q2 funding

At the very end, i am quite pleased what I achieved for this first version. The project delivered on all proposed bullets (with caveats, of course, but still :)

iLLaManati:

  • does work! I implemented model-agnostic code, and tested it with both official-ish Gemma 3 variants available on Hugging Face (onnx-community and onnxruntime)
  • is very fast. On my 7 year old computer I get 16 tokens/second on the CPU (Intel i7-9900X), and on a very old GPU Nvidia RTX 2080Ti, 80 + tokens/second. I had not had time to do detailed benchmarks and apples to apples comparisons with established frameworks, but this is similar to the speed that llamma.cpp achieves on my machine. So, surprisingly, not bad at all, given that it runs on ONNX Runtime, which is not very well adapted to running LLM models!
  • has a very simple API. Practically, no API. There’s just a function for loading the model config, and a couple extra functions that automatically plug the LLM generator and the tokenizer into the core.async or core.async Flow!
  • has a fairly elegant implementation. If you look at https://github.com/uncomplicate/iLLaManati , you’ll see it’s 100% Clojure (yes!). The whole LLM implementation for several different model architectures, and both CPU and GPU engines is less than 1000 lines of Clojure, and 3 supported tokenizers take up 800. And since there’s lots of nitty-gritty low-level code, I can even have ideas how to further squeeze some LOC-s, I just didn’t have time to do it during this funding round.
  • integrates into Clojure ecosystem naturally and seamlessly. What can I tell you, other than the only functions in the public API are the a couple functions that talk through core.async channels. Everything is automatized under the hood!
  • NOT require Clojurists to know anything about CUDA, ONNX, tensors, or linear algebra, to be able to use it. Please see the examples here: https://github.com/uncomplicate/iLLaManati/blob/main/illamanati-onnxrt/test/uncomplicate/illamanati/internal/onnxrt/generator_test.clj That’s haiku-level code!
  • runs on your laptop, server, or cloud; wherever JVM Clojure runs. It’s your choice.
  • be a great low-effort gateway for Clojurists to peek, as users, into high-performance and GPU computing. Although looking at the final result it may seem to you it was walk in the park, I had to conquer many subtle issues with diverse complex technologies. There’s lot of real-world tensor/GPU/CUDA/native interop primers here. Of course, the only reason it’s so elegant is because it leverages the power of Clojure, and many Uncomplicate libraries. Compare that to any other equivalent on the internet.
  • will be a very attractive topic to tell the world about. Unfortunately, it is not yet production ready since ONNX Runtime does not support many of the real world optimizations needed for server use, but I can solve that in the future by implementing backends in whatever LLM runner that offers C++ API! So, I’m optimistic that we’ll have a production-ready systems much earlier than an average Clojurian would be able to afford the proper hardware with enough memory for this kind of software :)

Now, some details about the work during these 3 months, especially the obstacles.

The heart: LLM runner

In the first and second month, I was focused a lot on the hammock, to learn the details of LLM implementations, and also several side quests to related topics. I also accomplished plenty of implementation, especially on the tokenizers and the logits sampler. I also created a LLM runner that worked, sort of, but needed lots of polishing and bugfixing.

In the third month, I concentrated on the implementation. In the first two months, I have already hit a few serious deficiences in ONNX Runtime when it comes to LLM support, so my hopes for a super-great solution were low. Specifically, ONNX Runtime lags when it comes to supporting high-performance implementations of more recent operations used in LLMs (such as those that Gemma 3 uses), and it generally does not expose the internals of such operations even when it supports them, so creating a server production-worthy general LLM with it is practically impossible now.

Too bad, you’d think, but hold your breath: even the state of the art runners, such as Nvidia’s TensorRT is like that, and that’s why Nvidia has a special runner, optimized just for LLM models: TensorRT-LLM. So, it’s not that ONNX Runtime is a bad model runner, it’s that LLM models are so specific that they require a more specialized runners. Bummer, ha? Not at all, since most of the code that I wrote during this project (and the lessons learned) fits perfectly into how LLM model works, and provide the parts of the solution orthogonal to specialized runners such as TensorRT-LLM, or llama.cpp.

So, in the third month I hoped to assemble what could be assembled and get at least a decent implementation. There were MANY obstacles. I hoped to leverage ONNX Runtime vendor execution providers, but it turned out both OpenVINO and TensorRT EPs were riddled with unsupported LLM operations, and heavy builds. I kept helping with upstream builds and bugfixes, but 5 hour builds are the norm there. At the end, I managed to make even these work, but their performance was much worse that stock CPU and CUDA EP so I stayed with stock ONNX Runtime. Also, information was scarce. Sometimes I felt I was the only one actually trying many of these code paths in practice, and I probably was :) Nevermind, these were long hours, but I learned lots of things, and, most importantly, REPL enabled me to debug my way out of most of that technical obstacles, so in the last seconds of the funding period, I managed to put everything into a very nice package. Maybe not suitable for production, but definitely something anyone can try on their machine and learn (it works on the CPU, too!).

The hammock

Lots of reading and thinking. And again.

Tokenizer

It turned out that the tokenizer I was using in the first two months (from Hugging Face, wrapped in Java by DJL) messes up with the CUDA context. It probably took me a week or two of hair-pulling chasing that weird bug, but REPL helped me again. I literally discovered this by eliminating every other possibility.

So, in the third month, I had to integrate another tokenizer, just to be able to work with CUDA. I integrated Sentencepiece, which is Google’s tokenizer, which has been created for Gemma. It’s a lot faster than Hugging Face, but it doesn’t support nearly as many models. But it works with CUDA…

So, now, we have 2 tokenizer integrations (HF and Sentencepiece), and we have my own streaming detokenizer implementation i pure Clojure (it’s fast while still being elegant).

The original superfast token sampler

I further polished this sampler and integrated it into the LLM loop. Although I hoped to, I didn’t have time to write up a scientific article, so I didn’t publish the source of that part yet. The LLM implementation woes took all of my time, so this source will be published as soon as I write that damn article. The CPU model doesn’t have this sampler algorithm implemented yet, so I provided a default logmax for now (it’s in the public part of the Snapdragan project) so people can try the code and the examples.

Miscellaneous

During all this time, whenever LLM models required something that’s not supported in mainstream matrix/tensor backends that I use (cuDNN, cuBLAS, BLAS…), such as long integers in tensors, or float16 in matrices, I created a workaround and implemented that under the hood of Neanderthal and Deep Diamond. Several Uncopmlicate libraries got new features under the hood along the way!

I’ll spare you the details of the C++ compilations and bug hunts. Too many bad memories :) I also don’t have any more strength (just a joke, I’m fine :) to itemize many detailed paths and activities that I had to do to make this work.

But I hope that I mentioned the most important thing: iLLaManati is here, and it’s not all that bad! :)

(Also worth noting: the code is human-written!)


Gloat and Glojure: Ingy dot Net

Q2 2026 Report 2. Published July 31, 2026

Today is the final day of my Q2 2026 Clojurists Together funding cycle for Gloat and Glojure.

This was my original commitment for the grant:

Make Gloat/Glojure binaries smaller and faster. Pass more of the Clojure Compatibility Test Suite. Create tutorial docs for using Gloat in the real world.

The short version is that Glojure now passes every enabled test in the current Clojure Compatibility Test Suite, Gloat’s default AOT binaries are leaner and the Glojure compiler has made major performance gains, a new tutorial series takes you from installation through compiling a binary and using Clojure in a Go project.

Gloat can now be seriously considered as a full replacement for GraalVM native-image when compiling Clojure programs to native binaries!

The second half also took Gloat somewhere I hadn’t imagined at the halfway point: it has become a front end for multiple Clojure compilation engines, including Glojure, let-go, and GraalVM native-image.

Since the Halfway Report

At the halfway point, Gloat was at v0.1.50 and the first upstream Glojure release of this grant had just landed.

Since then Gloat has had 16 more releases and is now at v0.1.67. Glojure has had another 8 releases and is now at v0.7.3.

The most important changes were:

  • A Glojure AOT runtime that Gloat that performs as well as GraalVM
  • Significantly smaller native binaries (as small as 7.5MB so far)
  • A large wave of compiler and runtime performance work
  • Addition of let-go as a second compilation engine
  • Added shared-library support for let-go (which doesn’t support it itself)
  • A GraalVM native-image engine for direct comparison and use
  • Better installation, formatting, coloring, paging, classpath, and REPL UX
  • A new Gloat tutorial series

The Performance Story

My long-term goal for Gloat was to make a common YAML framework for all programming languages which I call YAMLStar.

TL;DR YAMLStar now builds its shared library libyamlstar.so with Gloat instead of GraalVM, and the resulting binary is about half the size and has similar runtime performance. Being back by Gloat means I can release prebuilt binaries for 15 platforms instead of the 4 platforms it was confined to with GraalVM. YAMLStar now delivers identical YAML capabilities (via a Clojure engine) to 32 (and counting…) programming languages!

Happy ending, but until this week I was not confident that Gloat would get there by today.

About 4 or 5 weeks ago Norman Nunley from the let-go project offered to help with YAMLStar by getting let-go’s native lowering to match GraalVM’s performance. The Glojure and let-go projects have been in friendly competition for a while, challenging each other and contributing to each other’s projects. Soon after Norman’s work started, I decided that Gloat could and should do all its automations over let-go as well as Glojure.

Then 2 or 3 weeks ago James Hamlin, Glojure’s original author, came out of hiding and decided to do a major optimization push on Glojure to have it reach this goal the go faster than the Speed of GraalVM!

We all worked together and the result was a major success.

Soon there will be very few reasons to use GraalVM to compile Clojure programs to binaries instead of Gloat.

Smaller Binaries

A happy result of the performance work is that the AOT Go code (and thus the native binaries) became much smaller. Glojure used to produce a 50MB hello world that was about 40x slower than GraalVM’s 28MB hello world.

Now the Glojure hello world is 19MB and both Glojure and GraalVM binaries take 0.007s to run.

The existing Gloat -Xprune extension was also updated to work with the optimized runtime. The prune extension tree-shakes out the unused parts of clojure.core and all the transitive Go dependencies they would normally pull in.

On hello world, -Xprune produces a 7.6MB binary that runs in 0.004s here.

Pretty nice!

Gloat Became a Multi-Engine Tool

Gloat now has an -E / --engine option and a gloat --engines command. The current engine list is:

glj       Glojure (default)

lgvm      let-go bytecode VM
lglvm     let-go native lowering with VM fallback
lgl       let-go native lowering (not yet implemented)

graalvm   GraalVM Native Image (binaries only)

The goal is to have let-go be a replacement engine for all of the things that Gloat does with Glojure. Compiling to binary was easy, but let-go does not yet support shared libraries and I really needed that to try it out with YAMLStar. So I made Gloat add the things let-go was missing, and now it can produce shared libraries from let-go as well. Hopefully soon we’ll get this working in let-go itself.

Gloat+Glojure can convert Clojure to Go source code, but that isn’t yet supported for let-go. It should happen eventually.

I also added a GraalVM engine to Gloat. I was doing a lot of time comparisons between engines and wanted gloat users to be able to do the same. But most people likely aren’t familiar with using GraalVM, and even if they are, they have to install it and set up their environment to use it.

If you have Gloat installed, GraalVM is ready to go with a single command:
gloat -E graalvm hello.clj # produces ./hello binary

Doesn’t get easier than that.

New Tutorials

The final grant commitment was tutorial documentation.

I am finishing a new three-part Gloat tutorial series today:

  1. Introduction and Installation
  2. Compiling your first Glojure binary
  3. Using Clojure in a Go project

I really want to create at least a dozen tutorials because there’s so much cool stuff you can do with Gloat.

If you would like to see something covered as a tutorial, please find me on the Clojurians Slack or open an issue on the Gloat repository and let me know what you let me know what you would like to see.

Looking Back

I am very happy with the results of this grant. I have a working YAMLStar for myself, a Gloat that does away with all the shortcomings of GraalVM and a new community of dialect making friend that Gloat can help to do awesome things with Clojure.

That feels like a very good three months.

Thanks

Thank you to Clojurists Together, and to every person and company that funds it. The grant created the sustained time needed to work through compiler internals, runtime behavior, test suites, release engineering, examples, and documentation as one connected project.

Thank you to James Hamlin for the enormous Glojure optimization effort and for making upstream Glojure such an exciting place to work.

Thank you to Marcin Gasperowicz, Norman Nunley, and the let-go contributors for the ideas, benchmarks, collaboration, and friendly competition.

A special shout-out to Dmitri Sotnikov the author of the Jolt Clojure dialect, for his friendship and daily collaboration. Jolt is Gloat’s next engine target!

And thank you to everyone who tried Gloat, reported a problem, asked a sharp question, or followed these reports.

The grant ends today. The work most definitely does not.

Time to Gloat!



PluMCP: Shantanu Kumar

Q2 2026 Final Report 2. Published Aug. 15, 2026

I am happy to report my final progress on the scope of work for this sponsorship:

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

When I shared my first update of this sponsorship cycle, about half of the work was already complete, although no release had been made yet. The second half focused on completing the remaining pieces, including Task orchestration and the OAuth overhaul, and resulted in several releases covering the overall scope.

MCP 2025-11-25 implementation

The major feature item that received most of its implementation work in the first part was Task orchestration. It was fully completed and released in the second part, alongside a substantial expansion and overhaul of OAuth support. Thanks to these OAuth changes, PluMCP is now ready to move beyond Preview and is available for beta testing.

While the entire work has been tracked under a single pull request at Pull Request #6, with much of the Task orchestration work already reflected in its WIP changelog, there have now been several releases:

Progress in Second part

In the second part, I completed the remaining 4 of 9 major feature changes and 5 of 10 minor feature changes. The completed work includes:

Major Changes completed

  • Add support for OpenID Connect Discovery 1.0 to authorization server discovery
  • Enhance authorization flows with incremental scope consent via WWW-Authenticate
  • Add support for OAuth Client ID Metadata Documents as a recommended client registration mechanism
  • Add support for (potentially long running) tasks to enable tracking durable requests with polling and deferred result retrieval

Minor Changes completed

  • Review Security Best Practices Guidance - add required utility functions
    • Mostly inapplicable as the majority is meant for MCP proxy
    • MCP server Origin header check already implemented
  • Return Input validation errors as Tool Execution Errors rather than Protocol Errors to enable model self-correction
    • Validating “Required tool params” is implemented
    • JSON-Schema validation is pending; Malli-based validation is already available
  • Support polling SSE streams by allowing servers to disconnect at will
    • Already supported by PluMCP through session-level detachment of the SSE stream and server
  • Support polling in GET streams, resumption always via GET regardless of stream origin
    • Polling streams and resumption via GET is already supported
  • Align OAuth 2.0 Protected Resource Metadata discovery with RFC 9728, making WWW-Authenticate header optional with fallback to .well-known endpoint

Current status

Since all planned feature changes are now complete and included in the latest 0.3.0-beta1 release, PluMCP is now open for testing by users. A GA release is planned following beta testing.

Remaining work

Apart from squashing any bugs that arise during the beta testing period, the subsequent 0.3.x releases would focus on internal refactoring and code organization. It is also planned to deprecate some API in sync with changes introduced in the MCP 2026-07-27 spec version.

PluMCP Usage Documentation Enhancement

The PluMCP documentation website has been updated with the following new entries:

  • MCP Server
    • Completion
    • Logging
  • MCP Client
    • Roots
    • Sampling
    • Elicitation

The existing MCP Server → Tools entry has also been updated with a manual tool definition mechanism.

With these changes, the sponsored scope has been completed, and PluMCP 0.3.0 is now entering beta testing ahead of the planned GA release.

Permalink

Debates over AI consciousness are a trap

I think a lot about what the future will look like. I always did but even more so since I had a baby. To me it feels like we’re not in a steady state right now. Things are volatile and changing fast. It feels like this pace of change can’t possibly last forever. But it also seems obvious that LLMs are going to dramatically change the nature of my work, and many types of work, permanently.

I am effectively required to use AI at work now, so for now I’ve decided to accept that reality, remain employed as a software engineer, and learn how to use these new tools well. At this point I can’t see software engineering ever going back to the way it was before coding agents. But finding these tools useful doesn’t resolve any of my reservations about AI adoption more broadly, especially with how the main companies selling it act in society.

If anything, using AI every day has made the tension more obvious to me. I think it&aposs possible for a technology to be genuinely useful when applied well, use it regularly, and still insist its makers be accountable for its externalities. AI may be unusually capable and unpredictable, but unpredictability does not erase the responsibility of the people who build, deploy, profit from, and use it to act with care in society.

Upon closer inspection, they are all calling for the same thing: a view of AI systems as being so advanced and capable that no entity, human or corporate, could possibly be responsible for their actions. … This narrative is gaining traction as AI models become more complex and frontier labs reveal their incapability of containing the agents they’ve built. But we need to be careful not to buy into a carefully crafted fiction at the expense of real human lives.

I agree. I think this notion that increasing the sophistication or unpredictability of a tool somehow absolves its makers of any responsibility for its use is absurd. This is the perpetual fight the tech industry keeps having with governments and communities. Regulation has never been able to keep up with technology, and every time companies release a new capability they deploy it at enormous scale and then argue that because this technology is bigger or more sophisticated or whatever the existing expectations around liability and responsibility no longer apply. But scale and complexity don&apost erase accountability.

If you build a system, decide how it operates, where it deploys, profit from its operation, and have the ability to reduce the harms it causes but simply refuse to do so, you do bear some responsibility for those harms.

The fundamental flaw of framing AI as “conscious” by borrowing the language of neuroscience or animal rights is that it conveniently clouds the issue of what AI is: corporate-built software, with countless billions of dollars in investment behind it and an expectation that countless trillions of dollars in revenue will be generated from it for a few builders and investors.

I think this really gets at one of the core ethical issues with AI and how the companies developing it operate. There are some benefits to AI, but they almost exclusively accrue to a small and elite group of tech industry leaders. The costs and harms, meanwhile, are externalized -- onto creators whose work becomes training material, people subjected to synthetic abuse, workers whose jobs are disrupted, communities absorbing infrastructure and environmental costs, and ordinary people who become unwilling participants in experiments conducted at enormous scale. The asymmetry matters.

If society and average people are expected to tolerate substantial risk and disruption for the sake of pursuing the frontier, there needs to be some commensurate, convincing public benefit. Making a small number of already unimaginably wealthy individuals vastly wealthier is not a good enough reason for the rest of us to take on these risks.

There are currently dozens of cases around the world in which AI companies have been sued for a wide range of abuses. Grieving loved ones, aggrieved creators, and violated individuals have accused companies of willfully enabling self-harm or harm to others, generating child sexual-abuse material and nonconsensual nudes, reproducing copyrighted materials, and provoking psychosis.

Like any problematic technology, it’s really easy to overlook the harms it causes when we are benefiting from it. This isn&apost unique to AI, but it raises important questions around accountability. When a technology has both beneficial and harmful uses, the question becomes how to develop and deploy it responsibly. We should not just accept "we have no such obligation" as an answer.

Systems do not “attack” because they went “rogue” or are “manipulative” or “malicious.” Harms occur because companies were negligent in their rush to sell their products to as many people as possible to meet revenue targets.

I think this is very important distinction. When an AI system causes harm, describing the model itself as "malicious", "deceptive", "rogue", or "out of control" obfuscates the chain of human decisions that put it in a position to cause that harm in the first place. People built the model, trained it, set up the safeguards, and chose the conditions under which it got released. A small group of humans made deliberate decisions on behalf of everyone else about what level of risk was acceptable.

Responsibility can be distributed but it cannot be abdicated. Otherwise we end up with an untenable accountability vacuum where the more powerful and autonomous a technology becomes, the less responsibility its creators bear for deploying it. That is obviously backwards.

Discussing AI in anthropomorphic terms is a trap, distorting a legal system intended to protect us into one that protects corporate interests at the cost of countless human lives.

I think this is at least one small thing we can do to steer the conversation in the right direction. Language matters. It&aposs easy to slide into describing agentic systems in anthropomorphic terms, but we have to draw a line where that metaphor starts implying legal and moral consequences. AI is a technology, a tool. Not a person, not an independent actor with agency or an entity separate from the person using it. It is built, owned, operated, and deployed by people.

We have to hold those wielding this tool accountable for the harm they cause or we will be living in a world where those with access to it are free to act with impunity destroying the lives of those who don’t.

Permalink

cljrs - first steps

Notes

Installs cljrs (needs Rust to be installed first)

$ cargo install cljrs

Start repl

$ cljrs repl

Run a .cljrs file

$ cljrs run somefile.cljrs

Start nrepl

$ cljrs nrepl

Clojurust project structure

myproj/
├── cljrs.edn
└── src/
    └── myproj/
        └── core.cljrs

Start nrepl

$ cljrs nrepl

Compile project

$ cljrs compile --src-path ./src --main cljrs-project.core --out ./target/hello_cljrs_project

Code

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.