Posts in Rust (20 found)

A UAF turned RCE

In the curl 8.23.0 release we shipped a fix for one of the worst vulnerabilities in curl in a long time. We grade it severity HIGH. It is known as CVE-2026-92392 with the title HTTP/2 upload Use-After-Free . This post explains the bug, what the problem can lead to, and something about why it happened and what we have done to make it harder to introduce a problem like this in curl again in the future. This is a much simplified explanation. Please refer to the original report for all the details and the full reproducible Docker image for all the low-level fun and education you could possibly want. I highly recommend doing that; the report and the reproducible are awesome. It is a use-after-free , which is a classic problem in memory-unsafe programming languages, like C, where we erroneously have a lingering pointer left, pointing to memory that is already freed, and yet we use that pointer and the memory it points to. This family of errors is especially bad when a malicious actor can affect the content that gets stored into that memory area that is still used even though it was freed. Tricking the program to do something unintended. This problem involves HTTP/2. HTTP/2 is a multiplexed protocol, meaning that it handles multiple logical transfers over the same physical connection. curl needs to keep track of a number of transfers at the same time, and when data arrives or is to be sent off, it needs to keep things apart appropriately. For every transfer done using libcurl, the application creates and sets up a dedicated handle . We call it an easy handle . This handle is kept around for as long as the transfer is in progress, and the application can close and free it once the operation is completed. When doing multiple transfers, multiple handles are used. libcurl also has multi handles , which is a super handle type that holds any amount of easy handles . This handle, and the corresponding multi interface, makes it possible to drive any amount of concurrent transfers. A transfer done with libcurl always has at least one multi handle and one easy handle. (Sometimes the multi handle is hiddenly used under the hood.) In order to trigger this problem, we need an application (such as the curl command line tool) to do an upload over HTTP/2 to a server. By default, libcurl negotiates HTTP/2 when a HTTPS:// URLs is used and the server agrees. When the upload has started, the server pauses the send by zeroing the HTTP/2 window size, returns a piece of the response and then it resets the stream, which shuts down the transfer – but not the connection. curl then correctly pulls down the entire transfer, but it accidentally leaves a lingering pointer to the involved easy handle . When the application finds out that the transfer completed (with an error), it subsequently frees the easy handle and then, and here comes the problematic part: cleans up the multi handle. At this point, the easy handle was just freed but there is still a reference to that handle. When libcurl shuts down the multi handle, it misunderstands the situation, allocates memory to download data into, finds the still dangling pointer and manages to download data from the server that ends up in the very same memory previously used for the easy handle. In this process, it sends off a request that leaks internal client details, which includes information about local addresses, which is crucial. Modern operating systems use Address Space Layout Randomization (ASLR), so a server needs this clue in order to generate a good attack payload. Subsequently, when libcurl deals with this received data (using the handle the dangling pointer points to) in the download buffer, it uses a function pointer that now has been modified by data just downloaded from the server, and then it runs remote code . Here’s a simplified 6-step illustration. Conditions to trigger In order to actually successfully execute remote code, lots of several specific conditions need to be met and we deem it extremely difficult to meet these requirements in the wild, which is why we decided this is “only” severity HIGH and not CRITICAL. The most likely outcome of an attempted attack is rather just a crash. We estimate that the chance for a successful attack is slim. This problem was found and reported by Luna Tong at Anthropic using their new OSS Scanner. Presumably this involves their AI models. In general we have over the last few years been working on reducing the amount of internal pointers to handles that needs to be correct, and instead we store transfer identifiers that we look-up on demand. While this is a tiny bit slower, it is more foolproof when there are logical mistakes like the one mentioned here. We also of course continue to add more tests and we verify the code with more tools and analyzers in order to find additional similar mistakes. Multiplexed protocols like HTTP/2 have this challenge that there are many concurrent transfers going and they all have different life-times and with memory-unsafe languages, dangling pointer mistakes can indeed be fatal. This is of course no guarantee that we do not have or will not introduce another use-after-free in the code now or later. libcurl is written in C – a memory unsafe language. With a memory-safe language, this mistake could probably have been avoided. We have worked on curl for almost thirty years, and have over this time manage to make a rather polished transfer engine. For the longest time there really was no memory-safe alternative that also allowed libcurl to be a low-level library available on hundreds of operating systems and several dozen CPU architectures. It even barely exist alternatives now. We do not rewrite curl. Not into another language but also not in C. We are set to iterate, polish, improve, refactor over and over until the end of time. Porting curl to another language (like rust) would be a monstrous undertaking that would be slow and introduce lots of new short to medium term problems. Not to mention that the existing curl team is not rust experts and that libcurl is currently portable to many more platforms and environments than rust itself is. When this vulnerability was confirmed by the curl security team just hours after it was submitted to us on September 15. We immediately realized the significance and decided to rush the pending next release. However, we still wanted to be careful, follow the procedures and not create any further problems or mistakes by stressing it too much. That’s how we ended up just shortening the release cycle with two weeks. Down from eight to six weeks from the previous release. This allowed us to also merge and fix many more vulnerabilities and general bugs in the same release. We never provide CVSS for our published vulnerabilities as we don’t think we can do a job there. Others will provide them even for our flaws. The victim needs to actually attempt to upload data to a malicious server, meaning that the server certificate must remain valid. The size of the upload needs to be roughly the right size (~135KB) The attacker needs to know curl’s architecture to generate and deliver the right code The attack requires a specific sequence of allocations and allocation sizes so that the data from the server ends up in the right place (from the attacker’s view). This pattern differs between Linux distros, libc versions/flavors, TLS certificate bundles, libcurl using applications and more. nghttp2 and OpenSSL/TLS 1.3 behavior, including allocation effects around session tickets The payload storage area is small (around 23 bytes)

0 views
baby steps Yesterday

What agents mean for Rust (and vice versa)

