Posts in Rust (20 found)

Why I Still Believe in Mentorship

One of the best tips I ever got was from my table tennis coach. He watched me play and said, “You are leaning forward with the wrong foot!” I’d been playing for years and thought I had a good stance. I’m left-handed, and no one had told me that I should put my right foot forward. I had copied the stance of other players without noticing the problem. It was obvious once he pointed it out. But until then, I hadn’t thought to question it, and more practice hadn’t helped me notice. That single observation improved my game more than any amount of practice because it gave me better control over my forehand play. Note that I didn’t actively seek out advice on my stance. Someone had to watch me play, observe, and then point out the single most helpful thing that would set me on the right path. Mentorship in Software In software, we are often expected to struggle alone. We’re supposed to learn systems design, architecture, testing, and refactoring largely by sitting with the problems. The times when I grew the most as a programmer were through thoughtful code reviews, but they are rare. Besides, the reviewer sees the end result but not the (sometimes flawed) reasoning that produced it. Pair programming gets closer. But two equally inexperienced programmers can reinforce each other’s bad habits. An agent can control the keyboard instead of helping you reason through a problem. It may produce better code without making you much better at writing it. A session where a machine fixes everything is productive, but being productive and growing as a programmer are two different things entirely. This is part of why I still believe in mentorship, even now that an LLM can answer so many programming questions. Going beyond the superficial questions I run a one-on-one Rust mentorship program , so I have a personal stake in this. Most of my mentees come with a concrete question: Often, answering the concrete question is beside the point. We have to look at the thought process that led to it. Before deciding whether something should be generic, for example, it helps to know what is expected to vary and why. Otherwise, we can spend a long time discussing how to build an abstraction that might be unnecessary in the first place. A mentor has enough distance from the problem to question its framing and enough experience to recognize familiar patterns. They notice which details you leave out, which patterns you reach for, and which you ignore. You may sense that something is wrong without having the words to describe it. LLMs can question assumptions, too! But I’ve found they usually work best when you already know what to ask. The difficulty is that we often don’t know which of our assumptions need challenging. Knowing what to ask can matter more than knowing the answer, and it’s hard to ask about something you haven’t noticed. You can also use an LLM to examine a decision, or you can use it as an echo chamber to justify it. The second use may feel more rewarding in the moment, but it leaves you where you started. Mentoring in Other Fields Nobody is surprised when a professional athlete has a coach. Musicians keep taking lessons after decades of playing. We don’t take this as evidence that they haven’t learned the basics. Quite the contrary: it’s an indication that they are serious about improving and that they’ve outgrown generic advice. It requires introspection to recognize that you can benefit from someone who can observe you from the outside. And in some sense, you put yourself in a vulnerable position: the wrong mentor can set you back, while the right one can dramatically accelerate your growth. Experience doesn’t remove the need for mentoring in software either. You can become an experienced developer while still carrying counterproductive habits you’ve never examined. If those habits let you get the work done, you may have little reason to suspect them and the problem gets worse because bad habits get reinforced. I could play table tennis with the wrong stance, too, just worse. The people I mentor are already experienced software engineers. They are perfectly capable of using LLMs in their work. Many are in leading positions as staff or principal engineers. And yet, they’ve realized there’s something missing that’s holding them back. They feel like they’re not progressing as quickly as they could, and their companies haven’t provided the mentorship they need. Friction Is Part of the Learning Process Another problem is that software gives us very uneven feedback. A syntax error gets your attention immediately because the code won’t compile. But a poor architectural decision can compile, pass the tests, get approved in review, and become a major problem months later when you lack the time to make changes. One way to learn architecture is to make bad decisions and sit with their consequences. That’s how I learned! The trouble is that it takes years, and it’s certainly not motivating. A mentor can streamline that process by asking questions you may not think to ask yet: There is no single “correct” answer to these questions. The answers depend on the constraints and the stage of the project. A mentor should help you examine those constraints and point out risks you’ve missed, while leaving the decision to you. If someone else chooses the design, you lose the practice of choosing it yourself. You also still have to write the code and live with all the consequences. But you go in with a better understanding of what might go wrong. And if it does, you have someone to help you work out why. That is how you develop judgment you can use on the next project. It’s a bit like climbing with a partner. Does Everyone Need a Mentor? If you’re just starting out, a book, a course, or a friendly community might be better value. If you need an answer to an isolated question, documentation or an LLM is faster and cheaper. If mentorship were only about transferring information, it would be harder to make a case for it. Personal mentorship becomes valuable when the potential payoff is a multiple of the investment. That is typically the case in a professional setting, where focused mentorship can help you land a better job or switch to a different team with a more interesting project. That doesn’t mean personal mentorship is a guarantee of success. Our busy days leave little time for deliberate practice. We have to carve out time for learning, which is hard, but the alternative is stagnation. As with going to the gym, paying for help doesn’t spare you the exercise. One hour of conversation cannot make up for a week without practice. You have to try things between sessions if you want the next conversation to be different from the last. Easier access to answers hasn’t changed any of this. What I want from mentorship is the kind of help my coach gave me: someone paying enough attention to notice what I couldn’t see for myself. That’s what I aim to offer in my Rust mentorship , at least: we work through hard problems together, explore the design space, and make deliberate choices. If I do a good job, my mentee will eventually surpass me, and I’d be proud to help them get there. Would you make this generic? Is it okay to clone here? How can I handle this error? Is this abstraction really necessary? How would an error propagate across the system? How can we test this?

0 views
マリウス 1 weeks ago

s/Neovim/Helix/g

