CIDER 2.0: Sky is the Limit

Two weeks ago I wrote that CIDER 2.0 was brewing. Today the brew is ready - CIDER 2.0 (“Terceira”)1 is officially out! I promised the release would follow the preview within a week or two if nothing serious surfaced, and for once in my life I’m actually on schedule.

The preview post covered the big themes in detail - the transient menus, the call-graph browsers, cider-macrostep, the revamped tracing and enlighten, the ClojureScript improvements - so I won’t rehash all of that here. Instead I’ll focus on what changed between the preview and the release, and on the bigger picture of what CIDER 2.0 is actually about.

What CIDER 2.0 is about

Looking back at the (enormous) changelog, the release boils down to four themes:

  • Tackle some ambitious ideas that had been lying dormant for ages - inline macro stepping, rich (content-type) results, source-aware cross-referencing. Some of the issues closed by this release were filed the better part of a decade ago.
  • Polish the “understand your code” toolbox - the debugger, the macroexpansion facilities, tracing, enlighten, the stacktraces and the cross-references all got a serious amount of love.
  • Make the whole CIDER experience more consistent and discoverable - transient menus everywhere, one tree-view widget shared by all the browsers, and a naming cleanup that brought a bunch of stragglers in line.
  • Fix old annoyances - the friendly-session complexity that 1.22 started taming (remember the redisplay lag fix and default sessions?), the find-references gaps, the flaky SSH tunnels, the confused stdin handling.

Notice what’s not on that list - a pile of shiny new features. There are a few genuinely new things in 2.0, of course, but the heart of this release is that most of CIDER’s important features got overhauled (tastefully, I hope) or made more robust and faster. After 14 years you accumulate a lot of good ideas with rough edges; 2.0 is me going over them with fine-grit sandpaper.

What landed after the preview

Quite a lot, as it turns out - the last two weeks were busy. The headliners:

  • Rich results are now on by default. Evaluate something that returns an image and it renders inline; a result that points to external content (a file, a URL) gets a [show content] button that fetches it only when you press it. HTML renders as formatted text, URLs are clickable. This works for regular C-x C-e-style evaluations too, not just in the REPL (configurable via cider-eval-rich-content-destination). Fun fact: content-type support was added way back in 0.17, disabled in 0.25 after it got a bit overzealous with the fetching, and the interactive-eval part was requested in 2018. Better late than never, right?
  • The transient story got finished. The debugger and the inspector now have menus of their own (? and m respectively), and many menus grew argument flags - pick a pretty-printer per invocation, set test selectors once and reuse them across runs, toggle the refresh modes, pass aliases at jack-in. As before, your muscle memory is safe - the menus only help when you pause.
  • There’s a new cider-doctor command that checks your Emacs setup and your active session for common problems (version mismatches, stale byte-code, leftover obsolete config) and produces a copy-pasteable report. My hope is that it will make “CIDER doesn’t work” bug reports a thing of the past - or at least give us something to look at when they arrive.
  • Pending evaluations now show an animated spinner overlay right at the form you’re evaluating, instead of a spinner in the mode-line of a REPL buffer you probably can’t even see.
  • The debugger got dusted off properly: quitting a debug session finally restores point to where you started - an issue filed in 2016 - the force-step-out key works again, and cider-nrepl 0.62 fixed a batch of instrumentation bugs (records surviving instrumentation, clear errors for forms too big to instrument, and a few crashes).
  • Stdin handling got a long overdue overhaul - input prompts are routed to the session that actually asked for input, cancelling a prompt now interrupts the evaluation (instead of quietly letting it continue), and C-c C-d sends EOF for code that reads until end of input.
  • Clicking a stack frame for a top-level anonymous function now jumps to the actual source instead of clojure.core/fn - a bug from 2020 - and ClojureScript frames render their ns/fn properly.
  • A big consistency pass over the options: the REPL history browser is now cider-history, the inline-result options became a coherent cider-eval-result-* family, and the six per-buffer auto-select options collapsed into a single cider-auto-select-buffer. Every old name keeps working as an obsolete alias, so nothing breaks.
  • And a long tail of robustness work - a slow memory leak on the eldoc/completion path, cider-classpath on Windows, formatting no longer corrupting multi-line strings, theme-aware colors for the nREPL message log, and plenty more of the same ilk.

The documentation also got restructured to be more approachable - there’s a proper quickstart now, a keybindings reference page, dedicated pages on using CIDER alongside clojure-lsp and clojure-ts-mode, and a guide for full-stack Clojure + ClojureScript projects. The manual has grown organically for over a decade, and it showed; hopefully finding things is much easier now.

Upgrading

Despite the big scary version number, upgrading should be uneventful. All the renames ship with obsolete aliases, the transient menus preserve the classic keybindings, and the only removals are commands that had been no-ops for years. The one bit of muscle memory you may need to adjust: cider-macroexpand-all moved from C-c M-m to C-c M-m a, as C-c M-m is now a prefix for all the macroexpansion commands. If anything feels off after the upgrade, M-x cider-doctor is your friend.

Fourteen years later

CIDER 0.1 (well, nrepl.el 0.1) was released on July 10th, 2012 - fourteen years (and five days) ago.2 I’ve been reflecting on this a lot lately. Fourteen years is an eternity in our line of work - entire ecosystems have come and gone in that time - and yet here we are, still innovating, still improving, still moving forward. I dare say CIDER 2.0 is the strongest release in the project’s history, and it’s certainly the one I’ve enjoyed working on the most.3

None of this would have been possible without the people and organizations who have supported the project over the years - everyone who contributed code, reported issues, wrote about CIDER, answered questions, or backed the project financially. A special thanks to Clojurists Together for their long-standing support, and to everyone who took the snapshot for a spin after the preview post and shared feedback - several rough edges got filed down because of you.

So, go play with CIDER 2.0! Kick the tires, explore the menus, crack open some values in the inspector, step through a macro or two. And if CIDER makes your work a little nicer every day, consider supporting its future development

  • that’s what keeps CIDER and friends going.

Where to from here? The sky is the limit. The REPL is the inspiration. The best is always yet to come…

Keep hacking!

  1. Continuing the Azores naming streak started by 1.22 (“São Miguel”). “Terceira” literally means “the third” in Portuguese, which is a slightly confusing name for a 2.0 release, but naming things has never been my strong suit. 

  2. The full origin story is in CIDER Turns 10, if you’re curious how a prototype hacked on a flight to San Francisco ended up here. 

  3. That I can remember. My memory is not what it used to be. 

Permalink

Statistics made simple

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

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

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

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

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

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

Existing options

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

What is going on?

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

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

My solution

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

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

Setup

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

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

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

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

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

Request types

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

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

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

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

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

Feedbin feed-id:1373711 - 142 subscribers

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

Graphs

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

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

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

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

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