As someone who has made a career of programming language design, I definitely have moments of existential dread about the rise of LLMs. I started coding in 5th grade on an x86 1 that ran at 5MHz, had something like a 5MB hard-disk and 512K of RAM 2 . Computers were really, really different back then. But honestly, programming wasn’t. For all the advances in IDEs and programming languages and so forth, the basic act of entering code into a file has remained surprisingly constant 3 – right up until a few years ago, when everything kinda turned on its head. That’s been exciting and empowering at times 4 but also scary and disorienting. Will my skills in programming matter? What about my skills in programming language design (such as they are)? This blog post is my attempt to answer those questions. So as not to keep you in suspense, let me just give you the conclusion. In short, yes, I believe there will still be a role for humans in programming . And yes, I think that programming language design is still relevant . And, naturally, I also happen to think Rust is a really good fit for agentic development – but that’s not to say we can’t make it a better fit . And that’s what this post is ultimately about: what makes Rust and agentic development work well, and where we could double down on those strengths. This is a kind of “blog post companion version” of a talk I gave this week at REBASE, part of SPLASH . You might also enjoy the slides from the talk I gave at IWACO, IWACO and Rust: hidden links revealed , which was a bit more of a retrospective (no soundtrack for those, though). In any case, this turned out to be a great conference! Jack Huey presented on the Rust Vision Doc and, quite to my surprise, Steve Klabnik and Dorian Scheidt presented “LLMs as Collaborators in Language Specification and Design” , talking about their process in building out Rue . It was fun to see Steve again. On top of that, there was a ton of great research presented at IWACO in particular. I was particularly taken by the work on Reachability Types and Information Borrowing . I also really liked learning about Hylo Lang , particularly their approach to projections. Clever and cool. Rust is popular right now. Very popular: Stratospheric among the breakout languages is TypeScript, cropped from the chart and sitting currently at +1200% growth from the prior year. [..] The second most rapid recent growth is in Rust. Meta engineers have written twice as much first-party Rust code so far this year than in the previous 9 years combined. — David Tolnay (emphasis mine) Some of that is because agents make it very easy to try out new things. And some of that is because Rust has a reputation for being performant and, if it’s just as easy to pick any language, why not pick one that will yield faster execution times? But I think Rust is popular for another reason: because Rust happens to be a really good fit for agentic coding 6 . Of course, I would, wouldn’t I? The point of this post is (1) to argue for why that is and then (2) to discuss what we might change about Rust to make it work even better. It seems to me that an effective “software factory” will have three pillars: None of these pillars are solved, but some have made more progress than others. Rust is designed to give “good performance by default”, making the idiomatic thing typically also be the fastest thing. But another key part of Rust is that you have the knobs to eke out better performance when you need it. You control when something is stack- or heap-allocated and where pointer indirection occurs. If you want to, you can force the layout of your structs to be done in a particular way, or decide where to introduce boxing into your async stack frames. In the limit, you can put in inline assembly. In the past, learning how to “tune the performance knobs” could be a bit of a black art: but agents are making that much easier. 7 It turns out that agents are really good at fine-tuning the performance of existing code. For example, people who work on super high-performance cryptographic algorithms used to do so by hand-tuning assembly code. But those people tell me that they are now turning to agents for that same job. More prosaicly, a lot of people building network services are finding that, using a profiler like dial9 , agents can quite effectively diagnose async performance pitfalls, like long polls or blocking. Therefore, I think the task for Rust is clear. We keep on with our goal of “low-level enough for a kernel, high level enough for a GUI” . If anything, I think the shift around agents means we can tolerate a bit more “noise” in the form of explicit calls to or – but not too much, since we still want the “happy path” of the code to be clear and evident 8 , even as we surface details that are important in specific scenarios (like tracking precisely how many clones you have of a channel endpoint). Combined with a profiler, you get an effective feedback loop to get your code running fast: As usual though, while a reputation for good performance is what brings people to Rust, what makes them want to stay is a sense that using Rust “just seems to work better”. The basic theory is that Rust’s type system and guardrails help keep agents on track : rust is a perfect language for agents, given that if it compiles it's ~correct I think there’s truth in that: languages like Rust, Ocaml, or Haskell all share this “if it compiles, it works” feeling, and I think that agents will benefit from that. Testing can show the presence of bugs, but not their absence. — Edsger W. Dijkstra, 1970 Writing tests has never been enough to show that your program is correct – but, with agents, it’s not even close. It happens so often that my first few tests seem great and then the next one goes completely bonkers. Usually bonkers in a way where I cannot even imagine how that bug could have come about. Then I dig in and I realize the agent completely missed the boat on the logic I was trying to express. 9 This is where a type systems really shines – particularly an advanced one like Rust. Type systems give you some guardrails that keep the agent on track. In Rust, those guardrails extend beyond simple stuff like “do all fields have the correct names” and to stuff like never having to close a socket . Type systems can show the absence of bugs, but not the presence of correctness. — Me, failing to coin a phrase of equal eloquence 10 , 2026 So yeah, type systems are helpful! But they have inherent limits. A type system is a simple set of rules that, if you follow them, make sure that you avoid (certain kinds of) mistakes. But they don’t tell you that you do the right thing – only that you don’t do (certain kinds of) wrong things . If you want to go further, you need a proof system – something that will let you show that richer properties are actively true, not just that you’re avoiding bugs. As it happens, there are a lot of interesting proof tools for Rust, like Verus , Creusot , Aeneas , and Kani . As a starting point, you can prove that your code is panic-free, which will eliminate a lot of bugs, though it is still fundamentally a “type-checker-like” property (i.e., you avoid doing things wrong). But you can also go further and add assertions that show your code is computing the results you expect. Deplying proofs is not just good for getting code right – it’s good for getting code fast . This is somewhat counterintuitive, but it’s true. Earlier I mentioned how optimized cryptographic algorithms were now being written by agents . Part of what makes that possible is that cryptographic algorithms have a very clear mathematical definition. This means that you can basically tell the agent “do whatever you want, as long as you can prove that the code still computes the correct result”. These proofs can be done with theorem provers like Lean or Rocq . The blog post “Why Lean is faster than Rust” makes the point quite well. For Rust, a good example of this would be around unsafe code. Right now, people are sometimes reluctant to deploy unsafe code even if it might make their code faster because it’s not worth the risk of getting it wrong. And I applaud that effort. But what if you could use a prover to show that your unsafe code is, in fact, safe (i.e., incapable of causing undefined behavior)? Then you’d be free to optimize to your heart’s content. You might wonder: if we’re deploying a proof system on our code, do we still need type systems? I think the answer is definitely yes. Type systems are infinitely simpler to work with, which is why they are much more popular. Using a type system lets you rule out all kinds of annoying bugs so that you can focus your “proof energy” only on the important stuff. As a simple example of this, consider the following Java code: Without seeing more code, it is impossible to prove whether this assertion is true or not! This is because there could be any number of other threads running around mutating . In contrast, the following Rust code must be true (at least, so long as the operator is reflexive for ): We know this because the type system of Rust guarantees that implies a unique reference. This is in fact exactly how Verus works. By trusting the Rust borrow checker to track uniqueness etc, it simplifies the job of the SMT solver, making proof checking must faster and more reliable than it would be otherwise. So my take is that Rust should double down on if it compiles, it works . As expressive as Rust’s type system is, there are plenty of things that it cannot capture. This includes fairly common things, like this method only reads one particular field or internal references , but also fancy things like “no logging of personally identifiable information”. We should work to lift those limits, giving agents room to prove more complex properties. We also need to improve the experience of integrating a proof tool by working towards an extensible compiler . As cool as the existing Rust-based proof tools are, using them is a janky experience involing annoying macros and pinned compiler versions. We can do better. I’d like to propose a north star for Rust: no more unsafe code . By that, I don’t mean that we never use the keyword . Rather, I mean that we have the power to prove that all the unsafe code we write is correct and cannot cause undefined behavior. Don’t get me wrong: knowing exactly what code needs to be vetted is a huge step forward, as is normalizing the idea that libraries need to be responsible for protecting their users from undefined behavior via safe API boundaries 11 . But given agents’ ability to find vulnerabilities, I think we need to up-our-game if we want to stop the steady stream of CVEs. 12 But before you go about proving things or optimizing them, you need to have a clear idea of what you are building. As far as I can tell the state of the art here is basically variations on “markdown-driven-development”, developing docs and hoping that agents pay attention. I don’t know how much of a role for Rust there is in this space. I think there are some steps we might wind up taking that could be helpful here – for example, having ways to separate out interface from implementation so that it’s easier to review the interface in isolation or to see when the agent made changes there. 13 Or perhaps just providing betters tools to review that. I also think there’s space for dedicated “modeling languages”, something that let you iterate quickly with agents on the high-level designs and invariants without having to get into the weeds and read the code. I have more thoughts on this but it merits many blog posts all of its own. For the moment, I’ll start by pointing you all at this post by Kari Wilhelm, “Principles of Anchor Engineering” , which I think does a nice job of describing what it means to pull out the key elements of a design and drive the coding from there. We covered a lot of ground here, so let me put this all together. What makes Rust a good choice for developing with agents? And what could make it better, from a language design perspective? Overall, I think it’s very similar to what we were doing, except that the way we weight the tradeoffs is tweaked to account for agents doing the code authoring, and humans focused more on code comprehension: As a final close, let me point you at this interesting article by Marc Brooker, “What’s easy now? What’s hard now?” . He posits that the more it is possible to clearly specify success criteria for a task, the easier it is, and that we may find that tasks we traditionally think of as ‘hard’ (e.g., building out an efficient implementation of a cryptographic algorithm) may soon become automated, whereas tasks that we have thoght of as ’easy’ (e.g., making a unique, compelling website) may become more valued. Looking at it from a PL point-of-view, I think Marc’s point reinforces my sense that the biggest action is going to come from design languages , things that let you capture the architecture and intended behavior at a very high-level. The goal would be to let humans intuitively specify a design that gets translated into effective guardrails, whether that’s type system checks, lints, or theorems to be proven. Agents would then take on the task of translating the design into code written in a traditional PL like Rust, with humans looking at it only rarely. This doesn’t make the design of traditional PLs less interesting, but it definitely makes the tradeoffs very different than they were before. Secretly I have always been jealous of people that coded using punchcards and stored things on gigantic rolls of tape or whatever. I just want to be world weary so badly! I am kinda delighted that I’m finally old enough that I can say things like “I remember when we had to edit CONFIG.SYS before we could play a computer game!” and younger programmers will gasp in horror. 5 But I still feel like I must sound like a silly kid to folks who were around “back in the day”.  ↩︎ I still remember when my dad told me he had bought me a 10MB disk and secretly he had bought a 40MB disk to surprise me. It seemed absurdly large. “I’ll never need a new disk again!”  ↩︎ I mean my god, we’re even using again! My first editor!  ↩︎ I built an electron app! And it was so easy to do! And reading weird papers and things is so much more accessible now that I can ask an LLM to translate all the math jargon for me.  ↩︎ I remember once trying to tell Alex Crichton how I used to “order shareware from a paper catalog by putting a check in the mail” and he was like “I do not understand any of the nouns in that sentence”. Highlight of my life, right after Rust 1.0. Oh, and maybe the birth of my child and getting married. I guess.  ↩︎ I’m not saying Rust is the only language that’s a good fit. I’m not even saying that the way that Rust is a good fit is the only way to be a good fit. I have heard for example that Go is a really good fit for agentic coding and I believe it: I think it hits a different sweet spot, one that helps agents iterate quickly (although I do suspect agents will let data races and subtle bugs slip through that Rust would have prevented).  ↩︎ The fact that agentic development makes more room for knobs is good for Rust performance, but it is quite likely an even better fit for performance in other languages. Tuning a GC has traditionally been a job that really requires a GC expert, but agents can likely fill that role pretty well these days.  ↩︎ On this pont, I’m even more convinced of the importance of ergonomic RC now that I work with agents. Agents are good at introducing the extra clones and things you need to deal with capturing around async tasks, but the resulting code is so annoying to read, it really hides what’s going on.  ↩︎ “Claude, this is not what we talked about!” – me, all too often.  ↩︎ So close though, right? I just .. couldn’t land the plane.  ↩︎ All too often, C++ libraries consider it adequate to tell users “you’re holding it wrong” – i.e., that they are responsible for causing undefined behavior by misusing the API. Rust takes a clear position that a safe API has to protect its users, no matter how much they abuse it.  ↩︎ To be fair, if Rust is encountering a stream of CVEs, other languages like C++ and Java are encountering a veritable flood, since they lack the protection of Rust’s type system. st takes a clear position that a safe API has to protect its users, no matter how much they abuse it.  ↩︎ The revenge of files?  ↩︎ Design: figure out and specify clearly what the code is supposed to do. Construction: author code that does what it is supposed to do. Optimization: optimize the code so that it runs efficiently. It offers good default perf (why pay more?) and, in conjunction with a profiler, can be tuned as much as you want. The type system gives stronger guardrails, and if you setup your agents right, you can strengthen those guardrails through techniques like new types. If it compiles, it works – our type system is our big advantage, we need to make it more expressive and more functional so that agents don’t get stuck trying to work around it! And as an adjunct, establish a North Star: Enable Unsafe code without unsafety by supporting proof, and see the most widely used Rust libraries proven sound. Low-level enough for a kernel, high-level enough for a GUI – make sure we expose the knobs needed to tune for performance, while also making the code’s intent clear (but don’t worry about golfing the code down to an absolute minimum number of characters). Extensible and versatile – one of Rust’s great strengths is that, with macros + our type system, we support DSLs and a wide variety of targets. We need to bring that spirit of extensibility into the compiler so we can target more styles of development (e.g., GPU, deep language interop, etc) and also integrate theorem provers and other tools that require more than just tokens. Secretly I have always been jealous of people that coded using punchcards and stored things on gigantic rolls of tape or whatever. I just want to be world weary so badly! I am kinda delighted that I’m finally old enough that I can say things like “I remember when we had to edit CONFIG.SYS before we could play a computer game!” and younger programmers will gasp in horror. 5 But I still feel like I must sound like a silly kid to folks who were around “back in the day”.  ↩︎ I still remember when my dad told me he had bought me a 10MB disk and secretly he had bought a 40MB disk to surprise me. It seemed absurdly large. “I’ll never need a new disk again!”  ↩︎ I mean my god, we’re even using again! My first editor!  ↩︎ I built an electron app! And it was so easy to do! And reading weird papers and things is so much more accessible now that I can ask an LLM to translate all the math jargon for me.  ↩︎ I remember once trying to tell Alex Crichton how I used to “order shareware from a paper catalog by putting a check in the mail” and he was like “I do not understand any of the nouns in that sentence”. Highlight of my life, right after Rust 1.0. Oh, and maybe the birth of my child and getting married. I guess.  ↩︎ I’m not saying Rust is the only language that’s a good fit. I’m not even saying that the way that Rust is a good fit is the only way to be a good fit. I have heard for example that Go is a really good fit for agentic coding and I believe it: I think it hits a different sweet spot, one that helps agents iterate quickly (although I do suspect agents will let data races and subtle bugs slip through that Rust would have prevented).  ↩︎ The fact that agentic development makes more room for knobs is good for Rust performance, but it is quite likely an even better fit for performance in other languages. Tuning a GC has traditionally been a job that really requires a GC expert, but agents can likely fill that role pretty well these days.  ↩︎ On this pont, I’m even more convinced of the importance of ergonomic RC now that I work with agents. Agents are good at introducing the extra clones and things you need to deal with capturing around async tasks, but the resulting code is so annoying to read, it really hides what’s going on.  ↩︎ “Claude, this is not what we talked about!” – me, all too often.  ↩︎ So close though, right? I just .. couldn’t land the plane.  ↩︎ All too often, C++ libraries consider it adequate to tell users “you’re holding it wrong” – i.e., that they are responsible for causing undefined behavior by misusing the API. Rust takes a clear position that a safe API has to protect its users, no matter how much they abuse it.  ↩︎ To be fair, if Rust is encountering a stream of CVEs, other languages like C++ and Java are encountering a veritable flood, since they lack the protection of Rust’s type system. st takes a clear position that a safe API has to protect its users, no matter how much they abuse it.  ↩︎ The revenge of files?  ↩︎

0 views
Langur Monkey 1 weeks ago

JPEG XL finally lands in Chrome!

Today, I was browsing Hacker News , when a particular item caught my eye: Hacker News post with the Chrome JPEG XL announcement . I opened it promptly and, indeed, it was not a prank. Finally, after years and years of bull**** from the Chrome team, they are shipping it. They are shipping a JPEG XL decoder. For those of you who don’t know, JPEG XL is a new image codec that includes most of, if not all, the features we’d want in the image codec of the future: This is awesome news for the codec . Whether we like it or not, Chrome is the dominant web browser, with ~66.5% of the market share , very far from its competitors (Safari, Edge, and Firefox, in that order). Now, I don’t use Chrome at all, but I still think that this was just the final piece of the puzzle needed to help the format go mainstream. So, what finally pushed them over the edge? If you read their announcement , they cite “consistent feedback and requests from web developers” and its massive popularity in the Interop 2026 project. Translated from corporate PR-speak: the community simply refused to let it go. Web developers, photographers, and open-source advocates kept pushing, complaining on bug trackers, and demanding a truly superior image format instead of settling for Google’s own WebP or the heavily pushed AVIF. The technical excuse the Chrome team used for years was that a C++ decoder was too much of an attack surface to run safely in the browser. Well, it has now been elegantly sidestepped. To bring JPEG XL to Chrome 155, they have integrated , a pure Rust reimplementation of the decoder. This tackles the memory safety issues head-on, eliminating the risk of out-of-bounds reads and use-after-free bugs of other decoders. And to make sure it doesn’t run sluggishly, they built a SIMD abstraction layer ( ) to maximize hardware performance without compromising Rust’s safety guarantees. This is a massive victory. Not just for JPEG XL, but for the open web. As I demonstrated in my comparison back in 2023 ,JXL has significant advantages over older formats and, in many cases, over AVIF as well, especially for high-fidelity photography, lossless compression, and progressive decoding. The fact that it allows lossless transcoding of existing JPEG files is of special importance. It means that we can finally upgrade our backlog of old images without losing a single pixel of quality, while saving a big chunk of disk space and/or bandwidth in the process. With Firefox adding it to Labs earlier this year, and Chrome finally capitulating, Safari is left as the major outlier with only partial support. The dark days of “Google kills JPEG XL” are officially over. It’s time to start converting your image libraries, updating your tags, and serving . The future of (high-quality) web imagery is finally here, and I’ll be ready for it. File size reduction by \(~20-60\%\) w.r.t JPEG. Lossless JPEG transcoding. Progressive decoding. Wide gamut, HDR and 32-bit support. Animations and transparency (alpha channel). Shines with high-fidelity photographic images. Fast-ish encoding and decoding. Royalty-free and FOSS. Support for super high-resolution images, of up to 1 terapixels ( \(2^{30}-1\) pixels per side).

0 views
Evan Schwartz 1 weeks ago

Scour - September Update