A while ago I decided that I’d like to challenge my muscle-memory (and by that my brain) a little by pushing it out of its comfort zone. Last time I did that was back in 2024 with the switch from my endgame M60 to the Corne v3 and ultimately to the Kunai , which required me to re-learn to type, in a way. I remember how dreadfully inconvenient the experience had been in the first days/weeks, but over two years down the line I’m glad I made the move, and I feel like it has definitely improved my typing and my abilities, let alone my wrist posture. This time, I decided to challenge myself to switch away from the editor that I migrated to five years ago , Neovim, to Helix , the post-modern text editor . I had wanted to do this for a long time, and it just so happened that yet another troubling story unfolded within the Neovim community that had once again put the unprofessionalism of the core team on full display. And after years of the project being infiltrated and virtually taken over by bullshitters content creators , I decided that it was about time to move along and start this challenge. Despite many people in the community channel seemingly being loyal members of the Church of Emacs , I didn’t want to go back to an editor that required a GUI, because I enjoy the ability to fire up the exact same editor on any (remote) host (via Mosh ) and feel at home right away. After all that was one of the reasons that pushed me to leave the impeccable editor that is Sublime Text . Note: I know that there’s a TUI version of Emacs, but from what I read online, as well as in the community channel, it is inferior to GUI Emacs and people only use it when they have no other choice. For remote editing it appears that TRAMP is the way to go in Emacs. Anyhow, with Emacs being predominantly GUI-based, it didn’t quite check the boxes for me. I wanted something that is primarily a text editor and not an almost complete operating system. In addition, while I was never particularly fond of Neovim’s Lua, I’m even less so of Lisp, which would have added another layer of reluctance to the whole challenge. After scouring the web for a capable alternative to Vim that would ideally be written in Zig, I ended up with two options, neither of which is Zig-based though: Kakoune and Helix . Kakoune seemed like a very good candidate for something that is similar in spirit to Vim but offers a handful of differentiating features that would make it an interesting pick. However, because it is so similar to Vim, it also has similar requirements with regard to its configuration, making the ramp-up curve relatively steep. For example, just like Vim/Neovim requires you to spend a few hours/days/weeks to properly and thoroughly configure language servers, Kakoune, too, ships those as an external plugin that runs alongside the process and is in fact not even written on top of the same C++ stack that Kakoune uses, but in Rust. Kakoune is practically the same assembly of little bits and pieces that Vim/Neovim had always been, which comes with the same benefits, like endless extensibility, but also with the exact same drawbacks with regard to configuration complexity, breaking changes/incompatibilities, and a noticeable hit in terms of efficiency. My final Neovim config wasn’t as bad as what setups like LazyVim, LunarVim, SpaceVim, et al. usually deliver, but as soon as I was working with large files I could definitely feel the editor and, more precisely, its plugins, putting some load on my system. Hence I found the value proposition of Helix very interesting. One editor, one binary, with complex features like language-specific autocompletion and syntax highlighting built right into the core. Not only does this mean that the efficiency aspect might be noticeably better ( spoiler: it is! ), but also that it doesn’t take me hours/days/weeks to set up what I would call absolute basics in 2026. The only true downside of Helix seemed to be that it’s a Rust project, meaning that compiling takes almost the same time as building the Linux kernel, and that it might be yet another project that’s one wrong pronoun shy of falling apart. Nevertheless, I caved in to the curiosity and decided to try Helix. Both of the things that I had to unlearn first come from the same design decision, namely that Helix puts the selection before the action, so that every motion produces a selection and every command works on whatever is selected at that moment. In Vim I press and the editor waits for a motion that tells it how much to delete, while in Helix deletes what is already selected, and because a resting cursor is a selection of exactly one character, cuts a single character. There is no , because is not an operator that takes a motion. Instead, removing a line is to select it and to delete it. The same principle explains why the cursor moves to the next line when I press at the end of the current one. Helix treats a buffer as one sequence of characters instead of a list of lines, and moving right asks for the next grapheme boundary in that sequence, which after the last character of a line is the line break itself, and after that the first character of the line below. Vim stops at the last character unless is configured otherwise, but Helix has no equivalent option to switch the behavior off. What also follows from the selection-first model is that multiple cursors are built-in. selects the entire file, prompts for a regular expression and turns every match inside the current selection into its own cursor, copies the selection to the next line, splits a selection on line breaks and reverts back to a single cursor. In Neovim I would have renamed something using , but in Helix it is , , the pattern, and then and the new text, with every occurrence visible and editable while I type. Frankly I didn’t mind the way Neovim did it, but Helix’ approach is definitely more visual . Speaking of which, Helix ships by default with another visual feature that makes discoverability a lot better. In Neovim I had to manually configure the plugin for that, but in Helix pressing a prefix key automatically shows an infobox listing every continuation together with a description of what it does. Also, completes typable commands and shows their documentation and their flags while I type, and opens an interactive introduction that ships inside the binary. The list of things that work without configuration in Helix is frankly longer than I expected. The that ships with 25.07 defines 342 languages, pulls in 303 tree-sitter grammars and preconfigures language servers for 189 of those languages. Helix also supports files. Syntax highlighting, autocompletion, diagnostics, fuzzy pickers, a git diff gutter and a debug adapter client are all there without a single line of configuration, where my Neovim setup needed three dozen plugs (that would break at least once a quarter) to support roughly the same feature set. And it’s not like Helix’ configuration is some interpreted Lua glued on top of Vimscript. It is a set of easy to read and understand TOML files, and consists of a , a and, optionally, a . The Neovim setup I linked above was 35 plugins and 1,787 lines of Lua across 21 files, plus another 678 lines for my Netrunner theme, which, reading it, makes it even more absurd. What replaced it is a 65-line , a that includes everything upstream has plus a few overrides, and a 264-line , which is my own theme. And let’s not forget that each of those 35 Neovim plug repositories is maintained by someone else and can change at any time, and each of them could hit me with malware at any moment. The contains the general editor configuration, as well as the custom keybindings. Those can be nested, they can call typable commands, and when they start with they replay a key sequence, which means that the simple but stubborn muscle-memory can be satisfied, at least in some cases: Note: Commands and key sequences sadly cannot be mixed, which means that a binding like is not possible. Whatever feature the core does not implement is left to the shell, for which can be used to pipe the selection through a command and replace it with the output. Similarly, can be used to insert the output of a command before the selection, and can be used to only keep the selections for which a command exits with a zero status. can sort a block of lines, can align a table, and can reflow a paragraph. And as with most command line tools, you can use to suspend the editor and type to bring it back. Speaking of muscle-memory: There is a fork called Evil Helix that restores the Vim keymap for people who want the new core without having to re-learn the motions and commands. While I understand the appeal, installing it would have defeated the point of the exercise for me. However, if you’re on the lookout for an alternative editor to escape the rollercoaster that the Vim ecosystems have become, this might be the thing for you. To put the efficiency claim that I mentioned earlier into actual numbers, I built Markdown files by concatenating the last 180 posts of this site and cutting the result down to 100 KB, 1 MB, 5 MB and 25 MB, the largest of which is 543,015 lines. I ran Neovim with my old configuration, with all 35 plugins and the 70 tree-sitter parsers compiled. Both editors ran on the same 8-vCPU/16GB RAM KVM virtual machine that I used to simulate a resource-strapped environment on my Intel Core Ultra X7 368H . Both used a terminal window of 50 by 200 characters, and every number is the median of three runs after a warm-up. First screen describes the point at which the initial paint completed, and redraws done is when the editor stopped writing to the terminal. As you can see, Neovim paints its first full screen roughly five times faster, in 0.12s against 0.62s on the 1 MB file, because Helix appears to read and parse the whole file before it draws anything. Neovim, instead, draws immediately and keeps working in the background. So much for my assumption that the Rust binary would win the efficiency duel, huh? Well, not so fast, cowboy! As you can see in the next columns everything after that initial load/first paint goes the opposite way. Opening the 5 MB file costs Neovim 300 MB of resident memory and 2.9 seconds of CPU, against 85 MB and 0.6 seconds for Helix, and at 25 MB it is 329 MB and 4.1 seconds against 111 MB and 0.7 seconds. Helix stays close to flat across a 250-fold range in file size, while Neovim’s cost grows with every step. As for the syntax parser, both editors have a deadline and drop highlighting when it is missed, which I measured by counting the distinct foreground colors on a rendered screen whose content is identical in every file. Helix highlights reliably up to about 1.02 MB, intermittently up to 1.12 MB and not at all above 1.13 MB, while Neovim is solid at 2 to 3 MB but stops at 4 MB. That is also why Neovim finishes redrawing the 5 MB file faster than the 1 MB one, since past its own deadline there is a lot less left to do. On the 100 KB file, where both editors highlight the entire screen, Helix needs 38 MB against 65 MB and 0.5 seconds of CPU against 0.8. Jumping to the last line redraws consistently in about 43 ms in Helix, and between 21 and 111 ms in Neovim, so neither of them is really sluggish at that, but Neovim can, at times, take almost three times the time to redraw. My of six languages in Neovim, which I had assumed to be one of the expensive parts, doesn’t really cost anything, as Neovim with and without spell checking is within the presented numbers. Many lines are one problem, however a single line of extreme width is a very different one, and it is one of the things that would normally bring Vim down to its knees. I joined the same 180 posts into one line of 5,201,084 characters, 5 MB in total, and ran the same session in both editors: search for , step through the matches, replace all of them with , jump to the last character with and to the first one with , and then insert single characters at the very beginning. Every number is the time from the keystroke until the editor stopped drawing and stopped using CPU. Helix opens that file almost nine times faster and finds the pattern quicker. Neovim, however, beats Helix in the replacement task by a wide margin, because is a single operation while Helix builds 471 cursors and then edits every one of them. Inserting a single character at the beginning of that line costs Helix about a quarter of a second. Neovim pegs one core at 100 percent and allocates roughly 25 MB of memory per second while drawing absolutely nothing, and it kept doing that for the two minutes after which I cut the measurement off. By that time the session was at 9.1 GB of resident memory, but the character still hadn’t made it into the buffer. To be fair to Neovim, this one is not its direct fault, though. The exact same file and the exact same keystroke in inserts the character in well under a second and drops back to zero CPU at 150 MB, so what melts down here is my configuration, which is most likely the combination of spell checking and tree-sitter on a line that is five million characters wide. If you think that what I wrote about Helix so far reads too good to be true, and that there’s definitely a catch somewhere , then your intuition still works. For example, one thing I run into every day is the front matter of the posts on this site. In Neovim, a small plugin rewrote the field of the Markdown file on every write, and set the of a post I had just created. Helix, on the other hand, does not have a built-in feature like that, nor does it offer a user-facing API to custom build this. The only thing that runs on write is the that is configured for the language, which takes the buffer on standard input and writes it back on standard output. A front matter updater therefore has to be a filter over the entire document, and it has to coexist with whatever formatter is already configured for Markdown. Inserting a timestamp by hand is , which is fine for the of a new post, but definitely more cumbersome for a that is supposed to be maintained without me thinking about it. Another example concerns the keybindings. A binding made of two subsequent combinations works, so a section that contains gives me a chord. Pressing the first half pops up the infobox that shows the second half. What doesn’t work, however, is having the same key bound on its own and as the prefix of a longer sequence, which is what I had in Neovim: The keymap’s nodes can be either commands or submaps, but Helix doesn’t support a timeout that would disambiguate the two the way Vim’s does. Hence a key is either a binding or a prefix, and for quitting next to for quitting without saving is sadly not something one can express in Helix. A handful of smaller things from Vim have no equivalent either. There are no marks, and the “Marks” issue has been open since September 2021. Getting back to a position therefore requires a search or the jumplist. There is also no persistent undo, which means that closing a buffer discards its history. Two attempts at adding this feature were closed without being merged. There is also no session restore, and repeats only the last insert, not the last change the way it does in Vim, so a delete or a replace can’t be repeated with it. What’s left is to repeat the last motion and to jump to the last modification. Spell checking is another thing that’s missing entirely. Vim and Neovim ship with dictionaries, but Helix has nothing of the sort, so the only way to get my typos underlined is via a language server such as codebook or , configured per language in . Then there’s the Git support, which is basically just the diff gutter. Helix marks added, modified and deleted lines against by default and and jump between the changes, however there is no blame and no staging of individual hunks. and or a second pane can cover for that, but for people who would like to avoid the context switch for adding files or committing, an integration of the most basic Git functions would make sense. Speaking of files, the file picker gained a directory-browsing mode in 25.07 ( #11285 ), but a persistent tree sidebar of the neo-tree kind still doesn’t exist in Helix. I know that you’re not supposed to navigate via the sidebar and that fuzzy-finding and buffers are theoretically superior, but I also know how a big project structure can sometimes become increasingly difficult to efficiently fuzzy-find through. Lastly, the plugin system. There is no plugin system. Steel, the Scheme dialect that might become one, has been a draft pull request ( #8675 ) since October 2023, and it has 491 commits and 334 comments without being merged. It appears that in various forks it is already in use, which makes it even weirder. But that is essentially why there are no user-defined commands, no scripting and no ecosystem. However, it is also why nothing in my configuration has broken since the day I wrote it. Both things are consequences of the lack of plugins, and I am not sure yet which of the two I value more. Now to some more essential things that I would argue are crucial for many engineers and developers, but that are nowhere to be found in Helix. Amongst the most important features is continuous hard wrap. Helix does soft-wrap, at when is set, and it does hard-wrap on demand with , which rewraps the selected lines to the width given as an argument or to the configured . What it doesn’t do is wrap while I type, the way together with does in Vim, and the issue has been open since April 2022. Selecting a paragraph and calling from a keybinding works but it is obviously very cumbersome. What I have settled on for now is an on-save formatter such as or , which however means that the formatter rewraps the whole file including the paragraphs that I never touched, which turns a three-line edit into a diff over an entire file. Another essential that’s missing is the file watcher. Helix doesn’t watch the files it has open. When a buffer changes on disk, after a , a rebase, a formatter that ran in a different window or a sync over Syncthing, the editor keeps showing the old content until I run or . It does at least reject the write with , so nothing gets overwritten by accident, however all of the reloading is manual, while Neovim covers this with and . The pull request that implements file watching was opened in October 2025 and is still open. Then, there is the overtype mode, which also doesn’t exist at all. In Vim, replaces characters as I type until I press escape, which is how a fixed-width table, a box drawing or a column-aligned configuration file gets edited without pushing everything to the right. Helix uses to replace every character of the selection with a single one and to paste over the selection from a register. However, neither of them is sticky. The request has been open since February 2023 and a pull request implementing it since July 2025, and until that is merged, the workaround is selecting exactly as many characters as I am about to type and then replacing them. Speaking of selections, a line-wise selection mode is missing as well. Vim’s selects whole lines and and then extend the selection in both directions, while Helix has to extend the selection one line downward, to snap whatever is selected to whole lines and to shrink it back to them, none of which extends upward. Binding covers most of it, however extending upward still means entering select mode, moving with and pressing to put the selection back on line boundaries. Lastly, code folding doesn’t exist, and it is the one complaint I came across most often while reading through the issue tracker, although to be fair I personally don’t use this feature as extensively, so it’s not as much of a big deal for me. The issue has been open since March 2022 and has 48 comments, and a pull request has been open since October 2025. Vim has been folding since version 6.0 in 2001 and Neovim added tree-sitter-based folds on top of that, while Helix offers neither automatic nor manual folds, which for navigating a long file leaves the jumplist, the symbol picker and scrolling. With all that said, I’m going to stick to Helix for the time being, despite its bad and ugly parts. It is an overall solid piece of software, it has a relatively big community around it, and it is available on every platform that I care about. On top of that, it uses a fraction of the memory and CPU of the Neovim setup that I had in place before. It is also far easier to build on my Gentoo machine than Neovim. However, I do hope that Helix will continue to evolve into something that is able to offer slightly more flexibility than it does today, without compromising too much on its approach of delivering an editor with batteries included. I, too, hope that we’re not going to witness similarly poor stewardship as has been the case with Neovim, and that the Helix project can continue living somewhat in the shadows, overlooked by most content creators .

0 views
Xe Iaso 1 weeks ago

It took a year to ship WebAssembly in Anubis

After a year of work, hundreds of commits, 5 generations of pull requests, dozens of tests, rewriting part of Anubis in Rust, the first compiler bug of my career, and at least three times making my tower run out of ram I think I have finally done it. The next version of Anubis will ship with WebAssembly-based proof of work checks that admins can enable in their thresholds or bot rules: This makes Anubis challenges use a memory-hard proof of work function ( argon2id ) instead of just a CPU hard one. It also means that the "hey Claude vibeslop me a CUDA Anubis solver" route is on its way to being fundamentally dead. Overall, this project taught me a lot of things about how WebAssembly works in practice, what the rough edges are when operating on the bleeding edge like I am, and led to the first genuine compiler bug of my career. Today I want to take you through the journey and process behind adding these WebAssembly based proof of work checks and all the problems that came up along the way. The big impetus behind wanting to use WebAssembly in Anubis is that there's a constant tension that's underlined a lot of the performance work I've been doing: phone CPUs suck and trying to balance equitability to phone CPUs with trying to punish scraper CPUs is a huge challenge. Note To be clear: I am tackling the "Anubis makes my phone overheat" problem. It's hard to just lower the difficulty for phones without giving attackers enough information to lower the difficulty for scrapers. If you want to contribute performance data so that I can improve my classification process and/or target future development, please get in touch with me . I want to make things better but can't without data. I am actively testing with a Moto G8 Power alongside my normal testing steps. Using WebAssembly here means that the binary will run faster, which will make Anubis go away faster, which is what we all want, right? One of the other big advantages of this setup is that it lets clients and servers run the same binary in order to solve and validate challenges. Note that I didn't say the same code, I said the same binary . This means that the client and server are in lockstep, much like the CIC chip in the NES . This also does mean that a bug in the shared code means that the client could send an invalid value that the server would accept as valid, but I don't think this is a practical concern until something like that happens. Right now the challenge has two implementations in Anubis: one in JavaScript and the other in Go . Updating it in one place means making the same updates in other places and also making sure that everywhere else that assumes how challenges work is also updated. Note Yeah, it's probably bad to have made those assumptions about how challenges work across the codebase. There's a lot of weird technical debt here that needs to be solved at some point. I expect that to be a fair bit of effort to untangle, it may end up with keeping the challenge only for tests when the WebAssembly based routes are working out well enough to make the default/only option. Making the same binary run on both the client and the server means that future experiments like per-client program synthesis can also proceed. I'll get more into that sometime in the future. Right now the core of Anubis is written in Go. Go's standard library HTTP server is surprisingly performant and is the key component of things like Google's HTTP frontend service. I have no intention to rewrite Anubis in Rust or anything as drastic as that. However breaking up Anubis from one monolithic binary into what amounts to a plugin loader is a lot more interesting from a maintenance standpoint. One of the main problems with Anubis is that the combination of rules I've set for myself (no CGo allowed and everything must cross compile to prod from my MacBook) means that updating any part of Anubis means recompiling all of Anubis across prod. Making Anubis be able to have parts of it downloaded and swapped out at runtime is really interesting to me because it means being able to adapt to threats as soon as the threat actors change behaviour . Also I figured out pretty quickly that Rust builds some of the smallest WebAssembly binaries that run hilariously fast, so I wrote all the WebAssembly code in Rust (no-std, wasm32-unknown-unknown)! Along the way this also lets me fix one of the bigger administrative problems with Anubis: challenges scale incorrectly with the challenge. Anubis uses string comparison to count the number of leading zero nibbles in a hash instead of the number of leading zero bits . This means that adding one (1) to the difficulty of a challenge makes it 1024 (one thousand twenty-four) times as hard to solve in the worst case. The original post has an interactive diagram that shows the difference in scaling at play. This was a mistake in retrospect, but if we're changing details about how challenges work here then we may as well wrap up the fix into that. In a normal world you'd not have about half of the restrictions that I have when working with Anubis. Normally you just write your code, compile it to WebAssembly, and then make sure it runs on modern browsers. This is nice and simple. I wish I could live in this world. Comparatively, this is what it's like getting all of this working across browser versions, platforms, and so many other things: Anubis supports Chrome 75 and newer. I wanted to reduce the support range to not have to deal with Chrome that old (the feature difference between Chrome 155 and Chrome 75 is absolutely massive ), but there are a lot of smartphones, smart TVs, and other electronic devices running Android out there that are just marooned on Chrome that old with no path to upgrade it . As a result, I gotta target Chrome 75 for features, but out of an abundance of caution and to make sure that things are generally compatible with browsers older than Chrome 75 (eg: old iOS releases), I target my JavaScript to Chrome 66 as an abundance of caution. I haven't been able to test with iOS or Android versions of the same vintage as Chrome 75, so a lot of this is aspirational backwards compatibility. I sure hope it's good enough! When you're doing this kind of work, you're effectively writing an operating system kernel that makes the WebAssembly guests run as processes. Any calls you pass to the WebAssembly guest are effectively system calls into the host. Normally I'd love to be able to reuse the WebAssembly Component Model so that a lot of the "hard part" is done for me. WebAssembly Components make you define your calls in packages that contain interfaces with functions in worlds. Here's an example of the kind of WebAssembly Interface Types world Anubis would need: If this worked (I wrote this all out in one go based on how the current handcrafted ABI works and have not tested it, sorry), this would describe the shape of the API that we need for doing the proof of work operations. At the time I wrote the API/ABI that Anubis uses, the main tool for generating the server-side bindings for WebAssembly Component Model stuff in Go gravity didn't support passing records (structs) or bytestrings ( or / ) from the host to the guest. As such, I had to do it by hand. So given that we can't do it the "right way", we have to do it the "bad way". Also given that no matter what I pick for this I'm going to be "wrong", I just decided to treat the WebAssembly modules as dynamic libraries that just happen to use WebAssembly as an implementation detail. There only needs to be three buffers that are read from / written to, so let's just focus around those: Challenge modules also expose two entrypoints: Challenge modules also import from the environment so that they can periodically report their hash rate back to the frontend. This allows users to see a progress bar based on how long it should take to finish the process. This works enough for now. It'll be interesting to see how this falls short in the real world! Honestly, the first bit of this took a few days at most. Most of the hard work was making sure that pointers, offsets, and whatnot were all wired up correctly so that the browser worked the same way as the server. My experience building and messing around with many WebAssembly runtimes and egregious hacks meant that making it was really easy for me. The devil came out in the details. Here are all the things that came up while I was working on this. All of these problems added up is the sole reason that this took a year instead of a week. Early on in development I found out that WebAssembly has a SIMD extension . SIMD stands for Single Instruction Multiple Data and is a family of instructions that let you do operations on multiple values at a time. This gives programmers data-level parallelism (this is distinct from multi-threading) so that things like hash calculations and MP3 decoding can be done faster than they would be if each operation had to be its own instruction. CanIUse considers WebAssembly SIMD to be "baseline" (supported by browsers newer than Chrome 91), but I have a lower version bound of Chrome 75. However the benefits from SIMD on mobile devices are so drastic that it's worth having two builds of the WebAssembly code: one with SIMD and one without it. In the browser it dispatches which version to use by using wasm-feature-detect to probe WebAssembly functionality by trying to parse/run trivial minimal programs that exercise those features. Hopefully I don't need to add an additional build into this process, but there is nothing in the tooling that would prevent it! One of the big things that blocked this shipping for so long was not having an escape hatch of some kind to allow clients that disable WebAssembly by policy to get through the gate. In my experience most clients don't have JavaScript enabled but WebAssembly disabled, however there are a few notable usecases that forced my hand: iOS Lockdown mode and GrapheneOS' Vanadium 's default configuration. This combination of factors means that there would need to be another implementation of the proof of work code in JavaScript that would actually execute the number crunching. I don't want to make another implementation of the proof of work function (the entire point of this is to only have one implementation!) so while I was browsing around I came across the legendary talk The Birth & Death of JavaScript and got a horrible idea. What if you just compiled the WebAssembly to JavaScript? Would that even work? It's "just" turning one Turing machine into another, right? How would it fare in practice? Turns out I'm not the first person to think about this! The team behind binaryen have made this escape hatch in the form of wasm2js which takes WebAssembly binaries and produces moderately cromulent JavaScript in response. One of the main downsides is that the generated binaries tend to be rather large. For example consider this simple WebAssembly module that exposes a function that adds two numbers together: Seems simple enough, right? Here's the JavaScript that generates: As you can imagine, this only gets progressively worse as you end up making the Rust standard library get compiled from WASM to JavaScript, and even worse when you actually get hashing functions into the mix. The end result is probably very optimized when you run it through a JIT, but given that this runs on an interpreter it's probably gonna be slow no matter what I do. Sorry! I tried! This ended up working fairly well in testing, but I tried building it in GitHub Actions and ran into an issue. I noticed that wasm2js was packaged in Ubuntu and that the version in Fedora worked fine but the version in Ubuntu did not and threw an obscure error message about not understanding the tail call extension that Rust was using for some reason. I ended up bisecting versions of binaryen by downloading a tarball, building it from source, and then seeing if the result of running it on the Anubis WebAssembly modules worked in a browser. I ended up selecting Binaryen version 128, the newest version at the time. It was new enough that most of the distributions that package Anubis don't have that version of Binaryen packaged. However I didn't want to make my life more complicated by having some kind of conditional compilation step that would effectively tell end users "sorry, the admin is using an unofficial build that just so happens to not support your browser, please complain to them" because they'll just end up complaining to me. In my experience the kinds of people who run this exact combination of circumstances also tend to be the kind of people that have a wide variance in the level of kindness they display to the authors of open source programs that happen to be in their way. So I needed an escape hatch that would force build systems to use the exact version that I use in my builds. Then inspiration hit me as if Apollo himself sniped me from the heavens. We're dealing with WebAssembly here right? What's stopping us from just compiling the WebAssembly to JS tool to WebAssembly with some kind of reproducible build, committing that blob to the repo, and then moving on with life? I ended up finding a bug in LLVM around how it was iterating over exception handling blocks by the compiler iterating over them in machine pointer order. As a result each build would drift by about 29 bytes per build: This is what lead me to write I hate compilers as a combination blogpost/cry for help which made me realize this was actually an LLVM bug. I didn't instantly lean towards it being an LLVM bug until I figured out that disabling ASLR (via ) made the results consistent on the same host within the same boot. Honestly this is the first time in my career I've ever run into a compiler bug like this. When I do a lot of my work I usually work under the assumption that the compiler is bug-free and that my inputs are wrong somehow. As such, even thinking it could possibly be an LLVM bug was just outside of the realm of possibility for me. Once that LLVM bug got fixed and a new version of wasi-sdk with that fix got released, I was off to the races, updated my build of wasm-opt/wasm2js, made my build scripts run it with wasmtime (alongside a wazero-based fallback process that would be slower, but did work enough) and everything worked out. Every time Anubis builds the WebAssembly in CI it uses the version of wasm-opt and wasm2js that ships in the repo to ensure that everything is as byte-for-byte deterministic as possible. Your build tools can't differ from my build tools if I ship you the build tools I use. Then we get into the other big problem that made this difficult: browser testing. One of the most common failure modes of Anubis is that someone uses some browser that I don't test in CI and then things don't work with it. I'm tired of installing 50 different browsers on several machines to test things and I have gone through so many throwaway VMs that I'm sure it's reduced the lifetime of my SSD. Note Yes, I really have been testing Anubis by hand in god knows how many browsers. Why do you think it takes so long to tag new releases? I built a harness that I call "chromesweep" that lets me spawn many Googles Chrome (term c.f. Attorneys General, et.al) in their default configuration to try and hit a version of Anubis listening over HTTPS. Getting this far meant making a library of all of these browser versions. I've put that library up on Github at TecharoHQ/gubal in case it's useful for you. One of the other big problems I ran into while getting browser testing working was making sure that my Googles Chrome strictly stay within the bounds of my Kubernetes cluster's network. Chrome this old is actively radioactive and I want to treat it like the security threat it is. As such, I set up a strict NetworkPolicy to only allow it to access the Anubis instance under test and make DNS queries. I also wrap each Chrome pod in a microVM with Kata containers as an additional layer of security. Note It honestly terrifies me to think that I am putting more effort into securing these Googles Chrome than big AI companies are putting into securing their AI agent testing infrastructure . It literally doesn't take much to put a big dent into securing things! The current state of our industry boggles the mind. All I want is for Techaro's FelonyBench score to remain at 0, is that too much to ask? I also rigged the browser testing infrastructure up to a single slash command in GitHub pull requests. Doing it makes my office very warm so I try to avoid doing it when possible. This works well enough that it lets me move on to the next stage and has already caught something that lead to building all the JavaScript with the flag. I wonder if this is yet another case where making infrastructure for Anubis could result in that infrastructure alone being its own viable tech product. I run into a lot of those. In the process of doing that automated browser testing I found out that Chrome 75 had a weird error pop up when it tried to compile Anubis' WASM to native code: This also caused failures up to Chrome 100, so this signaled to me that something I was doing with my "strict MVP" build of Anubis' WASM wasn't in fact sticking to just the MVP features of WebAssembly. It turns out that the function referenced (probably somewhere in std::sync::Once ? I probably should have traced it down to the exact bit) was in the standard library. This surprised me because I assumed that building Rust code with CPU features selected would apply that to everything, including the standard library, right? No, turns out that when you download the component in rustup , that doesn't just download the standard library. To aid in cross compilation and I guess to avoid disk space waste, the Rust standard library is precompiled. This surprised me as Go typically has you recompile the standard library (and runtime for that matter) when doing normal builds and cross compilation. Note The actual issue here is that the stdlib function in question was compiled down to use reference types , which made references get stored as a table index instead of what MVP WebAssembly would put there. Chrome tried to read a null byte, got not a null byte, and then understandably exploded. I looked into the process involved for rebuilding the standard library twice: once with only MVP wasm features enabled and once with an "all yes config" like usual. Based on some research I did this seemed like a massive pain. However, I had gone through that effort to build reproducible WASI versions of wasm2js and wasm-opt. wasm-opt is a tool that lets you take compiled WebAssembly modules, optimize them, and more importantly remove features from them so they can run in older browsers. After a bit of hacking to make sure that the tools were able to run properly, I set up a Claude Opus / GLM 5.2 loop to fuzz various wasm-opt flags and make sure Chrome 75 could parse the output. I ended up with these flags: This strips away all the other WebAssembly features from the build like unwanted paint (the flag means "disable everything not in the original MVP definition of WebAssembly"). I think that it'd be safe-ish to enable reference types in the SIMD build (they were added before Chrome added SIMD), but it's not hurting anything to remove them so I'll just let cowardice win here. Either way, I threw the results into chromesweep and got a successful response, so win! If/when this comes to bite me I'll try and improve it. I'm pretty sure that this work isn't perfect , but at some point you gotta cut your losses, ship it, and then see where things fail to prioritize perfecting it. These issues include but are not limited to: Overall though, I'm hopeful that most of the worst parts of this can be solved. It would be nice if I didn't have to work what amounts to two full time jobs. I'm pretty sure that this is stable enough to ship as off-by-default in Anubis v1.28.0: Wuk Lamat . Based on the feedback I get from administrators and users, I'll enable it in the default configuration in Anubis v1.29.0. I hope this look into how Anubis is developed can give you ideas as to the scale and challenge involved. Making something like this is tireless and thankless work and it's really weird to see people talk about it in the same breath as Cloudflare or AWS' WAF. Have a good day all! Note AI was not used in the production of the prose of this article. I have my draft as a Google Doc so you can see exactly where and when I typed every word myself. The only use of AI was Claude Opus to help me make the visual bit/nibble diagram. The data buffer: up to 4096 bytes of challenge data. This is 4096 bytes so that it can (hopefully) land in its own 4Ki machine page. This is the only variable-length buffer in the setup, so it needs two calls: : return the pointer to the data buffer in WASM linear memory. This is used as the base address for copying data into the guest. : update the globally mutable "data length" variable to signal to the guest how much data was actually written into the data buffer. The combination of these two calls lets you treat that global data buffer as a slice. The result buffer: a challenge-defined buffer that usually has about 32 bytes of data. This is read out of the guest's linear memory when the challenge is done processing. : return the pointer to the result buffer in WASM linear memory. This is used as the base address for reading out of the guest. : return the length of the result buffer (a compile time constant based on the needs of the challenge, but the runtime can't know that). The verification buffer: a challenge-defined buffer that usually has about 32 bytes of data. This is written into when the server is validating a challenge. : return the pointer to the result buffer in WASM linear memory. This is used as the base address for reading out of the guest. : return the length of the result buffer (a compile time constant based on the needs of the challenge, but the runtime can't know that). : the main entrypoint for browsers. Given the data loaded into the challenge buffer, hash it in a tight loop until you get a solution that matches the difficulty. : the main entrypoint for the server. Given the data loaded into the challenge and verification buffers, ensuring that one run through the hashing function produces a result that both meets the difficulty demands and exactly matches what the client sent. The wasm2js flow doesn't currently have a way to update the progress bar with its import stubbed out. I have no idea how to properly wire that up with the constraints of my runtime, but I'm sure I can figure it out eventually. The WebAssembly that's shipped with this flow is ridiculously performant. This may mean you need to adjust the difficulty to compensate for this. Oops! Sorry! This is better on mobile phones, but I'm working on a mobile request classifier that will use a combination of IP address reputation, TLS fingerprinting, and other signals to determine if a request is from a phone and give it the appropriate amount of grace. This is hard. Please contact me if you have ideas on how to make my current prototype better. I need some way to dynamically rotate out challenge programs at runtime instead of just at compile time when I work on Anubis. Eventually I hope to have this pull WebAssembly files and supporting bundles from OCI/Docker registries, but I need to do a lot more experimentation with this before I can conclude how good/bad of an idea this is.

0 views

From Omarchy to Pop_OS!

I fell for the hype for a bit and was playing around with Omarchy on my ThinkPad. While I did like some of the opioniated setup (the theming system is very cool), a number of things bothered me. First and foremost, the controversy surrounding DHH is hard to ignore. I wasn't particularly well versed in it, but after doing some research I didn't feel great about using the distro. The second issue was the amount of AI. I said in my original article that some of the AI integration was pretty cool, and I still think so. Debugging a system crash with a model or leveraging AI to understand complex config files is a great use. But the problem is that everything is AI. It's a vibe coded Arch config, and that doesn't instill a ton of confidence when it comes to a daily driver. All that to say, I'm now testing out Pop_OS! with the new Cosmic desktop. I've used Pop_OS! before, back when it was basically a GNOME shell extension. I even own a System76 laptop and Launch Heavy keyboard. I support what the company is doing, and their CEO as done actual good (like fighting against age verification in OSS). It's a product I can feel good about supporting. So far, I'm pretty impressed! Cosmic has come a long way since the last time I tried out a beta. The theming is goregous and very customizable, workspaces is done very well, and the tiling is better than what's on offer in Omarchy land. I love that things were crafted rather than slopped together, and being built on Rust is a strong foundation. There's still some things missing, but overall I look forward to daily driving Pop_OS! and Cosmic. Heck, I might even try revitalizing my interest in coding by creating a app or two using their UI kit!

0 views
Armin Ronacher 1 weeks ago

Latent Powers

A few weeks ago I felt like it would be fun to see if I can make one of those cheap Chinese CarPlay dongles run something other than the stock firmware. The idea was that rather than just forwarding CarPlay, why not do something more interesting with them? They all work quite similarly: they act as bridges between your car and the phone. From there they deal with video and audio streams and pass some other data through. Most of them also bring up a custom UI for pairing and have a web interface that your phone can reach for updates. Long story short: I had a conversation with Fable and Sol via Pi about what could be done with such a dongle or whether I should use a Raspberry Pi instead if I wanted to do my own thing there. I figured it might be quite fun to run my own code while still allowing regular CarPlay to pass through. Through working with the LLM I learned about CatPlay , which is a Rust reimplementation of the CarPlay protocol that can run on Carlinkit devices. In particular, it can run on the Carlinkit Mini Ultra, which I figured would be easy enough to buy. I do have a few CarPlay adapters around, but I did not have that particular model, so I bought one on Amazon. Twenty-four hours later, I had a device in my hand that was branded as a Carlinkit Mini Ultra, but instead of being the Ingenic device that the original author used, it turned out to be something else. This is normally where the story would stop. However, it’s 2026. Armed with a bit of knowledge about how these systems work, I managed to have some fruitful discussions with Kimi K3 and Sol and figure out how flash the device and in turn, how to make CatPlay compile for that SoC. I guess that hacking these USB devices is not necessarily hard, but it’s laborious and you can easily end up bricking your devices. It also just sucks because sometimes you need to work with someone else’s code that does not itself run on your machine. In the past, I would abandon many such projects for lack of tenacity. But my clanker is tenacious. But so are all of our clankers . Some of the projects we’re now attempting are happening because of conversations we have with them. In this case I did not find or decide on CatPlay, the model did. It was not the only suggestion, but it became the best starting point after discarding others. And I discover this more and more. Particularly when we have solitary interactions with these models, some of us “independently” decide to work on similar projects. When I talked with an acquaintance about CarPlay he also mentioned recently that he decided to try something similar because he too wanted to see if he can get his own agent be hooked up with the car. And guess what: he too learned about the CarPlay hacking community, and that it’s an option, from the models and roughly around the same time. It really got me thinking about how this could create situations in which completely independent people end up building things they believe are their own ideas. Yet they were inspired or pushed towards doing something by a conversation with an LLM — a conversation that someone else also had. What if we took paths, because those were the paths that were more likely with current generation models? There is a running joke in the AI builder community right now that we’re all working on the same things, and in many ways it feels like we are. That might be because those things are obvious, or it might be partly because we all use the same models with the same capabilities. A few months ago, I first saw Lucas Meijer share the idea to make a model in Pi produce HTML reports rather than Markdown. I thought that was pretty unique. Except, well turns out the models are probably trained more and more for that (e.g. Claude Artifacts), and now it has become for many the default choice for sharing reports. How much of what we build comes from eliciting the same latent capabilities from the same models? Did the models make us prompt them that way? Was it because we shared ideas on Twitter and other communities that inspired us? Or is it all unrelated? There is something powerful and strange about how LLMs diffuse knowledge and capabilities, while perhaps also nudging us all simultaniously and independently toward building the same things.

0 views
codedge 2 weeks ago

Prevent deploying broken links to your blog

Having your own blog is fun. Checking internal links or also having an eye on all old URLs you ever linked is not. Fortunately you can automate link checking every time you deploy your website. I recently read about how links you once posted on your personal page or block become outdated. They are either put in private (403), they vanish completely (404) or they get a proper redirect (302). Whatever the case is, it would be cool the get all your links checked automatically when deploying your site, so you can either start fixing or removing them. For my Hugo site I wanted to do exactly, without going the write a scraper to extract links from my site and letting them run through curl . I wanted something to be run against my static HTML files, than I generate before deploying a new version of my page. I came across a very handy tool called lychee , that does exactly that. On their website they advertise it with Catch broken links in seconds Async, rust-powered simplicity for docs, sites, and codebases The cool stuff is, lychee works with I implemented it into my deployment workflow, scanning a folder , where my newly generated files are - and voila, I get a list of URLs with all their HTTP status codes. Of course you can configure ( see documentation ) which status codes are treated as good or errors. For example, I consider a not an error per se. You can also exclude specific URLs (or via regexp) to not being checked. I run this now on PR and on new deployments of my main branch. Works very well! Markdown files Websites (scraping all links)

0 views

A Syncthing and SQLite Gotcha

So, I have this little app, Epoch , that I use to keep a journal. It’s a tiny Rust web app that runs as a systemd service and uses SQLite as the database. I use a desktop and a laptop regularly, and use Syncthing to synchronize them, including Epoch’s database. That way I can use the app on both devices without needing a server to synchronize them, the tradeoff being that I have to make sure the sync is finished before performing any mutations. But I had this bug. Say I edit today’s entry on the laptop, come home, wait for Syncthing to finish, then I’d open today’s entry on the desktop, and the text would be missing. It’s not that the server is holding a lock on the file and preventing the sync: opening the database with the command line tool shows the new text is there. If I restart the server, Epoch can read the new text. My mental model was: The rusqlite object points to the database file. Syncthing swaps the file’s contents from under it. Subsequent queries go to the new file. Turns out there’s a very important part of POSIX filesystem semantics I was ignorant of. The standard way to replace a file safely (i.e. atomically) is the system call: Which Syncthing uses. This I know. What I didn’t know is: what happens if other processes had open file descriptors pointing to ? Do they see the new contents? No: those processes can keep reading and writing to the old file object , but the file is orphaned in that no path points to it. And once all file descriptors are released, the file becomes inaccessible. I’m used to thinking of filesystem operations in terms of “this syscall takes a path and gives you a pointer to the file, which you mutate directly”. Whereas works at the level of directory entries: it atomically mutates the mapping from pathnames to files but doesn’t touch files at all.

0 views
danluu 3 weeks ago

There's no reason for software to be slow anymore

The other day, I saw a viral tweet saying that people talking about how LLMs are causing slow, bloated, code are going to eat crow once they re-write everything in super-optimized assembly. We're not quite at the point where we want to write everything in assembly , but some variant of what Nolan Lawson said about testing, you can choose how many bugs you want now , which I less eloquently noted here , is becoming more true for performance. In response to a comment in my last post that the cost of formerly specialized performance work has dropped by many orders of magnitude and performance work that used to require a person or team that had a rare set of skills can be done by anyone who can type a few sentences 1 , which means that you can do all sorts of optimizations that used to be too expensive to be worthwhile for all but the largest scale or most lucrative projects, Marc Brooker responded with Completely agree with your closing point. Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome. (Which comes with all kinds of fun risks and opportunities of its own). Kind of reminds me of FFTW . And a ton of weird old demoscene techniques which were all about being super fast and small on a very particular problem (and often very particular hardware). For example, I remember a demo that re-used its code as textures to get great cache locality. And Michael Malis has noted There’s been a meme circulating about how AI doesn’t help because “code was never the hard part.” I think that’s true in some domains, but in others, writing the code absolutely was the hard part. JIT compilers are a great example of that. For many pieces of software, a JIT compiler would help a lot with speeding up the code. The rarity of JIT compilers makes me believe that implementing a JIT compiler historically was too difficult for it to be worthwhile. LLMs have lowered the barrier to entry and made it much easier to write a JIT compiler. This is the thesis behind pgrust. Databases historically were the hardest piece of software to build and were limited because of that. Now, with AI, we can be more ambitious about the type of software we build. Let's try this out with FRE , the regex engine we built in the last post. Recall that it was created by having an agent loop for a month on improving regex engine performance with access to the rebar regex benchmark suite . This resulted in FRE being heavily overfit to rebar until we warned our agent that we had a holdout benchmark, which caused the agent to generalize the optimizations enough that performance was ok-ish on our holdout. There's no particular reason to use a "software factory" regex engine that doesn't beat a well-tested regex engine on holdout benchmarks, but one notable thing about FRE was that the native AOT compiled version did quite well at longer searches. We noted that, it stands to reason that one could run the native code compiler in another thread while ripgrep was running its normal matcher and then cut over to the native code when it finished compiling and generally get better performance. Of course this will generally result in worse performance for short queries as we lose a thread to compilation, but I care a lot more about how long ripgrep takes when it runs for many seconds or minutes than when it runs for a few seconds, so I'm ok with that tradeoff. In the same way we could build a regex engine in a few minutes of human time, we can also just try this experiment in a few minutes of human time. I typed a few sentences and an agent went and did the work to allow this to happen (which would be a decent chunk of code surgery for a human) and it ran the benchmark on actual ripgrep queries that come from my codex history. For longer queries, we see a 2x-4x performance improvement here for a few very simple queries. But most queries are more complex, and when we run on representative holdout queries, for queries where AOT should be enabled 2 , we get about a 7% speedup. Not an earth shattering result, but also not a bad outcome for spending a few minutes typing to codex (and it's still doing more optimization and will presumably speed things up further). This is arguably a silly thing to do, since if we're repeatedly searching for text on a computer, the obvious thing to do to speed that up isn't to write a native code compiler for regex matching, it's to create an index. But the point here is just that this kind of technical work, which used to take a fair amount of time and expertise, can just be done trivially now. And if we wanted to build a text index, it just so happens that I worked on BitFunnel, the Bing search index that was specialized for constant/fast text ingestion that won Best Paper Award at SIGIR , so I can think of a few experiments to try if we're going to build a fast local index of our entire machine (the projects I've seen seem to be intended to index your code directories, but what really kills my machine performance is when codex decides to run ripgrep against huge temporary directories with a ton of generated files and then expands to looking at my whole machine when it misses, so I'd want an index of my entire disk and not just of the code for some projects). If I were working at an AI lab and had access to things like SOTA models running on Cerebras chips or other accelerators that greatly increase tok/s and therefore load/demand for search, I might actually survey the existing indexers to see if they're fast enough or if I'd want to build something custom myself. While the open source version of BitFunnel "only" contains a bytecode interpreter and one JIT, the Bing version contains multiple JIT compilers. A project that did that level of optimization used to be a major undertaking, but " I could do that in a weekend " is now actually true for some of these kinds of projects. With my lowly $200/mo account, I think a somewhat faster ripgrep plus any off-the-shelf index is fine, so maybe this fast-ingesting whole-machine index project can be left as an "exercise for the reader (who works at an AI lab)". The drastic reduction in the cost of optimizations has been true going back to November 2025 and maybe even somewhat before then with public models (and I'm sure before that still with what folks at AI labs had access to). For an example from the GPT-5.1 or 5.2 days, with no knowledge of game AIs, I tried building an Azul AI. This ended up being the strongest AI in the world for the game by a pretty large margin. From reading the thesis that describes the 2nd strongest AI , I think my AI is probably a bit better on the "AI" side of things, but the main place it wins is on optimization despite spending what looks like maybe two orders of magnitude less time (estimated by reading the thesis and seeing the process and comparison to my process) and also mostly working on my laptop vs. having a cluster of machines to use (which means much less bandwidth to run experiments with, do parameter tuning, etc.). For example, that other AI is single-threaded and my AI is multi-threaded. Since I have a native code version as well as a heinous shared wasm memory + javascript version , and two different search architectures for two different versions, which "require" completely different multi-threading algorithms (minimax for a very small and fast net and MCTS for a larger net), this would've been a fairly large undertaking if done by hand. And, because I let an LLM pick the multi-threading algorithm based on its own (incorrect) reasoning a couple times before spending 30 minutes reading about multi-threading algorithms for game AIs myself, I ended up re-writing (having codex re-write) the multi-threading algorithm multiple times. There's a bunch of standard stuff it makes sense to do to debug and verify a multithreading algorithm for something like this, like implementing replay from debug logs that can reproduce bugs despite the algorithm being nondetermistic. Doing that alone would've probably been days to a week of work had I done it by hand, but it's exactly the kind of thing an agent can trivially do in a loop (just have it try to replay logs and insert logging for non-determinism every time you don't get a perfect replay). A lot of the tedium it used to take to get a tricky optimization like this working is gone. This also applies to a lot of other tricky optimizations. From having written CPU microcode, done CPU verification, worked on optimizing a search engine index, etc., I have a lot of experience looking at optimizations and thinking "hmm, this would increase performance by 2%, but it's going to take N person-days to verify that this tricky optimization works" and making a call to go ahead or not based on whether or not it's worth the time to get the optimization working. Now that this N has dropped by a tremendous factor (variable but, in terms of human time, frequently 1000x / 10000x / 1000000x, probably more like 1000x on dollar cost if you compare token costs at metered rates vs. the Bing engineer who wrote the compilers at JITs that the search index used), the number of these kinds of optimizations it makes sense to do goes way up. The same goes for optimizations that you aren't sure will work out. I used to sometimes look at an optimization that I wasn't sure would speed things up and think "this will take M hours to implement to the point where we have a good enough measurement to guess at the performance impact". Many more of those optimizations make sense to try out now. Going back to the game AI case, at least for the AI I tried, it seems like you gain about 100 Elo for every doubling in speed (more than in chess, I suspect because draws are very rare). Just adding multithreading alone is enough to wipe the floor with an otherwise comparable AI on a large machine. If you stack in 10-20 more optimizations that seem too annoying for most people to do by hand, the difference in strength is tremendous and it's not really reasonable to try to keep up with a hand-written AI 3 . The game AI case is a little more annoying than for most software because a lot of the optimizations you want to do actually change the result and there isn't a cheap, trivial, way to tell if the speed increase + the change in result gives a better or worse actual result in practice. And, as we noted before , current publicly available SOTA models are pretty bad at experimental design, so I had to set up the framework they used to determine if an optimization is good, but once that was in place, it's like any other optimization problem. I guess people working on LLM optimizations also have to deal with this class of problem but most optimization problems are a lot more straightforward. To pick another example, as part of preparing for performance interviews, Jamie Brandon tried Anthropic's now public performance takehome . After trying it, he had Claude pick up where he left off and it got a much better result. When he looked at what Claude did that he didn't, he said a lot of the optimizations were things that occurred to him but he hadn't gotten to yet, and "[o]thers were just crazy shit that I would never try unless I was working on this for weeks" 4 . He's a reasonable performance engineer and he got an offer for the performance job he wanted, but on a well-defined optimization problem, he doesn't stand a chance against a decent model (I haven't tried the problem myself, but I suspect I also wouldn't stand a chance given remotely comparable time controls). Coming back to this part of Marc Brooker's comment: Dynamic custom software, fitted to a particular workload rather than a class of workloads, seems like a very likely outcome. This seems pretty inevitable. In another response to my post, Michael Malis of pgrust said something similar: [discussion of pgrust optimizations] ... I think it's easy enough to create these optimizations that we could look at a customers workload and add them as needed Without having any kind of framework or setup, right before I started writing this post, I had an agent do workload-specific optimization for my ripgrep queries (not the native code compiler switch, just the optimizations to the general FRE engine based on a set of benchmarks), which took about 2 minutes for me to launch. The optimizations run on a set of queries, and then there's a later holdout set of queries to run against. That's still running, but the initial results seem promising. After one pass of optimization, the workload optimized version is 2% faster than standard ripgrep on the holdout and it's still getting faster. 2% isn't a big deal for my local ripgrep usage, but considering that this took minutes of time and the optimizations done here got started when I started typing this point and are still improving, I'd take a 2% win here (note that this isn't combined with the native code compiler, which would give a larger overall win if combined properly). And recall that this is leveraging the FRE regex engine 5 , which was substantially slower than the Rust regex engine on holdout benchmarks and was stuck with slow improvement on holdouts because with me knowing nothing about regex workloads and SOTA LLMs not being good enough at experimental design to do unguided open-ended self-improving loops, we didn't have a good way to improve performance on our holdouts. But if what I care about is performance on my own workloads, I have plenty of data and am generating more all the time. As Marc Brooker noted above, we do have to be careful about overfitting if there's a regime change that's not in the old data, etc., but we're still in a better situation than we were before. In the more general case, if you're someone like Marc Brooker at Amazon or Michael Malis working on pgrust, it makes sense to not just do this as a one-off, but to work with customers to pilot a program that uses their data to optimize things for them and then figure out how to scale it out for customers in general. I'm not working at a company where that's the best use of my time 6 , but it's pretty wild that you can see that this is coming for larger companies with more scale, and given that it only takes minutes of my time to run these experiments for my personal workflows, it's pretty reasonable to mess with this kind of thing on personal projects. Thanks to Jamie Brandon, Michael Malis, andrea (@s__video), Artyom Bologov, and Max Bittker for comments/corrections/discussion. P.S. As I've noted in the last couple posts , with coding agents, the time it takes to run an experiment and see enough of a result to satisfy my curiosity has gone way down while the time it takes to make a result really rigorous hasn't changed or has gone up, so writing things up the way I used to would mean running very few experiments relative to the bandwidth I have for them. As a result, I've just been running these experiments and sharing the result with a couple of friends. As an experiment, I'm trying to write these up in a very quick and non-rigorous way instead of years of these experiments only being known to a few friends. Like the last post, I set a goal of writing this post and doing all the clean-up in half an hour and didn't time it but am pretty sure I missed that by a bit. Even doing this, the time it takes to write these up is long enough that I'm falling behind on sharing recent results, but I'm not inclined to switch to LLM-written posts (yet?), and I don't think I can realistically get the time to clean up the data and write a post like this down enough to turn a post around in less than half an hour. Just on the length of this post, typing this up should be something like 20-30 minutes including time to pause and think about what I'm writing, and then when I look at the data sometimes something will look wrong enough that I need to look into it more closely to see if there's an issue that needs to be fixed (this happened multiple times here, and I would expect that, because I didn't spend much more time, there are other data issues that I don't know about). Anyway, if you have opinions on these quick (and surely more wrong) writeup, let me know what you think ( X Bsky Mastodon )! I've been on the record for a long time as strongly disagreeing with the general sentiment that the developers of X are bad and should feel bad for writing slow code because there are a lot of different kinds of programming expertise and not only is it not the case that most programmers don't have performance expertise, it probably doesn't even make sense for them to develop (from the standpoint of what the business cares about, what the employment market looks like, etc.), so of course most projects will have very poor performance compared to what a performance expert can do. I can see why a performance expert would look at the growing gap between how fast a program can be and how fast programs actually are and think that it's ridiculous. I don't disagree that there's an absurdity to it, but if I think about the gap between how good a UI can be and how a good a UI I can make (by hand) is, I don't think that looks any less absurd, but I also don't think it really makes sense for me to spend time learning how to build a great UI, or even a decent UI, for the same reasons it doesn't make sesne for most people to spend time learning how to decent performance work. For the example above, Jamie Brandon got an offer from Anthropic and you probably can't afford him unless you're OpenAI, but you can afford to use a coding agent that can beat him on a bounded optimization problem. The agent doesn't have the judgement he has and will do worse on an open-ended problem (recall that when we tried building an optimized regex engine and just told it to not overfit, it was more than an order of magnitude worse than the best regex engines on our holdout benchmarks , but also recall that after telling the agent there was a holdout it was doing poorly on, it sped up regex engine performance enough to generally match 2nd tier regex engines in terms of performance, which is still extremely good compared to the general level of performance optimization in most code today), but that's plenty good to achieve reasonable performance on all sorts of problems. This post has generally discussed backend performance issues, but agents don't seem worse at front-end performance if you want to drive down a set of metrics like LCP and CLS. In fact, after inserting the interactive plots I've been using recently into posts, I found that my client-side perf numbers got worse, so I had an LLM spent 1% of my weekly quota optimizing those and the numbers are once again back to being good. This is a very simple site, but people do these kinds of optimizations on fairly complex apps that ship to many millions of users and it also works there, although it does cost a few more tookens. I still don't think someone is bad and should feel bad if their software has poor performance, but I do think that someone who doesn't know anything about performance and is a reasonable user of LLMs (just in general, not on performance problems in particular) should generally be able to create software that has decent performance. If you just tell an LLM to optimize, it will often do all sorts of incorrect things that are really bad that you have to catch, but that's generally true of using the LLM effectively in the first place, so getting decent performance is no longer a specialized skill. Here's some information about the distribution of riprep queries on my machine. I make no claims that this is at all representative of what's happening anywhere else. The pattern distribution of the length of the pattern that's searched has a lot more long patterns that I would've expected. The p50 is 55 unicode code points (I'll just call these characters for simplicity), which is already longer than things I grep for by hand, and the p90 is 119! We can also look at the number of alternation arms in regexes, which are once again much more complex than what I do by hand. Another view is to look at how these are correlated. Do we get more alternation arms in the regexes as the regexes get longer? Yes. What are these really long regexes, anyway? If we look at them, most of the longest are long alternations over function or tests names, such as the following regex, which appears to be related to FRE development. But some are funny numerical constructions, such as This is equivalent to (which, if run through ripgrep on the original input, has approximately the same performance; the shorter regex is technically a bit faster on the real query data, but only by a very small amount). The entire pipeline for that was which might be an odd thing for a human to do, but agents seem to do this kind of thing all the time. On another topic, if we look at how long ripgrep queries took, there are quite a few slow queries, e.g., p99 is almost 1 minute! And p999 is almost 10 minutes! And the maximum query over this time period (around a month on one laptop; queries and distributions seem likely to be different on the AWS hosts I run agents on, etc., but I haven't checked) is approaching 2 hours! In terms of command line options, we see the following. Perhaps unsurprisingly, codex often wants line numbers and, for whatever reason, it very occasionally uses PCRE2 regexes. I won't add plots or tables for these, but another thing to note is that there's fairly low locality for what patterns are searched for (about 94% of patterns only occurred once), which makes some sense given how long a lot of the queries were. However, there's fairly high locality in what files get searched and a file that got searched is relatively likely to get searched again soon, indicating that (for small enough files), they're likely to be searched in memory. Also, 99% of queries were regex queries (1% were non-regex string searches) and 99.9% of search queries were ASCII only, but in terms of files searched, approximately 45% were ASCII only and 55% contained Unicode, a higher percentage than I would've guessed for Unicode. On a draft of the last post, Peter Geoghegan noted It's also possible for a regex implementation to be faster by supporting fewer features. Some implementations don't support back references, etc. which is also true here. The workload-specific optimizations done here were fairly superficial because I just gave codex some short instructions and let it do whatever it wanted (which is, in general, not the most effective use of codex), but with a more detailed plan, more focused optimizations supporting the common use cases for my queries could be expected to yield larger gains. though, as we discussed in that post as well as before , the benchmarking and experimental design skills of SOTA models aren't good enough to do this in the general case without a human (or a skill) setting up the benchmarking environment for the agent. [return] we can see from our old benchmarks that , even with time to run the compiler, there are a lot of cases where the native code compiled version is slower than the Rust regex crate. If we look at why this is, these tend to be more complex queries where the Rust regex crate has some algorithmic optimization and the FRE native code compiler is falling back to something naive (the agent that created FRE spent much less time on the native code compiler than it did on the "normal" regex engine). [return] I have no doubt that a hand-written AI by someone who has real AI expertise, e.g., by someone who's written one of the top Go and chess engines in the world, could beat my AI on the strength of the "AI" side of things being better than what you get when someone who knows nothing about AI (me) creates an AI, but if the levels of expertise are remotely similar, the LLM-written version is going to dominate for any given amount of time spent. [return] it's arguably unfair to compare the result of an agent picking up where he left off, since his work is a starting point which might let an agent do much better than it would do on its own, so I tried giving the fresh task to an agent and it got a very similar score to what he got when an agent re-used his work (and a quick check by another agent didn't find evidence of cheating). [return] The performance probably would've been better if I had an agent just modify a ripgrep fork directly, but I was curious if this could also solve the FRE overfitting problem with respect to my queries. [return] a while back, I reduced the size of page in our signup flow from 50 MB to 5 MB and a revenue A/B test seemed to indicate that this increased revenue by about 0.5%. In general, I'm a huge fan of doing the simple and easy wins first, such as this , and there are probably a lot of higher ROI wins than we'd get out of building custom compilers or doing other highly specialized technical work here. [return]

0 views
Ginger Bill 3 weeks ago

Everyone Says Assembly Is Untyped—Everyone Is Wrong

TL;DR: I believe Odin;s inline assembly is currently the best out of any language.The most important aspects are of this article listed below. I am not aware of any other assembly (GCC/Clang/Rust/Go...) that would combine all of these aspects:Inline assembly is organized into ;templates;, similar to and callable as procedures. templates integrate with rest of the code, through bindings specifying clobbers, pinned, tied, and scratch registers.Assembly syntax is unified across ISAs and consistent with Odin syntax.Assembly is fully type checked...

0 views
danluu 1 months ago

The benchmarkpocalypse

There's been a lot of talk about the vulnpocalypse, to which I don't have much to add because I'm not a security person, but I haven't seen much discussion on the closely related (and to be fair, less serious, issue), the benchmarkpocalypse. While it's become easier than ever to make serious performance gains, it's also become easier than ever to reward hack a benchmark and make fake performance gains. The former is probably happening quietly across many different companies, but the latter is something I see at least once a week nowadays. Someone will claim they optimized X and got some huge performance improvement over existing software, but, when you look at it, what they did was make some optimization that improves benchmark performance without actually improving real-world performance. This is often some kind of "we rewrote X in Rust" 1 project or a new startup that's looking to either fundraise or sell something, but it happens on other kinds of projects as well. Of course, people have always trumpeted unrepresentative microbenchmarks to show that their pet project is great. It's always been easy to fake up an unrepresentative microbenchmark and that's never going to change. What's changed is that it used to take a lot of work to game a large benchmark suite, but an LLM and loop can just do it. There are quite a few famous examples of gaming large benchmark suites from back when this was hard. For example, way back when people cared about SPECint / SPECfp as proxies for workstation performance, CPU vendors would try to find compiler "optimizations" that would speed up the calculation in the benchmark, such as Sun finding a way to improve by 12x in SPECfp2000 . Skilled engineers spent a lot of time trying to find benchmark hacks like that. LLMs not only make this trivial, they do it by default, making formerly trustworthy benchmarks meaningless unless you audit the result or trust someone who did. Rather than point to someone's bad claim, I'll point to FRE, this regex engine I had an agent build , which I could claim is the world's fastest regex engine because it beats the Rust regex crate at the fairly comprehensive rebar regex benchmark suite . But this was created by putting an agent in a loop for a month with instructions to not overfit to the benchmark but no real supervision. For the most part, getting an LLM to give you a good benchmark score is fairly easy, and this case was no different; it took a couple weeks to roughly match Rust regex crate performance and then another couple weeks to get to 1.4x faster 2 on rebar. But agents are wont to reward hack and overfit unless you put serious guardrails in place to avoid that, which I didn't do in this case as an experiment. To check for overfitting, I somewhat arbitrarily 3 used the ripgrep benchmark corpus as a holdout benchmark it was 10x slower on cases where the benchmark didn't take forever due to an algorithmic blow-up, and there were cases where it took so long that it wasn't reasonable to even wait for the benchmark to complete. So much for being 40% faster! Andrew Gallant (aka BurntSushi)'s rebar benchmark suite is fairly comprehensive as benchmaark suites go, but even with a fairly comprehensive benchmark suite, agents have no problem getting a high score while overfitting in a way that doesn't necessarily give good general performance. The next step was using a trick we talked about before of not just telling the LLM not to cheat, but that there's a holdout benchmark set that it's judged against. After that, the LLM moderately generalized performance to the point where it's about 2.4x slower overall on the holdout. That sounds pretty good considering that we're comparing it to the fastest general purpose regex engine in existence. But, recall that these benchmarks were made by a coding agent. On looking at what the benchmarks measure, some of them really don't make sense to include, at least at equal weight. If we only look at the benchmarks that seem like they matter, FRE is 4x slower on the holdout 0 , which is a lot better than before applying the good ole' "tell 'em you have a holdout trick", but still pretty far from being 40% faster. There are a few things I thought were interesting about this: On (1), no wonder I'm seeing so many bogus claims. In the past, to build something like FRE that fakes performance well enough to be able to bogusly claim a 40% speedup, you would need a fair amount of expertise. At a minimum, you'd need to have a pretty good understanding of string matching algorithms, regex engines, as well as decent general code optimization and SIMD optimization skills. FRE also has a mode where it compiles the regex to machine code, so you'd also need some compiler expertise. Now you can get that kind of benchmark cheating (whether or not you want the cheating) with a few minutes of typing. On (2), I'm curious if this generalizes but haven't tried enough examples to be able to tell. On (3), there's no reason to use a vibe coded regex library that was almost no human effort that's slower than a robust, existing, well-tested, library, so I find the FRE artifact uninteresting. The thing I find interesting here is how much LLMs can substitute for what used to be rare, specialized, and expensive, knowledge. In the past, even if you had the knowledge, you probably wouldn't write a custom regex engine that's optimized for your particular workload. There are some large-scale use cases where people would do that level of customization, e.g., when I worked on the Bing index , the code contained multiple different compilers because someone who worked on it wanted to eke out maximal performance; since you care about both compile time and compiled performance in a search engine and the trade-offs are different in different places, you get better performance by writing a custom compiler for each place where a normal project might just use an interpreter or directly walk some data structure with "normal code". The person who wrote those compilers, working on regex-like code might also write multiple custom regex engines, but very few people have both the expertise and the inclination to do that, let alone the freedom to spend that kind of time on such specialized code for work. If you price out that Bing engineer (then a Partner-level engineer, promoted to Distinguished Engineer for their work on the search index) compared to the price of running an LLM in a loop, the cost of writing this kind of specialized code has gone down by many orders of magnitude. People who still think AI is fake will probably read the first part of the post and think "of course, AI produces fake things, so it produced a fake regex engine". But if we look at the results, being a bit worse than half the speed of the world's fastest regex engine on a holdout while being genuinely faster on many real workloads (most of the overfitting isn't that it special cased a particular benchmark pattern, but that it has some kind of optimization for things of same rough shapes and not of other rough shapes) it's pretty far from a fake regex engine. And, in fact, there's a native code compiled mode that actually beats the Rust regex crate on the holdout if you ignore compile time and are running repeated searches or a very long search (which is a reasonable thing to do for many actual use cases). If my goal with FRE was to produce a fast regex engine instead of producing whatever regex engine one can produce in a few minutes of human time, I suspect it would be fairly competitive on a broad range of holdout benchmarks (with some gaps that would only be found when people tried it on a diverse set of production workloads), and, even this quick and dirty version is very good at some real workloads. So, even though the overall FRE regex engine has worse performance than the Rust regex crate, the gains you can get for specializing to your workload or use case mean that, in some cases, it could be reasonable to insert your own specialized regex engine somewhere, and the same goes for various other kinds of low-level software. You don't have to be an AI maximalist to think that it's plausible that, within some number of years, we could see this kind of thing happening for larger things, like databases. Thanks to Yossi Kreinin, Jamie Brandon, Peter Geoghegan, Luke Burton, John Spurling, Dennis Snell, and Max Bittker for comments/corrections/discussion. P.S. Per the discussion here , with LLMs, the time it takes to poke at something for a bit and satisfy my curiosity has gone way down, while the time it takes to write something up and make it rigorous enough to publish on my blog hasn't really changed (for a variety of reasons, I think it's actually gone up). The result of this has been that I'm doing a lot more analyses than ever and sharing results with a few friends but not publishing them. As an experiment, I'm trying to write up some things very quickly, with a much lower standard for how cleaned up and rigorous things are than I'd normally have for something that appears on the blog; more like what I'd tell a friend in a casual conversation than what I'd normally put in a blog post. The goal for this post was to do the write-up in about half an hour , so it's something I could do over lunch and not really take time on. If you have opinions on this, let me know what you think! Of course, a caveat here is that all of the numbers have a higher risk of being wrong than usual. I looked at one benchmark for maybe a minute or two and found an issue, then I looked at another benchmark for a minute and found another issue. Both of those are fixed, but this implies there are other issues I haven't taken the time to chase down. But, with respect to bad benchmark numbers, that's highly realistic! Almost any time I look into benchmark numbers, such as here , or here , the numbers are wrong. Another aspect of the benchmarkpocalypse is that, at least for now, LLMs are good at doing bad benchmarking, so even if you have something that's a real performance improvement, you generally can't tell from some LLM-generated benchmark setup unless a significant amount of care has been taken to make sure that the benchmark setup is reasonable. One thing I found after I wrote the above but before publishing the post, was that the LLM's claim that FRE is 40% faster than the Rust regex crate on rebar was also wrong. Or, if not wrong, at least misleading. It wasn't actually running benchmarks in the same way rebar benchmarks were run. I checked this after spending a minute checking benchmark results found two issues. It turns out that, despite instructions to run rebar benchmarks as they're run in https://github.com/BurntSushi/rebar , the LLM changed the interface to allow FRE to make some optimizations that improve performance. After fixing that, instead of FRE being 1.4x faster than Rust on rebar, it was 1.5x slower (and "only" twice as fast as re2), so the original result was doubly fake. Not only was FRE highly overfit to the rebar benchmarks, it the results also involved cheating. But on the bright side, this means the difference in performance between FRE on rebar (1.5x slower than Rust) and on the holdout benchmarks (2.4x slower) isn't as big as it looked before, so the "tell the LLM you have a holdout" trick worked even better than it seemed to before. After that, I let an LLM hill climb for a few hours and it claimed that FRE was 1.28x faster, which sounds like a great improvement for only a few hours of LLM time, but then I decided to spend another minute looking for cheating and found multiple issues, including one case where a search for the count of matches of returned the count without even looking at the haystack (data). Another case of cheating was doing a multi-line grep where the benchmark is supposed to be done line-by-line. Finding these isn't surprising because this is the kind of thing that happens when you leave an agent in a loop for a month without defining strict guardrails. Whether this makes my point here stronger or undermines it isn't clear, but after fixing another set of these issues, FRE was back to being 1.4x slower. After leaving an agent to run overnight, FRE was allegedly back to being 1.5x faster. Since my original goal here was to see what happens when you run a current (public) SOTA agent in a loop (GPT-5.6 Sol) without much supervision on a non-trivial code optimization problem without any real supervision, rather than spend more time fixing things up to make the benchmarks fairer, I'll just stop here and put a few plots of the results. Overall, we can see that against Rust and RE2, FRE tends to outperform on the rebar benchmarks (and as noted above, much of this is due to overfitting), but not across the board (the graphs below don't necessarily match the numbers mentioned in the post because an agent is constantly making changes, so any snapshot is a point-in-time estimate that becomes obsolete immediately): If you're curious about performance on specific benchmarks or specific classes of rebar benchmarks, we have the following table (ratios above one mean FRE is faster; below mean FRE is slower): There's also an AOT compiler mode that takes a long time to compile a regex to native code before running it. There isn't AOT support for everything, but here are the results from the cases where it's supported. As we can see, the AOT compiler is very slow (it loses very badly in the compilation time benchmarks) and, despite spending quite a bit of time compiling, results are often slower than with the standard FRE regex engine (though it's also faster in many cases). And then there are the holdout benchmarks. As noted above, for the non-AOT FRE code, performance on the holdout isn't as good as on rebar. And as also noted above, considering that this is for a workload like ripgrep, the "hot search" set of benchmarks is probably more important than the others, so the FRE result is worse than the overall score would make it look. One thing to note here is that, for the holdout benchmark cases where we don't include compile time as part of the benchmark and we repeatedly run searches, AOT FRE outperforms on the benchmark. For a lot of use cases, you don't want a regex that takes multiple seconds to compile, but there are plenty of cases where this is fine, e.g., for something like ripgrep or Silver Searcher, it could start running with a regex that can start matching right away and then compile in another thread and cut over to the faster matcher when it's done compiling. Given how much of my CPU is spent on long ripgrep searches, it seems like a strategy like that could improve performance for work I personally do. Before LLMs, it probably wouldn't have made sense to spend the effort to write an optimizing regex compiler, but this is now do-able with a few tokens. Another thing to note here is that this comparison is arguably unfair because this was run on an ARM Graviton machine with SVE/SVE2 and FRE has SVE/SVE2 optimizations. Pre-LLM, it might not have been worth it to have regexes optimized for every combination of SIMD instructions out there, but with LLMs, it's fairly easy to generate ok-ish SIMD optimizations. I know human experts who find that they can generally outperform LLMs here, e.g., Jay Stelly said that the last time he tried getting an LLM to produce SIMD code, it took 20-some iterations to get the code as good as he wanted. But, on the flip side, LLMs have the capability to try more optimizations than a human could possibly try in any given amount of time, so they can still perform pretty well overall even if any specific optimization isn't as good as a human expert would produce. There's also the problem discussed in this post of overfitting. Depending on the context, that problem is somewhere from very easy to solve to a bit difficult to solve. I deliberately didn't try very hard to solve the problem here to see what would happen, but I did manage to solve the problem without an outsized amount of effort when working on this Azul AI (just for example), but a lot of these big benchmark claims come when people spend little to no effort trying to avoid overfitting, or even negative effort. In the pre-LLM era, people would often pick highly unrepresentative microbenchmarks to show off how great their pet project is which, at least at a non-conscious level, involves negative effort to avoid overfitting to a benchmark. Due to how humans are, I don't think people are going to stop making misleading claims and it's become easier than ever to make misleading claims, so of course we see more of them. Note that while this post has discussed non-AI software, everything said here goes double for AI software. For example, I've seen lots of people drop comments saying that Kimi K3 is Fable (5) level. But every single person I know who's used it has found it to be substantially worse than GPT-5.6 Sol and Fable. I'm not saying it's not an impressive engineering achievement, but the performance on a wide variety of real-world tasks isn't up to the level it is in benchmarks. This even applies to various eval-y problems, such as when a friend tried different coding agents on the ICFP 2026 contest problems. It also applies to security issues, which are something that I have no doubt AI labs are putting into their evals, e.g., a colleague of mine tried using Kimi K3 to scan for vulns in our software and found that it found approximately a quarter of the vulns GPT-5.6 Sol found, found no vulns that GPT-5.6 Sol didn't find, and didn't have any advantages in any dimension other than on cost. The people I know who are using cheaper models to find real security issues are using other models, such as GLM-5.2, which perform worse on benchmarks but better in practice. Back on the topic of FRE, one more note is that the holdout benchmark is an arbitrary subset of the ripgrep benchmark setup that was chosen by an agent for unknown reasons. I asked an agent to pull the entire benchmark suite, but that didn't finish in time for this post, so I don't know what the result will be once it's done. funnily enough, I have some faith in some of the projects that people are the most skeptical of, e.g., every time I see pgrust somewhere, there are a lot of skeptical comments. But, without having looked into the details of what he's optimizing, I would trust that they're not doing something shady with their benchmarks because Michael Malis started the project (and is still involved). I used to look at most benchmark claims that cross my radar in some detail, but there are so many of these now that I don't really have time to do that and generally assume that claims are false in spirit (even if technically correct) unless there's some reason to believe otherwise. Of course this will sometimes be wrong (e.g., if I didn't know Michael Malis, I would've guessed that pgrust is just another low-quality "have an LLM re-write this thing" project), but LLMs are such an incredible machine for DoSing human attention that I don't know what else I would do about it (I've tried having LLMs analyze performance claims and, while the result is correlated with what I'd think if I looked at something myself, the result is often quite wrong). Someone can spend seconds (or, if using the right framework, actually none of their time) generating something that takes people minutes to hours to understand. This is a topic for another post, but from talking to people about their experiences with this in the workplace, companies with poor norms for this kind of thing are really struggling with productivity today. The first few regex benchmarks I looked at had already been incorporated into , so they wouldn't work as a holdout. And, as previously discussed, current SOTA LLMs aren't very good at benchmarking , so I wouldn't be able to trust the LLM to come up with a holdout benchmark unless I knew enough about regex performance to judge the quality of the benchmark suite. Since I know approximately zero about string matching algorithms or regex performance, that was also off the table. It turns out that BurntSushi also maintains ripgrep and the benchmarks for ripgrep, which are big enough benchmarks that they didn't get bundled into , so I tried using those benchmarks as a holdout. It's trivial to "win" a non-trivial benchmark in a meaningless way even when you instruct agents to not reward hack or overfit to win the benchmark Once again, telling the LLM there's a holdout set worked better than just telling the LLM to do generalized work or not overfit or cheat Although the overall performance of FRE isn't that good, it is actually performs better for some use cases; in general, the cost of writing specialized code that used to require people serious engineering experience for some specific use case has gone way down funnily enough, I have some faith in some of the projects that people are the most skeptical of, e.g., every time I see pgrust somewhere, there are a lot of skeptical comments. But, without having looked into the details of what he's optimizing, I would trust that they're not doing something shady with their benchmarks because Michael Malis started the project (and is still involved). I used to look at most benchmark claims that cross my radar in some detail, but there are so many of these now that I don't really have time to do that and generally assume that claims are false in spirit (even if technically correct) unless there's some reason to believe otherwise. Of course this will sometimes be wrong (e.g., if I didn't know Michael Malis, I would've guessed that pgrust is just another low-quality "have an LLM re-write this thing" project), but LLMs are such an incredible machine for DoSing human attention that I don't know what else I would do about it (I've tried having LLMs analyze performance claims and, while the result is correlated with what I'd think if I looked at something myself, the result is often quite wrong). Someone can spend seconds (or, if using the right framework, actually none of their time) generating something that takes people minutes to hours to understand. This is a topic for another post, but from talking to people about their experiences with this in the workplace, companies with poor norms for this kind of thing are really struggling with productivity today. [return] This is referring to the geomean of all rebar benchmarks. This is probably not the right metric to use, in that this implicitly says that each benchmark is of the same importance, which probably isn't the case. Unlike something like SPEC CPU, the rebar benchmarks don't position themselves as something where you get a meaningful summary metric that tries to represent overall performance (the repo actually notes that it's " a biased barometer for gauging the relative speed of some regex engines on a curated set of tasks"). But, to get a number that is a useful summary metric, you'd have to know a lot about how people use regexes in practice, and I know approximately zero about that. For all I know, you should have two different numbers (like SPECfp and SPECint for SPEC CPU) or ten or a hundred because there are all sorts of different ways people apply regexes. [return] The first few regex benchmarks I looked at had already been incorporated into , so they wouldn't work as a holdout. And, as previously discussed, current SOTA LLMs aren't very good at benchmarking , so I wouldn't be able to trust the LLM to come up with a holdout benchmark unless I knew enough about regex performance to judge the quality of the benchmark suite. Since I know approximately zero about string matching algorithms or regex performance, that was also off the table. It turns out that BurntSushi also maintains ripgrep and the benchmarks for ripgrep, which are big enough benchmarks that they didn't get bundled into , so I tried using those benchmarks as a holdout. [return]

0 views

Concurrent Servers: Part 7 - Rust

This is part 7 in a series of posts on writing concurrent network servers. In this part, we discuss how the challenges described in earlier parts are tackled in the Rust programming language. All posts in the series: Several years have passed since the previous parts were published. I've recently went over them to make sure the information presented is still relevant and all the code samples build and run using modern toolchains. I strongly recommend reviewing the previous parts before reading this one. This post assumes a basic familiarity with the Rust programming language. It will only explain Rust constructs when we encounter code that wouldn't appear in an introductory book or tutorial. The first few parts in the series focused on a socket server that implements a simple state machine protocol. See part 1 for a complete description of the protocol. Let's start by showing how this protocol is implemented in a basic sequential Rust server: With the function serve_connection defined as: As a reminder, this server version is sequential because it accepts clients one by one; the main loop blocks on serve_connection until it's done (the client closes the connection), and only then goes back to accept the next client. Clearly, handling clients one by one won't do. In part 2 , we've discussed approaches that use OS threads to handle clients concurrently. Let's start with the unbounded one-thread-per-client solution in Rust: The spawn method returns a Result<JoinHandle<T>> ; on success, we allow the handle to be dropped at the end of the loop iteration. In Rust, this detaches the thread; we don't actually wait for it to complete. This is reasonable for our code sample, because the loop is infinite ; it never terminates anyway. The potential for runaway threads is just one of the issues with the unbounded threads approach discussed in part 2. The solution is to use a fixed thread pool. Before diving into the code, a quick note on the design: the thread pool is a fixed set of threads that await "jobs" and handle them to completion. In our case a "job" is serve_connection for a specific client. There are many ways to implement a thread pool; for our use case, I went with a set of threads that all get a shared channel to which the main thread sends jobs. A worker thread picks up the next job from the channel, serves it to completion, and goes back to waiting for the next job. Here's how this looks in code: What is Receiver ? It's a type from the crossbeam_channel crate: Rust's builtin channels in std are mpsc - multi producer, single consumer, but what we need for our job queue is a channel that supports multiple consumers (the worker threads). While std does have mpmc , this is an experimental API only available in nightly versions at the time of writing. Therefore, I've opted to include the crossbeam_channel crate that provides well-tested mpmc channels for this sample [1] . And here's the main function: Note that our job channel is bounded - it has a fixed size. This helps naturally implement a backpressure mechanism - if too many clients connect, the following clients will have to wait - the main loop blocks on tx.send and won't accept additional clients on the socket until jobs are cleared from the channel. In parts 4, 5 and 6 of the series we've discussed event-driven , or asynchronous servers. Let's see how it's done in Rust. Specifically, part 6 presented a gradation from callbacks to promises to async/await mechanisms; Rust supports all of these and - as you'd expect - modern code is usually written with async/await while hiding all the details of promises (called futures in Rust) underneath. Without further ado, here's our simple state machine protocol in asynchronous Rust: Rust takes an interesting approach to async programming: it supports some of its fundamental building blocks (like futures and the async and await keywords) in the core language, but leaves the actual async engine implementation (the thing that implements the event loop) to external crates. By far the most popular crate for async programming in Rust is is tokio , so that's what we're using here. After reading the JS code in part 6, the Rust snippet above should appear fairly familiar, except perhaps the explicit tokio task "spawn". Instead of enqueuing a callback on the connection returned by listener.accept , the code spawns a tokio task, which can be seen as a green thread , and hence uses similar terminology [2] . These tasks must not issue blocking calls; therefore, they are supposed to use tokio's I/O utilities instead of the usual, blocking std utilities. In fact, we have to implement an async version of serve_connection to make this work: Note how similar this code is to serve_connection from earlier; the only real differences are the await calls on socket reads and writes [3] , and the types involved. For example, instead of a std::net::TcpStream used in the synchronous samples, here we're using tokio::net::TcpStream . Tokio has an underlying dependency called mio to handle non-blocking APIs for all kinds of I/O. It wraps OS-specific event loops like epoll to do so efficiently. While most of the series has been using a simple state machine server as the driving example, part 6 switched focus to a server for primality testing which simulates long compute tasks. Let's see how this is done in Rust with tokio: This code is very similar to the previous snippet conceptually; isprime is: Note that this sample demonstrates a job that can block (simulated with a sleep in this case). This can be problematic in an async context, as the tokio documentation explains . One potential solution would be to dispatch a blocking task to a separate thread pool and use tokio channels to communicate with it; this is similar to the approach we've taken in the thread pool sample above. Part 6 also included a version of this server that caches data on a local Redis instance; the goal was to demonstrate the complexity of event-driven code when additional layers of callbacks are added and how async/await can help mitigate that. Here's our Rust version of this server, using the redis crate (that has a tokio component enabled explicitly to support async calls): In conclusion, while Rust provides excellent support for async programming, it doesn't solve its inherent issues like function colors and the need for careful separation between blocking and non-blocking tasks. These issues are typically surmountable with some extra care, and async programming with Tokio in Rust is very popular due to its performance benefits. All the code for this post is available on GitHub . Part 1 - Introduction Part 2 - Threads Part 3 - Event-driven Part 4 - libuv Part 5 - Redis case study Part 6 - Callbacks, Promises and async/await Part 7 - Rust (this part) Because of the function color problem , the redis crate has a connection constructor specifically for async: get_multiplexed_async_connection . Here we have an example of shared state between tokio tasks - the Redis connection. Note that we don't require any particular synchronization because MultiplexedConnection is Clone ; cloning it to different tasks is safe - and in fact that's what we do for each new task. There's no magic here; if you look inside MultiplexedConnection , you'll see that it already has all the synchronization mechanisms implemented internally, as needed. Due to the magic of async/await, the code in serve_client is nice and linear. We simply await on the Redis call, and once it's back we continue with the rest of the handler. Since we're using an async Redis connection, in case waiting is required, control will be ceded to some other task that's not currently blocked on I/O.

0 views
baby steps 1 months ago

Cylic trait implementations: motivation

Lately I’ve been thinking about cyclic trait implementations. This is a problem that I’ve been trying to understand for years and years and I finally feel like I’m geting somewhere. I’m going to try to write out a series of blog posts documenting those explorations and, hopefully, culminating in a design that could be RFC’d. In this first post, I want to talk about one of the interesting questions, what I am going to call “internal” vs “external” proofs. I know that this material can seem abstract, so I’m going to try and connect it to “real Rust” as much as possible! This particular blog post is an introduction, explaining the general problem and giving some motivation for why we care. Right now in Rust we require most traits to have non-cyclic , or inductive , implementations. To explain what I mean, let’s consider this trait: Now imagine that we have an impl of this for : A simple impl for and `Option : and finally a recursive type that has an impl as well: If I try to show that , I do that by There’s no cycle here – that is, I didn’t have to use impl L to show that impl L is valid. Now, when I said that “the impl L didn’t have to use the impl L to show that it is valid” that might not have sounded suspicious to you. In fact, it’s a pretty natural idea. After all, generally when you try to establish a logical argument, you aren’t allowed to use cyclic reasoning. That is, you can’t say: I know that Niko likes Rust because Niko likes Rust. So, in the same sense, it seems natural that I should not be able to say “I know that implements because implements ”. But actually, it would sometimes be really useful to say exactly that. One example is so-called “perfect derive”. In our impl above, we had one where-clause, . And if you were to create a custom derive for and write , the impl I showed is typically exactly what you would get. But it’s not necessarily what you want . Consider what you get with : Here, the derive is going to create an impl that requires . But if you look closely, you’ll see that all the fields only use , so in fact, we should be able to clone a even without ! But how is the compiler to know this? You might think that the compiler could do some super smarty-pants analysis on the fields to figure it out. And, in a way, it can: that is what cyclic trait solving is all about. The thing is, while the compiler can do that, the derive cannot – the derive doesn’t have access to the definitions of other types and so forth, and clearly we would need to know things about and to figure out whether is required here. But what we could do is to generate a different impl. Instead of adding for each type parameter, we could add a where-clause for each field type. This makes sense: after all, we are just going to be calling on every field, so it’s quite logical to say that the impl is valid if every field is cloneable: Under this formulation, we can see that all we have to be able to do is to clone an and clone an , neither of which require that . We call this idea [perfect derive][] and it’s been a goal for a while. The thing is, cyclic reasoning is tricky to get right. The example is actually an easy one: that one doesn’t really require cyclic reasoning: But if we use that same “cyclic derive pattern” to generate our impl, things don’t work out so well. Instead of just a bound, our impl now has two bounds: Now imagine we try to show . We begin by applying impl L1, which requires us to show that its where clauses hold: Ugh. Something’s tricky here! Now, maybe you think we can just accept any cycles. And for these examples, it would be fine: but it’s not correct if you consider supertraits . Consider this trait and impl pair: If you are naive, this weird trait-impl pair can be used to prove that any type is , regardless of whether it has a impl. For example: Uh oh, now we did something wrong. We proved that even though there is no impl. Something is fishy. Now clearly we can all see the problem here – the implementation of didn’t really add any information. It was just a tautology, saying that if . It’s not wrong , but implementing was supposed to tell us more than just the fact that there is an impl of , it was supposed to tell us also that the supertrait is implemented. And that’s not true here. But if you think about it, it’s hard to decide why we should reject impl M but accept the impl L1 of for . They both wind up with a cyclic proof. So what’s the difference? This is the question we’ll be exploring over the next few blog posts. This gets at an interesting question: what does it mean for the trait system to be sound . This seems obvious but actually it was a question I found kind of non-obvious for a long time. We’ve found two satisfactory answers to that question. One of them involves converting to dictionary-passing style. Nadri explained that in a blog post . I think that’s a great post to read. I’m going to give another definition here that doesn’t require converting to a dependently typed program 2 My rough definition is this 3 : the trait system is sound if, whenever it accepts some program P, that program cannot have a function that believes some holds for the , but there is no impl of that can be used. So in the case of and , it’s easy to write a program that shows simple cyclic trait solving is unsound: By my definition, any sound type/trait system must reject this program because, if it were to execute, then execution would reach and yet there is no impl that is judged to ber applicable to . Uh oh! As I promised, this post was mostly focused on “setting the scene”. My goal was to explain what the problem is that we are trying to solve – permitting “good cyclic impls” but forbidding bad ones. I didn’t spend a lot of time on the bad ones, but it turns out that there’s a wide variety of unsound things one can do, some of which the compiler currently gets wrong, others of which it would only get wrong if we started permitting cycles. My motivation for getting into this work is a bit complicated. I want perfect derive. But it’s also a loose end in our trait semantics that I really want to see nailed down before we move onto other tasks. Having auto traits (e.g., ) work differently from other traits is clearly a “smell”, and without a strong understanding of the logical underpinnings of our trait system it’s easy to get things wrong when we build extensions. In the next few posts I’ll go a bit deeper into the exploration I and others have been doing. I’ll talk about some of the “false starts” we took along the way and why they don’t work, and then about some of the solutions that are under consideration. Working through this stuff has really helped me to broaden my understanding of various areas of logic. By the time we’re done, we’ll cover 4 coinduction and productivity, modal logic and the later modality, and we’ll see how our techniques might even help us with resolving specialization 5 . I cited it earlier, but if you want to read other tasks on the same subject, I definitely recommend Nadri’s post on dictionary-passing style . Apart from the default bound, I’m ignoring that here  ↩︎ I have found that both the dictionary-passing interpretation and the logic approach I’m using are valuable. In the end, they’re more or less equivalent, which I guess shouldn’t be surprising if you’ve heard of the Curry Howard Correspondence , but I’ll talk about that later perhaps.  ↩︎ I would like to, but haven’t, define a simplified version of Rust that includes trait solving and simple type checkoing and show that it cannot “go wrong” .  ↩︎ In a shallow way, I’m no expert!  ↩︎ Plot twist, bet you didn’t see that coming! I sure didn’t.  ↩︎ Applying “impl L” to show that if Then applying “impl I” to show that To show that we have to show that… , which is easy because doesn’t have any where-clauses 1 uses the impl which requires… , which is again easy To show we use impl L1, which has two where-clauses: , this one is easy because the impl requires that which is true. But is tricky. The impl requires that… We need to prove , and then the impl requires that… We need to prove , but that is what we started with! That’s cyclic logic! Say we want to prove that . We observe that if a type implements , it must implement , so… We begin by proving . We use the impl M, which requires that we show , which is a cycle, so we accept it. Apart from the default bound, I’m ignoring that here  ↩︎ I have found that both the dictionary-passing interpretation and the logic approach I’m using are valuable. In the end, they’re more or less equivalent, which I guess shouldn’t be surprising if you’ve heard of the Curry Howard Correspondence , but I’ll talk about that later perhaps.  ↩︎ I would like to, but haven’t, define a simplified version of Rust that includes trait solving and simple type checkoing and show that it cannot “go wrong” .  ↩︎ In a shallow way, I’m no expert!  ↩︎ Plot twist, bet you didn’t see that coming! I sure didn’t.  ↩︎

0 views
Anton Zhiyanov 1 months ago

Relying on Go

Everyone is creating a new programming language these days, often one that's "like Go but with more features" or "like Rust but simpler". Solod , a systems language for C and Go developers, might look like one of those languages, but it takes a different approach. Solod is not "Go-like" in the usual sense, nor is it an attempt to "fix Go's mistakes". At the language level, Solod is literally a subset of Go. Solod reuses much of Go's existing tooling, including syntax highlighting, LSP, linters, and the package management system. Take this quick-start guide, for example: Quick start Install the So command line tool: Create a new Go project and add the Solod dependency to use the So standard library: Write regular Go code, but use Solod packages instead of the standard Go packages: Run without saving the binary: There's nothing new here. It's mostly standard Go workflow, except for , which is a Go program that mimics . Solod also reuses a lot of Go's standard library code and tests. Some of it is taken verbatim from Go's source code, like these two string functions: Of course, Solod retains the Go authors' copyright. Some code requires changes to support the manual memory management with explicit allocators used by Solod: You can probably see the resemblance. Go tools don't know that Solod is a subset of the full Go language, so they won't flag features Solod doesn't support, like function literals or iterators. These diagnostics come from the custom tooling: Also, although a substantial part of Go's standard library is ported verbatim or with minimal changes from the original source, that doesn't mean the code is automatically correct. Solod still needs its own tests, including ones that run under sanitizers and static analyzers. All Solod code is translated to regular C11 and then compiled with GCC or Clang. Solod therefore relies on C tooling and decades of optimization work just as much as on Go's. Solod code: Translated C code: The C version is noisier, of course, especially for more complex programs than this one. But it remains readable. And since there's no runtime, interoperability between Solod and C costs nothing. A new language doesn't necessarily need a new ecosystem. Solod relies heavily on Go, and I see that as a strength, not a weakness. Reusing Go's proven tools and standard library makes Solod more reliable and easier to work with. If you're interested, take a look at Solod's readme — it has everything you need to get started. Or try it online without installing anything.

0 views
Blog System/5 1 months ago

An old-new take on argument parsing in Rust

Over the years, I’ve written tens of command-line applications in many different languages—shell is probably the top contender, believe it or not—and for various ecosystems. Along the way, I’ve developed… let’s say… opinions on how they should behave . But behavior and implementation are different topics, and today I would like to talk a little about the latter in the Rust ecosystem. A big theme behind those opinions is that consistency usually wins: when designing an application, you should target an ecosystem and make sure the tool feels “at home” within it instead of reinventing the way it accepts arguments or presents help. But what is the ecosystem? Is it the language the tool is written in, or… is it the set of tools with which it plays? For example: if you were to write a command-line application in Go, you’d naturally reach for the built-in library to define flags. Doing so would make the tool feel normal to other Go developers and would make it easier to “read” to them—but the end user does not care, dare I say… at all , which language your tool is written in. So if they try to use such a tool in the context of standard Unix tools like those provided by coreutils or textutils, your tool will feel out of place. And that is what matters to me: I develop tools for a certain ecosystem, not for a language, and I want those tools to integrate well no matter which language they are written in. I mentioned Go right above because Go is the prime example of opinionated choices that “leak” in various ways. This article is about Rust, however, so let’s switch languages. But before we do, take a moment to subscribe to Blog System/5 to demonstrate your support. It’s free if you want it to be! When writing Rust command-line applications, the expectation nowadays—or rather, assumption—is that you’ll use the crate to parse options and arguments. Funnily enough, this assumption is so ingrained in the ecosystem that, when I asked a late-2025 coding agent to review a codebase of mine, it hallucinated that I was using even when such crate was nowhere to be found. Here is what a simple -based hello world app looks like: Sample clap-based tool. On the left, the source code. On the right, an invocation without arguments and one invoking help. I will not deny that the resulting app looks nice and that the declarative idiom to define this interface is concise and very powerful. But the result is… out of place with other programs because of all these colors (I know they can be disabled). Also, the code is a bit too magical, as usually happens with -style libraries (I know you can opt out of that). And yet… even with all the bells and whistles, the library doesn’t provide enough mechanisms to define an app “end to end”. You see: Rust’s can return an , which is enough to report success or failure to the caller, but this still leaves the application’s control flow in your hands. Because you have to explicitly call within , there is no guarantee that you do it at “the right time”: you might be tempted to parse config files before parsing arguments and other nasty things like that, which can then lead to weird behavior like not working if the config file is malformed or on an unavailable network drive. isn’t the only game in town though. There are indeed other Rust libraries to parse command lines, with being another popular choice. Some of these are also built around derives, some make different tradeoffs around help text and output style, and some smaller alternatives focus mostly on parsing options . This is all fine, but it still doesn’t give me the small Unix-y framework I wanted: something that treats options and positional arguments as one interface, validates both consistently, and owns the startup sequence from parsing to exit code. For all of the reasons above, I’ve developed “my own ways” to parse options and arguments in Rust so that they align with more traditional Unix-y programs. In doing so, I ended up writing my own library. I initially wrote this library in the context of the EndBOX where I had to implement various system services and wanted: to enforce consistency among them with as little code duplication as possible, and to ensure integration into the host’s ecosystem of Unix-y tools provided by the NetBSD base image. I called that library at the time and, in the fall of 2025, I thought of cleaning it up a little and publishing it. So, today, I want to belatedly announce . Mind you, I had drafted this article back in November but never published it, so today is the day. Better late than never. You might be thinking that is quite a mouthful, and even an ugly name. And you know what? That’s true. But the name is what it is because builds on and extends the ancient getopts , in the tradition of Unix-like systems. is largely unused today in the Rust ecosystem—except for the tiny little fact that itself uses it. OK, OK, if you want me to be perfectly honest… the reason exists at all is because is what I originally picked up in 2016 when I started learning Rust based on my previous knowledge of the POSIX and the GNU libc functions… and I never switched gears. is basically a wrapper over , extending it to offer argument-parsing facilities. Where leaves you with a list of free-form strings to validate by hand, lets you: declare positional arguments with cardinality constraints, validates those constraints for you, prints them as a distinct section in the generated help, and provides helpers for common application metadata such as version, bug-reporting, home page, and manual-page information. As such, its API tries to follow the same interfaces that offers, which means I’ve kept original names intact and modeled my own extensions in a similar fashion. This leads to rather cryptic method names and suboptimal Rust interfaces, but again, I tried to mimic as much as possible. What I’ve changed, however, is the way in which you should use the library. provides an “end-to-end” framework to define the method of an application, and it does so with three pieces. The first is the application , which is a struct that implements the builder pattern to register application metadata, options, and arguments. The second is the command-line , which extends the struct for parsed options with access to parsed arguments. And the third is an macro that facilitates writing the scaffolding for , delegating to a couple of functions. A sample, full-featured program looks like this: And then we can run it in various ways: You might still say: there is too much magic in those macros and builders! And you’re right, so you can also define the same app using imperative code and avoid all of that. To see how, I’ll refer you to the various upstream examples . I do not intend for this crate to replace the nicer or or the other libraries that exist out there. Heck, I do not even expect any of you to want to use it. But I still have a use for something like this in my own programs, and I wanted to factor out the code I had already written into a cohesive standalone piece, so I had to publish the crate. Head to https://github.com/jmmv/getoptsargs/ for more details! Is it the language the tool is written in, or… is it the set of tools with which it plays? Sample clap-based tool. On the left, the source code. On the right, an invocation without arguments and one invoking help. I will not deny that the resulting app looks nice and that the declarative idiom to define this interface is concise and very powerful. But the result is… out of place with other programs because of all these colors (I know they can be disabled). Also, the code is a bit too magical, as usually happens with -style libraries (I know you can opt out of that). And yet… even with all the bells and whistles, the library doesn’t provide enough mechanisms to define an app “end to end”. You see: Rust’s can return an , which is enough to report success or failure to the caller, but this still leaves the application’s control flow in your hands. Because you have to explicitly call within , there is no guarantee that you do it at “the right time”: you might be tempted to parse config files before parsing arguments and other nasty things like that, which can then lead to weird behavior like not working if the config file is malformed or on an unavailable network drive. isn’t the only game in town though. There are indeed other Rust libraries to parse command lines, with being another popular choice. Some of these are also built around derives, some make different tradeoffs around help text and output style, and some smaller alternatives focus mostly on parsing options . This is all fine, but it still doesn’t give me the small Unix-y framework I wanted: something that treats options and positional arguments as one interface, validates both consistently, and owns the startup sequence from parsing to exit code. Enter simpler times For all of the reasons above, I’ve developed “my own ways” to parse options and arguments in Rust so that they align with more traditional Unix-y programs. In doing so, I ended up writing my own library. I initially wrote this library in the context of the EndBOX where I had to implement various system services and wanted: to enforce consistency among them with as little code duplication as possible, and to ensure integration into the host’s ecosystem of Unix-y tools provided by the NetBSD base image. declare positional arguments with cardinality constraints, validates those constraints for you, prints them as a distinct section in the generated help, and provides helpers for common application metadata such as version, bug-reporting, home page, and manual-page information.

0 views
Unsung 1 months ago

“Gravity is worth asking about.”

I’ve enjoyed John Gruber’s posts about ads appearing on an increasing number of Apple surfaces: the App Store, Apple News, and – soon, perhaps – Apple Maps. (Just for reference, here’s an example of such an ad.) = 3x)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/1-framed.1600w.avif" type="image/avif"> In a post earlier this week , Gruber likened ads to stickers on laptops, and shared a fun Steve Jobs story: That’s what those stickers on PCs are: they’re ads. Intel pays for the “Intel Inside” stickers that booger up PC laptop palm rests. Longtime readers will recall that back in August 2007 , Apple held a Town Hall event to introduce new iMacs and some iLife and iWork software updates. In a post-event Q&A (imagine that), Bob Keefe of Cox Newspapers asked “Can you say why you all are not participating in the Intel Inside program, putting the stickers on your new or previous Macs?” This question was so absurd from the perspective of those who covered Apple closely that it prompted outright laughter. […] The 2007 exchange went as follows: Keefe: Why are you not participating in Intel Inside program and not putting stickers on your Macs? Jobs: Uh… what can I say? We like our own stickers better. (In case it’s not clear, this was a joke; Apple didn’t and doesn’t put any such stickers on their products.) In May, Gruber posted about Apple’s ads , too, and brought up the zero-one-infinity rule : I feel like a variation of Zero-One-Infinity is a good rule of thumb for ads, too. From the perspective of users — and probably developers — zero was the best number of ads for Apple to show in App Store search results. One was worse but acceptable. But now that they’re showing more than one, they’re on their way to infinity. They’ve started down the slippery slope. Remember when Google only showed one ad in search results? “Slippery slope” is a perfect term. But I wanted to add something here. In my experience, in the realm of UI, there is no middle notch. I’ve seen it time and time again… the moment you open the door to One, Infinity starts exerting its pull: Here are two examples I’ve been thinking about recently: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/2.1600w.avif" type="image/avif"> This – screenshotting in iOS – was originally just one fork: Save or Delete. Now it’s a staggering five options I have to choose from, every time, even if I never touch four of them: Once you wedge one thing in the door, it’s really hard to stop. My theory is that this is because digital interfaces are pretty much all infinitely extensible. There will always be a way to add one more button, one more link, one more setting, one more ad. If something doesn’t fit, you make it smaller. If making it smaller looks bad, you add a scrollbar. If a scrollbar doesn’t feel right, there’s always overflow. Not only it’s very hard to create interfaces that have limitations, but a bad decision is not just precedent – it’s code that can be copied and reused. Existing code always had tons of… well, gravity, even before LLMs. And so, products grow complex without anyone intending them to; a new team adds just one more thing, which in isolation always feels like nothing to worry about. The Hick’s Law , the extra mental load , the complexity all grow in between those moments, in a no-man’s land no team typically feels responsible for. The logic is always circular: Why would the team adding a third option have to do something a team adding a second option didn’t have to do? Why would the team adding the second option worry in advance about option number 5? This is why it’s important to hire and recognize people who will understand that those limitations have to be imposed arbitrarily, and empower them to be able to say, “Let‘s not add this. We like our own stickers better.” = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/4.1600w.avif" type="image/avif"> (My MacBook does have a sticker, which I bought and put on it since for some reason I find it really funny.) #complexity #system design adding just one setting will send a message that We Do Settings Now and more settings will follow, one uncomfortable exception followed by weeks of deliberations will inevitably open the door to subsequent mindless exceptions, one cheap or lazy approach can spread through the interface like rust, subconsciously telling people “cheap and lazy solutions are okay here.” This right click menu in Chrome started with just one fork (new window or new tab) – now there are three alts that I have to choose between, every single time, even if I only ever use one option: This – screenshotting in iOS – was originally just one fork: Save or Delete. Now it’s a staggering five options I have to choose from, every time, even if I never touch four of them:

0 views
Corrode 1 months ago

Understanding Dyn Compatibility

In Rust, some traits can’t be used as trait objects with . When a trait can’t be used with dynamic dispatch, we say it’s “not dyn compatible.” 1 This has an impact on how you can use these traits in your code. I think that’s one area where the Rust compiler could print a more helpful error message. Fixing the issue is mostly about tradeoffs between compile-time generics and runtime polymorphism and learning when each one fits. Once you understand the concept, you’ll know how to get around the issues by choosing a better design for your trait. If the compiler told you a trait is “not dyn compatible” , your trait can’t be used as because it has a method that can’t go through dynamic dispatch, usually one that returns , takes no , or is generic. To fix it, pick one: Continue reading to understand the tradeoffs between each approach. Here’s an example with code that won’t compile . Say you have a trait that has a method returning a copy of itself: …and there’s a button, which implements : If you tried to compile this code, you’d get an error like this: That all sounds pretty confusing. When you use , Rust creates a trait object . Trait objects use dynamic dispatch to call methods at runtime. Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. For dynamic dispatch to work, the trait’s dispatchable API must follow certain rules. These are simplifications: each method-level rule is really “…unless that method opts out with ”, which we’ll see in a moment. Traits also have a few item-level restrictions, such as no associated constants; we’ll summarize the fuller list later. For now, the rough version is enough to build intuition. In our example, we violate the first rule: the method returns , which means “the same type as the implementor of the trait”. When you use , the concrete implementor is hidden behind the trait-object interface. The vtable still points to the right concrete implementation, but the call site has no single concrete return type it can name for . That’s a problem, because the compiler needs to know the size of the return value at compile time, and could be any size . It needs to know the size, because the returned value has to live somewhere : the caller sets aside exactly the right amount of space (usually on the stack) before the call even happens. With a , the concrete type is erased from the caller’s static type, so there’s no single size the compiler could reserve for it. It will become clearer once we look at some fixes. Don’t worry, we won’t have to refactor all our code! All fixes use the same trait example. There are multiple ways to make it dyn compatible. We have a bunch of options: Each approach comes with different tradeoffs. Depending on the kind of dyn-compatibility issue, one might fit better than the others, or you might combine a few. Let’s look at each of these in detail. One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types at compile time , so the compiler knows the size of . The compiler generates a separate copy of the function for each concrete type that implements the trait. Then, at runtime, you no longer need to worry about any dynamic dispatch (which means “figuring out the type at runtime”). The compiler always knows which type it is dealing with, so it can pick the right method to call. Our trait stays the same: But now we change the function which uses the trait to use generics instead of : Note how we changed the function signature to use a generic type parameter that implements the trait. Here we tell Rust: “I have some type that implements , and I want to use it.” Rust then generates the necessary code for each type used. That’s close to using , but not quite the same. The difference is that with generics, the compiler knows the concrete type at compile time, so it can handle correctly. For instance, we might know that is in this case, so returns a . Now the confusion about what means is gone! The downside is that you can’t fully lean on dynamic dispatch anymore, and you might have to refactor a lot of code if you were using trait objects extensively before. Your binary size might also grow because of all the copies of the function that the compiler generates for each concrete type. What’s the benefit of fully leaning on dynamic dispatch? Fair question! Dynamic dispatch has a bunch of really nice properties: Another option is to keep using trait objects but change the problematic method to only work with concrete types. This means “this method can only be called when has a known size at compile time”, which is true for concrete types but not for trait objects. It’s more explicit, since you control how the trait can be used. The catch is that it limits the trait further down the line: some methods won’t be callable on every trait object, and changing the trait later becomes a breaking change. You won’t be able to call on , but you can still call it on concrete types like . So you keep most of the flexibility of trait objects (unlike with generics), as long as you remember that some methods won’t be available through . We can change the return type of the problematic method to return a boxed trait object instead of . This works because has a known size at compile time. It’s a pointer to an object on the heap. It’s actually a fat pointer : two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable; more on that later. What matters is that this size is fixed and known at compile time, unlike , which varies based on the concrete type. The downside is that tends to be viral in your codebase. You’ll end up writing more often than you’d like, which gets noisy. On top of that, this fix only works for methods that return . If your trait also has static methods or generic methods, you’ll need to combine this approach with one of the other fixes. Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. Maybe your code is silently trying to tell you that you are mixing up two different responsibilities and that they should be untangled. In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! Instead, we lean on composition and focus on behavior rather than mangling multiple ideas into a single trait. Here’s a more realistic example: separating rendering from widget creation. Factory methods are often static (no parameter), which makes them incompatible with . So we split them off into a separate trait. When you write , you’re creating a trait object . It’s a special kind of value that consists of two pointers (a “fat pointer”): As you can see, a trait object has: The vtable is created at compile time and contains pointers to the methods for the specific type. It is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. For dynamic dispatch to be sound, the vtable-facing methods need stable, concrete function signatures: If a trait has dispatchable methods that return or have generic parameters, there is no single vtable entry with one concrete signature that can represent all possible calls. That is the root cause of dyn compatibility issues. A trait is dyn compatible if it follows a list of rules . The rules boil down to the same core issue: the interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden. A Modern Gotcha: in Traits Since Rust 1.75 , you can write directly in a trait. But there’s a catch: a trait with an is not dyn compatible . An desugars to a regular method that returns , a hidden return-position . The type is called “opaque”, because we don’t know what it is, and the compiler doesn’t expose it to us. Opaque return types aren’t dispatchable (which means we can’t put them in a vtable of functions), so the trait can’t be used behind . If you need dynamic dispatch with async methods today, you have a few options: Dyn compatibility determines if a trait can be used with . The rules exist because: If your trait is not dyn compatible, don’t worry! Many standard library traits ( , , etc.) are also not dyn compatible. As we’ve seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. Which fix to reach for depends on what your trait needs and what you’re willing to give up: In practice you’ll often combine these. For example, splitting a trait and boxing a return value. I find it interesting to see how dyn compatibility evolved over time in Rust. If you do, too, here are some resources to dig deeper: The lang team also wants a “practical path” to call s through natively. It’s on the 2026 project goals , so the async gotcha above should ease over time. The concept used to be called “object safety” until Rust 1.84.0. If you’re reading older resources, they mean the same thing. The name got changed because it was confusing. “Object safety” suggests that Rust has “objects” in the traditional OOP sense and that the term is about “safety”, which is misleading. The new term “dyn compatibility” does a better job of saying that it’s about whether a trait can be used with for dynamic dispatch. I still don’t love either term, but I also can’t think of a better name that is both short and accurate. ↩ add to the offending method return instead of use generics instead of split the trait in two What does “not dyn compatible” mean? Shouldn’t the part take care of it? What’s a “vtable”, and why does the trait need to “allow building” one? What does it have to do with ? Dispatchable methods must not return . Dispatchable methods must have an allowed receiver ( , , , and a few related pointer forms). Plain static methods don’t have one. Dispatchable methods must not have generic type parameters. Use generics instead Opt out problematic methods with Return boxed trait objects instead of Split into two traits It’s very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. You could technically do the same with generics, but sometimes you can’t afford the increase in code size or compile times that come with monomorphization. It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a trait. Using dynamic dispatch, you can store them all in a single collection and call . If you were to try the same with generics, you’d end up with a lot of boilerplate code to handle each shape type separately. A data pointer that points to the actual data (the concrete type implementing the trait) A vtable pointer that points to a table of function pointers for the methods every dispatchable method needs a receiver that leads to the object and its vtable argument and return types must be expressible without knowing the hidden concrete the vtable must contain a finite set of function pointers, known at compile time Box the future yourself and return . Use the crate, which does that boxing for you. Use the crate, which generates a dyn-compatible wrapper for traits with . Trait objects use dynamic dispatch via vtables Vtables are static, compile-time structures, which hold method pointers Type information is erased at runtime to allow polymorphism The compiler must guarantee type safety at all times, even if it can’t see the concrete type 2014-09-22: RFC 255 - Introduced object safety (2014, before Rust 1.0) 2014-11-03: Issue #428 - Object-safety and static methods 2015-01-03: RFC 546 - Removed implied bound on traits 2023-08-24: Rust 1.72 - GATs can be opted out with 2023-12-28: Rust 1.75.0 - Stabilized and return-position in traits (though such traits still aren’t dyn compatible) 2025-01-09: Rust 1.84.0 - The docs had moved from “object safety” to “dyn compatibility” around this release cycle; the tracking issue notes that the rename unfortunately missed the release notes. The concept used to be called “object safety” until Rust 1.84.0. If you’re reading older resources, they mean the same thing. The name got changed because it was confusing. “Object safety” suggests that Rust has “objects” in the traditional OOP sense and that the term is about “safety”, which is misleading. The new term “dyn compatibility” does a better job of saying that it’s about whether a trait can be used with for dynamic dispatch. I still don’t love either term, but I also can’t think of a better name that is both short and accurate. ↩

0 views
<antirez> 1 months ago

Being Linux Torvalds

(This blog post was adapted from the transcription obtained from my YouTube video at https://www.youtube.com/watch?v=l6lxgYeVZqs) When Linus Torvalds developed the first Linux kernel, he had studied the Minix sources, he had studied computer architecture, he had the base knowledge needed, and he was obviously a very brilliant programmer. But that operation of writing a minimal yet working Unix kernel for the 386 (at the beginning Linux was, let's say, mono-architecture) was something within the reach of many other programmers and students. Many in the sense of, I don't know, 0.1%, one in a thousand, one in ten thousand. Obviously most people are not able to do this kind of feat, but a lot of people are. If you look at Hacker News in the latest years, you'll see how many projects of kernels written in C, microkernels implemented from scratch, kernels written in Rust, kernels made in all sauces and manners, small Unix systems created vertically for the Raspberry Pi, operating systems for the ESP32 and so forth. Writing a kernel is not something within everybody's reach, but it is something that many can complete, if they put enough effort into it. Then, of course, not everybody will do it well. He is a genius programmer, without any doubt, so he did it better. And yet, of Linus there is only one. This implementative capacity of his, in fact, would not tell us much about him: what we should focus on, instead, is what happened later. ## He stopped writing code Among the maintainers of the famous open source projects, he was one of the very few that, very early in the history of the development of Linux, almost completely stopped writing code in order to concentrate on the leading of the project. On being the leader, the coordinator, the single mind holding the clarity about what the goals of the project must be, and so on. And this is a rare thing. Many maintainers (myself included, for a long time) continue instead to implement things directly, to not delegate much, and so forth. This also starts from a different idea of software. Linux, necessarily, had to grow immeasurably: it is in the quality itself of a kernel that wants to embrace many devices, platforms, subsystems, and to continuously adapt to the times, to the needs of the new software, to the hardware that comes out little by little. So this was not a mistake. Redis, on the contrary, could remain something self contained. The other day I received a pull request on linenoise from Dr. Richard Hipp of SQLite: he too aimed at stability, at minimalism, at performances, but always keeping the code base very small, and he continued to write code for a very long time. Linus, instead, no. He understood immediately that he had to donate his time to something that was more important, for a project destined to become very big compared to what is the implementative capacity of a single person. So he became the project leader, the one that owns the ideas, the direction. And what is it that Linus does, then? He does not look at every patch line by line, every time. Of course it also happens to him to look deeply into a single implementation, in order to understand what is going on. It happened to him, over the years, to write some new subsystem, or even to rewrite one: I think he did it once with the USB layer, many years ago, and he did it with the virtual file system, that at some point I believe he reimplemented, changing the structure of the inodes and of the inode cache, and he did it for several other reasons. From time to time he continued to program, when he created Git, and so forth. But for the most part he does not look at the patches singularly, in detail, line after line: he communicates with the maintainers of the subsections, and understands if a given feature or a given direction is, or is not, a road to take. So, to say it in Brooks' terms, in Mythical Man Month terms, Linus holds the design concepts of the kernel, and continues to dialogue with everybody below him in the hierarchy of the kernel so that the kernel goes towards a certain direction. So that the developments go towards a certain direction, both from the implementative point of view (how these developments are implemented, what is the quality, what is the implementative idea in the very way the code is written), and from the design point of view: what is it that we want to do, what we don't want, what is the best strategy for the modules, for the scheduler, for the hardware support, for the integration of Rust or not. All this stuff here. Now, I believe that this was the real genius of Linus. He is not just a very brilliant programmer: there are others. He is also a maintainer, an incredible designer, and one capable of handling a huge project ideas and structure in a coherent way, dialoguing with many other people. This thing is not for everybody. ## We are Linus, now Now, when we program with the artificial intelligences, we are exactly that same thing. We are Linus Torvalds, not always with the talent that he has, but the role we should assume, in the projects where we don't do the review of every line of the code, is exactly of that type. It is exactly the role that he has. Only, the thing is simpler to dominate: unless we use a lot of agents in parallel, it is substantially simpler to dominate than a multitude of patches arriving from different ways. But it is much faster. It is as if, instead of interacting with a team composed of many people at human speed, we interacted with a team composed of one, two, three people, based on how many parallel branches of our project we are developing in that moment, but that are much faster, so they give us immediately a much faster feedback. This slightly changes the modality of the work, but in my opinion for the better: it is easier, less context switching, fewer people to deal with, many fewer problems due to the character, the attitude, and so forth. So, if we think that this role is important, we must not think that automatic programming is "I put the prompt, and the thing writes". Vibe coding is a wrong idea of what automatic programming is, and of what automatic programming will be for the majority of people. Vibe coding is a very interesting thing for who does not have technological abilities and wants anyway to have an impact on the construction of their own tools, and so forth: so, welcome, because it democratizes the possibilities. But it is not that. Automatic programming, instead, in the hands of people that are expert technicians, or expert programmers, expert designers, expert software architects, is to assume the role of Linus, with the agents and the LLMs assuming the role of the different maintainers of the different subsystems. And since not everybody is able to do it so well, automatic programming as well has need of talents that talk with the agents, that check the ideas, that know which are the implementations to do and the ones not to do, the way of communicating with the agents in order to make them do the best work, putting there those design hints that a great programmer intuits, that a good programmer intuits and manages to precompute. So automatic programming, when it is done well, means to assume the role of Linus. And this thing can be done well, it can be done badly, it can be understood, or it can instead be debased. And it is also something that needs training, that needs to be learned, exactly as Linus had to learn it: he surely had an innate talent for this, but he passed from "I implement everything" to that capability of handling a symphony, of being the orchestra director. That, for me, is the lesson of Linus, and it is one that should immediately be used as an argument of contrast for those that say that, well, with the LLMs programming is easy for everybody. Comments

0 views
Corrode 1 months ago

Hardening Rust Code For Production

We talked about patterns for defensive programming in Rust before, in which implicit invariants that aren’t enforced by the compiler lead to utter misery. But being careful isn’t enough! Even valid code can fail at runtime in ways that are hard to predict and control. That’s what we’re covering next. This article is for you if you want to… What happens when a Rust program panics? There is no single correct answer because is not a “single behavior.” For starters, there’s a difference between unwind and abort. invokes a closure, which captures the cause of an unwinding panic. But the Rustonomicon has the following to say about unwinding panics: We would encourage you to only do this sparingly . In particular, Rust’s current unwinding implementation is heavily optimized for the “doesn’t unwind” case. If a program doesn’t unwind, there should be no runtime cost for the program being ready to unwind. The alternative to unwinding is aborting the entire process. That does what it says on the tin: the program immediately terminates without unwinding the stack or running destructors. Halt and catch fire. Weirdly enough, that’s often the safer choice, especially when dealing with FFI boundaries or performance-critical code. That’s because unwinding across FFI boundaries is undefined behavior, and unwinding can be expensive in performance-sensitive code. To enable aborting on panic, add the following to your : And even if you did not explicitly configure this, catastrophic panics like stack overflows and out-of-memory errors always abort the process . That’s because unwinding in these situations is unsafe and can lead to undefined behavior. In practice, this shows up in two places: These failures are fundamentally different from ordinary panics in that they cannot be caught or recovered from. To handle them gracefully, you need to know exactly how and where your program will run, and design accordingly. For example, in the case of , avoid unbounded user input that could lead to excessive allocations. Another difference is between thread-level failures and process-level crashes. A common misunderstanding is that terminates the entire program, but in a multi-threaded application, that is not necessarily the case. For example, a background worker thread can panic while the main thread continues running. What sounds like a benefit can leave the system in a partially degraded state. This distinction becomes especially important in long-running systems (servers, workers, async runtimes, …). A panic in a request-handling thread might only abort that one request, while the rest of the service remains available. Here’s a small example using scoped threads ( Playground ): The interesting part of the output is this: Request 2 panics, but requests 1 and 3 still finish. The panic belongs to the worker thread. The main thread gets notified on but keeps running. 1 Whether this is acceptable depends on the system’s invariants. If a panic indicates a violated assumption confined to a small scope, like a single request, letting the process continue may be reasonable. But if it signals a global invariant violation, continuing execution can be outright dangerous. Panic behavior is part of your system’s failure model . Treating all panics as equivalent hides important distinctions and leads to fragile assumptions. Be explicit about whether a failure may take down a single task, a single thread, or the entire process. Never panic in an uncontrolled manner. If you maintain a library, you have less control over where your code runs and what a panic can take down. Consider enabling stricter Clippy lints such as and to catch common panic sources before they become part of your public API. Those lints can be noisy in applications, but they are often useful when panic freedom matters more than convenience. Now that you understand how panics work, let’s talk about operational hardening. When things go wrong, you want to know about it. But by default, Rust panics just print to and disappear into the void. In production systems, that’s not so great. You might prefer crash reporting or centralized failure handling, and that’s where panic hooks come in. A panic hook is a function that gets called whenever a panic occurs, giving you a chance to record the failure before the program terminates or unwinds. It will not make an invalid state safe again. Its job is to capture enough context to debug the failure, alert someone, and shut down cleanly when possible. Here’s a simple example of setting a panic hook: And here’s a panic hook that sends structured JSON data to a crash reporting service: What’s Inside ? The struct contains the panic message (via ) and the source location where the panic occurred (via ). Be aware that both can leak sensitive information: file paths may reveal internal directory structure, and panic messages might contain interpolated user data. And finally, here’s Sentry’s panic hook handler , which is even more sophisticated: Sentry’s panic hook: There’s a lot to learn from these few lines of code! Panic hooks are also your final opportunity to prevent information leaks. The sensitive data can come from two places: the panic payload and the panic location. The payload is whatever your code passed to , , , or an assertion. That means it can contain interpolated user input, internal state from output, request headers, tokens, email addresses, IP addresses, customer IDs, or other identifiers. The location can expose source file paths, workspace names, or CI/build machine directory layouts. A well-designed panic hook sanitizes these messages before they reach logs or crash reports. Better yet, avoid putting secrets or raw user data into panic messages in the first place. Prefer stable error codes, request IDs, or redacted domain types. Regexes can catch obvious patterns like email addresses and bearer tokens. UUIDs and IP addresses can also identify users. Treat those checks as your final fallback. You can look into crates like expunge or veil to automatically redact sensitive information from structs: Before the process terminates, you might want to flush logs, close network connections, or notify other systems that this instance is going down. Setting a hook is a great way to perform such cleanup operations. Panic Hooks Run in a Compromised Environment Be careful: one of the subsystems you want to interact with might be the cause of the panic you’re handling! For example, if your database connection pool panicked, trying to flush pending writes to that same pool will likely fail or hang. Keep cleanup operations fault-tolerant and avoid anything that can panic, block indefinitely, or depend on the subsystem that just failed. Panic hooks only run for unwinding panics. If your program aborts on panic, or if the panic is caused by a stack overflow or out-of-memory condition, your hook won’t execute. Never rely on panic hooks for correctness. They’re purely for observability and graceful degradation; don’t try to recover from logic errors as it is very hard to rely on a system’s fragile underpinnings at this stage. Okay, you handle errors gracefully and you know how your system behaves on panic. Panic behavior isn’t the only runtime failure mode you need to worry about. Here’s some simple recursive code. What is wrong with it? The problem is that recursion can quickly exhaust stack space. If you allow users to call this function with large inputs, it might crash your program. Rust does not guarantee tail-call optimization on stable Rust . Some compilers and languages can turn certain tail-recursive functions into loops, but you should not rely on that transformation in Rust. If recursion depth depends on user input or external data, rewrite the algorithm iteratively or put an explicit bound on the depth. It takes some experience, but for recursive algorithms where you’re not in control of the input size, it’s often safer to use an iterative approach: One of the most dangerous assumptions in Rust development is that debug and release builds are functionally equivalent. They’re not. In many ways, you’re shipping a different program than the one you tested. The most obvious difference is integer overflow behavior. Debug builds panic on overflow, while release builds silently wrap around. We covered that in Pitfalls of Safe Rust . But the differences run deeper than arithmetic. Release builds remove checks, enable optimizations, and may exercise different code paths behind . Unsafe code and FFI boundaries are especially sensitive to this: undefined behavior can appear harmless in debug mode and break only once the optimizer starts relying on Rust’s aliasing and validity rules. Here is a trivial example: In a debug build, trips the . In a release build, the assertion is gone. The subtraction can underflow and wrap around, turning an invalid discount into a huge number. If the check protects a real runtime invariant, use or return a instead of relying on . The fact that tests pass in debug mode does not prove that production behavior is correct. Run normal debug tests as the fast default, and add release-mode tests for critical integration tests, arithmetic-heavy code, unsafe or FFI-heavy code, and anything whose behavior depends on optimization or release-only configuration. Your code is only as safe as your dependencies. You should regularly audit your dependencies for known vulnerabilities. Two helpful tools for that are and . It’s recommended to run those as part of CI. mimalloc is a drop-in global allocator built by Microsoft. What’s special about it is that it also has a secure mode , which adds mitigations like guard pages, randomized allocation, and encrypted free lists to make some heap-corruption bugs harder to exploit. 2 Safe Rust already prevents most use-after-free and buffer-overflow bugs, and a secure allocator does not magically make memory-unsafe code safe. This is mostly defense-in-depth for programs with unsafe code, custom allocators, C/C++ dependencies, or FFI-heavy boundaries. To enable secure mode, put this in : Then use it as your global allocator: Now, all heap allocations in your Rust program will use mimalloc’s secure allocator. Measure the performance impact on your workload before rolling this out broadly; allocator choice can matter a lot for latency-sensitive services, games, packet processing, and other allocation-heavy programs. Even well-written Rust code can be compromised through its dependencies, environment, or C FFI boundaries. The idea is to reduce your blast radius. Now, how you do that depends on your deployment environment, but generally people use Docker and Linux, so I thought I’d share some techniques for those; specifically, how to build minimal container images and filesystem sandboxing. A minimal production image contains exactly what you put in it. Even if your service is compromised, the attacker has very limited tools at their disposal to do further damage. My recommendation is Google’s distroless images , but please do your own research 3 as I’m not an expert on this. Distroless images are minimal Debian-based images stripped of everything unnecessary, while still including TLS certificates and a non-root user. For a typical Rust web service, start with : it includes the C runtime libraries that a normal Debian-built Rust binary may dynamically link against, but no shell or package manager. (Check the latest version in the distroless README .) Here is an example Dockerfile using for dependency caching: Take this Dockerfile as a starting point, but please adapt it to your own project requirements. keeps dependency builds in a separate Docker layer, so changing your application code does not force all dependencies to rebuild. The important details are: use the same Rust version in all build stages, build with , scope workspace builds with when appropriate, and keep , , and editor files out of the build context via . For a deep dive on Docker images and build-time optimization, see Tips For Faster CI Builds . Keep the Debian suffix explicit instead of using the unversioned tag, and pin by digest if reproducible deploys matter to you. If you deliberately build a fully static musl binary, then or even can be a better fit. But don’t mix the two approaches: a glibc-linked binary needs a runtime image that provides the libraries it links against. A Note On Alpine Base Images Alpine base images are a well-known alternative, but they use musl instead of glibc. That can expose differences in DNS resolution, TLS/native dependencies, allocator behavior, and crates that assume a glibc-like environment. ( 1 2 3 ) That doesn’t mean Alpine or musl are wrong; just treat them as a deliberate target and test them like one. If you build on Debian and want a small runtime image, distroless is usually the less surprising default. Even inside a minimal container, your process still has access to any file the container mounts. Landlock is a Linux security module that lets a process restrict its own filesystem access. If your service is ever exploited, the attacker can only reach the files you explicitly allowed. 4 Landlock Is Deployment-Specific Landlock is Linux-only and requires kernel support. It landed in Linux 5.13, but older enterprise kernels, custom cloud images, or container hosts may not enable it. Check your actual deployment target. Also apply the sandbox only after you know which files your process needs. If your service executes helper binaries from , reads timezone data from , loads certificates, opens SQLite files, reads config from , or writes uploads to , those paths must be allowed explicitly. On non-Linux targets, look for equivalent sandboxing mechanisms instead of copying this exact snippet. Call as early as possible in , before spawning threads or accepting connections. The restrictions apply to the entire process from that point forward. The two approaches really go hand in hand: Don’t run as root in production, even inside a container. That’s one reason distroless images provide a user and why the example above uses the tag. If your service only needs to listen for HTTP traffic, prefer a high port like over running as root just to bind to port . Linux capabilities are another useful lever. Instead of giving a process full root privileges, grant only the specific capability it needs, such as for binding to low ports. If a process needs elevated privileges only during startup, drop them before accepting requests. The details vary by platform and orchestrator, so treat Linux containers as one concrete setup. For systemd services, Kubernetes, FreeBSD jails, macOS sandboxing, or Windows services, look up the equivalent least-privilege and sandboxing features for that environment. The big picture is that security hardening is about reducing the surface of things that can go wrong. Every capability your process holds unnecessarily is a liability and everything your code manages that could be delegated to the OS, init system, or container runtime probably should be. Miri is an interpreter for Rust’s mid-level intermediate representation (MIR) that can detect undefined behavior at runtime. It works by executing your Rust code in a special environment that tracks memory accesses, pointer validity, and other low-level details to catch issues that the compiler can’t statically guarantee against. More people should know about Miri, because it is really helpful for hard-to-detect race conditions in multi-threaded or async code; but it can do way more than that, of course. It has already detected a lot of real-world bugs , even in the standard library. Using it is as simple as running: This will run your tests under Miri’s interpreter. The docs also describe how to add miri to CI : (Make sure to check the latest instructions in the Miri repo, as the setup process may change over time.) If you’d like to learn more about Miri, there is a research paper from 2026 that goes into the design and implementation details: Miri: Practical Undefined Behavior Detection for Rust . A hardened service doesn’t just crash. Instead, it shuts down gracefully when asked. Aim to finish in-flight requests, flush your buffers, and release resources cleanly before you exit. The pattern is: listen for shutdown signals, stop accepting new work, drain existing work, then exit. Frameworks like Axum have built-in support for graceful shutdown . Use it! The key is handling signals like (sent by Kubernetes, systemd, or ) and (Ctrl+C). Here’s a minimal example using tokio-graceful-shutdown , which is a crate that provides good signal handling without much boilerplate. It introduces a concept of “subsystems” that can run concurrently and listen for shutdown requests. When an external service (database, API, cache) starts failing, you don’t want to keep hammering it with requests. A circuit breaker tracks failures and “trips” when a threshold is reached. For production use, consider crates like or the more actively maintained , which is based on failsafe. Unbounded resources are a common source of runtime failures. Everybody who was on call for a production service will tell you this. Set explicit limits on everything . SREs will thank you for it! Limits make your service more predictable, and they make misconfigurations obvious sooner. Common things you should limit include: Here are some examples of how to do this in practice: See Axum’s : Bound the number of items in every queue or channel in your system. Every unbounded resource is a potential DoS vector. Explicit limits turn those catastrophic failures into (annoying but harmless) graceful rejections. Ideally, your system should be able to recover from transient failures without human intervention. Health checks let load balancers and orchestrators know when something is wrong, so they can react. A typical setup has two endpoints, a liveness probe and a readiness probe. The liveness probe checks if the process is alive at all, while the readiness probe checks if the process is healthy enough to handle traffic. This could honestly be an entire article on its own, but here’s a quick example using Axum to illustrate the concept: What’s neat about it is that this maps directly to Kubernetes’ health check system: Do we really need both probes? Yes, because they serve different purposes: Finally, here are some more tools that help you catch problems before they hit production: The tools above help catch undefined behavior, memory safety issues, code coverage gaps, and performance bottlenecks. They are dynamic analysis tools that complement Rust’s static guarantees. This only holds for unwinding panics. If you compile with , or hit a stack overflow or out-of-memory failure, the whole process exits and never gets a chance to return . ↩ https://docs.rs/mimalloc-safe/latest/mimalloc_safe/ ↩ Data sources I found useful for this topic include this post and this comparison . ↩ This approach would have prevented a vulnerability in Meta’s crate , a tool for recording and displaying system data like hardware utilization and cgroup information on Linux. ↩ make your code resilient at runtime harden your Rust code for production know how Rust code can fail in unexpected ways and how to recover from that Panic Semantics Are Part of Your API Unwind vs. Abort Thread-Level vs. Process-Level Failures Observing Failures With Panic Hooks Example Panic Hooks Sanitizing Sensitive Data Cleanup Operations Limitations Stack Overflows And Runtime Behavior Release and Debug Builds Are Two Different Programs Testing Release Behavior Supply-Chain Security Secure Allocations With mimalloc Limit Your Runtime Attack Surface Minimal Docker Images Filesystem Sandboxing With Landlock Drop Privileges and Capabilities Miri: Detect Unsafe Code Issues Graceful Shutdown Handling Circuit Breakers for External Dependencies Resource Limits Request Body Size Limits Limit Queue Depth Set Timeouts on Everything External Health Checks and Self-Healing Runtime Hardening Tooling Panics that would unwind across an extern “C” boundary are defined to abort instead of unwinding, because letting unwinding cross that boundary is undefined behavior . And if a fails, it aborts the process . If that’s a problem, you need to proactively check for allocation sizes before allocating or avoid heap allocations altogether. Logs the panic information Preserves the previous panic hook behavior by calling Ensures the hook is only set once using minimal images limit what’s in the container Landlock limits what the process can touch at runtime. Upper bound on any user input (upload file size, parameter bounds, etc.) request body size timeouts on external calls concurrent connections to external services queue depth for background jobs thread count and DB connection pool size Kubernetes stops sending traffic (graceful degradation) if the readiness probe fails. It does not yet kill the pod. Kubernetes restarts your pod if the liveness probe fails (it’s self-healing!) – fuzz testing for Rust code – another fuzzer with Rust support – detects usage of unsafe code – runs Valgrind on Rust code to find memory errors – code coverage via rustc/LLVM source-based instrumentation ( ). It reports line and region coverage, works with and , and is a good default for new projects. – an older Rust coverage tool with strong Cargo and CI ergonomics. On Linux it defaults to a backend ( only); LLVM coverage is available through and is the default on macOS and Windows. Useful if its reports fit your workflow, but expect different platform and test-runner edge cases than . This only holds for unwinding panics. If you compile with , or hit a stack overflow or out-of-memory failure, the whole process exits and never gets a chance to return . ↩ https://docs.rs/mimalloc-safe/latest/mimalloc_safe/ ↩ Data sources I found useful for this topic include this post and this comparison . ↩ This approach would have prevented a vulnerability in Meta’s crate , a tool for recording and displaying system data like hardware utilization and cgroup information on Linux. ↩

0 views
Corrode 2 months ago

The Rust Foundation

Most Rust developers use the language, compiler, package registry, and tooling every day without thinking too much about the organization that helps keep parts of that ecosystem funded and sustainable. This episode is a re-introduction to the Rust Foundation: what it does, what it does not do, how it relates to the Rust Project, and why that distinction matters for teams using Rust professionally. My guests are Rebecca Rumbul, Executive Director and CEO of the Rust Foundation, Lori Lorusso, Director of Outreach at the Rust Foundation, and David Wood, Principal Software Engineer at Arm, Compiler Team Co-Lead in the Rust Project, and a Rust Foundation board member. Together we talk about the practical side of ecosystem stewardship: infrastructure, security, interop, maintainer support, governance, corporate membership, open-source funding, and the pressure new technologies like AI put on language ecosystems. CodeCrafters helps you become proficient in Rust by building real-world, production-grade projects. Learn hands-on by creating your own shell, HTTP server, Redis, Kafka, Git, SQLite, or DNS service from scratch. Start for free today and enjoy 40% off any paid plan by using this link . The Rust Foundation is an independent non-profit organization supporting the success, sustainability, and positive impact of the Rust programming language. Its work includes funding and supporting ecosystem infrastructure, security and interoperability initiatives, maintainer support, project administration, community programs, events, and collaboration with member companies and donors. The Foundation is separate from the Rust Project. The Rust Project governs the language, compiler, standard library, and technical direction through its own teams and decision-making processes. The Foundation provides organizational, financial, legal, and operational support around that work, without owning Rust’s technical roadmap. Rebecca Rumbul is the Executive Director and CEO of the Rust Foundation. She leads the Foundation’s work on organizational strategy, member engagement, sustainability, and support for the broader Rust ecosystem. Lori Lorusso is Director of Outreach at the Rust Foundation. Her work connects the Foundation with the Rust community, member organizations, trainers, contributors, and companies adopting Rust in production. David Wood is a Principal Software Engineer at Arm, CE-SW Rust Team Lead, Compiler Team Co-Lead in the Rust Programming Language Project, and a board member of the Rust Foundation. In this episode, David adds the perspective of someone involved in Rust’s technical work as well as Foundation governance. Mozilla - The first home of the Rust language Python Steering Council - The governing body of the Python Project How to Write a C++ Language Extension Proposal - Bjarne Stroustrup, the inventor of C++, on why C++ needed a standards committee SCRC - The Safety-Critical Rust Consortium FLS - The Ferrocene Language Specification, a specification of the Rust language that is required for certain steps in the certification of Rust for safety-critical applications Foundation Membership Tiers - The different quantifiable benefits from Diamond to Silver and Associate Memberships Rust Commercial Network - A group of organisations that use Rust in production working together with the Rust Project Rust-C++ Interoperability Initiative - An initiative of the Rust Foundation to improve interoperability between Rust and C++ Rust Embedded Working Group - An official working group of the Rust language to improve usability of the language in hardware-constrained environments An AI Security Engineer in Residence for the Rust Ecosystem - Describing the position of the security engineer made possible by funding from Alpha-Omega Rust Foundation Maintainers Fund - The Foundation’s fund to support Rust maintainers Rust Foundation Trusted Training - The Foundation’s accreditation program for Rust training providers Rust Foundation Website Rust Foundation Media Room Rust Foundation on GitHub Rust Foundation on LinkedIn David Wood’s website

0 views
baby steps 2 months ago

Battery packs: Let's talk about crates, baby

This blog post describes an idea I’ve been kicking around called battery packs . Battery packs are a curated set of crates arranged around a common theme. For example, there’s a CLI battery pack that has everything you need to build a great CLI , an opinionated pack for creating a backend web service , and one for embedded development (based on the Embedded Working Group’s Awesome Rust repository ). We’ve also got some smaller ones, such as the error-handling battery pack that shows how to handle errors in Rust. But this is just the beginning – a key part of the battery pack design is that anybody can create one. Battery packs are meant to address one of the most common things I hear from new Rust adopters. Everyone loves the wealth of high-quality crates available on crates.io. And everyone hates having to spend a bunch of time researching and comparing alternatives. Battery packs can serve as a good set of default choices. And they don’t lock you in. At heart, they’re basically just a list of recommended crates, so you can always swap something out if you find an alternative. We’ve got a prototype of the battery pack tool working today, so you can try it out if you’re curious. Just run and then try a few commands! For example, will show you the set of available battery packs, based on a crates.io search (as I’ll explain below, a battery pack is itself packaged and distributed as a crate, but not one that you take a direct dependency on). And will add batteries from a battery pack into your crate, so e.g. would let you select and add common CLI libraries. If you want to see a more involved demo, try out , which is derived from the Awesome Embedded Rust repository. One of the key ideas from battery packs is that anybody can publish one . They are just a crate named ; the dependencies of that crate are your recommendations. Features are designations of common sets of crates frequently used together. The examples are your templates. And so forth. Letting anybody create a battery pack is in contrast to the previous ideas for an “extended standard library for Rust” 1 , and it is intended to address some of Rust’s unique challenges. For one thing, it lets people publish battery packs that are tailored to specific requirements. For example, the CLI and backend service battery packs are targeting a “typical computer”. But I could imagine the Rust embedded working group publishing a battery pack with libraries focused on no-std and binary size optimization. Being open-ended also addresses the “who decides?” question. To my mind, the best people to recommend what libraries you ought to use are other people building systems like yours . This is why I mentioned the Embedded Working Group publishing an Embedded battery pack, for example, as I think they are clearly a set of people who know their space well. But even within the embedded space there are yet smaller groups, and I imagine that sometimes it’ll make sense to get narrower. For example, perhaps a battery pack targeted embassy and its associated ecosystem? Unclear. If you wanted to create a battery pack, how do you do it? One answer is that you just create a new crate. But a better approach is to use the “battery-pack battery pack” 2 , which bundles a template: This will prompt you for the name of the battery pack you want to create and a few other things and make your crate. Then you can just use dependencies to represent the libraries you want to recommend and publish. The “batteries” that you can add to your project aren’t always dependencies. They can also be “recipes” or templates. For example, the CI battery pack 3 can configure your project with the kind of “super neat-o” github actions you’ve always wanted but never wanted to bother configuring. To use it, select one or more of the templates to install: I expect this kind of “actions to improve your crate” to become a rich source of things. Right now we’re using a relatively lightweight template system built on minijinja , but I think we’re going to want to expand on this. Battery Packs also support more than just a flat listing of dependencies/features/templates. You can group dependencies and features into categories and then, for each category, distinguish between “pick at most one” or “pick any number”. For a fun example, try , which is derived from the Awesome Embedded Rust repository. If you run it, you’ll see something like this, which groups the choices thematically and, in some areas like “concurrency framework”, makes it clear that you want to pick one: So why am I so keen on battery packs? It’s largely because I’ve heard so many would-be or recent Rust adopters talk about picking crates as a challenge. But I feel they would help with some other problems as well. What I really want to see is working groups in the Rust Commercial Network banding together to publish battery packs and recommendations. These would cover the dependencies that they’re actually using. One of the reasons I want to have RCN-recognized battery packs is that they are a natural focal point to then prompt RCN members to fund the maintenance of those crates. I am imagining that for each sponsored battery pack vended within the RCN, there is an associated “ecosystem fund”. Companies or individuals could sponsor this fund to get access to early patches, security disclosures, etc or other perks. The money would be used to support the maintainers of those crates, to implement missing features, and so forth. Another value-add from battery packs is the ability to drive interop efforts. I think that as soon as we start talking about standardizing, we’re also going to recognize that there are some places where standardization is hard. For example, early conversations within the network service working group (unsurprisingly) immediately identified that while most people are using tokio , some major companies are using their own runtimes internally. It’s not like the need for “async runtime interop” is news . But right now, every crate winds up effectively implementing their own set of little traits to make it work. Sponsored battery packs offer the possibility of a neutral home for that sort of thing. There are some risks to people using battery packs. The most obvious is that the fact that anybody can publish a battery pack may mean that you just get a ton of battery packs, which doesn’t really help anybody! I’m not so worried about this because I think that there will be a few obvious places that most people go first, and then I think once people are oriented, they’ll get excited to explore what crates.io has to offer and start discovering more niche battery packs. Battery packs are designed to evolve. I’ve seen it happen a number of times that there is a dominant crate for something, often taking a “traditional approach”, but then somebody else comes along and presents an interesting alternative that gradually takes off. I love that and I don’t want to put it at risk. One example of evolution around CLI argument parsing. For a time, docopt was a popular way to parse command-line options. Then clap came along and presented a more structured alternative; that was nice, but then structopt came along and connected clap to an auto-derive, so you could just write your data structure and be done. And that was awesome. (That is now the standard in clap.) I want to be sure that, even if there is a CLI battery pack, there’s room for the next clap to come along. There are a few things about battery pack that I think will help us deal with this. First, they are a “thin abstraction”. You don’t “depend on” a battery pack, you depend on the crates within it. So if a new version comes out that uses clap instead of docopt, that doesn’t impact you at all. Your code keeps working same as it ever did. And of course it helps that anybody can publish a battery pack. You can now have variations on battery packs that are focused around a new approach to help it get started. Done right, I think that standardized battery packs can also help the ecosystem evolve and pivot. As it is now, knowledge of new crates has to spread by word-of-mouth. But if everybody is aligned around a new approach, adopting that new approach within a battery packs sends a clear signal that your group is aligned that something is the new hotness. I see always bet on the ecosystem as a key Rust design axiom. It’s the reason we chose a small standard library and a package manager in the first place. It’s also why battery packs are designed to be published by anyone. But just like plants sometimes need a trellis to grow taller, any successful ecosystem reaches a point where it needs another layer of structure to help it keep growing. Without that, you have this “layer of tacic knowledge” (in the words of a Rust Vision Doc interviewee ) that becomes an obstacle for folks. And I think we’ve reached that point with . I am hopeful that battery packs can provide that next layer of structure. But at the end of the day, if there’s a better approach, that’s fine too, so long as we find a way to help people find ( and fund! ) the crates they need. So let’s talk about it! My first recollection of it was the Rust Platform idea we floated in 2016!  ↩︎ Yo dawg…  ↩︎ Hat tip to Jess Izen, who proposed and developed the CI battery pack. Neat idea.  ↩︎ Oh, and: my apologies to Salt-N-Peppa .  ↩︎ My first recollection of it was the Rust Platform idea we floated in 2016!  ↩︎ Yo dawg…  ↩︎ Hat tip to Jess Izen, who proposed and developed the CI battery pack. Neat idea.  ↩︎ Oh, and: my apologies to Salt-N-Peppa .  ↩︎

0 views