Insights

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

Not implemented (yet)

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

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

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

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

One day.

How to get

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

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

Permalink

I am sorry, but everyone is getting syntax highlighting wrong

Translations: Russian

Syntax highlighting is a tool. It can help you read code faster. Find things quicker. Orient yourself in a large file.

Like any tool, it can be used correctly or incorrectly. Let’s see how to use syntax highlighting to help you work.

Christmas Lights Diarrhea

Most color themes have a unique bright color for literally everything: one for variables, another for language keywords, constants, punctuation, functions, classes, calls, comments, etc.

Sometimes it gets so bad one can’t see the base text color: everything is highlighted. What’s the base text color here?

The problem with that is, if everything is highlighted, nothing stands out. Your eye adapts and considers it a new norm: everything is bright and shiny, and instead of getting separated, it all blends together.

Here’s a quick test. Try to find the function definition here:

and here:

See what I mean?

So yeah, unfortunately, you can’t just highlight everything. You have to make decisions: what is more important, what is less. What should stand out, what shouldn’t.

Highlighting everything is like assigning “top priority” to every task in Linear. It only works if most of the tasks have lesser priorities.

If everything is highlighted, nothing is highlighted.

Enough colors to remember

There are two main use-cases you want your color theme to address:

  1. Look at something and tell what it is by its color (you can tell by reading text, yes, but why do you need syntax highlighting then?)
  2. Search for something. You want to know what to look for (which color).

1 is a direct index lookup: color → type of thing.

2 is a reverse lookup: type of thing → color.

Truth is, most people don’t do these lookups at all. They might think they do, but in reality, they don’t.

Let me illustrate. Before:

After:

Can you see it? I misspelled return for retunr and its color switched from red to purple.

I can’t.

Here’s another test. Close your eyes (not yet! Finish this sentence first) and try to remember what color your color theme uses for class names?

Can you?

If the answer for both questions is “no”, then your color theme is not functional. It might give you comfort (as in—I feel safe. If it’s highlighted, it’s probably code) but you can’t use it as a tool. It doesn’t help you.

What’s the solution? Have an absolute minimum of colors. So little that they all fit in your head at once. For example, my color theme, Alabaster, only uses four:

  • Green for strings
  • Purple for constants
  • Yellow for comments
  • Light blue for top-level definitions

That’s it! And I was able to type it all from memory, too. This minimalism allows me to actually do lookups: if I’m looking for a string, I know it will be green. If I’m looking at something yellow, I know it’s a comment.

Limit the number of different colors to what you can remember.

If you swap green and purple in my editor, it’ll be a catastrophe. If somebody swapped colors in yours, would you even notice?

What should you highlight?

Something there isn’t a lot of. Remember—we want highlights to stand out. That’s why I don’t highlight variables or function calls—they are everywhere, your code is probably 75% variable names and function calls.

I do highlight constants (numbers, strings). These are usually used more sparingly and often are reference points—a lot of logic paths start from constants.

Top-level definitions are another good idea. They give you an idea of a structure quickly.

Punctuation: it helps to separate names from syntax a little bit, and you care about names first, especially when quickly scanning code.

Please, please don’t highlight language keywords. class, function, if, elsestuff like this. You rarely look for them: “where’s that if” is a valid question, but you will be looking not at the if the keyword, but at the condition after it. The condition is the important, distinguishing part. The keyword is not.

Highlight names and constants. Grey out punctuation. Don’t highlight language keywords.

Comments are important

The tradition of using grey for comments comes from the times when people were paid by line. If you have something like

of course you would want to grey it out! This is bullshit text that doesn’t add anything and was written to be ignored.

But for good comments, the situation is opposite. Good comments ADD to the code. They explain something that couldn’t be expressed directly. They are important.

So here’s another controversial idea:

Comments should be highlighted, not hidden away.

Use bold colors, draw attention to them. Don’t shy away. If somebody took the time to tell you something, then you want to read it.

Two types of comments

Another secret nobody is talking about is that there are two types of comments:

  1. Explanations
  2. Disabled code

Most languages don’t distinguish between those, so there’s not much you can do syntax-wise. Sometimes there’s a convention (e.g. -- vs /* */ in SQL), then use it!

Here’s a real example from Clojure codebase that makes perfect use of two types of comments:

Disabled code is gray, explanation is bright yellow

Light or dark?

Per statistics, 70% of developers prefer dark themes. Being in the other 30%, that question always puzzled me. Why?

And I think I have an answer. Here’s a typical dark theme:

and here’s a light one:

On the latter one, colors are way less vibrant. Here, I picked them out for you:

Notice how many colors there are. No one can remember that many.

This is because dark colors are in general less distinguishable and more muddy. Look at Hue scale as we move brightness down:

Basically, in the dark part of the spectrum, you just get fewer colors to play with. There’s no “dark yellow” or good-looking “dark teal”.

Nothing can be done here. There are no magic colors hiding somewhere that have both good contrast on a white background and look good at the same time. By choosing a light theme, you are dooming yourself to a very limited, bad-looking, barely distinguishable set of dark colors.

So it makes sense. Dark themes do look better. Or rather: light ones can’t look good. Science ¯\_(ツ)_/¯

But!

But.

There is one trick you can do, that I don’t see a lot of. Use background colors! Compare:

The first one has nice colors, but the contrast is too low: letters become hard to read.

The second one has good contrast, but you can barely see colors.

The last one has both: high contrast and clean, vibrant colors. Lighter colors are readable even on a white background since they fill a lot more area. Text is the same brightness as in the second example, yet it gives the impression of clearer color. It’s all upside, really.

UI designers know about this trick for a while, but I rarely see it applied in code editors:

If your editor supports choosing background color, give it a try. It might open light themes for you.

Bold and italics

Don’t use. This goes into the same category as too many colors. It’s just another way to highlight something, and you don’t need too many, because you can’t highlight everything.

In theory, you might try to replace colors with typography. Would that work? I don’t know. I haven’t seen any examples.

Using italics and bold instead of colors

Myth of number-based perfection

Some themes pay too much attention to be scientifically uniform. Like, all colors have the same exact lightness, and hues are distributed evenly on a circle.

This could be nice (to know if you have OCD), but in practice, it doesn’t work as well as it sounds:

OkLab l=0.7473 c=0.1253 h=0, 45, 90, 135, 180, 225, 270, 315

The idea of highlighting is to make things stand out. If you make all colors the same lightness and chroma, they will look very similar to each other, and it’ll be hard to tell them apart.

Our eyes are way more sensitive to differences in lightness than in color, and we should use it, not try to negate it.

Let’s design a color theme together

Let’s apply these principles step by step and see where it leads us. We start with the theme from the start of this post:

First, let’s remove highlighting from language keywords and re-introduce base text color:

Next, we remove color from variable usage:

and from function/method invocation:

The thinking is that your code is mostly references to variables and method invocation. If we highlight those, we’ll have to highlight more than 75% of your code.

Notice that we’ve kept variable declarations. These are not as ubiquitous and help you quickly answer a common question: where does thing thing come from?

Next, let’s tone down punctuation:

I prefer to dim it a little bit because it helps names stand out more. Names alone can give you the general idea of what’s going on, and the exact configuration of brackets is rarely equally important.

But you might roll with base color punctuation, too:

Okay, getting close. Let’s highlight comments:

We don’t use red here because you usually need it for squiggly lines and errors.

This is still one color too many, so I unify numbers and strings to both use green:

Finally, let’s rotate colors a bit. We want to respect nesting logic, so function declarations should be brighter (yellow) than variable declarations (blue).

Compare with what we started:

In my opinion, we got a much more workable color theme: it’s easier on the eyes and helps you find stuff faster.

Shameless plug time

I’ve been applying these principles for about 8 years now.

I call this theme Alabaster and I’ve built it a couple of times for the editors I used:

It’s also been ported to many other editors and terminals; the most complete list is probably here. If your editor is not on the list, try searching for it by name—it might be built-in already! I always wondered where these color themes come from, and now I became an author of one (and I still don’t know).

Feel free to use Alabaster as is or build your own theme using the principles outlined in the article—either is fine by me.

As for the principles themselves, they worked out fantastically for me. I’ve never wanted to go back, and just one look at any “traditional” color theme gives me a scare now.

I suspect that the only reason we don’t see more restrained color themes is that people never really thought about it. Well, this is your wake-up call. I hope this will inspire people to use color more deliberately and to change the default way we build and use color themes.

Permalink

Lowering the Drawbridge

Drawbridge 0.4 is out! If your reaction is “Draw-what now?”, I can’t really blame you - Drawbridge is easily the most obscure project in the nREPL stable, and it has spent most of its life in a state best described as “technically maintained”. I’ve set out to change that recently, and this post is both a release announcement and the story of a 14-year-old project that never quite lived up to its potential. Hopefully, until now.

What’s Drawbridge, anyway?

In case you’ve never come across it: Drawbridge is an HTTP transport for nREPL, packaged as a humble Ring handler. You mount it in your web application, and suddenly you can REPL into that application over plain HTTP(S) - no open socket, no special network setup, just another route in your app:

(ns my-app.repl
  (:require [drawbridge.core :refer [secure-ring-handler]]
            [ring.adapter.jetty :refer [run-jetty]]))

(defn -main [& _]
  (run-jetty (secure-ring-handler :token (System/getenv "DRAWBRIDGE_TOKEN"))
             {:port 8080 :join? false}))

Chas Emerick created it way back in 2012, in the golden era of Heroku-style PaaS platforms.1 The problem it solved was very real: those platforms gave your application exactly one way in - HTTP on port 80/443 - and nREPL speaks bencode over a raw socket. If you wanted a REPL into your production app (and of course you did, this is Clojure), you needed the REPL to ride on HTTP. Drawbridge was that ride. The name suddenly makes a lot of sense, doesn’t it? It’s the thing you lower to let people into the castle.

When I took over nREPL’s maintenance in 2018, Drawbridge came along as part of the broader ecosystem cleanup and moved to the nrepl GitHub org. It got a couple of releases back then (0.2.0 and 0.2.1), and after that… silence. Seven years of it.

The bridge that never got you across

Truth be told, Drawbridge never really delivered on its promise. Let me try to explain why.

First, security. A Drawbridge endpoint is remote code execution as a service - that’s the whole point - but the project shipped with no authentication story at all. “Bring your own middleware” was the official answer, and the number of people who got that right on the first try was, I suspect, not large. Exposing your production REPL on the public internet with hand-rolled auth is the kind of thing that keeps security teams up at night, and rightfully so.

Second, the tooling never came. CIDER, Calva, and practically every other nREPL client connect over a plain socket with a host and a port. Drawbridge connections only worked through lein repl :connect and hand-written clients - so the audience was limited to people willing to REPL without their editor. That’s a tough sell.

Third, the transport itself was long-polling: the client keeps asking the server “anything for me yet?” over and over. It worked, but it always felt a bit sluggish, and it could flood your server with polling requests in the process.

Given all that, the community sensibly routed around the problem. If you had SSH access to the box, tunneling the nREPL port (ssh -L) was simpler and more secure than anything Drawbridge offered. And nREPL itself eventually grew native TLS support, which covered another chunk of the use cases. Between those two, “secure remote nREPL” mostly stopped meaning “HTTP”.2

But the original niche never went away. Plenty of environments still give you HTTP ingress and nothing else - PaaS platforms, container services, corporate networks where the only thing allowed through the firewall is 443. In those places SSH tunnels and TLS sockets aren’t options, and Drawbridge remains the only game in town. It just needed to stop being a liability there.

Dusting it off

The modernization happened in two waves. A quiet 0.3.0 earlier this year dragged the foundations into the present - current nREPL, Ring and friends, Clojure 1.10+, Java 17+, a real test suite and CI. The fun stuff landed now, in 0.4, and it goes straight after the three problems above - reach, trust and speed.

  • The headline feature is the bridge (drawbridge.bridge) - a small local process that presents a plain nREPL socket on localhost and relays everything to a remote Drawbridge endpoint. This single thing unlocks the entire nREPL ecosystem: CIDER, Calva, rebel-readline, anything that speaks bencode can now reach a Drawbridge endpoint. It even writes a .nrepl-port file, so cider-connect picks it up automatically.
  • Security is finally built in, and it’s opt-out rather than opt-in. secure-ring-handler gives you a complete endpoint with bearer-token authentication (constant-time comparison, naturally) in one form, and it flat-out refuses to create an unauthenticated endpoint unless you explicitly pass :insecure true. Exposing an open REPL should be a deliberate act, not an oversight.
  • There’s a brand-new WebSocket transport that replaces long-polling with real server push - output streams to your REPL the moment it’s produced. The bridge picks it automatically for ws(s):// URLs, and the server pings idle connections so aggressive proxies (looking at you, 55-second router timeouts) don’t sever your session mid-thought.
  • The old HTTP transport is still there as the lowest-common-denominator fallback, and it got some love too - including a fix for a subtle session-affinity race that had been quietly lurking in the client since the beginning.
  • The bridge CLI tries to be a good citizen: it reads the token from DRAWBRIDGE_TOKEN (command-line arguments leak via ps), validates its arguments, has a --help, and when it can’t reach the remote endpoint it tells you why right in your REPL instead of silently dropping the connection.
  • There’s a deps.edn now, so you can run the bridge straight from a git coordinate without installing anything.
  • A couple of ancient issues got closed along the way, including the “Drawbridge clobbers my Ring session” bug that had been open since 2018. (It had actually been fixed for a while - now there’s a regression test proving it.)