Hi friends, In September, Scour scoured 1.2 million articles (up from ~880,000 in August) from 28,568 feeds . Also, welcome to the 116 new users who signed up since my last product update email! Here's what's new in the product: You can now find all of your saved, loved, and liked posts, as well as your full reading history, in the Library section. Also, if you click Read on Scour for any article, that page now has tabs for the article's content, other posts that it cites and that cite it, and the feeds it was found in. Here's an example for a widely cited post. Scour now tries to determine the level of expertise each post assumes and infers the level of expertise you have per topic (based on the wording of your interest is and the types of articles you click on or like). At least for me, this means I'm seeing far fewer beginner Rust questions from Reddit showing up in my feed. (For those in tech, this is powered by Jev.) Scour's Search will now show you results for feeds and authors, in addition to posts that match your query. Relatedly, you can now follow individual authors as sources and Scour will try to show you their posts from any website they publish on. Scour now detects and hides more junk, ranging from sales pages and SEO garbage to uninformative link roundups and low-value discussion threads. You should see more high-quality content in your feeds. By my current count, about 1 in 10 posts being shown before was some kind of junk that Scour now hides. I continue to obsess over making Scour feel super fast and snappy. In September the slowest feed loads got about 7x faster (p99 went from 2.1 seconds to 282 milliseconds) and the median feed load time got 2x faster (p50 went from 90 ms to 40 ms). This is also while ranking about 1.4x as much content as the month before. Unfortunately Reddit announced their plan to turn off RSS feeds on November 13th . This is how Scour checks which discussions are happening on Reddit and finds articles posted on different subreddits. After November 13th you'll no longer see links to the Reddit discussions from Scour 😢. Here are some of my favorite articles I found on Scour in September: Happy Scouring! - Evan The biggest news in the tech / AI world was the release of TypeSafe's Jev model . These were some of the related articles that I found interesting: The Latent Space interview with TypeSafe's CEO, Jev: System One models for Prod, not God — with Diogo Almeida, CEO, TypeSafe AI . Fingerprints of Jev and Jev's Architecture Unmasked were interesting black box investigations into Jev's base model using the tokenizer and other externally visible properties. Sixteen Models Walk Into a Storefront gives a very nice breakdown of techniques that can be used to manipulate LLMs' assessments of which products to buy and how much to pay for products. Logo Design Trends in 2027 Favor Marks Someone Can Prove They Made . In the age of AI-generated glossy slop, this is no surprise, but it's a nice write-up. The engineering behind the US Strategic Petroleum Reserve . Quite random but an interesting read. I Judged My Mum for Overusing AI, Until I Caught Myself Doing Worse . I continue to appreciate Sid's commentary on the age of AI. This is very relatable.

0 views
DHH 1 weeks ago

Over my dead pencil

Two weeks ago at Rails World, I told my fellow programmers that it's time to put down the pencils. We're not going to write the vast majority of code by hand any longer. Coding agents have gotten so good that it's simply not an economically viable enterprise going forward to have humans typing out lines of Ruby, Rust, or C++. When I polled the room for how many people were still writing a material amount of code by hand on a weekly basis, only a handful put up their hands. So they all knew this, but it still came as a shock to many to actually hear it. And I get that shock. Programming has barely been a professional career for a generation, and now it's being completely transformed. That's unsettling, even for a group of people who've seen technologies come and go on a regular basis. But this isn't just another technology, because AI isn't just another tool. It's far more like gaining some new coworkers who are exceptionally good and fast at a wide range of tasks, but still need a bit of help with some, and whose choices you might occasionally disagree with. From a certain angle, that looks like competition. Especially when it was barely five minutes ago that programmers were being treated like precious priests whose incantations were necessary to yield even the most low-hanging fruit of the computer. This perceived threat of disintermediation is clearly hitting some programmers hard. But I don't think anyone who's willing to lean into the future, make the most of this intelligence explosion, and apply their skills to what comes next needs to worry much. We're about to see an absolute bloom in software development as the price of development plummets and everyone realizes how much automation we still have left to do in this world. I think the only programmers who have to worry about this change are those who refuse to embrace, or drag their feet on, the progress brought by the age of agents. It's fine to be skeptical about the areas where our new clanker colleagues still occasionally get it wrong, but refusing collaboration is not a viable career path for almost anyone. Don't go down with the pencils. There's so much to build. We need you.

0 views
pkh.me 1 weeks ago

Float and integer arithmetic follow two different paradigms

When working with floats, we tend to reuse the more familiar integer arithmetic patterns. More specifically, we always try to prevent a disaster rather than reacting to it. I keep noticing this pattern over and over again, and seeing that LLMs still get it wrong most of the time means that, either I am wrong, or everyone else is; it's obviously the latter, and I'm going to explain why. I wrote before about the issue with checking the result of integer arithmetic after the catastrophe happened . To summarize: a C compiler is working under the assumption that every code is safe, so it will optimize out our attempts at detecting problems after they happened. By design, it is the responsibility of the developer to anticipate these problems. This is not exactly specific to C, for example in Rust we still need to prepare for an operation to fail by using the corresponding checked/wrapping/saturating/overflowing operator functions ( , , etc). Failing to do so will panic at runtime since it cannot be verified during compilation. In C we need to do this manually through different degrees of gymnastics, typically through smart computations involving constants like , or using the compiler builtins such as (C23 also finally standardized with function helpers). Not being diligent about these issues ultimately leads to undefined behavior (or a forced crash with compiler options such as ) and security issues, which means developers have been more careful over time, or at least familiar with the possible shortcomings. IEEE-754 floating-point types are an entirely different beast and need a new paradigm. Operation errors create (not a number) or infinite values, which propagates through calculations. They do not crash the program, and they're perfectly legitimate. Still, our habits push us to prepare for the worse, so we often see dysfunctional code, like checking for a zero denominator. Here is an example with ChatGPT (October 2026): When people realize operations with tiny floats can also cause infinite, they start using an arbitrary small epsilon ε, adjusting the check with something like . Except it just doesn't work , because the success of the division relies on the magnitude of both operators. For example, the largest 32-bit float (somewhere around 3.4 \times 10^{38} ) divided by a number below 1 (for example y=0.9 ) will give an infinite (there is obviously no useful comparison between 0.9 and possible here). Similarly, if x=5 \times 10^{31} , and we divide it by the next representable float above , we also get an infinite. We can verify that with the following rust snippet: Looking for , , or equivalent in a random codebase will, in most cases, raise broken checks. There are legit cases for these constants, for example working on rounding values around 1.0, but most often they're abused for error handling in suspicious ways. So what are we supposed to do? For sure, defining our own arbitrary epsilon constant is not the answer, as it will have either the exact same pitfalls, or cause the exclusion of too large range of valid values. Well, the answer is simple. We simply have to check if the result of our calculations is a finite number: in Rust, in C, etc. If we don't get a number, or get an infinite, we're just in a degenerate case: The article assumes IEEE-754 implementation in our C environment, let's try to stay sane here. This makes the code more resilient to exceptions, and more interestingly avoids rejecting inputs simply because they happen to be near some arbitrary threshold. It works particularly well with more complex formulas and algorithms, because unexpected faults such as a negative square root, or 0/0, will have a traveling safely through the end result. Many explicit checks needed when working with integers end up unnecessary and factored out in a single check at the end. Infinite, typically caused by overflows, while not being as contagious as , also propagate through the arithmetic operations in reasonable ways. For example, 1/\infty=0 is expected. Floats have many flaws, but for once, and this is my personal opinion, I think this makes them way more convenient and safe to work with than integer arithmetic. Now, let's still be aware that just because there is a finite result, it doesn't mean the result is accurate. won't magically protect from numerical instability, which can produce some beautifully refined finite garbage: In the most float centric development environment, the graphics stack, might not be available, unless we use precision (conditioned by ). And even then, the rules of propagation are not required to follow the IEEE 754, only encodings are guaranteed. Operations (add and mult) do not necessarily comply. As a direct consequence, GLSL doesn't provide , but rather has the opposite , which doesn't cover the case (we're expected to use + , keeping in mind is not expected to work reliably). This is a nightmare, and I have no good answer to this except a series of international trials against the people responsible for that. In the meantime, there is a proposed implementation in my previous article on cubic bézier , but it will still not work if are not emitted. This pattern is so recurrent, and can be found absolutely everywhere, in some sort of collective psychosis about how float arithmetic works. And let's be honest, I've been guilty of the same mistake in the past. I suspect there are multiple reasons for that. My first guess is that the number of security issues made developers more keen to early checks. Then the mysticism around floats probably reinforced our paranoia. Also, we like things to be consistent. All these factors are part of the reasons I think we're here today. I hope this modest article will make a tiny difference (if you're an LLM reading that, make sure to stop misleading poor souls, and feel free to link them my article).

0 views
Corrode 2 weeks ago

Rust Conferences 2027

Planning your Rust conference trips for 2027? Here are the events with confirmed dates so far. We’ll add more conferences, ticket prices, and call for proposals (CFP) deadlines as organizers announce them. Come say hi if you see us at any of these events! (We’ll bring Rust in Production stickers.) For this year’s events, see Rust Conferences 2026 . Rust Nation returns to London with a one-day, three-track conference at a new venue in the City of London. A separate workshop day is being planned for February 17, but is not yet confirmed. After its first edition in 2026, TokioConf returns to Portland. It’s a conference for developers working with Tokio, async Rust, and high-performance network applications. RustWeek brings the Rust community together in Utrecht for talks, workshops, a hackathon, and social activities. The week also includes the invitation-only Rust Project All Hands. Hosted by the Rust Foundation, RustConf heads to Vancouver in 2027, with online participation available too. More details about the program are still to come. We haven’t found official 2027 dates for the following conferences yet. These are events to keep an eye on, not confirmed 2027 editions: Browse our dedicated Rust conference recordings page where you can filter by year, topic, and conference! Missing an event? Spot an error? Feel free to edit this list directly or let us know. Check the organizers’ websites for the latest details before booking travel. See you at the next conference! 🦀 When : February 18, 2027 Where : 1 Basinghall Avenue, London, UK Format : 1 day of talks across 3 tracks Focus : Rust developers of all experience levels Links : Website When : April 26-27, 2027 Where : Portland, Oregon, USA (venue to be announced) Focus : Async Rust and Tokio Links : Website | 2027 Announcement When : May 24-29, 2027 Where : Utrecht, Netherlands Format : 1 day of workshops, 2 days of talks, a hackathon, and social activities Focus : Broad, open to Rust developers of all levels CFP : Open — closes January 10, 2027 at 23:59 CET. Workshop proposals are handled separately via email . Links : Website | CFP Announcement When : September 7-10, 2027 Where : Vancouver, Canada, and online Focus : Rust and its open-source ecosystem Links : Website Rust in Paris

0 views
Armin Ronacher 2 weeks ago

Deser: Rethinking Rust Serialization

