Posts in Clojure (20 found)
(think) 3 weeks ago

Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness

It’s been a couple of months since the last neocaml release, and the reason is simple — for a while there I was genuinely out of ideas. Back when I shipped 0.6 I declared (again!) that I was done with new features, and this time I almost meant it. But ideas have a way of creeping back in, and 0.9 turned out to be a meaty release. Here are the highlights. The biggest chunk of work went into the REPL (toplevel) integration. I’m well aware that the OCaml toplevel isn’t terribly popular with seasoned OCaml developers — most of them reach for a proper build and a debugger instead. But I think newcomers get a lot of mileage out of a REPL, and (no surprise to anyone who’s followed my work) I’m a Lisper at heart with a real soft spot for interactive development. Clojure and Emacs Lisp spoiled me, and I want OCaml beginners to taste a bit of that too. So, what’s new: Choose your toplevel. The new lets you pick between , , and . Set it globally, or per project via : The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to. This one I’m particularly happy with. / files have to lean on for completion, but the auxiliary file formats have no language server at all. That’s exactly the kind of gap neocaml is meant to fill, so both and now ship a backend. In a file you get completion for stanza names, the field names valid for the enclosing stanza, and library names inside / fields: The library candidates combine your project’s own libraries with whatever’s installed in the active Opam switch. And it’s switch-aware — if there’s a project-local switch (an directory), neocaml detects it and queries it via , without any configuration on your part. The results are cached per project, so it stays snappy. In files you get completion for field and section names, and package names inside / / (sourced from ): Both backends can be toggled off via and if you’d rather not have them. A good chunk of this release is the unglamorous but important work of making things just behave correctly: There’s more in there too — integration ( ), clickable URLs and bug references in comments, a font-lock level selector, and richer menus across the modes. OCaml 5.5 was released on June 19th, so this felt like a good moment to ship 5.5 support in neocaml. The and grammars now track tree-sitter-ocaml v0.25.0, which brings the 5.5 grammar along with it. There’s a wrinkle here that’s worth explaining, because it’s shaped the last few releases. A tree-sitter grammar gets compiled into a parser that speaks a particular ABI version , and Emacs can only load parsers up to the latest ABI supported by the it was built against — it’s not the Emacs version itself that sets the ceiling. In practice a lot of Emacs 30 builds out there (notably Homebrew’s on macOS) are linked against tree-sitter 0.24, which tops out at ABI 14 ; you need an Emacs built against tree-sitter 0.25+ to load ABI 15 grammars. The trouble is that the tree-sitter 0.25 CLI now generates ABI 15 parsers by default, so any grammar regenerated with current tooling produces something those builds simply can’t load — you install it and it just errors out. Emacs 31 will ship with newer tree-sitter and make ABI 15 the common case, but it’s not out yet. (This isn’t a neocaml problem as such; it’s been biting tree-sitter modes across the ecosystem.) After a few users ran into exactly this, I’ve made a deliberate decision: stick to ABI 14 grammars until Emacs 31 is widely available. That effort started a couple of releases back — in 0.8.1 I lowered the ABI requirement from 15 to 14 across the opam, dune, and ocamllex modes, switched the menhir recipe to tmcgilchrist/tree-sitter-menhir , and pinned ocamllex back to v0.24.0, all of which target ABI 14 ( #42 ). 0.9 extends that policy to the core OCaml grammars. The catch with v0.25.0 is precisely that it generates an ABI 15 parser. Happily, the 5.5 grammar didn’t actually need any ABI 15 features — the bump rode along with the CLI upgrade — so an ABI 14 regeneration of the very same grammar is a drop-in. Big thanks to 314eter , the maintainer, for cutting a tag for exactly this purpose ( #141 ). The one snag was that tagging normally triggers releases to NPM, crates.io, and PyPI, so I sent a small PR to skip publishing for ABI-suffixed tags ( #142 ), and the tag followed. neocaml now pins both grammars to it. If you’re curious where neocaml is headed, I’ve started keeping a ROADMAP.md with ideas and guiding principles (short version: tree-sitter first, lean on the LSP stack for / , and own the auxiliary modes that have no language server). The project also has a proper documentation site now at neocaml.org , so there’s a real home for the details beyond the README. As always — update from MELPA , play with it, and let me know how it goes. The full list of changes is in the 0.9.0 release notes . Bug reports, feature requests, and pull requests are all welcome on GitHub . That’s all from me, folks! Keep hacking! A dedicated REPL per project. The REPL buffer is now named after its project (e.g. ), and the send commands route to the current buffer’s project REPL. You can have several projects running side by side without them stepping on each other. Choose your toplevel. The new lets you pick between , , and . Set it globally, or per project via : The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to. Send a phrase and step. ( ) sends the phrase at point to the REPL and moves on to the next one — perfect for walking through a file top to bottom while you experiment. from Emacs. loads a package into the running toplevel without you having to type the directive by hand. Restart on demand. kills and restarts the toplevel when things get into a weird state. Character literals and quoted strings at the syntactic layer. Tree-sitter fontifies , , and correctly, but the syntax table underneath was getting confused — which broke , , and around those constructs. The fix was a good old . I wrote up the whole story over on Emacs Redux in Tree-sitter Modes Still Need a Syntax Table , if you’re into mode-writing internals. integration. A directory with a file is now recognized as a project root (even without version control), and are ignored, and defaults to in dune projects.

0 views
(think) 1 months ago

nREPL Forever

Last week I announced Port , a small prepl client for Emacs. That post focused on Port itself, but writing it left me with the itch to do a follow-up on the bigger picture, because the socket REPL / prepl story is one I’ve been meaning to write up for years. If you’ve been around Clojure long enough, you remember the chatter. Socket REPL landed in Clojure 1.8 (January 2016), prepl in Clojure 1.10 (December 2018), and for a couple of years there was a steady stream of posts, tweets, and Slack threads to the effect of “this is what we should be building tools on. nREPL is on the way out.” Some serious people put their weight behind that idea, and some of them went and built tools to prove it. Now it’s 2026 and we can take stock. The pitch was good. Socket REPL is just the Clojure REPL exposed on a TCP port. prepl wraps it with a structured printer so the bytes coming back are EDN-tagged maps ( , , , ) instead of a human-readable prompt. Both ship with Clojure itself. No external server library, no middleware, no third-party namespaces. You start a JVM, you bind a port, you’re done. The intellectual case for moving off nREPL had been made by Rich Hickey himself, most clearly in a March 2015 clojure-dev post that’s worth reading in full. Rich didn’t actually attack nREPL by name in that message. What he did was argue carefully for what a REPL is : a thing that reads characters, evaluates forms, prints results, and loops, with those streams available to user code so that things like nested REPLs and debuggers compose naturally. The money line: While framing and RPC orientation might make things easier for someone who just wants to implement an eval window, it makes the resulting service strictly less powerful than a REPL. His proposal, in the same post, was that tools should open multiple connections to the running program: one for the human-facing stream, and dedicated channels for IDE operations. The socket REPL (which landed in 1.8 the following January) and prepl (which arrived in 1.10) were the official implementation of that worldview. A handful of editor projects took the cue and built clients: It was real momentum. If you were following Clojure tooling in 2018-2020, it genuinely felt like nREPL might be the past, and the future would be some combination of socket REPL plus a thin self-installing protocol on top of it. You can find a fair number of “RIP nREPL” hot takes from that period if you go looking. I went and surveyed each of those projects recently while working on Port. The pattern is depressingly consistent: Tutkain started on prepl. In November 2021, its v0.11 release explicitly stopped using prepl message framing and switched to a hand-rolled EDN-RPC protocol that Tutkain boots onto the raw socket REPL by sending it a base64-encoded blob. The new protocol has request ids, op dispatch ( , , , , , , …), and server-managed thread bindings. In other words: Tutkain grew into nREPL, just spelled differently. Chlorine never used prepl directly. It used socket REPL plus an -style upgrade blob. Its author’s successor project, Lazuli , abandoned the whole approach in favor of nREPL. The post-mortem is worth reading and is fairly blunt: tools that attempted prepl went back to nREPL because, honestly, it’s simply better. Conjure had a prepl client in its early Rust days. The current Lua/Fennel rewrite ships only an nREPL client. The author’s reasoning in the release notes was that nREPL “has complete ecosystem adoption and brilliant ClojureScript support.” Clojure-Sublimed technically still talks to a raw socket REPL, but only after sending it an EDN-printing prelude that upgrades the REPL to a structured protocol of tonsky’s own design. His post on the topic is one of the most thoughtful pieces I’ve read on Clojure REPL design, and his conclusion is roughly: the bare socket REPL is more useful than prepl because you can install your own protocol on top of it. Which is true. But notice that everyone who reached that conclusion ended up reinventing the same wheel: ids, ops, request/response correlation, completion support, lookup, interrupts. You know, the things nREPL has had since 2010. So the trajectory looks roughly like this: Pure prepl clients are nearly extinct in the wild. The one I found that qualifies is propel by Oliver Caldwell (of Conjure fame), which is delightful, about 70 lines of Clojure, and explicitly synchronous (one outstanding eval at a time). That works! But it’s not a foundation for the kind of feature set people expect from an editor. Here’s where I land. Rich isn’t wrong that prepl is closer to a “real” REPL in the strict sense. prepl genuinely is a more faithful encoding of read-eval-print: each form goes in, each result comes out, and the semantics match what you’d get at the standard REPL prompt. The thing is, “real REPL” is not the property you optimize for when you’re building editor tooling. The properties editor tooling actually needs are: nREPL was explicitly designed for those properties. The ops, middleware, and transport abstractions exist precisely because the people building it knew the consumers are not humans typing at a prompt, they’re programs negotiating a session. Calling nREPL “not a real REPL” is technically defensible and practically beside the point. Nobody on the consuming end is confused about what nREPL is for . I wrote about nREPL’s revival in 2018 . At that point I had just finished migrating the project out of Clojure Contrib, and the goal was to give it a real home and a working development process. It was a lot of work, but in hindsight things played out pretty well. Looking at where things ended up: Meanwhile prepl is, as best as I can tell, mostly a curiosity. It got me a side project I had fun with. It did not displace nREPL. The history of tooling protocols is full of cases where “purer”, “simpler”, or “more elegant” lost to “shipped, documented, and battle-tested.” LSP beat fifteen ad-hoc language protocols. DAP beat the same fifteen debuggers. nREPL beat prepl in the (Clojure) editor space. It’s not that the simpler thing is bad. prepl is a fine, elegant little protocol, and there’s a real case for embedding it in CI scripts, ops automation, deployment pipelines, or anywhere you want to drive a Clojure VM programmatically without pulling in a server library. Use it there. But for editor tooling? The Clojure community made an enormous, multi-year, multi-tool investment in nREPL. We have the protocol, the middleware, the manual, the books, the conference talks. nREPL works, it’s actively maintained, it’s increasingly portable across Clojure dialects, and the design decisions that Rich called out as un-REPL-like are the exact ones that make it a good substrate for editors. So I’ll say what I felt awkward saying back in 2018: nREPL forever. It’s the right abstraction for the job, and it’s not going anywhere. One more thing. After finishing Port I got curious what a minimal nREPL client would look like by comparison, so I went and built one. As you can imagine, it turned out to be significantly simpler. If that sounds interesting, take a look at neat , a small, language-agnostic nREPL client for Emacs. Keep hacking! Tutkain for Sublime Text Chlorine for Atom Conjure for Neovim (in its early Rust incarnation) Clojure-Sublimed by Nikita Tonsky a steady drip of smaller experiments around , , and friends Editor decides nREPL is too heavy or an undesirable external dependency and starts on prepl. Editor discovers prepl has no ids, no ops, no interrupts, no server-side completion, no namespace tracking, no test runner integration, etc. Editor rolls a custom protocol on top of socket REPL, or… Editor gives up and goes to nREPL. A way to correlate a request with its response when output and results are interleaved. A way to multiplex – one connection, several logical conversations. Server-side hooks for the operations every IDE expects: completion, lookup, go-to-definition, find-references, test running, stacktrace structuring, interrupt. A protocol stable enough that ten different editors can target it without each one inventing its own dialect. nREPL itself is healthier than it has ever been. Active maintainers, a proper manual , a steady release cadence, an actual ecosystem organization on GitHub. Most popular Clojure editors support it. CIDER , Calva , Cursive (via its own client), Conjure, vim-iced , you name it. babashka ships with nREPL built in. You boot a and you get an nREPL server, no extra dependencies. That’s how a lot of people use nREPL in scripting contexts today, and it’s been a hit. basilisp (the Clojure dialect on Python) has nREPL support . nREPL running on Python, talking to Emacs, evaluating Clojure. Nice. ClojureCLR has a working nREPL story now, and jank (the C++ Clojure) has nREPL on its roadmap too. The middleware ecosystem ( , , , , , …) is alive, well, and continues to add features.

0 views
(think) 1 months ago

neat: a language-agnostic nREPL client for Emacs

I think I’ll take my REPL neat My parens black and my bed at three CIDER’s too sweet for me… Last week I announced Port , a small prepl client for Emacs. Today I’m following it up with another small Emacs package. Meet neat , a tiny, deliberately language-agnostic nREPL client. For years I’ve been hearing some version of the same request: “could CIDER work with my non-Clojure nREPL server?”. Babashka, Basilisp, nREPL-CLR, even some homegrown servers people built on top of nREPL for languages I’d never heard of. 1 The answer was always the same kind of squishy “sort of, in theory, with caveats”, because while bare nREPL is genuinely language-agnostic, CIDER is not. CIDER was built for Clojure and assumes Clojure pretty much everywhere. I always thought the right answer was “let’s gradually make CIDER more language-agnostic.” That’s the kind of plan that sounds reasonable until you actually try it. The thing that pushed me over the edge was, oddly enough, building Port. Port is small, focused, and doesn’t try to be CIDER. Working on it for a couple of weeks reminded me how (deceptively) productive it is to start from a clean slate when the new requirements don’t match the assumptions baked into a mature codebase. Trying to retrofit CIDER into a language-agnostic shape would have meant fighting with every helper that ever assumed exists, every middleware contract defines, every project-type heuristic that knows about and and nothing else. A whole lot of “is the server Clojure, or is it the other thing?” branches. The Port experience reaffirmed that the right move for a genuinely different client is a new project , not a thousand cuts to an existing one. So was born. The name is short, says what it does (it’s neat, both in the small-and-tidy sense and in the “no deps, no special assumptions, just the protocol” sense), and conveniently leaves room for puns I haven’t fully committed to yet. I might land on a backronym one day. For now it’s just “neat”. neat is a small Emacs nREPL client. The code is split across four files: It only uses Emacs builtins. There are no external runtime dependencies, not even on , because neat doesn’t assume Clojure on the other end. If you write , , , or anything else that talks nREPL, you turn on in that buffer and it just works. The connection routing is also intentionally library-friendly. There’s a buffer-local override so downstream packages can implement their own routing logic, plus a global default for the simple “one server at a time” case that most people will want. Capability discovery is done at connect time via the nREPL op. neat doesn’t hardcode “this server has completions, this one doesn’t” assumptions. If the server reports a op, the CAPF backend lights up (with type annotations next to each candidate, when the server provides them). If it reports , eldoc starts working and jumps to definitions via an xref backend. If neither is there, you still get a perfectly serviceable raw REPL. Start an nREPL server. Anything that speaks the protocol will do. For a Clojure server: Then in Emacs: A REPL buffer pops up, the prompt follows the server’s reported namespace, and you can type expressions at it. Multi-line input works because only submits when the form parses as balanced under (Emacs Lisp syntax by default, which is close enough for any Lisp). Input history is persisted across sessions. If there’s a file in the project, the prompt defaults to its contents, so is enough to connect. To evaluate from a source buffer, turn on the minor mode: The familiar bindings are there, intentionally compatible with what CIDER users expect: ships the buffer contents as an op; uses the standard op instead, so the server can attribute file and line numbers to errors. Use the latter when you’re actually loading a file from disk and care about good diagnostics. sets the buffer-local , which gets sent as the field on every op from that buffer. For languages where the namespace is declared in the source (Clojure’s , etc.), swap in a parser via . For juggling multiple connections, opens a tabulated-list buffer with one row per live connection, where you can set the default or disconnect interactively. That’s roughly the whole user-facing surface today. There’s no jack-in command, no inspector, no debugger, no test runner. Likely there will never be, but if you need those you should probably be using CIDER anyways… If you write Clojure and CIDER works for you, keep using CIDER. It’s mature, full-featured, and supported, and I’m going to keep working on it for as long as people use it. Nothing about neat changes that. But if you find yourself in one of these situations: then neat might be a better fit. It’s small enough that you can read the whole thing in an afternoon, and the library/UI split ( and are perfectly usable from other packages) is genuinely designed for downstream consumers. neat is part of a broader push I’ve been chewing on for a while now: making nREPL a healthy multi-language ecosystem rather than a Clojure-only protocol. That push has three legs: This is also why I keep teasing a “reference CLI client” in conversations. An editor client is one thing, but a small command-line nREPL client written in a non-Lisp language would be a much sharper test of how language-agnostic the protocol really is. neat is plausibly a precursor to that. Time will tell how far I push this; for now I just wanted to get the Emacs side moving. As always, big thanks to Clojurists Together and everyone supporting my open source work. You make it possible for me to keep tweaking and improving CIDER, nREPL, clj-refactor, and friends, and occasionally try something “neat” on the side. isn’t replacing any of the existing Clojure tooling for Emacs. It’s just another tool in the box for the people who want it. Feedback, ideas, and contributions are most welcome over at the issue tracker . Keep hacking! https://github.com/clojure-emacs/cider/issues/3905   ↩︎ For a long time I planned to extract CIDER’s nREPL client code into a reusable package, but now that we have I probably will finally abandon this idea.  ↩︎ : bencode encode/decode. : TCP connections, request dispatch, the standard nREPL ops. : a comint-derived REPL buffer. : the entry point, customization group, and minor mode for source buffers. you write a non-Clojure language whose runtime ships an nREPL server, and you’ve been muddling through with a half-supported CIDER setup, you write Clojure but you value minimalism and don’t need the full CIDER feature set, you’re building an Emacs package that needs to talk nREPL and you want a small, dependency-free library to build on, 2 An actual nREPL specification. The spec.nrepl.org draft is (will be) the formal version of what today is “whatever nREPL the project does”. Reference clients. neat is one. The point of building a deliberately Clojure-free client is that it stress-tests the spec. Anywhere neat ends up needing to special-case the server, the spec has a gap. A compatibility test suite. The parameterised integration suite in neat already runs the same assertions against multiple servers and surfaces real divergences (Clojure batching into a single message where Basilisp emits two, for example). I’d like to grow this into a portable suite that any nREPL server can self-check against. https://github.com/clojure-emacs/cider/issues/3905   ↩︎ For a long time I planned to extract CIDER’s nREPL client code into a reusable package, but now that we have I probably will finally abandon this idea.  ↩︎

0 views
(think) 2 months ago

Port: a minimalist prepl client for Emacs

For ages I’ve had “add prepl support to CIDER” sitting somewhere in the back of my head. CIDER is built firmly around nREPL, but prepl ships with Clojure itself, and the appeal of dropping the external REPL server requirement is obvious. Recently, as part of a broader internals cleanup “mini-project” in CIDER, I finally sat down and put a prototype together: cider#3899 . The good news is that the prototype sort of worked. The bad news is that the more I poked at it, the more I kept running into the same pattern. CIDER assumes ops, sessions, request ids, and a whole structured protocol that prepl simply doesn’t have. The amount of CIDER code that would need to grow “is this nREPL or prepl?” branches added up quickly, and I’d be papering over prepl’s limitations in dozens of subtle places. The exercise was fun, but it ended up reaffirming my long-standing belief that nREPL is a much better fit for editor tooling than prepl is. The exercise did leave me thinking though. What if, instead of bolting prepl onto CIDER, I built a small standalone client in the spirit of inf-clojure and monroe ? Something tiny and focused that doesn’t have to pretend to be CIDER, and where prepl’s quirks would be the design rather than something to work around. Conveniently, I was on vacation in Portugal at the time, where I spent a few days in Porto, and the name pretty much picked itself. Port was born. It kept us firmly in the land of fun, drink-inspired Clojure-on-editor names: CIDER, Calva (after Calvados, the apple brandy), and now Port (the famous fortified wine). The protocol Port talks to is , over a TCP port , so the pun was hard to pass up. This time around I didn’t manage to land on a backronym I love (at least not yet). The contenders so far: Naming is hard. I remain open to better suggestions. :D Port is a side project. I don’t plan to invest serious time in it past the point I consider it feature-complete, which won’t be far beyond what’s already there. The deliberate goal is to keep it simple and focused, and its feature set will stay close to inf-clojure and monroe. Port is not competing with CIDER. If you want the full feature set (debugger, inspector, test runner, profiler, structured stacktraces, refactor support), CIDER + is, and will remain, the way. What Port gives you today is a small, dependable Clojure REPL that you can hook into Emacs without any external dependencies, just a stock Clojure JVM with a prepl listening on a port. If you’re up for the long version, doc/design.md goes deep. Here’s the short version of what prepl gives you compared to nREPL: That last point is the central design constraint. If you want to issue a request and reliably read back its result without accidentally consuming output from an unrelated background or , you need to layer correlation on top of the protocol. Port does this with two tricks: This is what nREPL provides via sessions and ops, just reinvented at the TCP layer. It’s a fair amount of work for something nREPL gives you for free, which only strengthens my view that nREPL is the better protocol for editor tooling. Still, it was an interesting and educational exercise. One thing I’m fairly proud of: Port has no hard dependencies. You’ll want either or installed for the source-buffer side of things, but Port itself only soft-depends on them via runtime checks. Hook it onto whichever one(s) you actually use. I intend to keep it that way. Dependency creep is a real problem in the Emacs (every?) ecosystem, and a small package should stay small. I tagged v0.1.0 yesterday. It’s small but already perfectly usable: MELPA submission is queued up next. After that, expect Port to be in burst-driven maintenance mode like most of my smaller projects. Feedback, ideas, and contributions are most welcome. The issue tracker is the right place. Funny thing, I’d never actually written any code against prepl until I started this project. It was fun to spend some quality time with the “competition” of my beloved nREPL. Working with a different protocol always teaches you something about the one you’re used to, and I came away from this with a renewed appreciation for both: prepl is genuinely elegant for what it is, and nREPL is genuinely well-designed for what we use it for. Big thanks to Clojurists Together and everyone else who supports my OSS Clojure work. You rock! Now if you’ll excuse me, I have new releases of CIDER, clj-refactor, and refactor-nrepl to get back to. Keep hacking! prepl omnipotent repl toolkit (my favorite so far) prepl-operated repl toolkit peak optimized repl toolkit No bencode . prepl emits EDN-tagged maps, one per line. This might be a feature or a problem, depending on your perspective. No middleware . Whatever the server prints is what you get. No interception, no extension surface. No sessions . There’s one thread per TCP connection. No ops . You send a Clojure form, the server evaluates it, and prints back tagged messages: , , , , plus an flag on errors. No request id . This is the main issue. Tags identify the kind of message, not which request produced it. Two sockets per session. A user socket drives the REPL buffer with raw streaming output, and a separate tool socket carries helper-command requests. Background prints from / on the user thread don’t bleed into the tool channel. A bootstrap form. On connect, the tool socket evaluates a one-shot form that defines a wrapper. Every subsequent helper call goes through it, which captures / and returns a tagged map containing the request id. The client matches the id against a pending-callback registry. “jacks in” (bootstraps) (auto-detects / / , starts a server and connects to it) single-buffer REPL with persistent input history, completion, and eldoc at the prompt interactive evaluation from source buffers with pretty-printed results structured stacktrace buffer with cause chain and navigable frames find-definition that follows into jar sources doc/source/apropos/macroexpand helpers

0 views
Brain Baking 2 months ago

Favourites of March 2026

It’s May! What happened? This weekend was unusually hot! What happened? Everyone knows but no-one admits or cares… Anyway, welcome to another month of 2026. I like May. It’s got a lot of national holidays. It signals the start of lots of great local food: strawberries in abundance, a strong asparagus month that you should enjoy while it lasts as in June the season is usually over, and we already ate some fresh French artichokes. It’s getting warmer but not as scorching as some of the coming months (although given the start of this month, that remains to be seen). But most of all: the end of May usually indicates the beginning of the exam period, which for me as an examiner instead of student is always interesting. Let’s light a candle and pray for not too many LLM-only submissions. Previous month: March 2026 . A miracle happened: I made some time to get back into gaming—and writing about games. In May, we’re finally digging into UFO 50 , in chronological order. If we play one a week we might finish in May 2027… So far, the first entry already is a home run. Related topics: / metapost / By Wouter Groeneveld on 3 May 2026.  Reply via email . I finally got to the Kirby spin-offs on the Game Boy: Kirby’s Pinball Land , Kirby’s Block Ball , and Kirby’s Star Stacker . They’re all really good! But then Robert and a GOG discount pushed me to finally try out The Drifter . What a thrill. I loved every minute of it. If you like gritty pixelated adventure games, you can’t miss this. After being turned off by the bad technical performance of Ruffy and the Riverside on the Nintendo Switch, I switched gears to other games. I picked it up and finished it. It’s an OK N64-inspired collect-a-thon that should be enjoyed on PC instead. Speaking of Robert, his /concerts slash page is very cool: it contains scans of all concert tickets he ever went to. Kenneth Reitz tells us to separate our identity from our work/projects , otherwise bad things happen (via Roy Tang ). Stefano Marinelli explains why he loves FreeBSD . The Power To Serve conveys such as strong message, its almost convincing me to jump ship! Until I read about the laptop gap . This year huh. Zakhary Kaplan stole the GBC logo from a ROM and made a cool web logo from it. Cal Newport’s In Defense of Thinking hits yet another nail on the head. Forrest’s essay On Pulling The Master Sword links Link’s (ha!) N64 behaviour to our capitalistic world. It’s a very long essay but well worth your time if you can stomach a game rant, some swearing, and philosophical questions about life and society. Drakenvlieg manages to pull more students into literature using journaling (in Dutch). Juhis shares his favourite two-player board games . Hive (pocket) is on the list! Chris Smith rates the movies he watched . I’m always interested in the rating systems other people employ when they do something like this. I liked blinry’s Do It Yourself soft drinks experiment. Translucent coke looks weird! Night’s Ham Stock examines the ending story of SKALD the video game I played in 2024 . It was great but I couldn’t make sense of the ending. Now I still can’t… Kain Klarden’s Gex Trilogy review saved me from throwing money at Limited Run Games. Again. It Fits On A Floppy is a strong manifesto for small software that more developers should read and take heart. Eli (Oatmeal) re-iterates something very important: “choose to truly care about something.” But then he goes much further. I need to re-read this a couple of times and let it sink in. It was also Eli who pointed out the existence of picoSYNTH . Richard Moss, the author known for The Secret History of Mac Gaming , is writing a book on Age of Empires ! Ruben Schade’s enthusiasm for the Commodore 64 knows no boundaries. The newly released C64 Ultimate looks very enticing, but where to put all these things? Amelia’s little blog website/host got hammered by AI bots . It’s yet another infuriating story but the visualisation part is very cool. There’s an interesting upcoming documentary on Clojure the programming language that might be worth checking out. https://www.codingfont.com/ is a cool way to help pick a monospaced editing font. I’m using JetBrains Mono for now. Did you know Windows was released for the Game Boy: I didn’t know palm rejection was a thing on Linux/KDE . The Underkeep Steam demo looks very promising; something to keep close tabs on! I don’t know what this is, but Listography looks like a lot of fun. I happen to like lists so I should be liking this. Isowulf is a very cool isometric perspective Wolfenstein 3D mod . You can build retro games using WebAssembly with https://wasm4.org/ I love the GoodEnough guestbook that even used to print the drawn images on thermal paper! Thomas Lehmann, the designer of one of my favourite card games ever Race for the Galaxy , took the deck building genre for another spin. The result is Dark Pact . Needless to say, it’s on my list.

0 views
Brain Baking 3 months ago

Favourites of March 2026

Our daughter turned three. We’re beyond exhausted but a ripgrep search in this repository yields five more instances of the word exhausted in combination of parenting so I’ll shut up. I guess we also celebrate that after three years of pure chaos, we’re… still alive? Previous month: February 2026 . I am just two levels short of finishing Gobliins 6 before deciding to throw in the towel. Thanks to the increased amount of moon logic presence, the entire adventure was more frustrating than relaxing. As a big Gobliins fan, I have to admit: the game left me a bit disappointed. It’s all right; I’ll just replay Gob3 again. As it left me wanting more, I went back to the original Gobliiins game that I somehow missed as back in the day my dad bought Gobliins 2 and we just continued with 3 without looking back. It’s still worth exploring but very basic and the presence of the life bar is a very strange (and bad!) design choice that fortunately was abandoned in the sequels. I charged the Analogue Pocket and hope to get in some good ol’ Game Boy (Color) games in the coming month. I read a depressing amount of personal genAI tales; more than enough to fill another blog post. I’ll try to keep these out of here as much as possible. My wife bumped into an hacker called Un Kyu Lee crafting his own micro journal hardware. The result looks very cool, including hinge to hang on the door as a physical reminder: I’d rather keep on journaling with my fountain pens, but still, very cool! Related topics: / metapost / By Wouter Groeneveld on 1 April 2026.  Reply via email . Michael vibe-code-ported an X11 window manager into Wayland ; an interesting Claude experiment to see how agentic development works. Greg Newman hosted the Emacs Blog Post Carnival 2025-07 on writing experiences and summarised the participating links. Lots of little gems in there. Rijksmuseum writes about the discovery of the new Rembrandt painting . Well, “new”—it’s been in private collection for years and only recently resurfaced. Peter Bridger shares his experience in the retro happening SWAG February 2026 . I wish we had something similar nearby! Chuck Jordan shares SimCity vibes . As one of the original programmers involved in the projects, he would know. (Via The Virtual Moose ) The 1MB Club has an interesting (older) article I read last month: consider disabling HTTPS auto redirects . I can’t remember why I turned this back on: I want my old WinXP machine to be able to reach as well without the extra TLS overhead. Funny though: they mention “You can freely view this website on both HTTPS and HTTP.”. I remove the in the protocol, press , and get redirected. Whoops. PolyWolf has been thinking about blazing fast static site generators . This is a goldmine as I have a wild idea to write my own generator in Clojure. When the exhaustion and brain fog go away, that is. According to Rishi Baldawa the reviewer isn’t the bottleneck . This one’s a bit AI flavoured, so beware if you’re coming down with an AI cold. (I know I have. Handkerchiefs full.) Marcin Wichary’s keyboard grandmastery again shines through in his Apple Fn endgame article . I wish his keyboard book wasn’t sold out. Wordsmith writes about the underrated simplicity of the original Harvest Moon (1996) video game. Dale Mellor defends sing a dynamically-produced blog site which is a nice change given the static site generator craziness. I’m still on Hugo and have little need for the points he brings up, but still, some others might. Tazjin tries out Guix as a Nixer . I was eyeing on Guix as a budding Lisp fanboy, but both options still can’t seem to fit in my head. I’ll let it stew for a little while longer. Homo Ludditus announces distro hopping time . The conclusion? “The madhouse could be a valid destination. But I’m still looking for better alternatives.” So far for 2026 as the year of the Linux desktop huh. The Digital Antiquarian writes about the year of peak Might & Magic , when New World Computing still was on top of the world. Here’s an interesting thought experiment by Andrey Listopadov: What if structural editing was a mistake? In this 2020 post by Vincent Bernat, photos of a bunch of cool vintage PC expansion cards are shared in conjunction with timeperiod-correct software that made great use of them. Gabor Torok switched to KDE Plasma , an interesting read because we both switched to OSX because of resons and are trying to crawl out of the Apple hole. I don’t know if I’m quite ready yet. Did you know there’s a relation between knitting and programming ? Abbey Perini does. Mykal Machon shares some insightful guiding principles to lead a fuller life. Judging by the principles, I don’t think Mykal has any young kids. I’m using this as a checklist to find out if I missed essential albums: Hip Hop Golden Age’s Top 40 Hip Hop Albums of 1998 . Here’s another GitHub “awesome” list; this time public APIs . Could be useful. Already used for my courses. It doesn’t hurt to link to the 2007 Slow Code manifesto . FontCrafter is a cool way to generate a real font based on your handwriting. WireTap is an open source Ngrok alternative. The Stump Window Manager is the only WM (except the obvious EXWM) I could find that’s written in Common Lisp. I should look into Ulauncher if I ever want to make the switch to Linux to replace Alfred. Christoph Frick shares a cool GitHub Gist showcasing you can write your AwesomeWM config in Fennel instead of Lua. Yazi looks like an Emacs Dired inside a shell?

0 views
(think) 4 months ago

Learning OCaml: PPX for Mere Mortals

When I started learning OCaml I kept running into code like this: My first reaction was “what the hell is ?” Coming from languages like Ruby and Clojure, where metaprogramming is either built into the runtime (reflection) or baked into the language itself (macros), OCaml’s approach felt alien. There’s no runtime reflection, no macro system in the Lisp sense – just this mysterious syntax that somehow generates code at compile time. That mystery is PPX (PreProcessor eXtensions), and once you understand it, a huge chunk of the OCaml ecosystem suddenly makes a lot more sense. This article is my attempt to demystify PPX for people like me – developers who want to use PPX effectively without necessarily becoming PPX authors themselves. OCaml is a statically typed language with no runtime reflection. That means you can’t do things like “iterate over all fields of a record at runtime” or “automatically serialize any type to JSON.” The type information simply isn’t available at runtime – it’s erased during compilation. One of my biggest frustrations as a newcomer was not being able to just print arbitrary data for debugging – there’s no generic or that works on any type. That frustration was probably my first real interaction with PPX. PPX solves this by generating code at compile time . When the OCaml compiler parses your source code, it builds an Abstract Syntax Tree (AST) – a tree data structure that represents the syntactic structure of your program. PPX rewriters are programs that receive this AST, transform it, and return a modified AST back to the compiler. The compiler then continues as if you had written the generated code by hand. In practical terms, this means that when you write: The PPX rewriter generates something like this behind the scenes: You get a pretty-printer for free, derived from the type definition. No boilerplate, no manual work, and it stays in sync with your type automatically. If you’ve used Rust’s or Haskell’s , the idea is very similar. The syntax is different, but the motivation is identical – generating repetitive code from type definitions. If you’re coming from Rust, you might wonder why OCaml doesn’t just have a built-in macro system like . It’s a fair question, and the answer says a lot about OCaml’s design philosophy. OCaml has always favored a small, stable language core . The compiler is famously lean and fast, and the language team is conservative about adding complexity to the specification. A full macro system baked into the compiler would be a significant undertaking – it would need to be designed, specified, maintained, and kept compatible across versions, forever. Instead, OCaml took a more minimal approach: the compiler provides just two things – extension points and attributes – as syntactic hooks in the AST. Everything else lives in the ecosystem. The actual PPX rewriters are ordinary OCaml programs that happen to transform ASTs. The ppxlib framework that ties it all together is a regular library, not part of the compiler. This has some real advantages: The trade-offs are real, though. Rust’s proc macros are more tightly integrated – you get better error messages pointing at macro-generated code, better IDE support for macro expansions, and the macro system is a documented, stable part of the language. With PPX, you’re sometimes left staring at cryptic type errors in generated code and reaching for to figure out what went wrong. That said, OCaml’s approach feels very OCaml – pragmatic, minimal, and trusting the ecosystem to build what’s needed on top of a simple foundation. And in practice, it works remarkably well. PPX wasn’t OCaml’s first metaprogramming system. Before PPX, there was Camlp4 (and its fork Camlp5 ) – a powerful but complex preprocessor that maintained its own parser, separate from the compiler’s parser. Camlp4 could extend OCaml’s syntax in arbitrary ways, which sounds great in theory but was a maintenance nightmare in practice. Every OCaml release risked breaking Camlp4, and code using Camlp4 extensions often couldn’t be processed by standard tools like editors and documentation generators. OCaml 4.02 (2014) introduced extension points and attributes directly into the language grammar – syntactic hooks specifically designed for preprocessor extensions. This was a much simpler and more maintainable approach: PPX rewriters use the compiler’s own AST, the syntax is valid OCaml (so tools can still parse your code), and the whole thing is conceptually just “AST in, AST out.” Camlp4 was officially retired in 2019. Today, the PPX ecosystem is built on ppxlib , a unified framework that provides a stable API across OCaml versions and handles all the plumbing for PPX authors. Before diving into specific libraries, let’s decode the bracket soup. PPX uses two syntactic mechanisms built into OCaml: Extension nodes are placeholders that a PPX rewriter must replace with generated code (compilation fails if no PPX handles them): Attributes attach metadata to existing code. Unlike extension nodes, the compiler silently ignores attributes that no PPX handles: The one you’ll see most often is on type declarations. The distinction between , , and is about scope – one for the innermost node, two for the enclosing declaration, three for the whole module-level. Tip: Don’t worry about memorizing all of this upfront. In practice, you’ll mostly use and occasionally or – and the specific PPX library’s documentation will tell you exactly which syntax to use. To use a PPX library in your project, you add it to the stanza in your file: That’s it. List all the PPX rewriters you need after , and Dune takes care of the rest (it even combines them into a single binary for performance). For plugins specifically, you use dotted names like . Let’s look at the PPX libraries that cover probably 90% of real-world use cases. ppx_deriving is the community’s general-purpose deriving framework. It comes with several built-in plugins: is the one you’ll reach for first – it’s essentially the answer to “how do I just print this thing?” that every OCaml newcomer asks sooner or later. The most commonly used plugins: A neat convention: if your type is named (as is idiomatic in OCaml), the generated functions drop the type name suffix – you get , , , instead of , , etc. You can also customize behavior per field with attributes: And you can derive for anonymous types inline: ppx_deriving_yojson generates JSON serialization and deserialization functions using the Yojson library: You can use or if you only need one direction. This is incredibly useful in practice – writing JSON serializers by hand for complex types is tedious and error-prone. If you’re using Jane Street’s Core library, you’ll encounter S-expression serialization everywhere. ( Tip: Jane Street bundles most of their PPXs into a single ppx_jane package, so you can add just to your instead of listing each one individually.) ppx_sexp_conv generates converters between OCaml types and S-expressions: The attributes here are quite handy – provides a default value during deserialization, and means the field is represented as a present/absent atom rather than . Two more Jane Street PPXs that you’ll see a lot in Core-based codebases. ppx_fields_conv generates first-class accessors and iterators for record fields: ppx_variants_conv does something similar for variant types – generating constructors as functions, fold/iter over all variants, and more. These Jane Street PPXs let you write tests directly in your source files: ppx_expect is particularly nice – it captures printed output and compares it against expected output: If the output doesn’t match, the test fails and you can run to automatically update the expected output in your source file. It’s a very productive workflow for testing functions that produce output. ppx_let provides syntactic sugar for working with monads and other “container” types: How does know which to call? It looks for a module in scope that provides the underlying and functions. In practice, you’ll typically open a module that defines before using : Note: Since OCaml 4.08, the language has built-in binding operators ( , , , ) that cover the basic use cases of without needing a preprocessor. If you’re not using Jane Street’s ecosystem, binding operators are probably the simpler choice. still offers extra features like , , and optimized though. ppx_blob is beautifully simple – it embeds a file’s contents as a string at compile time: No more worrying about file paths at runtime or packaging data files with your binary. The file contents become part of your compiled program. One thing that’s always bugged me about OCaml is the lack of string interpolation. ppx_string fills that gap: The suffix tells the PPX to convert the value using . You can use any module that provides a function. Most OCaml developers will never need to write a PPX, but understanding the basics helps demystify the whole system. Let’s build a very simple one. Say we want an extension that converts a string literal to uppercase at compile time. Here’s the complete implementation using ppxlib : The dune file: The key pieces are: For more complex PPXs (especially derivers), you’ll also want to use Metaquot ( ), which lets you write AST-constructing code using actual OCaml syntax instead of manual AST builder calls: The ppxlib documentation has excellent tutorials if you want to go deeper. One practical tip: when something goes wrong with PPX-generated code and you’re staring at a confusing type error, you can inspect what the PPX actually generated: Seeing the expanded code often makes the error immediately obvious. Most of the introductory PPX content out there was written around 2018-2019, so it’s worth noting how things have evolved since then. The big story has been ppxlib’s consolidation of the ecosystem . Back in 2019, some PPX rewriters still used the older (OMP) library, creating fragmentation. By 2021, nearly all PPXs had migrated to ppxlib , effectively ending the split. Today ppxlib is the way to write PPX rewriters – there’s no real alternative to consider. The transition hasn’t always been smooth, though. In 2025, ppxlib 0.36.0 bumped its internal AST to match OCaml 5.2, which changed how functions are represented in the parse tree. This broke many downstream PPXs and temporarily split the opam universe between packages that worked with the new version and those that didn’t. The community worked through it with proactive patching, but it highlighted an ongoing tension in the PPX world: ppxlib shields you from most compiler changes, but major AST overhauls still ripple through the ecosystem. On the API side, ppxlib is gradually deprecating its copy of in favor of , with plans to remove entirely in a future 1.0.0 release. If you’re writing a new PPX today, use exclusively. Meanwhile, OCaml 4.08’s built-in binding operators ( , , etc.) have reduced the need for in projects that don’t use Jane Street’s ecosystem. It’s a nice example of the language absorbing a pattern that PPX pioneered. Perhaps one day we’ll see more of this (e.g. native string interpolation). This article covers a lot of ground, but the PPX topic is pretty deep and complex, so depending on how far you want to go you might want to read more on it. Here are some of the best resources I’ve found on PPX: I was amused to see whitequark’s name pop up while I was doing research for this article – we collaborated quite a bit back in the day on her Ruby parser project, which was instrumental to RuboCop . Seems you can find (former) Rubyists in pretty much every language community. This article turned out to be a beast! I’ve wanted to write something on the subject for quite a while now, but I’ve kept postponing it because I was too lazy to do all the necessary research. I’ll feel quite relieved to put it behind me! PPX might look intimidating at first – all those brackets and symbols can feel like line noise. But the core idea is simple: PPX generates boilerplate code from your type definitions at compile time. You annotate your types with what you want ( , , , , etc.), and the PPX rewriter produces the code you’d otherwise have to write by hand. For day-to-day OCaml programming, you really only need to know: The “writing your own PPX” part is there for when you need it, but honestly most OCaml developers get by just fine using the existing ecosystem. That’s all I have for you today. Keep hacking! The ecosystem can evolve independently. ppxlib can ship new features, fix bugs, and improve APIs without waiting for a compiler release. Compare this to Rust, where changes to the proc macro system require the full RFC process and a compiler update. Tooling stays simple. Because and are valid OCaml syntax, every tool – editors, formatters, documentation generators – can parse PPX-annotated code without knowing anything about the specific PPX. The code is always syntactically valid OCaml, even before preprocessing. The compiler stays lean. No macro expander, no hygiene system, no special compilation phases – just a hook that says “here, transform this AST before I type-check it.” – registers an extension with a name, the context where it can appear (expressions, patterns, types, etc.), the expected payload pattern, and an expansion function. – a pattern-matching DSL for destructuring AST nodes. Here matches a string literal and captures its value. – helpers for constructing AST nodes. builds a string literal expression. – registers the rule with ppxlib’s driver. Preprocessors and PPXs – the official OCaml documentation on metaprogramming. A solid reference, though it assumes some comfort with the compiler internals. An Introduction to OCaml PPX Ecosystem – Nathan Rebours’ 2019 deep dive for Tarides. This is the most thorough tutorial on writing PPX rewriters I’ve seen. Some API details have changed since 2019 (notably the → shift), but the concepts and approach are still excellent. ppxlib Quick Introduction – ppxlib’s own getting-started guide. The best place to begin if you want to write your own PPX. A Guide to PreProcessor eXtensions – OCamlverse’s reference page with a comprehensive list of available PPX libraries. A Guide to Extension Points in OCaml – Whitequark’s original 2014 guide that introduced many developers to PPX. Historically interesting as a snapshot of the early PPX days. on type declarations to generate useful functions How to add PPX libraries to your dune file with Which PPX libraries exist for common tasks (serialization, testing, pretty-printing)

0 views
(think) 4 months ago

Building Emacs Major Modes with TreeSitter: Lessons Learned

Over the past year I’ve been spending a lot of time building TreeSitter-powered major modes for Emacs – clojure-ts-mode (as co-maintainer), neocaml (from scratch), and asciidoc-mode (also from scratch). Between the three projects I’ve accumulated enough battle scars to write about the experience. This post distills the key lessons for anyone thinking about writing a TreeSitter-based major mode, or curious about what it’s actually like. Before TreeSitter, Emacs font-locking was done with regular expressions and indentation was handled by ad-hoc engines (SMIE, custom indent functions, or pure regex heuristics). This works, but it has well-known problems: Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter modes) requires defining operator precedence grammars for the language, which is hard to get right. Custom indentation functions tend to grow into large, brittle state machines. Tuareg’s indentation code, for example, is thousands of lines long. TreeSitter changes the game because you get a full, incremental, error-tolerant syntax tree for free. Font-locking becomes “match this AST pattern, apply this face”: And indentation becomes “if the parent node is X, indent by Y”: The rules are declarative, composable, and much easier to reason about than regex chains. In practice, ’s entire font-lock and indentation logic fits in about 350 lines of Elisp. The equivalent in tuareg is spread across thousands of lines. That’s the real selling point: simpler, more maintainable code that handles more edge cases correctly . That said, TreeSitter in Emacs is not a silver bullet. Here’s what I ran into. TreeSitter grammars are written by different authors with different philosophies. The tree-sitter-ocaml grammar provides a rich, detailed AST with named fields. The tree-sitter-clojure grammar, by contrast, deliberately keeps things minimal – it only models syntax, not semantics, because Clojure’s macro system makes static semantic analysis unreliable. 1 This means font-locking forms in Clojure requires predicate matching on symbol text, while in OCaml you can directly match nodes with named fields. To illustrate: here’s how you’d fontify a function definition in OCaml, where the grammar gives you rich named fields: And here’s the equivalent in Clojure, where the grammar only gives you lists of symbols and you need predicate matching: You can’t learn “how to write TreeSitter queries” generically – you need to learn each grammar individually. The best tool for this is (to visualize the full parse tree) and (to see the node at point). Use them constantly. You’re dependent on someone else providing the grammar, and quality is all over the map. The OCaml grammar is mature and well-maintained – it’s hosted under the official tree-sitter GitHub org. The Clojure grammar is small and stable by design. But not every language is so lucky. asciidoc-mode uses a third-party AsciiDoc grammar that employs a dual-parser architecture – one parser for block-level structure (headings, lists, code blocks) and another for inline formatting (bold, italic, links). This is the same approach used by Emacs’s built-in , and it makes sense for markup languages where block and inline syntax are largely independent. The problem is that the two parsers run independently on the same text, and they can disagree . The inline parser misinterprets and list markers as emphasis delimiters, creating spurious bold spans that swallow subsequent inline content. The workaround is to use on all block-level font-lock rules so they win over the incorrect inline faces: This doesn’t fix inline elements consumed by the spurious emphasis – that requires an upstream grammar fix. When you hit grammar-level issues like this, you either fix them yourself (which means diving into the grammar’s JavaScript source and C toolchain) or you live with workarounds. Either way, it’s a reminder that your mode is only as good as the grammar underneath it. Getting the font-locking right in was probably the most challenging part of all three projects, precisely because of these grammar quirks. I also ran into a subtle behavior: the default font-lock mode ( ) skips an entire captured range if any position within it already has a face. So if you capture a parent node like and a child was already fontified, the whole thing gets skipped silently. The fix is to capture specific child nodes instead: These issues took a lot of trial and error to diagnose. The lesson: budget extra time for font-locking when working with less mature grammars . Grammars evolve, and breaking changes happen. switched from the stable grammar to the experimental branch because the stable version had metadata nodes as children of other nodes, which caused and to behave incorrectly. The experimental grammar makes metadata standalone nodes, fixing the navigation issues but requiring all queries to be updated. pins to v0.24.0 of the OCaml grammar. If you don’t pin versions, a grammar update can silently break your font-locking or indentation. The takeaway: always pin your grammar version , and include a mechanism to detect outdated grammars. tests a query that changed between versions to detect incompatible grammars at startup. Users shouldn’t have to manually clone repos and compile C code to use your mode. Both and include grammar recipes: On first use, the mode checks and offers to install missing grammars via . This works, but requires a C compiler and Git on the user’s machine, which is not ideal. 2 The TreeSitter support in Emacs has been improving steadily, but each version has its quirks: Emacs 29 introduced TreeSitter support but lacked several APIs. For instance, (used for structured navigation) doesn’t exist – you need a fallback: Emacs 30 added , sentence navigation, and better indentation support. But it also had a bug in offsets ( #77848 ) that broke embedded parsers, and another in that required to disable its TreeSitter-aware version. Emacs 31 has a bug in where an off-by-one error causes to leave ` *)` behind on multi-line OCaml comments. I had to skip the affected test with a version check: The lesson: test your mode against multiple Emacs versions , and be prepared to write version-specific workarounds. CI that runs against Emacs 29, 30, and snapshot is essential. Most TreeSitter grammars ship with query files for syntax highlighting ( ) and indentation ( ). Editors like Neovim and Helix use these directly. Emacs doesn’t – you have to manually translate the patterns into and calls in Elisp. This is tedious and error-prone. For example, here’s a rule from the OCaml grammar’s : And here’s the Elisp equivalent you’d write for Emacs: The query syntax is nearly identical, but you have to wrap everything in calls, map upstream capture names ( ) to Emacs face names ( ), assign features, and manage behavior. You end up maintaining a parallel set of queries that can drift from upstream. Emacs 31 will introduce which will make it possible to use files for font-locking, which should help significantly. But for now, you’re hand-coding everything. When a face isn’t being applied where you expect: TreeSitter modes define four levels of font-locking via , and the default level in Emacs is 3. It’s tempting to pile everything into levels 1–3 so users see maximum highlighting out of the box, but resist the urge. When every token on the screen has a different color, code starts looking like a Christmas tree and the important things – keywords, definitions, types – stop standing out. Less is more here. Here’s how distributes features across levels: And follows the same philosophy: The pattern is the same: essentials first, progressively more detail at higher levels. This way the default experience (level 3) is clean and readable, and users who want the full rainbow can bump to 4. Better yet, they can use to cherry-pick individual features regardless of level: This gives users fine-grained control without requiring mode authors to anticipate every preference. Indentation issues are harder to diagnose because they depend on tree structure, rule ordering, and anchor resolution: Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: This is the single most important piece of advice. Font-lock and indentation are easy to break accidentally, and manual testing doesn’t scale. Both projects use Buttercup (a BDD testing framework for Emacs) with custom test macros. Font-lock tests insert code into a buffer, run , and assert that specific character ranges have the expected face: Indentation tests insert code, run , and assert the result matches the expected indentation: Integration tests load real source files and verify that both font-locking and indentation survive on the full file. This catches interactions between rules that unit tests miss. has 200+ automated tests and has even more. Investing in test infrastructure early pays off enormously – I can refactor indentation rules with confidence because the suite catches regressions immediately. When I became the maintainer of clojure-mode many years ago, I really struggled with making changes. There were no font-lock or indentation tests, so every change was a leap of faith – you’d fix one thing and break three others without knowing until someone filed a bug report. I spent years working on a testing approach I was happy with, alongside many great contributors, and the return on investment was massive. The same approach – almost the same test macros – carried over directly to when we built the TreeSitter version. And later I reused the pattern again in and . One investment in testing infrastructure, four projects benefiting from it. I know that automated tests, for whatever reason, never gained much traction in the Emacs community. Many popular packages have no tests at all. I hope stories like this convince you that investing in tests is really important and pays off – not just for the project where you write them, but for every project you build after. This one is specific to but applies broadly: compiling TreeSitter queries at runtime is expensive. If you’re building queries dynamically (e.g. with called at mode init time), consider pre-compiling them as values. This made a noticeable difference in ’s startup time. The Emacs community has settled on a suffix convention for TreeSitter-based modes: , , , and so on. This makes sense when both a legacy mode and a TreeSitter mode coexist in Emacs core – users need to choose between them. But I think the convention is being applied too broadly, and I’m afraid the resulting name fragmentation will haunt the community for years. For new packages that don’t have a legacy counterpart, the suffix is unnecessary. I named my packages (not ) and (not ) because there was no prior or to disambiguate from. The infix is an implementation detail that shouldn’t leak into the user-facing name. Will we rename everything again when TreeSitter becomes the default and the non-TS variants are removed? Be bolder with naming. If you’re building something new, give it a name that makes sense on its own merits, not one that encodes the parsing technology in the package name. I think the full transition to TreeSitter in the Emacs community will take 3–5 years, optimistically. There are hundreds of major modes out there, many maintained by a single person in their spare time. Converting a mode from regex to TreeSitter isn’t just a mechanical translation – you need to understand the grammar, rewrite font-lock and indentation rules, handle version compatibility, and build a new test suite. That’s a lot of work. Interestingly, this might be one area where agentic coding tools can genuinely help. The structure of TreeSitter-based major modes is fairly uniform: grammar recipes, font-lock rules, indentation rules, navigation settings, imenu. If you give an AI agent a grammar and a reference to a high-quality mode like , it could probably scaffold a reasonable new mode fairly quickly. The hard parts – debugging grammar quirks, handling edge cases, getting indentation just right – would still need human attention, but the boilerplate could be automated. Still, knowing the Emacs community, I wouldn’t be surprised if a full migration never actually completes. Many old-school modes work perfectly fine, their maintainers have no interest in TreeSitter, and “if it ain’t broke, don’t fix it” is a powerful force. And that’s okay – diversity of approaches is part of what makes Emacs Emacs. TreeSitter is genuinely great for building Emacs major modes. The code is simpler, the results are more accurate, and incremental parsing means everything stays fast even on large files. I wouldn’t go back to regex-based font-locking willingly. But it’s not magical. Grammars are inconsistent across languages, the Emacs APIs are still maturing, you can’t reuse files (yet), and you’ll hit version-specific bugs that require tedious workarounds. The testing story is better than with regex modes – tree structures are more predictable than regex matches – but you still need a solid test suite to avoid regressions. If you’re thinking about writing a TreeSitter-based major mode, do it. The ecosystem needs more of them, and the experience of working with syntax trees instead of regexes is genuinely enjoyable. Just go in with realistic expectations, pin your grammar versions, test against multiple Emacs releases, and build your test suite early. Anyways, I wish there was an article like this one when I was starting out with and , so there you have it. I hope that the lessons I’ve learned along the way will help build better modes with TreeSitter down the road. That’s all I have for you today. Keep hacking! See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎ Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter modes) requires defining operator precedence grammars for the language, which is hard to get right. Custom indentation functions tend to grow into large, brittle state machines. Tuareg’s indentation code, for example, is thousands of lines long. Use to verify the node type at point matches your query. Set to to see which rules are firing. Check the font-lock feature level – your rule might be in level 4 while the user has the default level 3. The features are assigned to levels via . Remember that rule order matters . Without , an earlier rule that already fontified a region will prevent later rules from applying. This can be intentional (e.g. builtin types at level 3 take precedence over generic types) or a source of bugs. Set to – this logs which rule matched for each line, what anchor was computed, and the final column. Use to understand the parent chain. The key question is always: “what is the parent node, and which rule matches it?” Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎

0 views
Brain Baking 5 months ago

Why Parenting Is Similar To JavaScript Development

Here’s a crazy thought: to me, parenting feels very similar to programming in JavaScript. The more I think about it, the more convinced I am. If you’re an old fart that’s been coding stuff in JavaScript since its inception, you’ll undoubtedly be familiar with Douglas Crockford’s bibles , or to be more precise, that one tiny booklet from 2008 JavaScript: The Good Parts . That book covered by a cute O’Reilly butterfly is only 172 pages long. Contrast that with any tome attempting to do a “definitive guide”, like David Flanagan’s, which is 1093 pages thick. Ergo, one starts thinking: only of Javascript is inherently good . And that was 18 years ago. Since then, the EcmaScript standard threw new stuff on top in a steady yearly fashion, giving us weird and wonderful things (Promise chaining! Constants that aren’t constants! Private members with that look weirder than ! Nullish coalescing?? Bigger integers!) that arguably can be called syntactic sugar to try and disguise the bitter taste that is released slowly but surely if you chew on JS code long enough. If that’s not confusing enough, the JS ecosystem has evolved enormously as well: we now have 20+ languages built on top of JS that compile/transpile to it. We have TypeScript that has its own keyword that has nothing to do with , go nuts! We have ClojureScript that lets you write your React Native components in Clojure that compiles to JS that compiles to Java with Expo that compiles your app! We have and and and god-knows-what-else that replaces and possibly also ? At this point, I’m starting to transpile JS into transpiration. Parenting often feels like Javascript: The Good Parts versus JavaScript: The Definitive Guide . With our two very young children, there are many, many (oh so many) moments where we feel like we’re stumbling around in the dark, getting lost in that thick tome that dictates the things that we should be doing. When the eldest has yet another I’ll-just-throw-myself-on-the-floor-here moment and the youngest keeps on puking and yelling because he just discovered rolling on his tummy, I forget The Good Parts . To be perfectly frank, in those moments, I often wonder if Crockford had been lying to us. Are there even any good parts at all? We all know JS was cobbled together overnight because Netscape needed “some” language to make static languages a bit more dynamic. A language for the masses! What a monster it has become—in both positive and negative sense. It often feels like Wouter doesn’t exist anymore. Instead, there’s only daddy. It has been months since I last touched a book, notebook, or fountain pen. It has been months since my wife & I did something together to strengthen our relationship which currently is being reduced to snapping at each other because we’re still not perfectly synced when it comes to educational rules. Perhaps just writing and publishing this is reassurance for myself: proof of existence. Hi! This is not a bot! JavaScript is a big mess. Parenting feels like that as well. The ecosystem around JS rapidly changes and only the keenest frontend developer is able to keep up. I have no idea how to keep up with parenting. During our day-to-day struggles, you barely notice that the kids are growing and changing, but when you look back, you’re suddenly surprised yet another milestone has passed. Is that part of the Good Parts or the Bad Parts ? Maybe Flanagan’s Definitive Guide should be used to smack people on the head that do not obey to the latest EcmaScript standard best practices. I often have the feeling of getting smacked on the head when trying to deal with yet another kid emergency situation. I’m exhausted. Last week I yelled so hard at our eldest that she and I both started crying—she on the outside, me on the inside. I have no idea who I am anymore. I’m not like that. But it seems that I am. Our children successfully managed to bring out the worst in ourselves, even parts that I didn’t even know where there. I’ll let you be the judge of whether that bit belongs in the Good Parts . Yet I love JS. I love its dynamic duck type system (fuck TypeScript), I love its functional , , roots, I love prototypal inheritance. But I often forget about it because it’s buried in all that contemporary mud. Of course I love my children, but right now, I can’t say that I love parenting, because it’s buried in all that attention demanding and shouting that reduces our energy meters to zero in just a few minutes. My wife made a thoughtful remark the other day: We’re no longer living. At this point, we’re merely surviving. Every single day. As I write this, it’s almost 17:30 PM. The kids spent the day at my parents so I don’t even have the right to complain. Every minute now, they can come back and the bomb will explode again. There’s a little voice in my head that says “just get to the cooking, get them to eat and shove them in bed. Only an hour and a half left.” I don’t know if that’s sad or not. I need to get cooking. Only an hour and a half left. Don’t blame me, I no longer live. We’re merely surviving. If someone manages to write Parenting: The Good Parts in only 172 pages, let me know. Related topics: / javascript / parenting / By Wouter Groeneveld on 13 February 2026.  Reply via email .

0 views
Brain Baking 5 months ago

Favourites of January 2026

The end of the start of another year has ended. So now all there is left to do is to look forward to the end of the next month, starting effective immediately, and of course ending after the end of the end we are going to look forward to. Quite the end-eavour. I guess I’ll end these ramblings by ending this paragraph. But not before this message of general interest: children can be very end-earing, but sometimes you also want to end their endless whining! Fin. Previous month: January 2026 . Is Emacs a game? I think it is. I spent every precious free minute of my time tinkering with my configuration, exploring and discovering all the weird and cool stuff the editor and the thousands of community-provided packages offer. You can tell when you’ve joined the cult when you’re exchanging emails with random internet strangers about obscure Elisp functions and even joining the sporadic “let’s share Emacs learnings!” video calls (thanks Seb ). Does receiving pre-ordered games count as played ? I removed the shrink wrap from Ruffy and my calendar tells me I should start ordering UFO 50 very very soon via . Now if only that stupid Emacs config would stabilise; perhaps then I could pick up the Switch again… The intention was to start learning Clojure but I somehow got distracted after learning the Emacs CIDER REPL is the one you want. A zoomed-out top-down view of the project, centered on Brain Baking (left) and Jefklak's Codex (right). Related topics: / metapost / By Wouter Groeneveld on 4 February 2026.  Reply via email . Nathan Rooy created a very cool One million (small web) screnshots project and explains the technicalities behind it. Browsing to find your blog (mine are in there!) is really cool. It’s also funny to discover the GenAI purple-slop-blob. Brain Baking is located just north of a small dark green lake of expired domain name screenshots. Jefklak’s Codex , being much more colourful, is located at the far edge, to the right of a small Spaceship-domain-shark lake: Shom Bandopadhaya helped me regain my sanity with the Emacs undo philosophy. Install vundo. Done. Related: Sacha Chua was writing and thinking about time travel with Emacs, Org mode, and backups . I promise there’ll be non-Emacs related links in here, somewhere! Keep on digging! Michael Klamerus reminded me the BioMenace remaster is already out there. I loved that game as a kid but couldn’t get past level 3 or 4. It’s known to be extremely difficult. Or I am known to be a noob. Lars Ingebrigtsen combats link rot with taking screenshots of external links . I wrote about link rot a while ago and I must say that’s a genius addition. On hover, a small screenshot appears to permanently frame the thing you’re pointing to. I need to think about implementing this myself. Seb pointed me towards Karthinks’ Emacs window management almanac , a wall of text I will have to re-read a couple of times. I did manage to write a few simple window management helper functions that primarily do stuff with only a 2-split, which is good enough. Mikko shared his Board Gaming Year recap of 2025 . Forest Shuffle reaching 500 plays is simply insane, even if you take out the BoardGameArena numbers. Alex Harri spent a lot of time building an image-to-ASCII renderer and explains how the project was approached. This Precondition Guide to Home Row Mods is really cool and with Karabiner Elements in MacOS totally possible. It will get messy once you start fiddling with the timing. Elsa Gonsiorowski wrote about Emacs Delete vs. Kill which again helped me build a proper mental state of what the hell is going on in this Alien editor. Matt Might shared shell scripts to improve your academic writing by simply scanning the text for so-called “weasel words”. Bad: We used various methods to isolate four samples Better: We isolated four samples . I must say, academic prose sure could use this script. Robert Lützner discovered and prefers it over Git . I’m interested in its interoperability with Git. Charles Choi tuned Emacs to write prose by modifying quite a few settings I have yet to dig into. A friend installed PiVPN recently. I hadn’t heard from that one just yet so perhaps it’s worth a mention here. KeepassXC is getting on my nerves. Perhaps I should simply use pass , the standard unix password manager. But it should also be usable by my wife so… Nah. Input is a cool flexible font system designed for code but also offers proportional fonts. I tried it for a while but now prefer… Iosevka for my variable pitch font. Here’s a random Orgdown cheat sheet that might be of use. With RepoSense it’s easy to visualise programmer activities across Git repositories. We’re using it to track student activities and make sure everyone participates. Tired of configuring tab vs space indent stuff for every programming language? Use EditorConfig , something that works across editors and IDEs.

0 views
Manuel Moreale 5 months ago

Nikita Prokopov

This week on the People and Blogs series we have an interview with Nikita Prokopov, whose blog can be found at tonsky.me . Tired of RSS? Read this in your browser or sign up for the newsletter . The People and Blogs series is supported by Eleonora and the other 122 members of my "One a Month" club. If you enjoy P&B, consider becoming one for as little as 1 dollar a month. I am from Siberia. I studied CS there, got my first job in IT, and moved to Germany in 2018. Apart from programming, I am passionate about movies and filmmaking, UI design, experimented with standup, play badminton. I started writing in LiveJournal when I was still in uni, found a very nice Russian-speaking FP community there. Had a lot of eye-opening and often very heated discussions. Experimented with publishing in collaborative blogs (Habr, approximately Russian dev.to) but felt that author’s identity gets lost there. Personal blog was my attempt at reaching a wider English-speaking community. Livejournal was already dying by then, and I was smart (lucky?) enough to not choose Medium (TBH, it looked very promising in 2014). I am pretty happy with that decision. The older you get, the less you believe any startup has your best interests at heart. This leads to the only possible conclusion: self-hosting. It is hard to start but once you get your core audience there’s no limit to your growth. I usually collect ideas for a while (pictures, phrases, links, thoughts). This happens in the background and can take years. Once it reaches critical mass, I sit down to organize it all in a coherent whole. I don’t do separate drafts; it’s more like a pile of ideas — first pass — reflection — reorganization/cleanup — review — publish. A mandatory part of the reflection phase is questioning myself: why am I writing this, nobody is going to read it, this is stupid/silly/trivial/too complicated. That’s how you know you are writing something truly great. I usually ask a friend or two for feedback, Grammarly/ChatGPT/built-in Apple AI to do proofreading. I can only write in Sublime Text because it’s a tool I use daily for coding and it has become second nature to me. I feel very uncomfortable in any other tool when some minor detail behaves slightly different from what I am used to. iA Writer is fantastic and I tried to reproduce it as close as possible, its only downside being not being Sublime Text. I recently bought a NuPhy keyboard (Air60 v2 Cowberry) for my PC because of its compact size and cute looks, but was surprised that it sounds amazing and now I am addicted to typing on it. Apart from that, no: any place, any time, any device. No sounds, no music, as I find both distracting. I used to use Github pages but got tired of Ruby/Jekyll local installation breaking on macOS every year or so. I don’t blog often, so it’s the worst: you come back to your blog once every few months, completely without context, and you need to spend hours just restoring it to the status quo. Wrote my own engine in Clojure and has been happy ever since. For some reason I didn’t go with the static generator route. I do a good old CGI style approach, with an actual server rendering your pages. It’s more fun that way, and allows for more interactivity, although I didn’t explore it much yet. No, I am totally happy with where I am. Server costs €35/mo, but I co-host a lot of other projects there. Domain is €25/year. I used to have Patreon, but it was not just for blog, also for my open-source projects. I never tried monetizing writing, not sure how well that would go, but I have nothing against it. Off the top of my RSS feed: Fira Code is a nice programming font you might like. Now that you're done reading the interview, go check the blog and subscribe to the RSS feed . If you're looking for more content, go read one of the previous 126 interviews . Make sure to also say thank you to Ken Zinser and the other 122 supporters for making this series possible. Jamie Brandon https://www.scattered-thoughts.net/log/ Rakhim Davletkaliyev https://rakhim.exotext.com/ Marcin Wichary https://aresluna.org/ Ilya Birman https://ilyabirman.net/meanwhile/

2 views
Brain Baking 6 months ago

Favourites of December (And a Short 2025 Recap)

A late happy new year to everyone! I almost forgot to publish last month’s favourite (blog) posts, and since last month was the last one of 2025, let’s do a short recap as well. Previous month’s recap: November 2025 . Last year was another eventful year. Browse the full 2025 Brain Baking archive for more juicy details. I selected one post per month that for me stands out: Our son also kicked me out of my cosy home office upstairs. Luckily, our renovations were finished in time, so we moved the living room and I took the old space hostage . One of the advantages of directly staring at a larger window is being able to admire the seasonal view: The window at my desk showcases snowy trees. For 2026, I only wish for one thing: stability . Let’s stop the craziness and try to get things settled down. No more kids, renovations, job changes, broken bicycles, and serious sickness please. Just, you know, breathing. Whoosah . Last month I joined the Advent of Code challenge using Clojure, a language I know absolutely nothing about. Since then I’ve been obsessed with Lisp-based dialects. Forgive me if most of the links below are programming-oriented: it’s been invigorating to learn something new and actually enjoy a programming language for a chance. It’s the reason I’m typing this in Emacs now, although I haven’t even installed CIDER yet. All in due time… Ok that was definitely too much Emacs stuff. The lack of other links shows how much I’ve been obsessed with the editor lately. No other random links for this month! Related topics: / metapost / By Wouter Groeneveld on 10 January 2026.  Reply via email . In January, I had the idea to compile your own philosophy . So far, I have collected lots of notes and summarised too many previous ones, but nothing has been published yet. In February, I shared my stationary drawers . I should really clean out all those fountain pens. In March, I dug up a photo of my first console , the SEGA Genesis/MegaDrive. In April, I learned that my sourdough starter has twins somewhere in Switzerland. In May, more thoughts about writing and publishing popped up. In June, I debunked (or confirmed?) the fact that IT freelancers earn more than their employee counterparts . In July, I got influenced by other board game enthusiasts and admitted to having too many games and too little time . In August, we welcomed our second little one and I turned forty —in that order. Yes, that is important to me. In September, I wrote too many articles about trick taking games and local traditions . In October, I fondly looked back at years of downloading warez software . In November, I recovered my late father-in-law’s 1994 IBM PC invoice . In December, I started shaving Emacs yaks . I haven’t stopped ever since. Nick George reports on building static websites with Clojure . Nathan Marz describes how he invented Specter to fill Clojure’s mutability hole. I don’t understand 90% of the technicalities there, but one day, I will. More Clojure stuff. Sorry… Mikko Koski helped me get started: 8 tips for Advent of Code 2022 in Clojure. A more official one, but just as interesting: the State of Clojure 2024 results . 76% of the people using it build web apps, 40% is on Emacs/CIDER, and Babashka is super popular! This Advent of Code GIF archive is crazy. Victor Dorneanu wrote about his Doom Emacs to Vanilla migration. I tried Doom/Spacemacs for about one whole day and then started back from scratch, but damn, it’s very challenging, even though you can “do what you want”—if you’re an Emacs/Elisp acolyte, that is. I’m planning to get babtized in the Emacs Church very soon. Alice from The Wallflower Digest shares her thoughts about personal curriculums ; a way to get started with deliberate life-long learning. (via Joel , I think?) Karthinks found fifteen ways to use Embark , a wonderful context-aware Emacs package. More “Emacs from scratch” blogs to share: this one’s from Arne and lies out the foundations in case you want to get started. Thanks, Arne. You’re in my RSS feed now. Frank Meeuwsen writes (in Dutch) about AI tooling and how they democratise digital literacy. Or rather, how they should . Gregory J. Stein wrote a guide on email in Emacs using Mu and Mu4e . I have more thoughts on that saved for a separate blog post. If you’d like to know how many Emacs packages you’re currently rocking, Manuel Uberti has an Elisp for you (via Sebastián ) Kristoffer Balintona helped me better understand the Vertico completion-at-point-function stack .

3 views
Abhinav Sarkar 7 months ago

Solving Advent of Code 2025 in Janet: Days 5–8

I’m solving the Advent of Code 2025 in Janet . After doing the last five years in Haskell, I wanted to learn a new language this year. I’ve been eyeing the “New Lisps” 1 for a while now, and I decided to learn Janet. Janet is a Clojure like Lisp that can be interpreted, embedded and compiled, and comes with a large standard library with concurrency, HTTP and PEG parser support. I want to replace Python with Janet as my scripting language. Here are my solutions for December 5–8. This post was originally published on abhinavsarkar.net . This post is a part of the series: Solving Advent of Code 2025 in Janet . All my solutions follow the same structure because I wrote a template to create new empty solutions. Actually, I added a fair bit of automation this time to build, run, test and benchmark the solutions. Parsing the day 5 input was a bit involved because of the two different formats. Other than that, the function is the most interesting part. Since I sorted the ranges in , I needed to do only one linear scan of ranges, merging the current one with the previous one if possible. The trick here was to be correct about finding overlapping ranges and calculating the merged range. I made multiple mistakes but eventually figured it out. Day 6 was entirely a parsing-based problem, and Janet was well suited to it. Parts 1 and 2 required the input to be parsed differently, so the is parameterized. In part 1, I ignored whitespaces in numbers, while in part 2, they were significant. So I passed two different patterns to parse numbers in and . I had to write the function because it is not built into Janet. Rest of it was straightforward. Notice how I used threading macros to write the computations linearly. I solved part 1 of day 7 by simply folding over the input rows, propagating the beam, and splitting it when required. I used a set of indices to keep track of the current indices at which beam was present. Only tricky thing here was using a dict to simulate a set because Janet does not have sets built-in. That’s what the code is doing. Part 2 was harder. I first wrote a brute-force solution to count the number of paths, but it never finished running. The number of paths is \(O(2^n)\) , and impossible to solve with brute-force. I know that there may be better solutions possible, but I simply added a dict-based cache, and that made it work. Day 8 required me to do several new things. It was immediately clear to me that I needed a Disjoint Set to keep track of the connected points. So I wrote one in object-oriented Janet! Object-orientation in Janet is prototype-based , pretty much like JavaScript. You can see the and methods in the above. I first computed all unique pairs and distances between them, and sorted the pairs by distances. In part 1, I union-ed closest \(k\) pairs, while in part 2, I kept going till all points were connected in one circuit. This worked but it took really long to run: over 600ms. I was not satisfied. After a night’s sleep, I realized that I do not need to sort all pairs but only top \(k\) , where \(k\) is much smaller than total number of pairs (~500000). So I rewrote the function to use a max binary heap that keeps only the closest- \(k\) pairs. The function changed to pass \(k\) as a parameter to , which after a bit of experimentation, I set to 5500. The rest of the functions stayed unchanged. This change provided over 10x speedup, reducing the run time to under 60ms 2 ! You can see the mutable nature of Janet in all its glory in this solution. I had several gotcha moments when I tried to mix higher-order functions—such as , , and —with mutable date structures in Janet. Not only they are confusing, but they also result in slower code because Janet does not have Persistent data-structures like Clojure. Every etc. result in a new array being created. My advice is to not mix functional programming code with procedural programming code in Janet. That’s it for now. Next note will drop after 4 or 5 days. You can browse the code repo to see the full setup. If you have any questions or comments, please leave a comment below. If you liked this post, please share it. Thanks for reading! The new Lisps that interest me are: Janet, Fennel and Jank . ↩︎ You may ask why I didn’t write the max-heap as OO-Janet code. Well, I did and I found that it was 50% slower than the procedural version shown here. I guess the dispatch overhead for methods is too much. ↩︎ This post is a part of the series: Solving Advent of Code 2025 in Janet . If you liked this post, please leave a comment . Days 5–8 👈 The new Lisps that interest me are: Janet, Fennel and Jank . ↩︎ You may ask why I didn’t write the max-heap as OO-Janet code. Well, I did and I found that it was 50% slower than the procedural version shown here. I guess the dispatch overhead for methods is too much. ↩︎ Days 5–8 👈

0 views

I want a better build executor

This post is part 4/4 of a series about build systems . The market fit is interesting. Git has clearly won, it has all of the mindshare, but since you can use jj to work on Git repositories, it can be adopted incrementally. This is, in my opinion, the only viable way to introduce a new VCS: it has to be able to be partially adopted. If you've worked with other determinism-based systems, one thing they have in common is they feel really fragile, and you have to be careful that you don't do something that breaks the determinism. But in our case, since we've created every level of the stack to support this, we can offload the determinism to the development environment and you can basically write whatever code you want without having to worry about whether it's going to break something. In my last post , I describe an improved build graph serialization. In this post, I describe the build executor that reads those files. Generally, there are three stages to a build: There are a lot more things an executor can do than just spawning processes and showing a progress report! This post explores what those are and sketches a design for a tool that could improve on current executors. Ninja depends on mtimes, which have many issues . Ideally, it would take notes from and look at file attributes, not just the mtime, which eliminates many more false positives. I wrote earlier about querying the build graph . There are two kinds of things you can query: The configuration graph (what bazel calls the target graph ), which shows dependencies between "human meaningful" packages; and the action graph , which shows dependencies between files. Queries on the action graph live in the executor; queries on the configuration graph live in the configure script. For example, / , , and query the configuration graph; and query the action graph. Cargo has no stable way to query the action graph. Note that “querying the graph” is not a binary yes/no. Ninja's query language is much more restricted than Bazel's. Compare Ninja's syntax for querying “the command line for all C++ files used to build the target ” 2 : to Bazel's: Bazel’s language has graph operators, such as union, intersection, and filtering, that let you build up quite complex predicates. Ninja can only express one predicate at a time, with much more limited filtering—but unlike Bazel, allows you to filter to individual parts of the action, like the command line invocation, without needing a full protobuf parser or trying to do text post-processing. I would like to see a query language that combines both these strengths: the same nested predicate structure of Bazel queries, but add a new predicate that takes another predicate as an argument for complex output filtering: We could even go so far as to give this a jq-like syntax: For more complex predicates that have multiple sets as inputs, such as set union and intersection, we could introduce a operator: In my previous post , I talked about two main uses for a tracing build system: first, to automatically add dependency edges for you; and second, to verify at runtime that no dependency edges are missing. This especially shines when the action graph has a way to express negative dependencies, because the tracing system sees every attempted file access and can add them to the graph automatically. For prior art, see the Shake build system . Shake is higher-level than an executor and doesn't work on an action graph, but it has built-in support for file tracing in all three of these modes: warning about incorrect edges; adding new edges to the graph when they're detected at runtime; and finally, fully inferring all edges from the nodes alone . I would want my executor to only support linting and hard errors for missing edges. Inferring a full action graph is scary and IMO belongs in a higher-level tool, and adding dependency edges automatically can be done by a tool that wraps the executor and parses the lints. What's really cool about this linting system is that it allows you to gradually transition to a hermetic build over time, without frontloading all the work to when you switch to the tool. The main downside of tracing is that it's highly non-portable, and in particular is very limited on macOS. One possible alternative I've thought of is to do a buck2-style unsandboxed hermetic builds, where you copy exactly the specified inputs into a tempdir and run the build from the tempdir. If that fails, rerun the build from the main source directory. This can't tell which dependency edges are missing, but it can tell you a dependency is missing without fully failing the build. The downside to that is it assumes command spawning is a pure function, which of course it's not; anything that talks to a socket is trouble because it might be stateful. Tracing environment variable access is … hard. Traditionally access goes through the libc function, but it’s also possible to take an in a main function, in which case accesses are just memory reads. That means we need to trace memory reads somehow. On x86 machines, there’s something called PIN that can do this directly in the CPU without needing compile time instrumentation. On ARM there’s SPE , which is how works, but I’m not sure whether it can be configured to track 100% of memory accesses. I need to do more research here. On Linux, this is all abstracted by . I’m not sure if there’s equivalent wrappers on Windows and macOS. There’s also DynamicRIO , which supports a bunch of platforms, but I believe it works in a similar way to QEMU, by interposing itself between the program and the CPU, which comes with a bunch of overhead. That could work as an opt-in. One last way to do this is with a SIGSEGV signal handler , but that requires that environment variables are in their own page of memory and therefore a linker script. This doesn’t work for environment variables specifically, because they aren’t linker symbols in the normal sense, they get injected by the C runtime . In general, injecting linker scripts means we’re modifying the binaries being run and might cause unexpected build or runtime failures. Here I describe more concretely the tool I want to build, which I’ve named . It would read the constrained clojure action graph serialization format (Magma) that I describe in the previous post; perhaps with a way to automatically convert Ninja files to Magma. Like Ekam , Ronin would have a continuous rebuild mode (but unlike Bazel and Buck2, no background server). Like Shake, It would have runtime tracing, with all of options, to allow gradually transitioning to a hermetic build. And it would have bazel-like querying for the action graph, both through CLI arguments with an jq syntax and through a programmatic API. Finally, it would have pluggable backends for file watching, tracing, stat-ing, progress reporting, and checksums, so that it can take advantage of systems that have more features while still being reasonably fast on systems that don’t. For example, on Windows stats are slow, so it would cache stat info; but on Linux stats are fast so it would just directly make a syscall. Like Ninja, Ronin would keep a command log with a history of past versions of the action graph. It would reuse the bipartite graph structure , with one half being files and the other being commands. It would parse depfiles and dyndeps files just after they’re built, while the cache is still hot. Like , ronin would use a single-pass approach to support early cutoff. It would hash an "input manifest" to decide whether to rebuild. Unlike , it would store a mapping from that hash back to the original manifest so you can query why a rebuild happened. Tracing would be built on top of a FUSE file system that tracked file access. 3 Unlike other build systems I know, state (such as manifest hashes, content hashes, and removed outputs) would be stored in an SQLite database, not in flat files. Kinda. Ronin takes a lot of ideas from buck2. It differs in two major ways: The main advantage of Ronin is that it can slot in underneath existing build systems people are already using—CMake and Meson—without needing changes to your build files at all. In this post I describe what a build executor does, some features I would like to see from an executor (with a special focus on tracing), and a design for a new executor called that allows existing projects generating ninja files to gradually transition to hermetic builds over time, without a “flag day” that requires rewriting the whole build system. I don’t know yet if I will actually build this tool, that seems like a lot of work 5 😄 but it’s something I would like to exist in the world. In many ways Conan profiles are analogous to ninja files: profiles are the interface between Conan and CMake in the same way that ninja files are the interface between CMake and Ninja. Conan is the only tool I'm aware of where the split between the package manager and the configure step is explicit. ↩ This is not an apple to apples comparison; ideally we would name the target by the output file, not by its alias. Unfortunately output names are unpredictable and quite long in Bazel. ↩ macOS does not have native support for FUSE. MacFuse exists but does not support getting the PID of the calling process. A possible workaround would be to start a new FUSE server for each spawned process group. FUSE on Windows is possible through winfsp . ↩ An earlier version of this post read "Buck2 only supports non-hermetic builds for system toolchains , not anything else", which is not correct. ↩ what if i simply took buck2 and hacked it to bits,,, ↩ Resolving and downloading dependencies. The tool that does this is called a package manager . Common examples are , , Conan 1 , and the resolver . Configuring the build based on the host environment and build targets. I am not aware of any common name for this, other than maybe configure script (but there exist many tools for this that are not just shell scripts). Common examples are CMake, Meson, autotools, and the Cargo CLI interface (e.g. and ). Executing a bunch of processes and reporting on their progress. The tool that does this is called a build executor . Common examples are , , , and the phase of . It does not expect to be a top-level build system. It is perfectly happy to read (and encourages) generated files from a higher level configure tool. This allows systems like CMake and Meson to mechanically translate Ninja files into this new format, so builds for existing projects can get nice things. It allows you to gradually transition from non-hermetic to hermetic builds, without forcing you to fix all your rules at once, and with tracing to help you find where you need to make your fixes. Buck2 doesn’t support tracing at all. It technically supports non-hermetic builds, but you don't get many benefits compared to using a different build system, and it's still high cost to switch build systems 4 . In many ways Conan profiles are analogous to ninja files: profiles are the interface between Conan and CMake in the same way that ninja files are the interface between CMake and Ninja. Conan is the only tool I'm aware of where the split between the package manager and the configure step is explicit. ↩ This is not an apple to apples comparison; ideally we would name the target by the output file, not by its alias. Unfortunately output names are unpredictable and quite long in Bazel. ↩ macOS does not have native support for FUSE. MacFuse exists but does not support getting the PID of the calling process. A possible workaround would be to start a new FUSE server for each spawned process group. FUSE on Windows is possible through winfsp . ↩ An earlier version of this post read "Buck2 only supports non-hermetic builds for system toolchains , not anything else", which is not correct. ↩ what if i simply took buck2 and hacked it to bits,,, ↩

0 views
Abhinav Sarkar 7 months ago

Solving Advent of Code 2025 in Janet: Day 1–4

I’m solving the Advent of Code 2025 in Janet . After doing the last five years in Haskell, I wanted to learn a new language this year. I’ve been eyeing the “New Lisps” 1 for a while now, and I decided to learn Janet. Janet is a Clojure like Lisp that can be interpreted, embedded and compiled, and comes with a large standard library with concurrency, HTTP and PEG parser support. I want to replace Python with Janet as my scripting language. Here are my solutions for Dec 1–4. This post was originally published on abhinavsarkar.net . All my solutions follow the same structure because I wrote a template to create new empty solutions. Actually, I added a fair bit of automation this time to build, run, test and benchmark the solutions. Day 1 was a bit mathy but it didn’t take too long to figure out. I spent more time polishing the solution to be idiomatic Janet code. , the PEG grammar to parse the input was the most interesting part for me on the day. If you know Janet, you can notice this is not the cleanest code, but that’s okay, it was my day 1 too. The most interesting part of the day 2 solution was the macro that reads the input at compile-time and creates a custom function to check whether a number is in one of the given ranges. This turned out to be almost 4x faster than writing the same thing as a function. Notice , the PEG grammar to parse the input. So short and clean! I also leaned into the imperative and mutable nature of the Janet data-structures. The code is still not the cleanest as I was still learning. The first part of day 3 was pretty easy to solve, but using the same solution for the second part just ran forever. I realized that this is a Dynamic Programming problem, but I don’t like doing array-based solutions, so I simply rewrote the solution to add caching. And it worked! It is definitely on the slower side, but I’m okay with it. The code has become a little more idiomatic Janet. Day 4 is when I learned more about Janet control flow structures. The solution for the part 2 is a straightforward Breadth-first traversal . The interesting parts are the , and statements. So concise and elegant! That’s it for now. Next note will drop after 4 or 5 days. You can browse the code repo to see the full setup. If you have any questions or comments, please leave a comment below. If you liked this post, please share it. Thanks for reading! The new Lisps that interest me are: Janet, Fennel and Jank . ↩︎ If you liked this post, please leave a comment . The new Lisps that interest me are: Janet, Fennel and Jank . ↩︎

0 views

I want a better action graph serialization

This post is part 3/4 of a series about build systems . The next post and last post is I want a better build executor . As someone who ends up getting the ping on "my build is weird" after it has gone through a round of "poke it with a stick", I would really appreciate the mechanisms for [correct dependency edges] rolling out sooner rather than later. In a previous post , I talked about various approaches in the design space of build systems. In this post, I want to zero in on one particular area: action graphs. First, let me define "action graph". If you've ever used CMake, you may know that there are two steps involved: A "configure" step ( ) and a build step ( or ). What I am interested here is what generates , the Makefiles it has created. As the creator of ninja writes , this is a serialization of all build steps at a given moment in time, with the ability to regenerate the graph by rerunning the configure step. This post explores that design space, with the goal of sketching a format that improves on the current state while also enabling incremental adoption. When I say "design space", I mean a serialization format where files are machine-generated by a configure step, and have few enough and restricted enough features that it's possible to make a fast build executor . Not all build systems serialize their action graph. and run persistent servers that store it in memory and allow querying it, but never serialize it to disk. For large graphs, this requires a lot of memory; has actually started serializing parts of its graph to reduce memory usage and startup time . The nix evaluator doesn’t allow querying its graph at all; nix has a very strange model where it never rebuilds because each change to your source files is a new “ input-addressed derivation ” and therefore requires a reconfigure. This is the main reason it’s only used to package software, not as an “inner” build system, because that reconfigure can be very slow. I’ve talked to a couple Nix maintainers and they’ve considered caching parts of the configure step, without caching its outputs (because there are no outputs, other than derivation files!) in order to speed this up. This is much trickier because it requires serializing parts of the evaluator state. Tools that do serialize their graph include CMake, Meson, and the Chrome build system ( GN ). Generally, serializing the graph comes in handy when: In the last post I talked about 4 things one might want from a build system: For a serialization format, we have slightly different constraints. Throughout this post, I'll dive into detail on how these 3 overarching goals apply to the serialization format, and how well various serializations achieve that goal. The first one we'll look at, because it's the default for CMake, is and Makefiles. Make is truly in the Unix spirit: easy to implement 2 , very hard to use correctly. Make is ambiguous , complicated , and makes it very easy to implicitly do a bunch of file system lookups . It supports running shell commands at the top-level, which makes even loading the graph very expensive. It does do pretty well on minimizing reconfigurations, since the language is quite flexible. Ninja is the other generator supported by CMake. Ninja is explicitly intended to work on a serialized action graph; it's the only tool I'm aware of that is. It solves a lot of the problems of Make : it removes many of the ambiguities; it doesn't have any form of globbing; and generally it's a much simpler and smaller language. Unfortunately, Ninja's build file format still has some limitations. First, it has no support for checksums. It's possible to work around that by using and having a wrapper script that doesn't overwrite files unless they've changed, but that's a lot of extra work and is annoying to make portable between operating systems. Ninja files also have trouble expressing correct dependency edges. Let's look at a few examples, one by one. In each of these cases, we either have to reconfigure more often than we wish, or we have no way at all of expressing the dependency edge. See my previous post about negative dependencies. The short version is that build files need to specify not just the files they expect to exist, but also the files they expect not to exist. There's no way to express this in a ninja file, short of reconfiguring every time a directory that might contain a negative dependency is modified, which itself has a lot of downsides. Say that you have a C project with just a . You rename it to and ninja gives you an error that main.c no longer exists. Annoyed of editing ninja files by hand, you decide to write a generator 3 : Note this that this registers an implicit dependency on the current directory. This should automatically detect that you renamed your file and rebuild for you. Oh. Right. Generating build.ninja also modifies the current directory, which creates an infinite loop. It's possible to work around this by putting your C file in a source directory: There's still a problem here, though—did you notice it? Our old target is still lying around. Ninja actually has enough information recorded to fix this: . But it's not run automatically. The other problem is that this approach rebuilds far too often. In this case, we wanted to support renames, so in Ninja's model we need to depend on the whole directory. But that's not what we really depended on—we only care about files. I would like to see a action graph format that has an event-based system, where it says "this file was created, make any changes to the action graph necessary", and cuts the build short if the graph wasn't changed. For flower , I want to go further and support deletions : source files and targets that are optional, that should not fail the build if they aren't present, but should cause a rebuild if they are created, modified, or deleted. Ninja has no way of expressing this. Ninja has no way to express “this node becomes dirty when an environment variable changes”. The closest you can get is hacks with and the checksum wrapper/restat hack, but it’s a pain to express and it gets much worse if you want to depend on multiple variables. At this point, we have a list of constraints for our file format: Ideally, it would even be possible to mechanically translate existing .ninja files to this new format. This sketches out a new format that could improve over Ninja files. It could look something like this: I’d call this language Magma, since it’s the simplest kind of set with closure. Here's a sample action graph in Magma: Note some things about Magma: Kinda. Magma itself has a lot in common with Starlark: it's deterministic, hermetic, immutable, and can be evaluated in parallel. The main difference between the languages themselves is that Clojure has (equivalent to sympy symbolic variables) and Python doesn't. Some of these could be rewritten to keyword arguments, and others could be rewritten to structs, or string keys for a hashmap, or enums; but I'm not sure how much benefit there is to literally using Starlark when these files are being generated by a configure step in any case. Probably it's possible to make a 1-1 mapping between the two in any case. Buck2 has support for metadata that describes how to execute a built artifact. I think this is really interesting; is a much nicer interface than , partly because of shell quoting and word splitting issues, and partly just because it's more discoverable. I don't have a clean idea for how to fit this into a serialization layer. "Don't put it there and use a instead" works , but makes it hard to do things like allow the build graph to say that an artifact needs set or something like that, you end up duplicating the info in both files. Perhaps one option could be to attach a key/value pair to s. Well, yes and no. Yes, in that this has basically all the features of ninja and then some. But no, because the rules here are all carefully constrained to avoid needing to do expensive file I/O to load the build graph. The most expensive new feature is , and it's intended to avoid an even more expensive step (rerunning the configuration step). It's also limited to changed files; it can't do arbitrary globbing on the contents of the directory the way that Make pattern rules can. Note that this also removes some features in ninja: shell commands are gone, process spawning is much less ambiguous, files are no longer parsed automatically. And because this embeds a clojure interpreter, many things that were hard-coded in ninja can instead be library functions: , response files, , . In this post, we have learned some downsides of Make and Ninja's build file formats, sketched out how they could possibly be fixed, and designed a language called Magma that has those characteristics. In the next post, I'll describe the features and design of a tool that evaluates and queries this language. see e.g. this description of how it works in buck2 ↩ at least a basic version—although several of the features of GNU Make get rather complicated. ↩ is https://github.com/ninja-build/ninja/blob/231db65ccf5427b16ff85b3a390a663f3c8a479f/misc/ninja_syntax.py . ↩ technically these aren't true monadic builds because they're constrained a lot more than e.g. Shake rules, they can't fabricate new rules from whole cloth. but they still allow you to add more outputs to the graph at runtime. ↩ This goes all the way around the configuration complexity clock and skips the "DSL" phase to simply give you a real language. ↩ This is totally based and not at all a terrible idea. ↩ This has a whole bunch of problems on Windows, where arguments are passed as a single string instead of an array, and each command has to reimplement its own parsing. But it will work “most” of the time, and at least avoids having to deal with Powershell or CMD quoting. ↩ To make it possible to distinguish the two on the command line, could unambiguously refer to the group, like in Bazel. ↩ You don’t have a persistent server to store it in memory. When you don’t have a server, serializing makes your startup times much faster, because you don’t have to rerun the configure step each time. You don’t have a remote build cache. When you have a remote cache, the rules for loading that cache can be rather complicated because they involve network queries 1 . When you have a local cache, loading it doesn’t require special support because it’s just opening a file. You want to support querying, process spawning, and progress updates without rewriting the logic yourself for every OS (i.e. you don't want to write your own build executor). a "real" language in the configuration step reflection (querying the build graph) file watching support for discovering incorrect dependency edges We care about it being simple and unambiguous to load the graph from the file, so we get fast incremental rebuild speed and graph queries. In particular, we want to touch the filesystem as little as possible while loading. We care about supporting "weird" dependency edges, like dynamic dependencies and the depfiles emitted by a compiler after the first run, so that we're able to support more kinds of builds. And finally, we care about minimizing reconfigurations : we want to be able to express as many things as possible in the action graph so we don't have the pay the cost of rerunning the configure step. This tends to be at odds with fast graph loading; adding features at this level of the stack is very expensive! Negative dependencies File rename dependencies Optional file dependencies Optional checksums to reduce false positives Environment variable dependencies "all the features of ninja" (depfiles, monadic builds through 4 , a statement, order-only dependencies) A very very small clojure subset (just , , EDN , and function calls) for the text itself, no need to make loading the graph harder than necessary 5 . If people really want an equivalent of or I suppose this could learn support for and , but it would have much simpler rules than Clojure's classpath. It would not have support for looping constructs, nor most of clojure's standard library. -inspired dependency edges: (for changes in file attributes), (for changes in the checksum), (for optional dependencies), ; plus our new edge. A input function that can be used anywhere a file path can (e.g. in calls to ) so that the kind of edge does not depend on whether the path is known in advance or not. Runtime functions that determine whether the configure step needs to be re-run based on file watch events 6 . Whether there is actually a file watcher or the build system just calculates a diff on its next invocation is an implementation detail; ideally, one that's easy to slot in and out. “phony” targets would be replaced by a statement. Groups are sets of targets. Groups cannot be used to avoid “input not found” errors; that niche is filled by . Command spawning is specified as an array 7 . No more dependency on shell quoting rules. If people want shell scripts they can put that in their configure script. Redirecting stdout no longer requires bash syntax, it's supported natively with the parameter of . Build parameters can be referred to in rules through the argument. is a thunk ; it only registers an intent to add edges in the future, it does not eagerly require to exist. Our input edge is generalized and can apply to any rule, not just to the configure step. It executes when a file is modified (or if the tool doesn’t support file watching, on each file in the calculated diff in the next tool invocation). Our edge provides the file event type, but not the file contents. This allows ronin to automatically map results to one of the three edge kinds: , , . and are not available through this API. We naturally distinguish between “phony targets” and files because the former are s and the latter are s. No more accidentally failing to build if an file is created. 8 We naturally distinguish between “groups of targets” and “commands that always need to be rerun”; the latter just uses . Data can be transformed in memory using clojure functions without needing a separate process invocation. No more need to use in your build system. see e.g. this description of how it works in buck2 ↩ at least a basic version—although several of the features of GNU Make get rather complicated. ↩ is https://github.com/ninja-build/ninja/blob/231db65ccf5427b16ff85b3a390a663f3c8a479f/misc/ninja_syntax.py . ↩ technically these aren't true monadic builds because they're constrained a lot more than e.g. Shake rules, they can't fabricate new rules from whole cloth. but they still allow you to add more outputs to the graph at runtime. ↩ This goes all the way around the configuration complexity clock and skips the "DSL" phase to simply give you a real language. ↩ This is totally based and not at all a terrible idea. ↩ This has a whole bunch of problems on Windows, where arguments are passed as a single string instead of an array, and each command has to reimplement its own parsing. But it will work “most” of the time, and at least avoids having to deal with Powershell or CMD quoting. ↩ To make it possible to distinguish the two on the command line, could unambiguously refer to the group, like in Bazel. ↩

0 views
(think) 8 months ago

Burst-driven Development: My Approach to OSS Projects Maintenance

I’ve been working on OSS projects for almost 15 years now. Things are simple in the beginning - you’ve got a single project, no users to worry about and all the time and the focus in world. Things changed quite a bit for me over the years and today I’m the maintainer of a couple of dozen OSS projects in the realms of Emacs, Clojure and Ruby mostly. People often ask me how I manage to work on so many projects, besides having a day job, that obviously takes up most of my time. My recipe is quite simple and I refer to it as “burst-driven development”. Long ago I’ve realized that it’s totally unsustainable for me to work effectively in parallel on several quite different projects. That’s why I normally keep a closer eye on my bigger projects (e.g. RuboCop, CIDER, Projectile and nREPL), where I try to respond quickly to tickets and PRs, while I typically do (focused) development only on 1-2 projects at a time. There are often (long) periods when I barely check a project, only to suddenly decide to revisit it and hack vigorously on it for several days or weeks. I guess that’s not ideal for the end users, as some of them might feel that I “undermaintain” some (smaller) projects much of the time, but this approach has worked for me very well for quite a while. The time I’ve spent develop OSS projects has taught me that: To illustrate all of the above with some example, let me tell you a bit about copilot.el 0.3 . I became the primary maintainer of about 9 months ago. Initially there were many things about the project that were frustrating to me that I wanted to fix and improve. After a month of relatively focused work I had mostly achieved my initial goals and I’ve put the project on the backburner for a while, although I kept reviewing PRs and thinking about it in the background. Today I remembered I hadn’t done a release there in quite a while and 0.3 was born. Tomorrow I might remember about some features in Projectile that have been in the back of my mind for ages and finally implement them. Or not. I don’t have any planned order in which I revisit my projects - I just go wherever my inspiration (or current problems related the projects) take me. And that’s a wrap. Nothing novel here, but I hope some of you will find it useful to know how do I approach the topic of multi-project maintenance overall. The “job” of the maintainers is sometimes fun, sometimes tiresome and boring, and occasionally it’s quite frustrating. That’s why it’s essential to have a game plan for dealing with it that doesn’t take a heavy toll on you and make you eventually hate the projects that you lovingly developed in the past. Keep hacking! few problems require some immediate action you can’t always have good ideas for how to improve a project sometimes a project is simply mostly done and that’s OK less is more “hammock time” is important

1 views