The end-to-end experience today looks like this. On the server - the secure-ring-handler example above. On your machine:

$ export DRAWBRIDGE_TOKEN=<token>
$ clojure -Sdeps '{:deps {nrepl/drawbridge {:mvn/version "0.4.0"}}}' \
    -M -m drawbridge.bridge --url https://my-app.example.com/repl

…and then cider-connect (or lein repl :connect http://localhost:7888, or whatever you fancy). Your editor neither knows nor cares that there’s HTTP in the middle.

Where you might actually use it

A few scenarios worth trying:

  • REPL into an app on a PaaS or container platform that only exposes HTTP(S) - the original 2012 use case, finally with your editor along for the ride.
  • Debugging a staging environment from behind a corporate firewall that only lets 443 through.
  • Ops-style poking at a running service where provisioning SSH access would be a bureaucratic adventure, but adding one authenticated route is a code review away.
  • The WebSocket transport within your own infrastructure, simply because server push makes for a much nicer remote REPL than polling ever did.

To be clear - if you do have SSH access or can expose a TLS socket, those remain excellent options, and I’m not here to talk you out of them. Drawbridge is for everywhere they don’t reach.

Feedback, please!

Drawbridge spent so long in hibernation that I’ve genuinely lost track of who still uses it and for what. If you take 0.4 for a spin - or if you looked at it years ago and bounced off - I’d love to hear about it on the issue tracker: what works, what doesn’t, what’s missing. I have some ideas of my own for where to take it next (smarter session handling is high on the list), but I’d much rather steer by real use cases than by my own guesses.

Big thanks to Chas Emerick for building Drawbridge (and nREPL!) in the first place - reviving a well-designed project is a pleasure, even 14 years later. And big thanks to Clojurists Together, Nubank, and all the other organizations and people supporting my Clojure OSS work - none of this would be happening without you.

In the REPL we trust! Keep hacking!

  1. The docstring of nREPL’s url-connect still lists http://your-app-name.heroku.com/repl among its examples, to this very day. 2012 was a different time - deploying to Heroku was the height of fashion and we all thought dynos were the future. 

  2. Fun trivia: Leiningen’s REPL client (REPL-y) has been quietly bundling Drawbridge all these years to make lein repl :connect http://... work. Every Leiningen user has been carrying a copy of Drawbridge 0.2.1 around without knowing it. 

Permalink

Annually-Funded Developers' Update: May & June 2026

Hello Fellow Clojurists!

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


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

Bozhidar Batsov

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

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

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

Below are the details, project by project.

CIDER

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

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

It also fixed a long list of small annoyances: severe editor lag in unlinked buffers, several TRAMP and SSH tunnel problems, request id leaks, and a bunch of broken menu entries.

Right after that I switched the development version to 2.0 and most of the planned work is already done. The headline items so far:

That last one deserves a special mention: evaluation results that are images now render inline out of the box, and file/URL results offer their content on demand, six years after the feature had to be disabled over its safety problems. There was also a big cleanup pass: consolidated configuration options, the REPL history browser renamed to cider-history to end a long-standing naming clash, theme-aware faces instead of hardcoded colors, refreshed docs and a regenerated refcard.

CIDER 2.0 is available from MELPA snapshots and I’d love for more people to take it for a spin before the final release.

cider-nrepl

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

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

Along the way the project’s build was migrated from Leiningen to tools.deps, which required a new MrAnderson release (see the blog posts below).

Orchard

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

  • 0.42.0 and 0.43.0 continued the inspector polish, added symbol classification to orchard.meta, a programmatic listener API for the tracer, and protocol/multimethod introspection in orchard.xref. The project also moved to tools.deps and its CI now covers JDK 26.

Sayid

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

  • 0.2.0 was the big modernization pass: new mx.cider/sayid coordinates, a documented nREPL middleware API, a consolidated op surface (37 ops down to 26) and fixes for the most annoying Emacs client breakages.
  • 0.3.0 followed with usability work: no more frozen Emacs during the reload workflow, simpler query commands and help buffers generated from the keymaps.

port

port is a brand new project I started in May: a minimalist Clojure interactive programming environment for Emacs, built on prepl instead of nREPL. It went from nothing to three releases in the course of the month:

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

I don’t have any particular plans for the future of this project - it was just something I’d wanted to experiment with for a while. I see it as an interesting option for people looking for some middle ground between inf-clojure and CIDER.

neat

neat is the other new arrival: a small, language-agnostic nREPL client for Emacs. 0.1.0 has the essentials in place: a pure-elisp bencode codec, a comint-based REPL, and a source-buffer minor mode with eval, completion, eldoc, xref and doc lookup, tested against Clojure, Babashka and Basilisp. It’s early days, but it’s a nice testbed for exercising the nREPL protocol outside CIDER.

This project also means I’ve dropped any plans to try to make CIDER a language-agnostic development environment. Going forward CIDER will focus only on Clojure-like languages, and everything else will be covered by neat.

Piggieback and Weasel

The nREPL org saw some ClojureScript-flavored action:

  • Piggieback 0.6.2 and Piggieback 0.7.0. The 0.7.0 release makes load-file evaluate the editor’s buffer contents instead of re-reading from disk, tears down ClojureScript REPLs when their sessions close (no more leaked Node processes) and surfaces ClojureScript status in the describe response.
  • Weasel 0.8.0 modernized the WebSocket REPL: the client now uses the platform’s native WebSocket, so it runs in any modern JavaScript runtime (browsers, Node 22+, Deno, Bun, workers), and the minimum requirements moved to Clojure/ClojureScript 1.12.

I also backfilled proper GitHub releases for the historic tags of both projects, so their release history is finally browsable.

Improving the ClojureScript support in CIDER has long been a major objective for me, and these small changes were some initial steps in that direction.

refactor-nrepl and clj-refactor

refactor-nrepl got three releases: 3.12.0, 3.13.0 and 3.14.0, the last one making the AST-based indexing much faster and more reliable. clj-refactor.el received a round of maintenance on master as well, and will get a new release after I wrap up the work on CIDER 2.0.

I’m still pondering the future of both projects, as I plan to move the most useful refactor-nrepl features (those that don’t carry a lot of complexity) to CIDER and cider-nrepl eventually, and I’m not sure the flagship AST-powered refactorings are very competitive these days (compared to clojure-lsp and static project-wide analysis a la clj-kondo in general). I’d certainly appreciate more feedback from the users of clj-refactor on the subject.

Blog posts

I wrote a few articles related to the work above:

What’s next

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

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


Clojure Camp

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

What happened:

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

Plans:

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

Eric Dallo

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

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

ECA

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

eca-context Dallo MayJune 2026 smaller

0.132.0 - 0.144.0

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

ECA clients

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

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

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

clojure-lsp

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

Details about the release below:

2026.07.06-14.34.19

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

Jeaye Wilkerson

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

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

Further optimization work
Beta release preparation

Optimization work

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

Beta release

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

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

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

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

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

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


Michiel Borkent

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

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

To see previous OSS updates, go here.

Sponsors

I’d like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn’t exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!

Updates

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

Babashka Conf 2026 and Dutch Clojure Days

Three years after the initial installment, Babashka Conf 2026 happened on May 8th at the OBA Oosterdok library in Amsterdam, with David Nolen, primary maintainer of ClojureScript, as our keynote speaker. Thanks to our sponsors Nubank, Exoscale, Bob, Flexiana and Itonomi, to Wendy Randolph for hosting, and to all the speakers, volunteers and attendees who made it such an inspiring day. You can watch all the videos here. Thanks to Ray for recording! The day after, Dutch Clojure Days 2026 rounded out a full weekend of Clojure in Amsterdam, where I did a presentation about ClojureScript and async/await. The video of that is hopefully coming soon.

image

Babashka Conf 2026. From left to right: David Nolen, Jen Myers, Adrian Smith, Josh Glover, Rahul Dé, Arne Brasseur, Christoph Neumann, Timo Kramer, Jynn Nelson, Wendy Randolph.

Upcoming: babashka workshop at the Clojure/conj

I’m pleased to announce that Rahul Dé and I will be hosting a babashka workshop at the Clojure Conj 2026. The workshop will showcase various use cases of babashka. This hands-on workshop covers the whole lifecycle of a babashka tool, from a quick script to a published, installable CLI app. We assume you know the basics of Clojure and won’t explain the language itself. Topics include:

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

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

Blog posts

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

and a ClojureScript reference on async functions:

Projects

Babashka CLI got the most attention this cycle. I added automatic --help generation for dispatch-based CLIs and shell tab completion for bash, zsh, fish, PowerShell and Nushell. There’s a dedicated post with a “build your own git” walkthrough linked above. I also made Babashka CLI Squint compatible, so CLIs built with it run on Node.js and in the browser, published as the @babashka/cli npm package. Also ClojureDart support for Babashka CLI got added.

Squint saw a large amount of work that kept going right into early July: a browser nREPL, dynamic vars and binding that survive across separately-compiled ESM modules, an EDN reader, cached lazy seqs, defrecord and a wide set of core protocols, and a big compatibility push to make it pass jank’s clojure-test-suite. Replicant now runs on Squint too. I added key diffing to Reagami and did some benchmarks, showing that Reagami on squint performs in the ballpark of React. The benchmark also shows that Replicant on Squint performs even a tad better than on ClojureScript. Not that this makes a huge difference in practice, but it’s nice to validate the idea that Squint, for typical apps, can be a valid CLJS replacement while not giving up that much in terms of Clojure features.

A security issue in SCI deserves a callout. A string type-hint could bypass the :classes allowlist and statically initialize any class on the classpath at analysis time. If you sandbox untrusted code with SCI, upgrade to 0.13.53. ClojureDart support and fine-grained interop control (which was needed for cljd support since it has no reflection) also got added. You can now make REPLs for your mobile apps!

Since porting was a theme these past months, I’ll mention another one: babashka.fs now runs on Node.js via ClojureScript and squint, published as the @babashka/fs npm package.

Here are some highlights per project. See each project’s CHANGELOG.md for the full list.

  • babashka CLI: Turn Clojure functions into CLIs!

    • Automatic --help generation for dispatch CLIs, plus shell completions for bash, zsh, fish, PowerShell and Nushell (#112, #24, #95). I wrote a full post on it with a “write your own git” walkthrough: babashka CLI: automatic –help and shell completions
    • Exposed the underlying building blocks so you can roll your own custom CLI parsing: parse-opts*, coerce-opts, validate-opts, apply-defaults, table->tree
    • dispatch now accepts a tree directly (as returned by table->tree), and subcommand order is preserved in printed help and completions
    • Squint support and a new @babashka/cli npm package
    • ClojureDart support (#182)
    • opts->table accepts :columns to override the auto-detected columns (#148, thanks Jan Seeger)
    • Better error messages: negation errors now name the base option, --no-foo on a non-boolean option errors instead of silently coercing, and :edn :coerce now requires an explicit value (#166, #174)
    • Thanks to @lread for a lot of documentation review and general maintenance during this cycle
    • Full changelog
  • Squint: CLJS syntax to JS compiler

    • Browser nREPL support landed, followed by a number of REPL/nREPL fixes: #815 (str wrapping tripping esbuild), #819 (macro changes not picked up in watch mode), #820 (:macros option ignored from JS callers) and #832 (nREPL server hanging on advertised-but-unimplemented ops)
    • The CLI now gets its --help, usage and error handling from babashka.cli’s dispatch, plus shell tab completion
    • Dynamic vars and binding now work via a mutable box, safe across separately-compiled ESM modules; syntax-quote resolves symbols through the current namespace and aliases like Clojure. defprotocol got :extend-via-metadata support.
    • reify added
    • clojure.walk added
    • Added squint.edn/clojure.edn with a ~300-line EDN reader
    • Printing is now done through *print-fn*, print, pr and with-out-str, like CLJS
    • Lazy seqs are now cached instead of recomputed on every consumption, matching CLJS’s chunked-seq behavior
    • A big push for compatibility with jank’s clojure-test-suite: dozens of core functions (sorted-map, hash-map, subvec, pop, merge, keys/vals, peek, transducers, = on dates/regexes/lazy seqs, and more) now throw or behave exactly like CLJS instead of the old loose JS semantics, alongside full built-in cljs.test support
    • #771: dead-code elimination for varargs/multi-arity functions, now emitted via ... spread
    • Replicant support landed, with an example
    • Added defrecord, record? and the IRecord marker protocol. Records store their fields as own string-keyed properties and implement the map-facing protocols, so keyword lookup, keys, seq, assoc, conj and = all work through the regular core functions; the generated implementations are shared runtime functions imported only by files that use defrecord
    • Added a large set of core protocols so custom types participate in the standard functions: ILookup, IAssociative, IMap, ICounted, ICollection, IEquiv, ISet, the transient protocols, and IAtom/IDeref/IReset/ISwap/IWatchable (so a reagent-style reactive atom can be a plain deftype)
    • Compile-time namespace resolution: cljs.analyzer.api/resolve now sees vars of built-in library namespaces like clojure.string, plus :squint/compile-time forms and fixes for macro self-use
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

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

    • ClojureDart support, with a Flutter REPL example
    • Instance/static method and field overrides plus a :closed allowlist for :classes, giving fine-grained control over host interop; see the interop control docs. Also 1.6x faster instance-method interop on babashka
    • Security fix (sandbox escape): a string type-hint (e.g. ^"some.Class" x) bypassed the :classes allowlist, loading and static-initializing any class on the classpath at analysis time. Only affects sandboxing of untrusted code via :classes; upgrade to 0.13.53
    • Add an :interrupt-fn option: a zero-arg function called on every interpreted fn entry, so host code can interrupt or cancel a running SCI eval (thanks @whilo)
    • Add sci.interrupt/interrupt! to throw an interrupt that sandboxed try/catch cannot catch, and gate finally and the regex functions (re-matches/re-find/re-seq, JVM) through :interrupt-fn too, closing off ways to mask an interrupt and escape the sandbox #1044
    • Fix copy-var incorrectly marking a function as inlined when its unqualified name collided with a clojure.core/cljs.core inlined var (e.g. a custom get), silently breaking with-redefs (@verberktstan)
    • Fix cross-namespace defrecord/deftype type symbol resolution via alias (e.g. (instance? r/Foo x)), fixing nbb#410
    • Fix a self-require (a namespace requiring itself) being reported as a cyclic load dependency
    • Full changelog
  • fs: file system utility library for Clojure

    • Released 0.5.34 with Node.js support (#265): fs now runs on Node.js via ClojureScript and Squint / JavaScript, published as the @babashka/fs npm package. Most functions are supported. The JVM behavior is the reference implementation so all operations are synchronous, and the glob syntax is reimplemented from scratch to match the JVM. File times are BigInt nanoseconds to preserve sub-millisecond precision. zip is left out since Node.js has no native support for it
    • Added spit and slurp on both the JVM and Node.js
    • exec-paths returns [] when PATH is unset or blank instead of throwing
    • @lread did a thorough review pass making the return values of copy, copy-tree, delete-tree, zip/unzip, gunzip and the setters explicit and documented/tested (#197)
  • Babashka: native, fast starting Clojure interpreter for scripting.

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

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

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

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

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

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

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

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

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

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

Contributions to third party projects:

  • ClojureScript: documented the async/await support from last cycle on the ClojureScript site, including an enhanced reference (#423, #424)
  • Nexus: a data-driven state management library by Christian Johansen. I ported the core engine and test suite to run under squint and added a cljs test runner alongside the existing kaocha setup, so both babashka and squint stay covered in CI (#15, #16, merged)
  • Replicant: a data-driven DOM rendering library by Christian Johansen. I made Replicant itself run under Squint (converting dom.cljs to .cljc, adjusting core.cljc for portability), added babashka/squint test runners and wired them into CI, and fixed a multi-root render bug under squint by switching DOM state tracking to a node-map (#71, #72, merged)

Other projects

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

Click for more details

Permalink

Clojure 1.13.0-alpha4

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

Destructuring changes and additions

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

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

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

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

  • CLJ-2964 :select directive in map destructuring

  • CLJ-2966 :defaults directive in map destructuring

  • CLJ-2967 tests for nested destructuring

Other changes since Clojure 1.13.0-alpha3

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

Try it out

Update your deps.edn :deps with:

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

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

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

Permalink

Dual Duet

Two gigs in one night: not quite a first (the same happened in Kathmandu many years ago), but perhaps the first representing two completely different projects on the same stage. First up is Khyal Geometries with Shama Rahman on sitar, second is The Printer Jam with Evan Raskob on printer, both sets as part of ACM Creativity and Cognition 2026. The former will be live-coded (Max, Node.js, ClojureScript); the latter will be controllerism.

It’s only now that I realise that my carefully crafted blog taxonomy cannot deal with a single blog post that spans more than one project.

Permalink

rab Coding Agent

rab Coding Agent

2026-07-13 - Building a TUI-based coding agent in Rust from scratch - the agentic loop, tool dispatch, what went well and what didn't, and why I went back to pi
Keywords: rust, coding-agent, agentic-loop, tool-calling, llm, tui, clojure, pi

Permalink

KiCad helpers and caliper improvements

Hi friends,

This newsletter we’re streamlining the process of creating circuit boards and revisiting the DIY electronic calipers project from two years ago.

Kevin’s KiCad Helpers

I first started playing around with electronics via Arduino back in 2010, and I designed my first circuit board back in 2015 for my walnut and leather cell phone. Since then, I’ve made around a dozen PCBs, with a notable spike during the pandemic making weird mechanical keyboards.

Averaging roughly one PCB/year is a maximally frustrating frequency: I have 100’s of hours of cumulative experience, but it’s sufficiently spread across the forgetting curve that every time I start something the details are only vaguely familiar and I’ve got to re-orient myself again. It reminds me of filling out my taxes, where I also furiously consult my notes and attempt to interpret them against new versions of the UI where input boxes have been hidden across other forms and new sub-menus.

Anyway, armed with a coding agent, I externalized everything I kept forgetting how to do in a pile of scripts: Kevin’s KiCad Helpers.

This is tailored to my needs — designing PCBs in KiCad 10 to be manufactured by JLCPCB — but if yours are similar you might find ‘em helpful. Even if you don’t make PCBs, it might be good inspiration for how to apply LLMs to sand down rough edges of whatever convoluted infrastructure is required for your projects.

A quick tour of the tools:

DXF import

I specify all of my mechanical stuff — board outlines, mounting hole positions, etc. — in Autodesk Inventor since it has a constraint solver and allows me to directly reference complex geometry driven by other objects.

KiCad’s GUI has a DXF import tool, but it doesn’t have a mechanism to replace already imported geometry, which makes it tedious to iterate.

This script:

  • imports geometry from a DXF file into a board layer specified by the filename (panel_Edge.Cuts.dxf imports a group ID’d “panel” to the Edge Cuts layer)
  • deletes any geometry previously imported with that ID
  • polls the file for changes, so you get “live reload”

I combine this with a similar poll + export-to-file script in my mechanical CAD tool (shown above) to get live syncing to KiCad (shown below):

DXFs map into KiCad’s origin at top-left of the page outline and the DXF positive Y direction points up, so most of my PCBs end up drawn outside of the page ¯\_(ツ)_/¯.

STEP export

I also need the reverse direction: Import a 3D model of a circuit board and its components back into my mechanical CAD program so I can verify clearances, etc. KiCad has native STEP export, but unfortunately some of the parts have extremely detailed 3D models. My M1 Macbook Air running Windows CAD software in a virtual machine does not do well when every pin of every chip is a separate solid body.

This script generates a STEP export where all parts have been replaced with axis aligned bounding boxes:

Parts database

I design all of my boards to be assembled by JLCPCB, so it’s extremely helpful to have a low-latency way to query their parts. This script downloads CDFER’s daily JLCPCB parts sqlite database and consolidates everything into a single table with a numeric price column and lots of indexes so that searching is fast:

I use DB Browser for SQLite but you can of course use whatever interface you like.

It’s also extremely useful to point LLM agents at this database:

My dude, I need an H-bridge that can drive +/- 30 Volts, please query ~/foo/bar/parts.db and give me a table with 5 options for integrated ones showing price / stock / description. Please also make a table showing options for drivers with external transistors. Include links to the datasheets.

JLCPCB (EasyEDA) schematic and footprint import

Shout out again to CDFER for their JLCPCB KiCad Library, which has all of the “basic” jellybean parts and some of the extended ones as well.

For the parts that are not already available in here, I need to import them. Rather than draw them entirely from scratch, I use uPesy’s easyeda2kicad.py.

However I don’t always want to create an entirely new symbol and footprint if the part actually corresponds to something that’s in the KiCad standard library. So my import tool tries to match existing footprints (including 90 degree rotations) and spawns a terminal UI so you can interactively compare potential matches with the EasyEDA footprint:

Schematic analysis

The most interesting helper I’ve created so far is a general schematic analysis framework, which imports the KiCad netlist and schematic instance properties into a DataScript graph database to run various queries/checks.

For example, this lil’ function calculates the capacitance (of all the explicit capacitors, anyway) on a given net:

(defn net-capacitance
  [db net-name]
  (some->> (d/q '{:find  [?ref ?v]
                  :in    [$ ?net]
                  :where [[?n :net/name ?net]
                          [?n :net/nodes ?node]
                          [?node :node/pin ?pin]
                          [?i :instance/pins ?pin]
                          [?i :instance/ref ?ref]
                          [(clojure.string/starts-with? ?ref "C")]
                          [?i :instance/value ?v]]}
                db net-name)
           (keep (comp parse-capacitance second))
           seq
           (reduce + 0.0)))

This function can then be used to check the total capacitance on, e.g., the power nets, and throw an error if it exceeds, e.g., the maximum 10uF allowed by the USB specification.

(defn check-total-capacitance!
  [db]
  (let [rows (->> ["VCC" "VBUS"] ;;TODO: make this configurable
                  (keep (fn [net]
                          (when-let [c (net-capacitance db net)]
                            {:net net :total-uF (format "%.2f" (* c 1e6))}))))]

    (when (seq rows)
      (print "total capacitance:")
      (clojure.pprint/print-table rows)

      (doseq [{:keys [net total-uF]} rows]
        (assert (< (Double/parseDouble total-uF) 10) (str "Net " net " exceeds USB spec 10uF capacitance"))))))

(It’s easy to accidentally exceed this limit if you keep incrementally adding ICs and their recommended bypass capacitors.)

Since KiCad allows one to add arbitrary key/value pairs to schematic instances, it’s easy to check and print other data as well. For example, I record the i2c address(es) of each chip this way (note i2c and max_mA fields in property inspector on left):

Within the schematic text labels I reference using the KiCad text variable format. (E.g., the above Addr: 0x49 label is defined as Addr: ${U1:i2c}.)

The analysis script throws an error if an address maps to multiple chips:

(defn i2c-addresses
  [db]
  (d/q '{:find [?hex-addr (distinct ?ref)]
         :where [[?instance :instance/ref ?ref]
                 [?instance :instance/attributes ?attribute]
                 [?attribute :attribute/name "i2c"]
                 [?attribute :attribute/value ?addrs]
                 [(clojure.core/identity ?addrs) [?addr ...]]
                 [(clojure.core/format "0x%x" ?addr) ?hex-addr]]}
       db))


(defn check-i2c!
  [db]
  (let [refs-by-addr (i2c-addresses db)]
    (when (seq refs-by-addr)
      (print "i2c addresses")
      (clojure.pprint/print-table (sort-by :addr (for [[addr refs] refs-by-addr]
                                                   {:addr addr :refs (clojure.string/join " "  (sort refs))})))

      (doseq [[addr refs] refs-by-addr
              :when (< 1 (count refs))]
        (throw (ex-info (str "Addr " addr " matches multiple refs: " refs)
                        {:addr addr :refs refs})))

      (println ""))))

This also prints out a helpful table of everything on the bus every time I build the project:

| :addr | :refs |
|-------+-------|
|  0x20 |    U3 |
|  0x49 |    U1 |
|  0x60 |    U2 |
|  0x61 |    U2 |
|  0x62 |    U2 |
|  0x63 |    U2 |
|  0x64 |    U2 |
|  0x65 |    U2 |
|  0x66 |    U2 |
|  0x67 |    U2 |
|  0x68 |    U2 |
|  0x69 |    U2 |
|  0x6a |    U2 |
|  0x6b |    U2 |
|  0x6c |    U2 |
|  0x6d |    U2 |
|  0x6e |    U2 |

(In this example, U2 is an LED driver and exposes each channel on its own i2c address, so its i2c property is specified as 0x60..0x6F.)

Check/build scripts

Speaking of builds, most of the functionality described above is packaged up as a script, so you just run kkh build in a folder and it’ll create an output directory next to every *.kicad_pro it finds in any subfolder. The outputs are named with the date, git revision, and also indicate whether there are unstaged changes in the repository working tree:

2026-07-07-73c5c1
├── bom.csv
├── designators.csv
├── netlist.ipc
├── positions.csv
├── receiver-gerbers-2026-07-07-73c5c1.zip
├── receiver.full.step
├── receiver.simplified.step
└── schematics
    ├── receiver-pcb-back.pdf
    ├── receiver-pcb-front.pdf
    └── receiver-sch.pdf

So one command runs DRC, ERC, and custom analysis checks and then builds all of the output files required to place a JLCPCB assembly order.

The build script also exposes the version string as a KiCad variable, so if you add ${KKH_VERSION_DATE} to your PCB silkscreen, the actual version will appear in the output Gerber files.

I hope by making a proper build script I will never again relive the shame of forgetting to run ERC and having a PCB manufactured where I literally forgot to connect some IC pins entirely…

Caliper updates

About two years ago I had my first foray into digital signal processing and made some electronic calipers.

I haven’t touched the project since I published that post, but a few folks recently asked me about it, and since I had some spare LLM credits I figured I’d spend five minutes sending off the little dude at the problem.

And I literally mean five minutes — these are very rough, rambly dictated prompts that I didn’t even bother to edit. I’m sharing here to remind everyone (especially myself!) that not everything needs to be A Big New Project and sometimes you can have great success from a quick, low-effort attempt.

I spawned a Claude session in the project repo, dictated the following prompt, and went to brush my teeth:

This is a project that I worked on a while ago and the accuracy that came out was alright but I’m wondering if there’s anything I can do to improve the accuracy purely from a computational way rather than making new hardware. Be sure to read the blog post mentioned at the top of the read me to get a background

After I brushed my teeth, it had come up with a few plausible sounding ideas, so I replied with the following and went to sleep:

I’d like you to set this up and come up with a few different ideas. These are pretty good ones and what I want you to do is Wire them up so that I can test them out Individually as a series of experiments with a given protocol and I want you to set everything up as much as possible So I can do it Quickly on my end without having to get in and change the code or anything like that so what you can do is you can make a branch and then Have maybe different entry points or something for each of these different improvements and then Write like an overall program or something like that Which I can just run to test through it and then make it interactive I guess so that I can You know start it up in the hardware Let it sit still for a certain amount of time Move it a fixed amount and back and then You know or some protocol like that and then I’ll tell you what I’m done and then we can just run through that program To test out all of the different ideas and potentially combinations of ideas in like a 10 or 15 minute setting and then We’ll figure out from that which ones are working most effectively

Again, I’m really not trying that hard here.

I woke up the next morning and spent about 20 minutes setting up the caliper PCBs and then running the programs it generated to collect new measurement data. It tried the following improvement hypotheses (LLM-generated text):

knob idea it tests
window size longer coherent integration (noise ∝ 1/√N), incl. one 50 Hz line period
mean_sub remove DC so ADC offset drift can’t leak into the phase
hann suppress spectral leakage from non-integer-cycle windows
smoothing averaging in I/Q (phasor) space instead of phase space; EMA vs block
hysteresis the current 0.1 rad dead-band vs smaller vs none

and based on the initial results and a bit more chatting, the agent proposed an improvement that is, of course, completely obvious in hindsight. My initial parameter sweep (two years ago) used a fixed 2kHz spacing. However, the actual signal and sampling timings generated by the microcontroller are driven by integer divisions of a fixed clock frequency — so most of these sampled timings don’t “line up” nicely in terms of an integer number of signal periods.

Thus, there’s always a bit of DC bias in the signal, which causes undesired noise.

With this change, the LLM-generated experimental code reports a noise floor of around 50um, which is about 10x better than what I was getting before. I’m currently on holiday away from my lab, but I expect it’ll take 30 minutes once I get back to code it up myself and validate.

Anyway, the main takeaways for me are:

  1. Write up your work in blog posts so that both people and LLMs can quickly get context and help you out
  2. There’s still plenty of low-hanging fruit out there to pick, and with LLMs it’s extremely cheap to just ask

Misc. stuff

Permalink

Q3 2026 Funding Survey

Greetings Clojurists!

Please take a moment to complete the Q3 2026 Funding Survey which helps inform our Q3 project awards. It is not a heavy lift - maybe 5 minutes of your time. Your input is invaluable! A link to the survey was sent to your email in the last few weeks - and just in case it made its way to spam, you can look for “We Need Your Input - Q3 2026 Funding”. The survey closes midnight PST on July 15, 2026.

Thanks as always for your support of Clojurists Together and for being a part of this awesome community.

Any questions, please email me at kdavis@clojuriststogether.org

Kathy Davis Program Manager Clojurists Together Foundation

Permalink

Destructuring with computed keys

In Clojure you can destructure a map using an arbitrary expression as the key. For example, here kw is a local binding.

(let [kw :key
      {a kw} {:key 1}]
  a)
;=> 1

Usually this syntax is demonstrated as {sym0 :kw0 sym1 :kw1 ...}, which doesn’t reveal that the keywords are actually in expression position, or an evaluation context. The reason why this more recognizable syntax works is because keyword literals are self-evaluating.

(let [{a :key} {:key 1}]
  a)
;=> 1

The basic rule for expanding these expressions is:

(let [{binding expression} map])
=>
(let [binding (get map expression)])

So that code is equivalent to:

(let [a (get {:key 1} :key)]
  a)

Symbols are not self-evaluating syntax in Clojure, so they must be quoted:

(let [{a 'key} {'key 1}]
  a)
;=> 1

Applying the rule makes the need more obvious:

(let [a (get {'key 1} 'key)]
  a)

Destructuring is pleasingly compositional. This ability to drop down to computed keys makes destructuring available in many more situations than if all keys were required to be statically declared. I found a few examples in my own code and other libraries where this flexibility has been useful.

An example that dereferences the var u/expr-type to compute the key:

(let [{cargs :args
       res u/expr-type} (-> expr-noinline
                            ana2/unmark-top-level
                            ana2/unmark-eval-top-level
                            (check-expr expected opts))]

Another example that uses three class literals as computed keys:

(let [r (reflect-u/reflect cls)
      {methods clojure.reflect.Method
       fields clojure.reflect.Field
       ctors clojure.reflect.Constructor
       :as members}
      (group-by
        class
        (filter (fn [{:keys [name] :as m}] 
                  (if constructor-call
                    (instance? clojure.reflect.Constructor m)
                    (= m-or-f name)))
                (:members r)))]

A snippet of code that destructures nested maps using a mix of keywords, quoted symbols and computed vectors-of-locals as keys. Notice that the vectors are in binding position sometimes to introduce names, then in expression position to perform lookups.


(let [...
      {{[x1 x2] 'x} :fv
       {[y1] 'y [z1 z2] 'z} :idx} remap
      {{{[y1_x1 y1_x2] 'x
         [y1_y1 y1_y2 y1_y3 y1_y4] 'y} [y1]
        {[z1_x1] 'x
         [z1_y1] 'y} [y1 z1]
        {[z2_x1] 'x
         [z2_y1] 'y} [y1 z2]} :idx-context} remap]
  (is (= {:fv {'x [x1 x2]}
          :idx {'y [y1]
                'z [z1 z2]}
          :idx-context {[y1] {'x [y1_x1 y1_x2]
                              'y [y1_y1 y1_y2 y1_y3 y1_y4]}
                        [y1 z1] {'x [z1_x1]
                                 'y [z1_y1]}
                        [y1 z2] {'x [z2_x1]
                                 'y [z2_y1]}}}
         remap)))

You’ve probably seen code like this that destructures booleans from a group-by:

(let [...
      {anns false inits true} (group-by list? normalised-bindings)]

This Malli snippet nests :keys destructuring under a local binding key, method:


(-value-transformer [_ schema method options]
  (reduce
   (fn [acc {{:keys [name qname default transformers]} method}]

And this example elegantly destructures a nested map using keywords and locals, supporting the common pattern of updating a nested value in an atom then destructuring the swapped-in value’s relevant parts.


(defn remove-stale-cache-entries
  [nsym ns-form-str sforms slurped opts]
  {:pre [(simple-symbol? nsym)]}
  (when ns-form-str
    (let [{{{forms-cache ns-form-str} nsym} ::check-form-cache}
          (env/swap-checker!
            (env/checker opts)
            update-in
            [::check-form-cache nsym]
            (fn [m]
              (some-> m
                      (select-keys [ns-form-str])
                      not-empty
                      (update ns-form-str select-keys sforms))))]

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.