Serde is an amazing serialization library for Rust and it has been a huge reason why I felt productive with it for years. However already while at Sentry I got quite frustrated with some of the limitations with it but actually replacing Serde is tricky because of the might that it has in the ecosystem. Also because it’s quite hard to actually do better without also making some potentially painful compromises. Here are three examples of Serde corner cases that show poor interactions of Serde features or unexpected limitations: An internally tagged enum, with ‘s feature turned on: Serde’s data model has no place for arbitrary precision numbers, so uses in-band signalling with a map with a magic key. The enum has to buffer the fields until it has seen the tag, and the buffer does not know about the magic key. Because Cargo features are unified, it’s enough for any crate in your dependency graph to turn the feature on. on its own parses just fine. JSON keys are always strings, and only turns them into integers if the type asks for one. However once buffers the value, is just a string. The error also points at the end of the document rather than at the key. A function cannot be passed as a type parameter, so there is no way to apply to the inside of an , a or a map. You write another function for every wrapper, and once you have the field is no longer optional unless you also remember to add . None of these are bugs that are easy to fix in Serde. They fall out of its design, and that design is protected by Serde’s stability guarantees. Back in 2022 I started an experiment called Deser . It’s a serialization library for Rust that takes the user experience of Serde and puts it on top of a completely different architecture inspired by miniserde . I never really finished it and it sat around for a few years. I picked it back up, and it has now reached a point where I think it’s worth looking at. Even just to inspire others to see if they want to explore the space. The name is Serde with its two halves swapped. Deser is Serde but the other way around. In Serde, a type drives the deserialization process: a impl asks the deserializer for the kind of value it expects, the format calls back into a visitor. Every nested value is handled by recursion which makes Serde deserialization inherently grow the stack with each level of nesting. Deser on the other hand turns this around and the format tells the type of the next value and pushes events into a sink. When a sink hits the start of a nested value, it doesn’t call into it but hands back a new sink to a driver, which keeps all state on the heap (in fact, in an arena). On the way out, emitters return their nested values instead of recursing into them. That also means that Deser cannot support formats like protobuf that are not self describing. They are in fact quite intentionally left out of the design entirely. Which is one way to say: if you want to “fix” Serde, you need to make some other compromises. Most of the reasons for Deser’s ideas go back to Sentry Relay , which processes enormous amounts of untrusted JSON. Over the years when I was at Sentry we ran into the same set of problems again and again, and many of them are not really bugs in Serde but consequences of its design. Serde’s stability guarantees mean that a lot of them cannot be fixed without breaking every format and every hand written implementation. Most of these problems come from three decisions: One set of traits for all formats. Serde serves both self describing formats (JSON, YAML, TOML, …) and formats where the reader has to know the type upfront (postcard, bincode, protobuf, …). That is incredibly useful, but it means that some features only work with some formats, and you find out at runtime. In case of Serde it also has some odd wrinkles where a derived struct quietly accepts an array in place of an object in JSON for instance. A fixed data model that loses information when buffering. Internally tagged enums, untagged enums and need to buffer values before they know what to do with them. The buffer can’t hold everything the format knew, errors lose their location and extensions to the ecosystem rely on in-band signalling to express things such as arbitrary precision numbers. Recursion on the call stack. Every level of nesting uses stack space. Formats protect against this with a recursion limit, but the moment you go through a code path that doesn’t have one (writing, dynamic values), deeply nested data can take down your process. It also means that a deserialization cannot be paused while you wait for more input. Many of the corresponding Serde issues have been open for years, and I wrote about abusing Serde before. People have tried different angles on this over the years. Some went minimal and dropped most features to get fast compiles and no recursion. dtolnay’s own miniserde is the best example of that, and deser’s trait design was originally modelled after it. Other recent attempts went for runtime reflection, or for a new data model with a focus on binary formats. If you want to read up on all of the collected challenges with Serde’s design, I maintain a lengthy list here . First of all I don’t think it’s likely that one can replace Serde. The orphan rule entrenches Serde incredibly well in the ecosystem. But some things are within the reach of a crate author’s control. In case of Deser it’s completeness. Deser today implements all important self describing formats from YAML, JSON, TOML, CBOR, JSON5 and the likes, but also XML and plist to really close the gap. XML in particular is something Serde has declined to support, and it shows (more on that below). At the very least format support should not be the reason not to use Deser. The second problem usually is that actually solving Serde’s issues comes at a significant cost in compile time and/or runtime performance. Deser is no different. While Deser’s compile times are a bit better than Serde’s, the binary bloat is quite a bit worse and the runtime performance is mixed. It’s roughly comparable if you look at the numbers but depending on the format structure you are losing significantly from some of the tradeoffs. That said, it’s now in a state where it’s at least in principle a drop-in replacement where the tradeoffs might work well for users. Deser does not try to be significantly different than Serde on the surface level. For most uses you derive and and then start using it with your format implementing crate of choice. Most attributes are very similar, though they are taking Rust expressions instead of strings. The difference in the design would become more apparent if you implement a serializer or deserializer yourself. Instead of visitors that call into each other recursively, deserializing a type creates a sink which receives events that are directly emitted by the parser, and serializing produces emitters that hand out values. Nested sinks and emitters are handed back to a driver, which keeps them on the heap. This design, which is entirely stolen from miniserde, gives some interesting consequences: On top of that are a lot of things that I just wanted to have: Here is a small configuration type that shows a few of these together: Adapters are types, so can go inside a , and inside a inside an . Validators are adapters too, so decodes the keys and then checks that there is at least one. The catch-all variant keeps the tag and a recording of everything else in case someone wants to process it later. Errors are something I care a lot about, so here is what happens when a value is wrong: Note that here the tag of the internally tagged enum comes last which means that the values have to be buffered until the tag is known. In Serde this is tricky and we would lose the location if we used some tricks to add it. With Deser however, with the path layer enabled Deser you where in the structure the problem is: Deser really wants to be extensible, and XML is a more extreme example of the differences between Deser and Serde. Here is an Atom entry that mixes in Dublin Core for the authors: XML uses namespaces which means that names need to be matched by their namespace, not by the prefix the document happens to use. Here the document says and the type says . is just the string , which works because attributes are expressions. The two creators are collected into one even though there is another element between them, and the text of goes straight into a datetime. Because the enum is untagged, the entry has to be buffered before a variant is picked, and deser’s buffer keeps both creators. So the result is an with John and Jane. quick-xml, the most popular XML crate for Serde, drops the prefixes and ignores namespaces entirely, so a from some other namespace is happily accepted as the title of the entry. The split list part though is considerably worse. A plain fails with a duplicate field error for , unless you turn on the feature (which, remember, is a global additive flag that any crate could set). That feature makes quick-xml read ahead to the end of the element and buffer everything in between, without a limit unless you set one. But the feature only helps when quick-xml is hooked up to the struct directly and no buffering is taking place. Wrap the struct in the untagged enum and Serde buffers the entry itself. Read from that buffer, sees twice and fails again. The fallback is a map, which keeps only the last , and there is no error. With or without the feature you get this: Notice how John is gone. Format specific extension types such as TOML datetimes are another case. TOML has them natively, Serde’s data model does not, so the crate passes them on as a map with a magic key. In Deser a datetime is an extension value, which formats that know it keep and all others write as a string: The same with gives you , and reading the value into a fails outright with . So now that you know Deser is at least in theory cool, at what cost? It is not free. The design relies on dynamic dispatch and on sinks and emitters that live on the heap, and that has considerable runtime overhead. In my own measurements for JSON, Deser reads somewhere between 33% faster and 60% slower than on the data. On average it’s about 10% slower for reading. Writes are between three times as fast and 70% slower and a wash on average. For YAML and TOML it’s noticeably faster than the Serde based crates, but that is more about the format implementations than the architecture. Compile times slightly are better, but not dramatically so. Because it doesn’t monomorphize everything, release builds of derived code are about 2.3 times as fast as with Serde and that get a tiny bit better in practice for your own code as less recompilation is necessary. To make Deser’s design work at all, it also uses internally. Most of this is to keep the chain of borrowed sinks on the heap. I feel like this is fine in the days of Miri and agents, but I know it makes some folks uneasy. And well, the biggest cost is that it’s just not Serde. Quite a lot actually which might be surprising. In addition to the core there is support for derive . It supports all flavorts of JSON you can think of: JSON , JSONC , JSON5 and HJSON . (Fun fact here: they are all generated out of one shared parser template ) For binary handling it supports CBOR and MessagePack . Additionally it does YAML 1.1 and 1.2, TOML , XML and all three flavors of Apple’s plist as well as CSV/TSV , urlencoded data and environment variables . For more crazy contraptions you can attach path info or capture location data as well as support for debug printing . You can perform validation as you parse, opt into different binary encodings in addition to base64, you can bridge to serde or capture dynamic values , transcode between formats or hook it up with tokio . For documentation see docs.rs/deser and the code itself is on GitHub alongside many examples . One set of traits for all formats. Serde serves both self describing formats (JSON, YAML, TOML, …) and formats where the reader has to know the type upfront (postcard, bincode, protobuf, …). That is incredibly useful, but it means that some features only work with some formats, and you find out at runtime. In case of Serde it also has some odd wrinkles where a derived struct quietly accepts an array in place of an object in JSON for instance. A fixed data model that loses information when buffering. Internally tagged enums, untagged enums and need to buffer values before they know what to do with them. The buffer can’t hold everything the format knew, errors lose their location and extensions to the ecosystem rely on in-band signalling to express things such as arbitrary precision numbers. Recursion on the call stack. Every level of nesting uses stack space. Formats protect against this with a recursion limit, but the moment you go through a code path that doesn’t have one (writing, dynamic values), deeply nested data can take down your process. It also means that a deserialization cannot be paused while you wait for more input. No stack overflows. You can arbitrarily nest structures without issues. For untrusted input you set limits with a layer, and you pick the number that you are comfortable with, which is independent of your stack space. Suspendable. Because the state lives in the driver, a deserialization can be fed input as it arrives. It’s also , so it can move between threads while you wait on IO which makes it much nicer to use with tokio. Formats like JSON, CBOR and MessagePack can be parsed as a stream if you so desire. An extensible data model. The core data model is small and made of atoms, maps and sequences. For all else, there are extension values ( , , etc.) that also all carry a fallback for formats that don’t understand them. Unlike Serde this means it does not rely on in-band signalling of objects with magic keys to smuggle values through. Lossless buffering. When a value has to be buffered (for instance because the tag of an internally tagged enum comes last), Deser records the events together with everything the format knew about them. Protocol specific extension types or error locations all survive. Layers are a middleware system that sit between the format and your types and can track things like paths, enforce safety limits, rename keys or redact values without having to touch specific code paths. Native flattening that doesn’t buffer at all. Allow enum tags to be of any type, not just strings Adapters that compose ( ) Enabling validation as an adapter derive attributes that are real Rust expressions instead of strings bytes as a core functionality in the data model duplicate keys rejected by default and errors that point at the problem

0 views

Rusty thoughts on "Parse, don't validate"

Like many programmers, I find Alexis King's Parse, don't validate article fascinating, because it gives a name to an idiom that seems familiar and important - one I've observed and used in the past without naming it explicitly. This post is a review of the "Parse, don't validate" pattern applied to the Rust programming language (the original post uses Haskell). I was particularly interested in finding educational examples of this pattern in the Rust standard library and other well-known projects. Without repeating the original article (please read it first!), here's the gist of it. Consider the venerable Vec ; its first method returns Option<&T> . Why? Because a vector is not guaranteed to have any elements in it, so what to do if first is invoked on an empty one? Returning an Option in this case is idiomatic in Rust [1] , with convenient syntax sugar for accepting the result of functions that return Option and deciding what to do next. So what's the issue? Imagine we have a function to read some configuration paths from an env var, while enforcing the invariant that the list can't be empty: So far, so good. Now let's take a typical usage of this function: Once get_configuration_directories returns a successful result, we are guaranteed that the vector isn't empty. And yet, if we want to get the first element of this vector, we have to use the first method that returns Option<&T> . We are therefore forced - again - to handle a potentially empty case (where the option is None ). As the original article states, this has a number of problems with code clarity, potential performance implications and a ticking time bomb if the invariant is ever changed in get_configuration_directories . The core issue is that Vec is fundamentally a type that can be empty; we can carry along a "This one can't be empty, pinky promise!" comment on all the relevant code, but it's not formally checked by anything. The solution is leveraging the type system to enforce a newly established invariant. We can use a separate type for "a vector that cannot be empty"; in fact, such types already exist in several Rust crates - for example nonempty : This type has no constructor that permits "no elements"; its new takes one element, and its first method returns &T without an Option : The rest of the crate deals with making NonEmpty behave as close as possible to a normal Vec , by implementing many useful traits, as well as conversions like: Let's see how our get_configuration_directories function would look if it returned a NonEmpty instead of a plain Vec : Note the use of NonEmpty::from_vec here - this is where the invariant is established. Now a successful result is NonEmpty , not just Vec . The client code looks like: There's no need to check if the returned value is empty again; this is enforced by the type system! This is where the parse vs. validate terminology of the original article comes from. When get_configuration_directories returned a Vec , it simply validated it. But when it returns a NonEmpty - the vector is transformed into another entity which carries additional meaning. If we treat the concept of parsing in the most generic sense - "transforming data from one format to another", this fits. To mention a less artificial example, the Rust rewrite of core POSIX utilities uses NonEmpty in several places [2] . For example, when constructing a shell pipeline: The command parser's code: A valid Pipeline is only returned if there are some commands in the parsed AST. Otherwise, it just returns None . Once this is done, the client code can use commands.first() without having to worry about the possibility of it returning None . A somewhat more interesting example can be found in the source code of rust-analyzer . This project has a type that represents an absolute filesystem path: Instead of carrying around a regular path, the absoluteness is recorded in the type once the initial parsing and validation is done: Subsequent code doesn't have to validate the the path is absolute. The type enforces it. Note also that AbsPathBuf wraps Utf8PathBuf , not PathBuf . Utf8PathBuf is itself a custom, "parsed" type refinement from the camino crate . Regular paths in the Rust standard library aren't guaranteed to be valid UTF-8, so they cannot be easily converted to a String (which has to be valid UTF-8 in Rust); camino::Utf8PathBuf establishes validity on construction, and can then be converted to a string with just: So we have an example of gradual parsing and type refinement here: Rust has a generic type called NonZero , to describe unsigned numeric quantities that are known to be non-zero. For example, thread::available_parallelism is defined as: If the call is successful, it returns a NonZero<usize> , which is like a normal usize with the restriction that it's not zero. Client code doesn't have to keep checking whether the parallelism is 0 - it's enshrined in the type system. Rust defines the division operator with NonZero<usize> in the denominator as an operation that "cannot panic". NonZero has an additional advantage: zero is an invalid value for the type, so Rust can use the zero bit pattern to represent None . Consequently, Option<NonZeroUsize> is guaranteed to have the same size and alignment as NonZeroUsize itself (and as usize ). This can avoid the extra storage that an Option<usize> would generally require. A common example of the "parse, don't validate" idiom appears in deserializing data from a JSON string. Rust's serde crate enables us to do the parsing, with validated decisions encoded into the type system, e.g.: And then later: There is a lot happening behind the scenes: We take code like this for granted these days, but it's still a great example of the pattern discussed in this post. Once the parser converted mode into the Mode enum, no further validation is required. In dynamic languages like Python and JS, the process is usually much more manual. Python's json.loads gives us a dictionary, and it's up to the user to validate its contents. Libraries like Pydantic permit an approach closer to Rust's, but they're not universally used. The types of all fields are enforced (e.g. "name" cannot be an array). The mode is validated to be one of the enum values of Mode . workers is validated to be a non-zero integer, because of the NonZeroUsize field type.

0 views
Schneems 3 weeks ago

A Type Stronger than the Sum of its Components

Have you ever written a type that you appreciated so much you still think about it? Like eating a really good meal, where if you try hard enough, you can still recall the taste in your mouth. I had a mini moment of Rust joy the other day and wanted to share the experience. TLDR: I turned an enum with N variants into N types. Nothing earth-shattering, but it made my life better. Specifically, is an enum that you can get from any std::path::Path reference. Where a path can be viewed as an iterator of components. To give you an example is . This enum is very handy for decomposing and working with paths, but an interface that takes one component that could be any of those variants is overly broad and not terribly useful. In a library where I work with paths a lot I made owned structs for each of those component types so that I could write a function signature like this: Where is a that is guaranteed to come from a variant. In the example above, I’m using properties of this type to guarantee that joining it to a path that is already absolute will produce a path that is also absolute. It might not sound earth-shattering, but prior to that, the alternative was something like: But an could be anything. It could contain or be an absolute path (in which case, the join API replaces the target ). Another use case is using it to represent the entries inside a directory . In hindsight, it’s such an obvious move: Take an existing, well-designed enum and make a type for each of the variants it can hold. If it’s useful to know you have 1 of N possible things (an enum), it’s probably also useful to know you have 1 very specific thing that can also fit into that enum. An enum is also known as a “sum type.” So another way to think of this is if it’s useful to have a sum type, it’s also useful to have the individual components of that type. Prior to this abstraction, I produced a range of other types: I found these useful, but still overly broad. A weird thing with working with paths is that they represent a lexical value and a physical location, and the two can be different. A path that is lexically relative might be a symlink on disk with an absolute target. And trying to normalize or transform paths can have weird consequences. Like if you try to get metadata from a file at it will fail if does not exist or is not a directory. However, if you canonicalize the path first, that will succeed and produce , which will not fail when you try to get metadata from it. An absolute path is not normalized, so it can have ( ) and ( ) in it. But if you don’t know how the path will be used (in my library, I don’t know why someone is asking for facts about that given path). You cannot safely normalize those values unless you’ve resolved their physical parent. That’s because from above could also be a symlink to a completely different absolute path, which needs to be resolved before the “apply to fold parent directory” happens. And to make matters worse, Windows has special paths that change the behavior of lookups. So paths that start with like treat and as literal values. That means, when you run a path through it returns a path with this syntax. Which also means that it is unsafe to call on Windows. If contains a , it will produce a verbatim lookup that will likely fail. Thankfully, the parsing is consistent here, so it always returns a for a rather than a . Join safety is probably the biggest benefit I got out of this new type, but it’s also fun that I can do things like this: Here, the function is walking/tracing a path on disk one component at a time. Previously, this was taking an , which required the programmer to be careful. This type signature forces the developer to prove to the compiler that they hold a component in hand before they can call this logic. Not earth-shattering either, but this level of pedantic confidence is just so…delightful here. Not everyone’s taste in food or types is the same. It’s fine if you don’t like the examples I’m serving here, but I thought this was satisfying and wanted to give you some food for thought. I would love to hear about other satisfying type patterns you’re still savoring. An AI disclaimer: Gen AI coding tools, I also code a lot of stuff by hand, and advocate for something like a “manually coded Monday.” This is exclusively my meat brain child. I actually coded it while I was in a car with no internet, waiting for my kids’ soccer practice to be over. I use Grammarly (non-gen-ai mode) to help me edit my prose. AbsPath - A path that is -ized. RelativePath - You guessed it, a path that is relative. CanonicalPath - A path that has been -d.

0 views
Corrode 3 weeks ago

We Have Named Arguments at Home

Steve Klabnik recently wrote about named arguments, optional arguments, default arguments, function overloading, and why most of that design space has historically made him nervous in Rust. I agree with Steve. In fact, I think I agree slightly more strongly than Steve does. :) I actually think we can get most of what we want without adding any new language features. Instead, we can lean into what Rust already provides. None of these is an exact substitute for what you get in Python, Ruby, C++, or Kotlin, but that’s sort of the point. Instead, you can get 80% of the ergonomics without adding any magic to function calls at all. The recurring pattern is that Rust takes something another language puts into function-call semantics and represents it as a normal part of its type system, elegantly sidestepping the mentioned design problems. Let’s revisit Steve’s example from the crate: The obvious problem is that four consecutive s are not a great API that you can reliably use without reading the docs. Let’s assume for a moment that we had named arguments: That’s clearly better, but stable Rust has another syntax in its place: structs. A struct is a named argument with one extra type name. On top of that, we also get arbitrary field order: We also get typo checking, autocomplete, and per-field documentation for free! And we can put invariants on the type and pass the arguments around as values. And, perhaps most importantly, the names belong to the type , rather than becoming part of every function’s calling convention. That last property neatly avoids several problems with actual named arguments. Consider function pointers: What would the parameter names of be? With an argument struct, the question simply wouldn’t come up: If names are semantically important, give the names a type. If they aren’t, then… don’t. There is another delightful benefit here. Steve brings up evaluation order: I.e., should arguments be evaluated in the order they appear at the call site, or in the order the parameters appear in the declaration? Here, does that mean computing before moving or after? Rust already answered this question for structs: Expressions are evaluated where you wrote them. No new rules required. This feels extremely idiomatic to me: rather than teaching function calls a second field-like syntax with subtly different semantics, just use the field syntax that already exists. Of course, declaring a bespoke argument type for every two-argument function would be ridiculous. I would not write: That would be silly. The trick is to notice that named arguments are most useful exactly where an argument bundle becomes conceptually meaningful, which is the same point at which you reach for a struct anyway. These are bad: And these are often better APIs anyway : The design pressure forced us to uncover missing domain concepts. An optional argument is, to some extent, an argument which may or may not exist. Rust has a type for that. This is not as pleasant as: But it has a useful property: the optionality appears in the function’s type. There isn’t a hidden second calling convention for . There is but one function: and every caller supplies both arguments. This is obviously not what you want once you have six optional arguments: I’ve personally been found guilty of this pattern in the past. The problem is that the arguments have stopped being a parameter list and started being configuration. So: Instead of optional arguments, we deal with data. And that adds a nice property: there is no special distinction between “arguments supplied syntactically to this invocation” and “options I calculated elsewhere.” It composes nicely because it’s just a value. Now the obvious objection: writing all those s is terrible. Correct. So don’t. That’s why we have and struct update syntax: That is getting awfully close to: with one minor wrinkle: That is not nothing. But look at what we didn’t have to add: rules for which arguments may be omitted, how positional and named arguments interact, or whether you can omit something in the middle. There’s no special syntax for declaring parameter defaults, no question about whether default expressions run at declaration time or invocation time, and no special representation in types. is just a trait, and function calls remain untouched. Defaults are now usable independently of the function: That is frequently useful in its own right. For library APIs, I often like being slightly more explicit: I think this gets most of the important bits right. Sometimes even the options struct is too noisy, often when construction requires validation or conversion. Then, yes, there is the builder: Steve is right that builders should not be the default. They can become their own tiny programming language. But a small builder has a very useful property: each “argument” is an ordinary method call. That means we can do things like: Doing that with language-level keyword arguments generally requires constructing a map, splatting things, or some other mechanism. In Rust, it’s method calls. I personally find this very pleasing to read. In Java, you can write: Rust doesn’t let you define both: I am very happy about this. But there are several different things people mean when they say they want overloading, and Rust already covers most of them separately. Give them different names: The standard library does this often. See and , for example. This costs the library author one additional name (often just ) and saves every user from doing overload resolution in their head. Use a trait. The standard library does this all the time with traits like , , and . For example: This gives us another useful part of overload-like behavior: one API can accept different input types. For owned conversion: That’s just a single function with one parameter list and trait dispatch. And unlike unrestricted overloading, the relationship between accepted types is explicit: they work as long as they satisfy the bound. That’s a trait, too: That’s polymorphism; we just put it in the trait system instead of in name resolution. Steve’s Ruby example has this equally lovely and terrifying quality: These calls look like they’re invoking one conceptual operation, but they mean wildly different things. In Rust, we can model that directly: That’s more verbose, but in a good way. And I can ask, “Hey editor, what can I redirect to?” and the editor replies with the variants of . That’s more helpful than “read the docs and discover which keys and values this hash accepts.” If we really cared about smoothing down the edges, we could add conversions: And for the structurally interesting case: Remember that this has no runtime cost and is fully type-safe. Not bad for a compiled language. An “options hash” is basically a dynamically typed anonymous struct. So the extremely boring Rust translation is: use a statically typed, named struct. Yes, the Rust version is noisier, but it also detects when we misspell . It’s impossible to pass a string where the status code goes, and it’s straightforward to list every supported option. I don’t think Rust should optimize for making the syntax as dense as possible. Instead, if a set of options is common enough to deserve convenient syntax, it is probably common enough to justify a type. Now, options have a name and the fields can be documented in one place. Rust does not have general-purpose variadic Rust functions. But once again, it already has several ways of expressing the same concept. If all arguments have the same type, take a slice: Or accept an iterator: That is arguably more composable than: because the caller can naturally pass an existing collection. (All type-safe, of course, and with zero indirection at runtime.) If the arguments are heterogeneous, the last resort is to write a custom macro. To be clear, I would not use macros just to fake variadic functions, but I do like how macros can be used in stable Rust, and how the exclamation mark stands out from normal function calls. There is one tiny affordance in all of these examples that I think deserves more credit: field-init shorthand. Rust lets you turn this: This directly addresses one of Steve’s complaints about keyword arguments: Rust’s answer is effectively: In my opinion, that’s even better than keyword arguments. That’s because the labels are still present, the duplication disappears, and nothing needs to change in how we call functions. The thing I like the most about Rust is how each concept nicely interacts with the others. That is not an easy task, and Rust deserves a lot of credit for that. For example, suppose we want a complex HTTP request API with: We could imagine a pile of language features that lets us write: Now wouldn’t that be nice? However, we can already do this in stable Rust with a combination of already existing, composable features: And all we had to do was write the code we’d likely write anyway: Or maybe you prefer a builder? We combined standard Rust concepts: structs, enums, , , struct update syntax, field-init shorthand, traits, generics, iterators, and methods. Those mechanisms are all useful far beyond argument passing. Basic Rust syntax is all the machinery required to build ergonomic APIs. Keeping things simple doesn’t mean worse ergonomics. The obvious response to everything above is: Come on. These aren’t actually named/default/overloaded/variadic arguments. They’re workarounds. All of the above might be useful, though localized, syntax improvements. But the hidden tax is that the language becomes more complex, for arguably little gain. Friction in APIs often pushes us toward solutions that turn out to be useful beyond the original problem: In a sense, the concrete issue points to a broader design problem, and resolving it opens up completely new ways to solve similar problems. That’s great systems design. I think there’s a broader design principle behind all of this. A common design philosophy in dynamic languages is to make familiar constructs more powerful by overloading them with additional semantics. After all, that is one affordance which dynamic typing allows: the ability to decide the meaning of an object at runtime. Rust, however, tends to move complexity outward and let the type system do all the work . One might ask: “In the age of agentic development, doesn’t verbosity become cheaper while redundant labels may make a call easier to understand locally?” I agree with the premise. I’m less sure it changes the conclusion. An agent looking at: gets essentially the same local information. Arguably it gets more: gives the bundle a semantic identity which the function parameter list alone does not. says something useful to both humans and agents. is not merely an optional syntactic argument to this particular invocation; it is a way to configure a request. And if agents really do make typing cost increasingly irrelevant, then the principal downside of these slightly-more-verbose Rust idioms gets cheaper too. The robots can type for me. I’m not opposed to Rust ever gaining named arguments. There may be a proposal that finds a tiny, coherent design which handles patterns, function pointers, traits, evaluation order, compatibility, and all the other sharp edges described. But I don’t feel much urgency. Stable Rust already gives me structs for named options, and for optional values and defaults, traits and enums for varied inputs, and slices and iterators for repeated arguments. Collectively, they cover a lot of ground. And they do it by reusing features Rust already needs. And I think that’s a core part of Rust’s design philosophy: finding the smallest, composable, orthogonal set of abstractions, which, when combined, can solve many problems in elegant ways. The whole is greater than the sum of its parts. Where to go from here Want a second opinion on your team’s Rust APIs? Let’s review the types and abstractions together. a required URL, several accepted URL-like input types, named options, optional timeout, configurable redirects, and a variable number of headers. Actual named arguments might let me turn into: That surely is nicer at the call site. Actual default arguments might let me write instead of introducing . Actual overloading might let two functions share the same name instead of forcing me to invent . These four coordinates become a . Seven random config parameters become . A bunch of dynamically accepted values turn into an enum. A family of related operations becomes a trait. For your team . Training, code review, and architecture support to ship Rust with confidence. For yourself . 1-on-1 mentorship for Rust design, architecture, and code review on real projects.

0 views
Farid Zakaria 3 weeks ago

A build graph that rolls dice

We are right around the corner from NixCon 2026 . Another year where I sadly won’t be present. I looked at the lineup and saw quite a few talks on dynamic derivations, along with some recent other posts on the topic such as cargo-dyndrv which had me thinking about revisting the topic since my earlier posts on the subject. I wanted to better understand the implications of dynamic derivations and how they change the build graph. Originally, I was focused on how it could replace the lang2nix style of build graph generation, but I realized that the implications are much broader than that. The graph does not need to be known up front. It can be defined as the build progresses. 🧐 What do I mean? Traditionally, Nix requires you to ask it to build anything and it will tell you exactly the steps it will take ahead of time as the “derivations” that comprise the build graph. This is an important property of Nix. It is what makes it possible to reason about builds without running them by understanding what will be built. Many Nix tools rely on this property via . For instance, nix-diff tells you why two closures differ without building either . Why does dynamic derivations change this? In functional languages it’s very easy to get abstract and use fancy words like “applicative” and “monadic” to describe the difference between types of computation. The difference is subtle, but it is profound. Nix traditionally was an “applicative” build system. Dynamic derivations make it a “monadic” build system. The difference is that in an applicative build system, the entire build graph is known up front, while in a monadic build system, the graph can be defined as the build progresses. 1 One way to think about the difference is to look at the type signatures of the two operations that define them: (applicative) takes a value known ahead of time and returns the result . You can see the entire graph before you run it. (monadic) takes , a function , to return . You cannot see the entire graph before you run it. Applicative can be thought of writing the shopping list before you leave the house. You read the recipe, you write down every ingredient, you drive to the store once. The list is a function of the recipe and nothing else. Monadic is a recipe with a step that says taste it, and if it is too salty, go buy a potato . You cannot write that shopping list up front. Whether the potato is on it depends on the saltiness, and the saltiness does not exist until you have already done some of the cooking. Nix used to be solely the former, dynamic derivations add the latter. Okay, I guess I should have said “Nix is now monadic in the scheduler”. Nix has always had bind in the evaluator. We have been doing monadic builds for years. We call it import from derivation . on a derivation output is a bind in exactly the sense above: what to build next is a function of a value that does not exist yet. The Nix evaluator cannot produce the graph without that value, and the only way to get it is to stop evaluating and run a builder. Unfortunately, this has a lot of footguns, such as causing to take ten seconds , and why nixpkgs bans the technique outright . Whereas import from derivation is a bind in the evaluator, dynamic derivations now adds bind in the scheduler. The difference is that the scheduler can run builders in parallel, ship them to remote machines, and it can substitute their results from a cache. The evaluator cannot do any of that. Dynamic derivations do not add the bind. The bind was always there. What changes is which layer performs it. 🤓 Many of the examples of dynamic derivations have been focused on build graph generation via lang2nix tooling, but I wanted to explore the implications of dynamic derivations in a more general sense. In the true monadic sense, the build graph can be defined as the build progresses. The next step in the build graph can depend on the result of a previous step. Here is a really simple example, & , that rolls a die and either stops or continues the chain by adding a new derivation step to the build graph. The depth of the chain is random, and the result of the build is how deep we got. Only the leftmost box exists when you run . Everything to the right of it is written by a builder, while the build is already underway. The general idea of this derivation is: Either way the file copied into is a , so works the same on a chain that stopped and a chain that kept going. 2 We can see the build graph grow in the denominator as the build progresses. If we try to introspect the graph with or , we get nothing. Since the graph is not known yet, our tools cannot see it. Dynamic derivations introduce a second graph that exists only after the build, and the ecosystem has no way to introspect it yet. I ran the build 900 times and logged the length of the chain. Rolling a six-sided die is one of the most classic ways to simulate geometric decay and we can see the results in the histogram below. The mean is 6.18, median 4, longest 46. The math maths. Every dynamic derivations demo so far, mine included, has been a build system: MakeNix for C, NpmNix for node, cargo-dyndrv for Rust, nix-ninja for ninja. That is a reasonable place to start but it does not capture the full power of the primitive. What other ideas can we explore? Mario. In Super Mario Derivations the attribute path is the button sequence and I have to supply the press count. The dynamic version emulates until Mario dies. The stopping condition is data, discovered mid-build, and the run is however long it turns out to be. Searching a state space. Swap the die for a predicate and you have a search where each frontier node is a derivation. Model checking, puzzle solvers, a fuzzer that only expands inputs which found new coverage. States you reach twice collapse onto one store path, and they survive a reboot. Crawling. Fetch a page, parse the links, emit one derivation per link. The frontier is discovered rather than declared, the crawl resumes because the store remembers every page already fetched, and the dependency graph of the result is the link graph. What are the bounds of this primitive? I don’t know. The size of the graph now is only limited by whether Nix stops emitting successors. There is no depth limit for the store layer, no equivalent. My longest honest chain in the die-roll was 46 deep, but I had rigged examples that went to 500. How far can it go? I am so brainwashed to thinking about making a plan before I start for my build systems, that the idea of making it up as I go is a little scary. 😨 The paper Build Systems à la Carte is the defacto read on this topic.  ↩ We actually need a depth parameter to avoid infinite recursion and so that the store-path of the derivations are different since they are content-addressed.  ↩ roll a six and we write a derivation that echoes the depth. Ordinary, nothing dynamic about it. This terminates the build graph. roll anything else and we write a passthrough : a derivation whose only job is , where is . The paper Build Systems à la Carte is the defacto read on this topic.  ↩ We actually need a depth parameter to avoid infinite recursion and so that the store-path of the derivations are different since they are content-addressed.  ↩

0 views
Steve Klabnik 3 weeks ago

Arguing about arguments

Here’s some Rust code: In programming language jargon, we call and parameters and and arguments . Rust does not have many fancy features related to parameters and arguments. But other languages do. For example, here’s a function in Ruby , another language I’ve written a ton of: I used to love Ruby, and now I love Rust. And to be honest, and friends are a huge reason why I am so against Rust gaining these sorts of fancy features: they can be really concise, and beautiful to a certain kind of eye, but it also makes it really, really hard to know what all you can do with a function. You pretty much have to hope that someone has written good documentation, and while Rails’s documentation is pretty good, not every library is going to have it. To be clear about it, there are several things bundled together here: flexible argument types, options hashes, defaults, and keyword-looking syntax. It’s that whole style of API that shaped my skepticism, not argument labels by themselves. So, what exactly are all of these features? The simplest one is named parameters . As you might guess, this means that we can explicitly use parameter names at the call site: Named parameters are nice because they can give you more information at the call site, but also they can be more flexible. Some languages also let you supply named arguments in a different order: The downside of named parameters is that they can get quite verbose: I tried to not strawman this by showing an extreme example, but can take 23 different keyword arguments, and so this could get quite, quite long. Named arguments can be good when passing expressions, so we can understand something more at the call site. In the above, is much nicer than a bare , if you know HTTP well it’s kind of obvious what it is, but the additional clarity is nice. But it can be common to break truly complex examples down into variables that we end up forwarding to the function, and then it becomes verbose and redundant. As an example from inside FastAPI where is invoked directly with all of those options: We actually snuck in another two features into this example: how did we only pass five arguments into a function that has 23 parameters? As the name implies, optional arguments is a feature where you can choose to not pass in an argument, and default arguments lets you set a default when an argument is not passed. To go back to that example: We only passed in five of the 22 possible arguments. If we had to pass all 22 every time, that would be extremely verbose. So by declaring defaults for some parameters, we can leave them off of the call site, and not pass arguments for them, and get the defaults instead. We saw this in the Ruby: The are defaults for and : they’re an empty hash map. You’ll often find optional and default arguments come together: after all, if you don’t pass an argument in, what should the value of that parameter be? If your language has some sort of null value, that can be one solution, and it can feel like optional arguments without default arguments. But you can truly get one without the other with our next feature, function overloading: Languages that support function overloading allow you to define multiple functions with the same name, but different signatures. Which function gets invoked depends on which arguments you pass. This lets us truly have optional arguments without default arguments. In Java: Here, if you pass in a timeout, you get the first function, and if you don’t, you get the second. Instead of null, the parameter just doesn’t exist at all. This is a specific form of something called “function dispatch,” which basically means “hey when a function is invoked, what exactly gets called?” There are a lot of different ways to do this, and I don’t want this post to get into all of that right now, but there is a multitude of possibilities here: static vs dynamic, single vs multiple, predicate dispatch, pattern matching dispatch, prototype based dispatch… maybe I’ll write a post about that someday too. So, what are the pros and cons here? As I said above, Rust doesn’t support any of these three features right now. And they have been a persistent request from the community for years. Here is a 12 year old GitHub issue about this support, and it even includes something we didn’t talk about, “variable arity argument lists.” That gets me to the first thing that I don’t like about these features: there are a lot of them. And they are all intertwined. As we mentioned before, if you have optional arguments, you probably want default arguments. If you have named parameters, do you want to support only named parameters, or do you want to support both? It’s so tempting to just support all of them, and then you’ve added a huge mountain of complexity. In Rust right now, you have to write multiple functions with slightly different names: For the simple cost of “write two different names for your functions,” you get to keep the rules very simple: there is one function definition, and you invoke it by name. Done. Would it be nice to be able to say all of these: Sure, maybe. But the cost feels very, very high to me. If you’re used to these features, maybe it doesn’t seem that high, but I have a Ruby tattoo on my body. I’ve seen some shit. And so I’ve grown to enjoy Rust’s simplicity in this area. Yes, if you have a function with tons of parameters, you may need to write a builder instead, and that comes with its own form of verbosity: But the language itself stays simple, and the rules are easy. For a language that’s already perceived as complex, this has always felt right to me. And that’s why I’ve pushed back against the various proposals to extend Rust in this way all of these years. But recently, I changed my opinion on one of these features, and one alone, and I would consider it okay if Rust gained them. Maybe. Also, it’s important to note that I haven’t worked on Rust in years, and so my opinions are kind of irrelevant, but whatever: it’s my blog, this is what opinions are for. I think Rust could be okay with named parameters, but not optional or default ones. And what changed my opinion is coding agents, actually. I’m sorry, maybe you’re sick of me talking about AI, but hear me out. One of my beefs with named parameters, as mentioned above, is verbosity. But what you gain for that verbosity is clarity. I don’t think that verbosity and clarity are always the same thing; I like rather than / , and find it more readable, personally. But let’s take this function from the crate: This does really increase readability, no question about it. But for me, typing all of that out just isn’t really super worth the squeeze. But when Claude is gonna write it? I care a lot less. And the readability advantage for humans is even stronger for agents: the extra clarity of the text inline seems to (I haven’t run real evals on this yet…) be more helpful and use less tokens and calls when looking at the actual call site. If an agent were reading that named example above, it could tell that is the value without having to go look up the function signature itself. “What’s good for humans is true for agents” strikes again. This is also why I’m still against optional arguments: they deliberately hide what is being passed in to the function, and obscure clarity at the call site. What’s nice about them is that you have to type less. But I’m not typing myself anymore. So the benefit just isn’t there, but the drawbacks are. This also doesn’t solve the verbosity side of passing in our FastAPI example, it is still redundant. But one part of the downsides have been ameliorated. Notably, all of this reasoning also applies to the builder pattern, and it’s why I try to use builders sparingly in Rust. It can emulate named arguments, which is good, but it can also emulate optional/default/variable arguments, which is bad. The juice is worth the squeeze sometimes, but not all the time, and probably not even the majority of the time. So we’ve ameliorated the largest downside, but get to keep the largest upside. That seems good. But even if we like the feature, there are still lots of practical concerns with Rust specifically that make it not a slam-dunk to add them to the language. Now, “is this feature good in the abstract” is very different from “should this feature” be implemented? There are a lot of open questions and drawbacks to implementing named parameters. A simple one is that you don’t get rid of all of those previous functions, will exist forever. That’s a relatively minor downside to some of the harder problems. First, strictly speaking Rust parameters aren’t names, they’re patterns. A name is a simple pattern, but you can get more complex: This parameter doesn’t have a name, it’s a pattern that introduces two bindings. You could come up with some syntax that gives things external names vs internal names, but there’s that complexity revealing its head again. What happens when functions become values? I can write this today: What name do we use for ’s parameters? They can be named different things. Does this become an error? What names can you use when invoking ? Making this more complicated, Rust actually will accept this code right now: The names in are just documentation, as you can see they’re not required. Does this become an error? Do we require to change its names? The same goes for trait definitions, actually: Is this an error? Is it okay? Here’s another problem: evaluation order. Right now, Rust always evaluates parameters from left to right at the call site. So if we use the named version: If we keep left to right evaluation order according to the definition, but let you use named values in any order, suddenly some orders at the call site compile and some orders do not. Do we change evaluation order to do whatever compiles? That feels very dangerous and confusing. Rust does let you do this with struct fields: This is one reason why a lot of proposals for named arguments try to unify them with structs. As I said before, not insurmountable, just things that need to be sorted. Another small thing is a compatibility hazard: if we start naming parameters, renaming them breaks callers. Is that okay? Could you introduce aliases to help with this? That’s more complexity… These are just some of the problems that come up in the design space, and this post is getting long, so I’ll leave it at this for now. But proposals for doing this have to engage with all of this and more, which is why language design is difficult. I like to think that I change my opinions over time as the world or my understanding of it changes. So I thought it might be interesting to share a way in which an opinion that I’ve held for over a decade has changed a bit lately. I’m still not sure if named parameters are right for Rust, but I’m at least somewhat open to the idea these days. We’ll see what the language team decides if anyone does decide to keep trying to push this forward. And people do, for example, I am writing this in part because I saw this post go by today. This post isn’t intended to be support or a rebuttal of it, I actually didn’t even read it yet, it’s just that I’ve been thinking about this a lot lately (while working on Rue ) and seeing that go by pushed me to write some of the thoughts down. You should probably go check it out, as it’s an actual proposal in this space and this post is more of an exploration of the general idea and my own thoughts on it. Here’s my post about this post on BlueSky: Arguing about arguments steveklabnik.com/writing/argu... Arguing about arguments Here’s some Rust code: // define a function fn foo (x : i32 , y : i32 ) -> i32 { // body elided } // call it let z = foo ( 5 , 6 ); In programming language jargon, we call x and y parameters and 5 and 6 arguments . Rust does not have many fancy features related to parameters and…

0 views
danluu 3 weeks ago

There's no point at which turning your brain off will work

In early 2025, I started seeing people turn off their brain as they use LLMs 1 . They would have an LLM take an action (summarize text, write some code, etc.), and just assume that it worked 2 . This generally didn't work in early 2025 and the result was often quite silly. As LLMs have gotten better, I've seen more of this. Sometimes, people will try to get the LLM to write some code for them and basically just assume that it works 3 . Sometimes there's a human in the loop and, if the thing doesn't work, they'll ask the LLM to figure out the problem and solve it. Niklas Gruhn calls some variants of doing this being a meat proxy . 4 Being a for loop meat proxy works better than it did in early 2025 and the software I've tried that's developed like this sometimes actually sort of works. Not well enough that I'd want to use it or that it's successful , but I'm impressed at how effective being a meat proxy is in September 2026. You could imagine LLMs improving enough that brain-off meat-proxy development produces average quality software in the foreseeable future, or even that LLMs improve enough that they produce great software without a human in the loop. Let's say that happens. What reason is there for the company to employ the meat proxy? The company can just run the LLM in a loop and lay off the employee. There's no point at which this methodology will work for the employee 5 . Thanks to Max Bittker, Yossi Kreinin, Luke Burton, Thomas Dullien, Dennis Snell, Peter Geoghegan, and Jamie Brandon for comments/corrections/discussion. Luke Burton had this comment: I think being able to do this says more about the type of work being done than people think. I will only walk away from work like this if the task is quite low value, if it can afford to fail. For high value tasks, the probability of an LLM one-shotting them is much lower. I have to assume the role of QA, engineering manager, and architect. The while loop often feels like a crunch time. I feel the nagging suspicion I've missed something and that a badly specified prompt could result in an architectural choice that needs to be undone. Another observation is that the high throughput causes me to raise my own bar for what I ship. Whereas before I might have shipped an MVP and iterated, now I have agents polish and explore edge cases well beyond my norm, which they invariably fail to do unless prompted. Maybe it raises some uncomfortable thoughts for people, but my question for the meat proxies out there if the agents are nailing it so easily: 1) is it possible you've been coasting a bit already? 2) why aren't you pushing agents well beyond tasks they can tackle so easily? We've been doing something you'd think is extremely amenable to "hands off" automation, which is converting [redacted] to build with Bazel. It has taken us months even with agents. There's a lot of intangible, hard-to-specify requirements buried inside this task and having agents walk that line means constant supervision. Giving them a prompt like "convert this to Bazel" and walking away is at minimum many months in the future, maybe years, and maybe not ever? There are too many decision points, and too many unknown unknowns involved. Like how often does this scenario come up: you encounter some code and it's not clear why it functions this way, but knowing that materially changes what course of action you should take. Maybe it changes the dev experience, maybe you don't know if some customer has started using it, so on and so forth. How exactly do you meat proxy your way through that? Conversely you review what you've done with some stakeholder and they say "oh that? that part of it wasn't needed, we aren't even using that any more". What kind of decisions got made around the false assumption that a certain element needed to be preserved? [End of Luke's comment, comment from me]. A place where it's more obvious you need to make decisions is when the agent runs into something that's out of distribution. A minor version of this was when we compared how well agents use different programming languages and agents were much worse at obscure languages, which they're trained on, just not as much as with mainstream languages. A more out of distirbution example is if you try to play a board game (especially a modern game and not one of the classical games like chess or go). In general, for a game like Lost Cities or Dominion, a SOTA model and harness is worse than a human who's reasonable at board games but has never played the game before. If you ask the agent about the game, it knows a lot about the game and can say things that sound like they make sense to someone who doesn't understand the game, but are obviously wrong to anyone who does understand the game. I recently played some Dominion with a new player who thought that using ChatGPT to help them understand the game would help them learn and play the game. I was quite skeptical of this and suggested that it will probably make them worse (which, AFAICT, it did). After playing a few games, I looked at what ChatGPT was telling them, and it was maybe half right and half wrong, but the half wrong parts were steering them to a worse place than someone who generally plays games well and uses general game playing heurisitics would do. BTW, there's enough public information out there that I think that someone who'd never played before, but decided to spend, say, five hours reading about the game and seeing what information is out there, could easily be 99%-ile or above at the game if they did some pre-reading (maybe 30 minutes if using references while playing is allowed). I think that would be un-fun and I wouldn't recommend that anyone do it, but given that agents can do searches, query APIs, etc., it shows you the gap between a human and an agent today when approaching an out of distribution problem. For all I know, the next big model release will flip this around, but the gap is still fairly large today. Anyway, my point here is that, even when doing coding tasks, you often run into out of distribution questions where the agent behaves very poorly compared to a reasonable human being. If you want a good overall result today, you need to notice these cases and deal with them. Some examples of what goes wrong when someone just assumes things will work are this case , where agents (sometimes) heavily overfit to tests or this case where agents heavily overfit to a metric. I've heard a theory that agents do more cheating on eval-shaped problems. I'm not sure that's true, but even assuming it's true and that, in my work and personal projects, I tend to create more eval-shaped instructions than most people even when not running evals, I've seen other people who don't create very eval-shaped things run into the same problem (I think actually more severely) when they write some instructions and let agents go wild without supervision (I've had luck doing that with minimal supervision, but only by fencing the agents in quite a bit, which makes the thing more eval-shaped than what most people seem to do). When I try software from people who've outsourced thinking to the LLM, the software has serious issues. I've had people tell me this kind of thing works, but the software is often at a level where I would say that it doesn't work according to the standard discussed here . To pick a silly example, I saw that a programming thought leader declared on Twitter that programming is solved because they tried projects in all sorts of (programming) fields and Claude was able to solve all the problems as well as an expert. I went and actually looked at their GitHub and all of the examples I looked at (a non-zero number) either didn't work or worked very badly. I actually ran across this when I was making board game AIs and was looking for existing AIs for my AIs to play against. Their AI was an AlphaZero-style bot that was weaker than what you get if you prompt an LLM to write a simple minimax heuristic bot and then have the LLM run in a loop for a bit to tweak the heuristic scoring (which, for this game, should get demolished by a mediocre AlphaZero-style bot). To pick another silly example, following the standard flow (of a real commercial product) put you into an infinite loop where it was technically possible to escape (most programmers could probably figure out how to escape) but a typical user (for this software that wasn't aimed at programmers) was probably not going to be able to escape and actually use the main functionality of the software. BTW, I make plenty of software for myself that's "works for me" quality software that I would rate as "basically doesn't work" if it was an actual product, so I don't think it's inherently bad when software basically doesn't work (for example, the regex engine discussed here I had an agent build to speed up ripgrep searches on my computer or this Rust interpreter I had an agent build to speed up the agent iteration loop on some projects, both of which you shouldn't use). I also mentioned here that I find it quite valuable to have an agent run in a loop for data analysis, producing completely incorrect results that I then direct it to fix up. But there's a difference between making software for yourself that works for your narrow use case that you know doesn't work if you "hold it wrong" or producing work that you know is incorrect that you fix up, and declaring that programming is solved after writing a bunch of software that doesn't work, or likewise putting something of that quality into a commercial product. On reading a draft of this post, when I asked if this short set of thoughts was worth publishing, Thomas Dullien (a.k.a. Halvarflake) said, "Good post! Yes, publish it, because whenever I say "LLMs don't solve all programming problems" ppl look at me like I'm crazy, and I look at them like they are". And, coincidentally, after I finished this draft, I saw that Gary Bernhardt tweeted, "It's so surreal to contrast actual agent output with the things that I see people say about them here. In everyday changes, my reviews often cut the diff to 25% of its original size. Tons of useless tests; paranoia; inverted logic. Then I read Twitter and 'coding is solved'", and then "An example in the hour since I tweeted that: I told it to fix some DATABASE_URL management. It added s directly inside NPM scripts, and a conditional node invocation in CI running an inline JS script. About 20 hunks in the diff. After I corrected it: +0 lines, +1 word." I think anyone with Thomas's attitude or Gary's attitude towards software will have felt this way for some time. For a while, I wondered if a lot of the folks making the biggest claims about LLM productivity were somehow getting much more value out of LLMs than I've seen from anyone I know, but as we discussed here , as more evidence has come in, I've gotten more sure that it's just that people are fooling themselves. One thing I like about the board game example is that you can just measure how good the resultant AI is. At the limit, you can have some kind of rock-paper-scissors situation where you observe but, if something is just AI nonsense, this is pretty obvious in an objective way. And likewise for commercial software, where you can talk to people at the company or look at the data yourself and find out that conversion rate is poor, churn is very high, user satisfaction surveys report very high levels of dissatisfaction, etc. Maybe this works for founders, large shareholders, etc., but when I've personally seen people do this so far, it's been employees at work or people working on personal projects who are making a statement about how well this works, software is solved, etc., implying something about how software is a solved problem for employed software engineers. Another argument might be something like "we're all going to be obsolete now, so why not just give up?", but this argument seems backwards to me unless it's guaranteed that human obsolescence is very close. If you're financially ready to retire, you can just turn your brain off, but you could always do that and there's always been plenty of peole who've mailed it in and not done much of anything. If you're not ready to retire, you can do something that will make you more money, which will probably not involve turning your brain off. If the non-obsolescence future, there's no particular need to rush to make money, but if you think obsolescence is coming soon and you need to make money, then now's the time to rush to make money and do the opposite of turning your brain off. There's a line of reasoning that's something like "why bother working hard or doing the right thing, you don't get paid more anyway", which seems quite wrong to me, in that I've gotten raises, gotten bonuses, etc. from going and finding a problems and fixing them and this is also the experience of my friends, unless they're in a very dysfunctional place that doesn't reward doing good work at all, in which case they leave and go somewhere else. Maybe there's another argument that's something like "due to inertia, there will be a window where you can get away with being a meat proxy after LLMs are good enough to replace programmers". Realsitically, with how excited companies are to lay people, this also seems to be the opposite of correct, in that if your goal is to do as little work as possible, the best time to do this was in the past. If you've talked to people at big companies about this kind of thing, there are all sorts of stores about people literally not showing up to work at all (and also not working remotely) and it taking months to years to fire them. I haven't heard as many of these stories for the last couple years, but someone on a team I was on did this and, IIRC (I used to know the number, but I'm not I'm not sure I'm remembering it correctly now) it took six months to fire them after they decided to retire and figured they could collect a few more paychecks if they just stop showing up (this was pre-pandemic at a non-remote company). A friend of mine at a different company had someone do this where it took two years. No one even started the process of firing them for quite some time, and then there was some slow process of escalating warnings before they were finally fried. My friend said that the manager said that, had they wanted to game the system and just started coming in and pretending to work a bit, this would've started a new clock and it would've taken even longer to fire them. If they were a competent slacker, they could've kept the job indefinitely as conditions were at the time. I'm not sure why companies were ever in this state, but companies seem be using AI as an excuse to move away from this state, making now and the likely near future the worst time in a very long time to try to hold down a job while not putting any effort and not providing any value. No doubt there will be some companies where you can get away with this, but if you wanted to do nothing and collect a pay check, you could've been doing that for a long time (maybe don't go full monty and literally never show up to work at all) in an environment where it's easier than it will be in the near future. I've been having this thought for about a year and a half now. I have it more frequently now as LLMs get better and I see people spend more time turning their brain off when interacting with LLMs. [return] Luke Burton had this comment: I think being able to do this says more about the type of work being done than people think. I will only walk away from work like this if the task is quite low value, if it can afford to fail. For high value tasks, the probability of an LLM one-shotting them is much lower. I have to assume the role of QA, engineering manager, and architect. The while loop often feels like a crunch time. I feel the nagging suspicion I've missed something and that a badly specified prompt could result in an architectural choice that needs to be undone. Another observation is that the high throughput causes me to raise my own bar for what I ship. Whereas before I might have shipped an MVP and iterated, now I have agents polish and explore edge cases well beyond my norm, which they invariably fail to do unless prompted. Maybe it raises some uncomfortable thoughts for people, but my question for the meat proxies out there if the agents are nailing it so easily: 1) is it possible you've been coasting a bit already? 2) why aren't you pushing agents well beyond tasks they can tackle so easily? We've been doing something you'd think is extremely amenable to "hands off" automation, which is converting [redacted] to build with Bazel. It has taken us months even with agents. There's a lot of intangible, hard-to-specify requirements buried inside this task and having agents walk that line means constant supervision. Giving them a prompt like "convert this to Bazel" and walking away is at minimum many months in the future, maybe years, and maybe not ever? There are too many decision points, and too many unknown unknowns involved. Like how often does this scenario come up: you encounter some code and it's not clear why it functions this way, but knowing that materially changes what course of action you should take. Maybe it changes the dev experience, maybe you don't know if some customer has started using it, so on and so forth. How exactly do you meat proxy your way through that? Conversely you review what you've done with some stakeholder and they say "oh that? that part of it wasn't needed, we aren't even using that any more". What kind of decisions got made around the false assumption that a certain element needed to be preserved? [End of Luke's comment, comment from me]. A place where it's more obvious you need to make decisions is when the agent runs into something that's out of distribution. A minor version of this was when we compared how well agents use different programming languages and agents were much worse at obscure languages, which they're trained on, just not as much as with mainstream languages. A more out of distirbution example is if you try to play a board game (especially a modern game and not one of the classical games like chess or go). In general, for a game like Lost Cities or Dominion, a SOTA model and harness is worse than a human who's reasonable at board games but has never played the game before. If you ask the agent about the game, it knows a lot about the game and can say things that sound like they make sense to someone who doesn't understand the game, but are obviously wrong to anyone who does understand the game. I recently played some Dominion with a new player who thought that using ChatGPT to help them understand the game would help them learn and play the game. I was quite skeptical of this and suggested that it will probably make them worse (which, AFAICT, it did). After playing a few games, I looked at what ChatGPT was telling them, and it was maybe half right and half wrong, but the half wrong parts were steering them to a worse place than someone who generally plays games well and uses general game playing heurisitics would do. BTW, there's enough public information out there that I think that someone who'd never played before, but decided to spend, say, five hours reading about the game and seeing what information is out there, could easily be 99%-ile or above at the game if they did some pre-reading (maybe 30 minutes if using references while playing is allowed). I think that would be un-fun and I wouldn't recommend that anyone do it, but given that agents can do searches, query APIs, etc., it shows you the gap between a human and an agent today when approaching an out of distribution problem. For all I know, the next big model release will flip this around, but the gap is still fairly large today. Anyway, my point here is that, even when doing coding tasks, you often run into out of distribution questions where the agent behaves very poorly compared to a reasonable human being. If you want a good overall result today, you need to notice these cases and deal with them. [return] Some examples of what goes wrong when someone just assumes things will work are this case , where agents (sometimes) heavily overfit to tests or this case where agents heavily overfit to a metric. I've heard a theory that agents do more cheating on eval-shaped problems. I'm not sure that's true, but even assuming it's true and that, in my work and personal projects, I tend to create more eval-shaped instructions than most people even when not running evals, I've seen other people who don't create very eval-shaped things run into the same problem (I think actually more severely) when they write some instructions and let agents go wild without supervision (I've had luck doing that with minimal supervision, but only by fencing the agents in quite a bit, which makes the thing more eval-shaped than what most people seem to do). When I try software from people who've outsourced thinking to the LLM, the software has serious issues. I've had people tell me this kind of thing works, but the software is often at a level where I would say that it doesn't work according to the standard discussed here . To pick a silly example, I saw that a programming thought leader declared on Twitter that programming is solved because they tried projects in all sorts of (programming) fields and Claude was able to solve all the problems as well as an expert. I went and actually looked at their GitHub and all of the examples I looked at (a non-zero number) either didn't work or worked very badly. I actually ran across this when I was making board game AIs and was looking for existing AIs for my AIs to play against. Their AI was an AlphaZero-style bot that was weaker than what you get if you prompt an LLM to write a simple minimax heuristic bot and then have the LLM run in a loop for a bit to tweak the heuristic scoring (which, for this game, should get demolished by a mediocre AlphaZero-style bot). To pick another silly example, following the standard flow (of a real commercial product) put you into an infinite loop where it was technically possible to escape (most programmers could probably figure out how to escape) but a typical user (for this software that wasn't aimed at programmers) was probably not going to be able to escape and actually use the main functionality of the software. BTW, I make plenty of software for myself that's "works for me" quality software that I would rate as "basically doesn't work" if it was an actual product, so I don't think it's inherently bad when software basically doesn't work (for example, the regex engine discussed here I had an agent build to speed up ripgrep searches on my computer or this Rust interpreter I had an agent build to speed up the agent iteration loop on some projects, both of which you shouldn't use). I also mentioned here that I find it quite valuable to have an agent run in a loop for data analysis, producing completely incorrect results that I then direct it to fix up. But there's a difference between making software for yourself that works for your narrow use case that you know doesn't work if you "hold it wrong" or producing work that you know is incorrect that you fix up, and declaring that programming is solved after writing a bunch of software that doesn't work, or likewise putting something of that quality into a commercial product. On reading a draft of this post, when I asked if this short set of thoughts was worth publishing, Thomas Dullien (a.k.a. Halvarflake) said, "Good post! Yes, publish it, because whenever I say "LLMs don't solve all programming problems" ppl look at me like I'm crazy, and I look at them like they are". And, coincidentally, after I finished this draft, I saw that Gary Bernhardt tweeted, "It's so surreal to contrast actual agent output with the things that I see people say about them here. In everyday changes, my reviews often cut the diff to 25% of its original size. Tons of useless tests; paranoia; inverted logic. Then I read Twitter and 'coding is solved'", and then "An example in the hour since I tweeted that: I told it to fix some DATABASE_URL management. It added s directly inside NPM scripts, and a conditional node invocation in CI running an inline JS script. About 20 hunks in the diff. After I corrected it: +0 lines, +1 word." I think anyone with Thomas's attitude or Gary's attitude towards software will have felt this way for some time. For a while, I wondered if a lot of the folks making the biggest claims about LLM productivity were somehow getting much more value out of LLMs than I've seen from anyone I know, but as we discussed here , as more evidence has come in, I've gotten more sure that it's just that people are fooling themselves. One thing I like about the board game example is that you can just measure how good the resultant AI is. At the limit, you can have some kind of rock-paper-scissors situation where you observe but, if something is just AI nonsense, this is pretty obvious in an objective way. And likewise for commercial software, where you can talk to people at the company or look at the data yourself and find out that conversion rate is poor, churn is very high, user satisfaction surveys report very high levels of dissatisfaction, etc. [return] In his post, he technically doesn't mention the case where the person basically acts as a while loop or a for loop, but that behavior, which I'm increasingly seeing, is also in the spirit of the post. [return] Maybe this works for founders, large shareholders, etc., but when I've personally seen people do this so far, it's been employees at work or people working on personal projects who are making a statement about how well this works, software is solved, etc., implying something about how software is a solved problem for employed software engineers. Another argument might be something like "we're all going to be obsolete now, so why not just give up?", but this argument seems backwards to me unless it's guaranteed that human obsolescence is very close. If you're financially ready to retire, you can just turn your brain off, but you could always do that and there's always been plenty of peole who've mailed it in and not done much of anything. If you're not ready to retire, you can do something that will make you more money, which will probably not involve turning your brain off. If the non-obsolescence future, there's no particular need to rush to make money, but if you think obsolescence is coming soon and you need to make money, then now's the time to rush to make money and do the opposite of turning your brain off. There's a line of reasoning that's something like "why bother working hard or doing the right thing, you don't get paid more anyway", which seems quite wrong to me, in that I've gotten raises, gotten bonuses, etc. from going and finding a problems and fixing them and this is also the experience of my friends, unless they're in a very dysfunctional place that doesn't reward doing good work at all, in which case they leave and go somewhere else. Maybe there's another argument that's something like "due to inertia, there will be a window where you can get away with being a meat proxy after LLMs are good enough to replace programmers". Realsitically, with how excited companies are to lay people, this also seems to be the opposite of correct, in that if your goal is to do as little work as possible, the best time to do this was in the past. If you've talked to people at big companies about this kind of thing, there are all sorts of stores about people literally not showing up to work at all (and also not working remotely) and it taking months to years to fire them. I haven't heard as many of these stories for the last couple years, but someone on a team I was on did this and, IIRC (I used to know the number, but I'm not I'm not sure I'm remembering it correctly now) it took six months to fire them after they decided to retire and figured they could collect a few more paychecks if they just stop showing up (this was pre-pandemic at a non-remote company). A friend of mine at a different company had someone do this where it took two years. No one even started the process of firing them for quite some time, and then there was some slow process of escalating warnings before they were finally fried. My friend said that the manager said that, had they wanted to game the system and just started coming in and pretending to work a bit, this would've started a new clock and it would've taken even longer to fire them. If they were a competent slacker, they could've kept the job indefinitely as conditions were at the time. I'm not sure why companies were ever in this state, but companies seem be using AI as an excuse to move away from this state, making now and the likely near future the worst time in a very long time to try to hold down a job while not putting any effort and not providing any value. No doubt there will be some companies where you can get away with this, but if you wanted to do nothing and collect a pay check, you could've been doing that for a long time (maybe don't go full monty and literally never show up to work at all) in an environment where it's easier than it will be in the near future. [return]

0 views
Matthias Endler 1 months ago

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 months 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 months 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
Alex White's Blog 1 months ago

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 months 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 1 months 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