Blog Postshttps://donsz.nl/rss.xmlJana Dönszelmann's blog postsen60Optimizing the new trait solverhttps://donsz.nl/blog/new-solver-performancehttps://donsz.nl/blog/new-solver-performanceFri, 07 Aug 2026 00:00:00 +0000 Optimizing the new trait solver
Press for table of Contents

This is a more detailed writeup of all the changes we made in the past months to optimize the performance of the next trait solver. Basically, anything that didn't fit in the blog post on https://blog.rust-lang.org/2026/08/21/enabling-next-solver-on-nightly/.

In April, we started tracking the performance of the new solver better by adding more benchmarks to our performance benchmarking infrastructure that used the new solver. Importantly, that way we could also start better tracking the performance over time.

One spectacular example is thet wg-grammar benchmark, which used a lot of opaque types in a way that caused exponential slowdowns. The solution here wasn't micro-optimizations, but algorithmic changes to increase the likelyhood of cache hits.

A plot of the wg-grammar benchmark from perf.rust-lang.org between may and august 2026

The y-axis here shows billion (G for giga) instructions executed while compiling the benchmark.

We run these benchmarks on every single merge into rust's main branch, providing valuable statistics. However, this set of benchmarks will always be slightly synthetic. So Rémy Rakic (aka lqd) started doing a crater-like run, but for performance.

To do so, Rémy downloaded the 20 000 most downloaded crates from crates.io, and measured how much slower the new solver compiled it compared to the old solver. A process which took multiple weeks of compute time. Based on these results, several people started addressing the outliers.

To create the graphs in this document, we took a sample of 1 000 out of these 20 000 crates, and followed them over time.

To show what happened when we started looking more into these outliers, let's look again at the graph from the main blog post:

The performance of 1000 crates (on the x axis) plotted against their slowdown factor (logarithmic) on the y axis. Many crates around the 1.0 mark (no slowdown), with at both ends major outliers up and down. Colors show progression over time.

note that all graphs in this document have a logarithmic y axis!

Each vertical slice in this plot shows a single crate. Each slice ideally contains 4 datapoints (this is not always true when in an older version a crate didn't compile at all with the new solver). Each red dot represents the performance at the end of May, Green at the end of June, Cyan at the end of July, and light blue is right now.

So you can see that on the left, there are some red dots between 4x and 16x. That means that in June, there were crates that were 4 to 16x slower with the new solver than they were to compile with the old solver. In this range, 4x to 16x, we only see red and green dots, because a lot of these crates became a lot faster! The first blue dot can be found between 2x and 4x. Still bad, but much better.

More interesting to me, is the fact that on the right, we also see a lot of dots above the 1x line. So, by optimizing the very slow crates, we accidentally made them not just as fast as the old solver, but in many cases much faster.

Finally, there are a large number of crates (over 900 of the 1 000) for which the blue dot lies between 0.8 and 1.2. In other words, most crates are barely affected by the change in trait solvers.

Generating these benchmarks

These benchmarks show instruction counts (not wall time), measured with perf. They involve 3 separate cargo commands, because I'm specifically interested in the root crate's performance, not its dependencies. So for a crate named mrow, it would look something like this:

cargo check
cargo clean -p mrow
perf ... -- cargo check

The cargo clean in the middle deletes only the specific crate given, in our case the root crate mrow. On each compiler version, on each crate, I'd then run that sequence of commands twice. Once with -Znext-solver=coherence and once with -Znext-solver=globally to see the difference in instruction counts.

See my code to gather the data here: https://git.donsz.nl/jana/new-solver-perf And no, I won't show my plotting code. It's the ugliest python I've ever written. Oh my gods. You'd die reading it. And I've written some ugly python code in the past

Change from last week

The graph above was generated with the newest datapoints on nightly-2026-08-16. However, I've been running the benchmark script almost every day this past week. Looking at a similar graph from last week (nightly-2026-08-09), a lot of changes can be seen:

The same graph as before, but generated with a nightly from last week.

The older graph's line of points has a slightly smoother bend in the bottom-left, but that's just a funny coincidence I think. The change I'm interested here is that fewer points in the middle (where the line is around 1.0x) have older datapoints (red, green, cyan) above them. In this old graph, some crates are not as optimized yet, and are sorted towards the left, where in the new graph these points are sorted towards the center of the graph (i.e. their current performance is closer to 1.0x and closer to all the other crates).

Zoomed in around 0.5x - 2x

As requested on lobsters, here's a zoomed-in version from between y=0.5 to y=2.0. Before you ask, the ticks inbetween are √2 and 1/√2 because that's what happens to equal spacing on log plots :3

The same scatter plot, but zoomed in between 0.5x and 2x

Individual sorting

Several people who I showed the above graphs asked me if I could make a small change, sorting all datapoints.

The same graph as before, but the sorting of datapoints happens independently per compiler version

I find this slightly less informative, since you can't see the per-crate progression as easily. But it does show the total progression over time better.

Crates as lines

Finally, I'd like to show you the very first way I tried visualizing this data, by following the progression of individual crates as lines.

Line plot of 1 000 crates tracked at 4 points in the past two months, showing the ratio between old and new solver performance.

The result, I admit, is a little messy. That's a lot of lines! However, there are interesting things to learn here anyway:

  • A couple crates used to be extremely slow two months ago (up to 16x slower with the new solver than they were with the old solver), and many of those are much closer to 1x now, even if not fully mitigated. These are the cases that we've been caring a lot about recently.
  • The density of crates between 1.5x and 4x slower has gone down significantly (from 42 to 24 of the 1 000 in this plot).
  • A lot of crates (more than 900 of the 1 000 in this plot) hover between 0.8 and 1.2x slower, and are roughly stable there. That means many crates have similar performance with the old and new solver.
  • A couple of outliers are massively faster with the new solver! Those are all the lines below 1x.

We didn't label any crates in any of these graphs, but some of the outliers that might be interesting are:

  • typenum-1.20.1 (consistently around 2.1x slower than the old solver).
  • nacl-0.5.3 (which we brought from 4.5x slower to only 1.7x slower than the old solver).
  • nom-8.0.0 which went from 6.2x slower to only 1.6x slower.
  • datafusion-0.5.3 which is one of the crates that is almost 4x faster under the new solver, one of the lines far below 1x.
  • astrology-3.0.3 is a crate that started at 4.1x slower, and over the past months went down to being 20% faster than the old solver.

A short list of performance issues resolved in the past months

Note, the explanations in this section are majorly simplified to hopefully be interesting to a larger audience.

TypingMode::Erased

See #155443, #156246, #157910,

Queries to the trait solver involving opaque types (return-position impl trait), often result in cache misses. To mitigate, we pre-process such queries, stripping out all mentions of opaque types, noting down whether the evaluation of the query actually ever uses the information of these opaque types. If it turns out we don't, the result without opaques is valid, and the cache entry is more general. If the opaque types were relevant, we will retry such queries with the original opaque types.

Since we now sometimes do double work, there are cases in which this is a slowdown. However, much more often than not it helps with performance.

Quadratic slowdowns in unic-ucd-name

In all the graphs above, one datapoint is purposefully omitted. The crate unic-ucd-name used to be 214x slower with the new solver than with the old solver... It simply didn't fit on the graph. After #160982 it's now at 1.07x slower, which is much more acceptable. The culprit was code like this (minimized by mira):

pub const A: &[&[&str]] = &[
    &["A"],
    &["A"],
    &["A"],
    // ...
    &["A"],
    &["A"],
];

With the new solver, this code would scale quadratically in the number of items in the outer slice. And unic-ucd-name has a slice of tens of thousands of these (enumerating unicode codepoints). The problem was that the new solver can sometimes cause lifetime "infer vars" (like type variables but for lifetimes) to be unified. And, somwhere in NLL code, we'd call into the trait solver, and then undo all changes to the lifetime inference storage (a union-find datastructure). This undoing would iterate over every lifetime in the storage. However, there's also a flag: if no lifetimes were touched, we'd skip the iteration. The old solver wouldn't touch lifetimes, so would never do this iteration. Since we run this code once for each slice in the example above, and each time we undid an operation we'd iterate over N lifetime variables (two per slice, also one for the string inside), we get quadratic performance.

These kinds of situations occur frequently, where we change something in the new solver, that has a cascading effect into unrelated code that suddenly increases the computational complexity. The above change of adding TypingMode::Erased has a similar (exponential) speedup effect on some crates.

Less re-evaluation of stalled queries

Query evaluation is a fixpoint loop. We evaluate a list of queries, and find out that some cannot be evaluated yet. We say that those queries are stalled, until some progress is made on another query. After we've attempted to make progress on all queries, we could naively restart at the start of the list, retrying all queries to see if they can now make more progress.

However, a lot of work went into not unnecessarily retrying queries when we already know they cannot make further progress. For each query, we keep some metadata, trying to remember what they are stalled on. We then only retry a query if something changes that can actually affect it.

Time invested in optimizing this stalling process turned out to give major gains:

  • #156187 skips more goals by ensuring more up-to-date information is used
  • #160479 micro-optimizes allocation in the fixpoint loop, streamlining the hottest paths as much as possible
  • #158042 the check to abort queries that are stalled lived at the start of one of the hottest functions in the solver, but also one of the biggest functions. LLVM dealt with this poorly, not properly optimizing the fast path independently from this big function with lots of logic. By splitting it into a smaller function handling the fast path, and a larger function for the slow path lots of performance was gained.
  • #160886 and #158436 making the datastructure that contains metadata about stalled queries smaller.

Note that this list is far from complete. A lot of micro-optimization was also done which are harder to "explain" or give context for. See https://github.com/rust-lang/goals/issues/113 for a complete list of work.

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
It's the people that matterhttps://donsz.nl/blog/externally-implementable-itemshttps://donsz.nl/blog/externally-implementable-itemsMon, 16 Mar 2026 00:00:00 +0000 It's the people that matter
Press for table of Contents

Over the past two years, I've been involved with designing and implementing a new feature of rustc, called "externally implementable items". This is not a finished feature, though if you'd like you can try it on nightly already! This is the story of how externally implementable items were invented, implemented, and how some day they might be stabilized. I'm sure that is interesting to some of those who read this.

That's not the main thing this blog post is about, though. Instead, I'm using it as an example, to show you how the Rust project operates. To show how many people are involved with a change, how they learn from each other, how I've learned so much from them myself. An example of how important social interaction is to open source. Much more than writing the code itself.

This is a written-out version of a talk I gave at Rust in Paris 2026.

The problem

This story starts, almost two years ago, in the days after RustNL 2024. An RFC was opened by m-ou-se's github avatar m-ou-se called Externally Implementable Functions, which was actively being discussed. The idea is as follows:

In Rust, the ordering of modules doesn't matter. As long as a function is visible enough (using pub qualifiers), any function can call any other function, regardless of definition order or location in the module tree. Though it's probably not a great idea to call into this code, the following code does compiles in rust:

fn foo() {
    bar();
}

fn bar() {
    foo();
}

That's not the way that, for example, C works. In C, the global scope is not order-independent, and declarations are processed top-top-bottom. That means that you can only call a function after it's declared. The way to work around this is by using what's called a forward declaration: you can declare the type of a function first, without giving a definition. Then, other functions can use the declaration as a promise that a definition of a certain type will exist somewhere, and finally the definition can be given satisfying the declaration.

In C, you'd have to write the following to achieve something similar to the earlier Rust example:

// forward declaration, no body
void bar();

void foo() {
    bar(); // use the forward declaration
}

// definition, with a body
void bar() {
    foo();
}

In C it is common to group such forward declarations in header files, but in Rust we don't need header files within a single crate, because the global scope is order-independent.

While rust's global scope is order-independent, the crate graph is not, which instead must be a directed acyclic graph (DAG). Oops! Now we actually have the exact same problem as C does. You might imagine that a crate would want to "forward declare" a function, which another crate can implement. But in Rust we currently can't, and that's what "Externally Implementable Functions" solves, by introducing an extern impl fn:

// In a crate called `log`
extern impl fn logger() -> Logger;

These can be implemented in another crate using:

// In a dependent of `log`
impl fn log::logger() -> Logger {
    Logger::to_stdout().with_colors()
}

This proposal caused a lot of opinions. As RFCs sometimes tend to do, what started as a Request For Comments turned into Rustaceans Commenting Forever. Both on the RFC PR and in-person, since so many of us were together at the conference. Only a few days later, this discussion led to another RFC to be opened, called Externally Definable Statics. It proposes introducing an extern static. This would be equally powerful, since a static can contain a function pointer, or alternatively just a struct with methods on it:

// In a crate called `log`
extern static LOGGER: Logger;

Another crate can then assign to this static:

// In a dependent of `log`
impl static log::LOGGER = Logger::to_stdout().with_colors();

When a crate defines externally implementable items, either in the form of statics or functions, you could see this as if a crate exports a trait, with several associated items. A downstream crate has to implement that trait for compilation to succeed. Both of the proposals so far even use the same keyword, impl. So, after even more discussion, a third proposal was made, in yet another RFC called Externally Implementable Traits, where a crate can define an extern trait;

// In a crate called `log`
extern trait GlobalLogger {
    fn logger();
}

Which another crate can then implement:

extern impl log::GlobalLogger {
    fn logger() -> Logger {
        Logger::to_stdout().with_colors()
    }
}

What this gains us is that it allows externally implementable items to be grouped together.

Avoiding bikeshedding

At this point, I think the discussion saturated a little. None of the three proposals is obviously better, with most of the differences being syntactic. And without an actual implementation, we were mostly bikeshedding, which wouldn't get us anywhere. So in the summer of 2024, to sidestep that discussion completely, m-ou-se's github avatar m-ou-se and I sat down, came up with a fourth implementation. Yes, yes, competing standards and all that.

But I don't think requesting more comments at that point were helpful. If we had an implementation to play around with we could have a much more meaningful discussion, instead of speculating about arbitrary trade-offs.

Going forward with option 4

If you didn't know, there are actually already two externally implementable items in Rust. If you've done embedded development before you'll have certainly encountered them. These are the panic handler, and the global allocator.

Every program in Rust must have a panic handler, which is the function that gets executed when a program panics. panic!() is defined in core, but core doesn't necessarily know what to do when a panic happens. Instead, one crate in the crate graph must provide a panic handler to core, marked with the #[panic_handler] attribute, like this:

#[panic_handler]
fn my_panic_handler(pi: &PanicInfo) -> ! {
    loop {}
}

If you've done embedded programming you may have seen this, though usually the implementation of the panic handler is given by std. There's one thing that core does know however, which is the function signature that the panic handler will have.

So what happens is that any code that panics, will call a function with predetermined linker symbol. When rustc finds a function that's annotated with #[panic_handler], it checks that the signature is correct, and that no other panic handler already exists. Then, it generates that function with that same known linker symbol. Finally, the linker sees the two symbols, and makes sure that the final program runs.

So in a way, core is using a forward declaration of the panic handler.

The allocator works similarly, with the #[global_allocator] attribute. This attribute actually corresponds with four individual "externally implementable functions" that also get known linker symbols:

fn __rust_alloc(size: usize, align: usize) -> *mut u8;
fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize);
fn __rust_realloc(ptr: *mut u8, old_size: usize, align: usize, new_size: usize) -> *mut u8;
fn __rust_alloc_zeroed(size: usize, align: usize) -> *mut u8;

Both the panic handler and global allocator have their own, dedicated, code paths in the compiler. So the way we started implementing Externally Implementable Items (which I will from now on call EIIs), was by generalizing these existing two code paths.

What this gains us is twofold: we have less complexity spread throughout the compiler, and the declaration of these EIIs becomes explicit in core. Instead of being compiler magic, we can document the signature, refer to it by name. That last point is important actually, by making EIIs participate in name resolution we can properly mangle their symbol names. They no longer have to be predetermined.

The design

An externally implementable function, is a function signature without a body, plus an attribute that marks another function that provides the body. In the case of the panic handler, core would write:

#[eii] // <- mark this signature as externally implementable
fn panic_handler(_: &PanicInfo) -> !;

And now other code can write:

#[core::panic::panic_handler]
fn my_panic_handler(_: &PanicInfo) -> ! {
    loop {}
}

What's neat about this is that it's backwards compatible with the existing panic handler attribute (we would add #[panic_handler] to the prelude, i.e. imported from core by default).

#[eii] itself expands into two parts, a foreign function and an attribute macro:

unsafe extern "Rust" {
    safe fn panic_handler(_: &PanicInfo) -> !;
}

macro panic_handler() { /* compiler generated */ }

This macro syntax might be unfamiliar to you. It's an unstable alternative to macro_rules!, that has more sane name resolution behavior. That's nice since it's automatically generated by the #[eii] attribute. We use this syntax in some places in core and std as well, though there's a chance it will never be stabilized. We might never stabilize that syntax, but inside the compiler it gives us a consistent way to define attribute macros.

Rust allows the same name to be in scope multiple times, as long as they are in different namespaces. So, the fact that there's now a macro and a function with the same name is actually not a problem. When you call panic_handler, you call the foreign function, and when you write #[panic_handler], you refer to the macro. If you want to explicitly avoid the ambiguity though, we also support #[eii(macro_name)] to explicitly set the macro's name. For example:

#[eii(panic_handler)] // <-- still generates the `#[panic_handler]` attribute
fn call_to_panic(_: &PanicInfo) -> !;

This also makes it possible to re-export the macro and not the function, or the other way around.

Shaving a yak while we're at it

To name resolve EIIs properly, there's one last step missing: name resolution has to understand that this macro and foreign function belong together somehow. So, the actual expansion looks more like:

unsafe extern "Rust" {
    safe fn function_panic_handler(_: &PanicInfo) -> !;
}

#[eii_macro_for(function_panic_handler)] // link the two together
macro macro_panic_handler() { /* compiler generated */ }

It turns out, that implementing #[eii_macro_for()] is anything but simple. In fact, Rust somewhat fundamentally doesn't support doing name resolution inside an attribute. Attributes themselves can be resolved, but nothing in their contents, since the contents of attributes is represented as raw tokenstreams, similar to what proc macros get.

In Rust, name resolution works by giving every identifier an id, called a node id. We then construct a hashmap that links references to definitions, identified by these ids. Giving every token in a tokenstream a node id turned out to be completely infeasible. We'd need to completely redo how attributes were represented in the compiler.

So, naturally... that's what we did!

This attempt at avoiding RFC-bikeshedding turned into one of the larger yaks I've ever shaved. I've given a talk about this project at Eurorust in 2025 if you're interested.

The initial PR took about a week of implementation work, and then a month of convincing various people to actually go forward with this. That involved finding the right reviewers for this massive change; figuring out in what order to merge things since a single PR was much larger than could be asked from someone to review; convincing people that a rewrite would actually be good, since the initial PRs actually temporarily made the situation worse, which meant promising that I wouldn't just disappear after the initial PR merged.

At the time of writing, March 2026, we've finally converted the parsing infrastructure for all of rust's roughly 200 different built-in attributes over the course of 250 or so pull requests, created through the combined effort of more than 20 people. What's awesome, I think, is that this refactor was an amazing source of "good first issues", tasks that beginners could solve. Of those 20 people, several had never or rarely contributed to the rust compiler before. And through the attribute refactor, one of those ( jonathanbrouwer's github avatar jonathanbrouwer ) is now one of our newest compiler team members, starting their own projects in the rust compiler. I've seen other contributions from other first-time contributors as well, and I wouldn't be surprised if the compiler team will grow more as a result.

I'm not a big fan of large language models, for various reasons. Still, I'm sure some of these smaller issues could have been solved by an LLM. But clearly, that wouldn't have had the same effect here! Open source contribution isn't just about solving bugs. Naturally, people come and go at public projects like the rust project, and sometimes that creates problems where there's sometimes nobody around who knows how to work on some corner of the compiler. Therefore, to keep maintenance feasible, it's important that new people are constantly being taught about how to work on the compiler.

One of the moments where this happens is in pull request review, which I see as a learning opportunity for both the author and the reviewer. I really enjoyed being added to the review rotation of rustc, because it meant that I got to see some parts of the compiler I had never seen before. If the author is an LLM, or in large parts defers to LLMs, this becomes entirely one-sided. As a reviewer, you're not teaching a person, you're talking to an LLM. And it's the other way around too. If review often defers to an LLM, they don't learn either.

And sure, it's different when an author defers all interactions to an LLM, compared to when they just use it as a tool. Maybe to generate some boilerplate code, or to search and summarize something. But I truly believe that if we generate all our boilerplate code with an LLM, we stop investing in tools to reduce having to type boilerplate. And time and time again, it's been shown that you learn much more from summarizing yourself. That's true in general, but was also recently shown by the TU Eindhoven in the context of large language models.

I hope this story shows you that we can't operate like that. That's not how we can keep maintaining the Rust project, or any open source project for that matter. If we stop learning from each other, the world grinds to a halt. And that's what makes me more willing to invest my time knowing that another party is learning from it. Maybe that's why I liked being a teacher before I worked on rustc :3

Linking it all together

Externally implementable items are, fundamentally, a linker feature. In rust, rustc processes each crate one by one, passing down only a .rmeta file down to the next crates with metadata for crates that depend on it. This is why rust's crate graph has to be a DAG. There's no way to pass any information back up the graph.

Rust has to deal with this at various points. For example, with generics, rust does what's called monomorphization. It re-instantiates every generic function with its type parameters, and re-codegens it. Since an upstream crate doesn't yet know what instantiations will exist downstream, it cannot do codegen for generic functions. Thus, one thing .rmeta files contain is information on how to instantiate and codegen all generic functions of a crate.

EIIs are like forward declarations. An upstream crate has to assume a downstream one provides the item, and worst case it will only find out whether it was provided when linking it all together. To avoid linker errors, we pass information down the crate graph of whether we've already seen an implementation. If a crate sees that two different crates provide an EII, then we error out with a nice rust error, avoiding the linker error.

Defaults

This gets slightly trickier when you introduce EII-defaults, Let's use logging as an example again though; using an EII default would look like simply adding a body to the declaration:

#[eii]
fn logger() -> Logger {
    Logger::default()
}

If the user never implements the EII, the default is used. This is a feature we'd need for the global allocator. By default, rust uses the system allocator, unless user code overrides that.

What makes this harder is that there needs to be a point, at the end of compilation, where we decide "oh, we haven't seen an explicit implementation", and decide to compile the default function instead. At first I hoped this would be as easy as looking at the crate type, classifying them into "final" and "non-final". A crate that generates a binary is final for example, so when we're compiling a binary crate, and we haven't seen an explicit implementation yet, put the default in.

Now that breaks down when you consider that there are other crate types. For example, what do we do for a cdylib? Is it final? We do still intend to link it to other code, but not necessarily with the rust compiler. So if we don't insert the default, we might not get the chance again. But if we do, it might cause linker errors down the line.

Now this gets worse when you consider that Rust can sometimes, in one go, generate both an rlib and a cdylib. It is very hard to make it such that Rust inserts symbols in only one of the two. It's either both, or none.

Asking the linker, nicely

Now, so far, these are technically solvable challenges, with more engineering time. But during Rust Week 2025, I sat down with ojeda's github avatar ojeda , from Rust for Linux. They're using rustc a little bit like a C compiler: generating object files, which they then link themselves to C code from the kernel. Or, I think that's roughly what was going on. I also learned that this is a concern in other places, amanieu's github avatar amanieu told me google is doing things similarly? In fact, anytime you're using a custom external build system like bazel or buck2 that wants to be in charge of creating the the final binary, this strategy doesn't work. The important difference with Rust here is that rust likes to drive the linker, and so if an external build system does this driving it doesn't work as well!

So our only option seems to be to resolve EII defaults when linking. Fortunately, almost all linkers support what's called weak linker symbols. Weak symbols are simply de-prioritized by the linker, and when another non-weak symbol of the same name is found it is preferred. This avoids duplicate definition errors, and neatly matches the behavior of defaults for EIIs.

Several months later, I was talking to cramertj's github avatar cramertj . She was interested in how EIIs would interact with foreign function interfaces (FFI). Specifically compatibility with C++. We concluded that it would be extremely desirable if we can guarantee that EIIs desugar to weak linker symbols. That way, code written in C and C++ can easily interact with EIIs, even implement them from over an FFI, since all the resolution actually happens when the C and Rust code are linked together.

These are the kinds of discussions that make it so useful to have the various Rust conferences and to have an all-hands again in May (a couple of days where all the rust project members come together). Furthermore, I think that these stories are actually great news. Where a year earlier we were bikeshedding and speculating, now we are having focused discussions about requirements of the final implementation.

The road to stabilization

At the end of December, this version of EIIs, with weak symbols for defaults, merged into the main branch of the compiler. That means that it's available on nightly, just add #![feature(extern_item_impls)]. Though keep in mind that it currently only works on linux and macOS. The latter turned out to be a little trickier since Mach-O binaries support code for multiple targets in a single file.

Now that there is a base implementation, it's also much easier for people to contribute to the project going forward. In fact, there's a Tracking Issue on the rust-lang/rust repository to track what still needs to happen. And simply by communicating the list of TODOs clearly, just like before with attributes, people indeed started helping out again. And I've once again been enjoying that thoroughly, seeing people learn to contribute to new parts of the compiler.

To encourage that more, in December we restarted organising what's called the "compiler office hours". Because all the people that work on the compiler live spread-out all over the world, we don't end up talking to each other too much except in reviews. Unlike working together in an office, rust has no hallway where people talk about what they've been busy with today.

So now we have a call, twice a week, with whoever can make it. All kinds of people join, some I had never talked to before. Not the same people every week, but that's what makes it fun. Every time we meet, I learn a tiny bit of what people are up to, the things that people have been proud of working on, or are completely stuck on. Bits of progress, related to codegen backends, LTO, miri, reflection, the never type, automatic differentiation, pattern matching, rustdoc, or just Ben's pepper plants. Several of the discussions about how to continue with EIIs as a feature have taken place here.

Windows

For example, I've been chatting with bjorn3's github avatar bjorn3 somewhat regularly. His knowledge about compiling for Windows, and linkers and code generation in general has been absolutely invaluable. One of the issues that's still open with EIIs is how we handle Windows. On Windows, weak symbols are supported, but they work slightly differently than on other platforms.

Weak symbols have to be named explicitly in flags, passed to the linker. Giving those is mostly doable, since Windows allows you to pass linker flags in the headers of binaries. Except, LLVM can apparently sometimes further mangle the names of symbols. And the linker flag must exactly match the final, changed, name that LLVM will generate. So we will likely need to make Rust do the same mangling, and predict those symbol names.

Unfortunately, we will have to work this out before we can switch the global allocator to use EIIs. Though I might try and convert the panic handler to an EII soon-ish. The big difference with the panic handler is that it doesn't require any defaults.

Dynamic linking

Office hours is also where we first discussed dynamic linking. Rust's story around dynamic linking isn't great. In general, Rust can provide very few guarantees about the code that is dynamically linked in. In theory, some of that can be mitigated as long as the dynamic loading happens at program startup (when the program is loaded into memory). But especially with dlopen, it's easy to create unsoundness.

In fact, setting Rust's global allocator is in fact currently unsound in combination with dlopen. It becomes possible to allocate an object in one allocator using Box::new, pass it to the dlopened dynamic library, where it will be deallocated on a different allocator.

However, it came up, in the office hours, that it would be cool if we could use EIIs for rustc's query system. This is the abstraction layer that allows rust to do incremental compilation. And if EIIs don't support dynamic linking, that would be a problem, because the rust compiler itself is a dynamically linked library. Try it yourself!

du -h ~/.rustup/toolchains/<toolchain-version>/bin/rustc

For me returns 644K, which is way too small to be an entire compiler. But if you then run ldd on it:

ldd ~/.rustup/toolchains/1.92-x86_64-unknown-linux-gnu/bin/rustc
	linux-vdso.so.1 (0x00007f9e05e1d000)
	librustc_driver-d31eb41759495bb2.so => /home/jana/.rustup/toolchains/1.92-x86_64-unknown-linux-gnu/bin/../lib/librustc_driver-d31eb41759495bb2.so (0x00007f9dfd800000)
	libdl.so.2 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib/libdl.so.2 (0x00007f9e05e10000)
	librt.so.1 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib/librt.so.1 (0x00007f9e05e0b000)
	libpthread.so.0 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib/libpthread.so.0 (0x00007f9e05e06000)
	libc.so.6 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib/libc.so.6 (0x00007f9dfd400000)
	libLLVM.so.21.1-rust-1.92.0-stable => /home/jana/.rustup/toolchains/1.92-x86_64-unknown-linux-gnu/bin/../lib/../lib/libLLVM.so.21.1-rust-1.92.0-stable (0x00007f9df3400000)
	libgcc_s.so.1 => /nix/store/r7vdyf8pwlqsgd5ydak9fmq3q1i5nm3m-xgcc-15.2.0-libgcc/lib/libgcc_s.so.1 (0x00007f9e05dd7000)
	libm.so.6 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib/libm.so.6 (0x00007f9e05cdf000)
	/nix/store/wb6rhpznjfczwlwx23zmdrrw74bayxw4-glibc-2.42-47/lib/ld-linux-x86-64.so.2 => /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51/lib64/ld-linux-x86-64.so.2 (0x00007f9e05e1f000)
	libz.so.1 => /nix/store/ri9paa3mri4kqakljak8ldvbcp7lpmif-zlib-1.3.1/lib/libz.so.1 (0x00007f9e05cc1000)

The second entry is librustc_driver.so, which is 150MiB large. That is where the compiler lives. Rust works this way so clippy and rustfmt, can also call into the compiler. In addition to librustc_driver.so, Rust uses dlopen to dynamically open codegen backends like cranelift, or gcc. So, if EIIs didn't support dynamic linking, that would in general be fine-ish, but we wouldn't be able to use EIIs in the compiler itself.

And office hours was exactly the kind of space where we could chat about various solutions to this. Together with jyn514's github avatar jyn514 , we thought of an approach where we optionally desugar EIIs to thread-locals, and resolve either the default or explicit implementation at runtime. This would trade some amount of performance for full support of dynamic linking, including dlopen!

Going forward

In the end, I must admit that there is a chance we will never even stabilize EIIs. I would be fine with that, actually. I'm certain we'll use them in the standard library and compiler at some point, since they unify the code paths of the panic handler and the allocator. We might make it easier to change the behavior on specific kinds of panics. Like integer overflow. But we'll have to decide, after an RFC, whether this is something we want users to interact with directly.

But as I said in the intro, this blog post isn't really about that. It's not just about the technical challenges, though maybe you found some of the things I mentioned interesting. Every step of the way. It's about how many people, from inside and outside the rust project, have all the various bits of knowledge required to come to a single implementation. And how important it therefore is that we maintain this community, where we keep learning from each other, from human to human.

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
A window to the pasthttps://donsz.nl/blog/a-window-to-the-pasthttps://donsz.nl/blog/a-window-to-the-pastThu, 22 Jan 2026 00:00:00 +0000 A window to the past

My guitar tab of "a window to the past", by John Williams

  6/3           9/3                 6/3           9/3
e|-0-----0---2-|-3---2-0-----------|-5---7-3---2-|-3---2-0-----------|
b|-----3---0---|-----------0-----0-|-------------|-----------0-----0-|
g|---0---------|---0-----0-----0---|---5-----0---|---0-----0-----0---|
d|-------------|-------------------|-------------|-------------------|
a|-------------|-------------------|-0-----0-----|-------------------|
E|-0-----0-----|-0-----0-----0-----|-------------|-0-----0-----0-----|

  6/3           6/3           6/3           6/3            3/3      3/3
e|-5---7-3---2-|-3---20----7-|-0---------3-|--------------|--------|-------|
b|-------------|-------7-----|-----313-----|-1---0\-0-----|--------|-------|
g|---5-----0---|---0-----7---|---0-----0---|------2-----2-|------2-|-------|
d|-------------|-------0-----|-------------|---2------1---|---1----|-------|
a|-0-----0-----|-------------|-3-----2-----|-0------2-----|--------|-0-----|
E|-------------|-0-----------|-------------|--------------|-2------|---3-2-|

  6/3           9/3                 6/3           9/3
e|-------------|-------------------|-------------|-------------------|
b|-------------|-----------0-----0-|-------------|-----------0-----0-|
g|---0-----0---|---0-----0-----0---|---0-----0---|---0-----0-----0---|
d|-2---0-2---4-|-5---4-2-----2-----|-7---9-5---4-|-5---4-2-----2-----|
a|-------------|-------------------|-------------|-------------------|
E|-0-----------|-------------------|-------------|-------------------|

  6/3           6/3           6/3           6/3            3/3      3/3
e|-------------|-----------2-|-0---------3-|--------------|--------|-------|
b|-------------|-------------|-----313-----|-1---0\-0-----|--------|-------|
g|---0-----0---|---0-----2---|---0-----0---|------2-----2-|------2-|-------|
d|-7---9-5---4-|-5---420-----|-------------|---2------1---|---1----|-------|
a|-------------|-------------|-3-----2-----|-0------2-----|--------|-0-----|
E|-------------|-------------|-------------|--------------|-2------|---3-2-|

repeat 1x, end:

 4/3
|-------0-|
|-----3---|
|---0-----| 
|---------|
|---------|
|-0-------|

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
The turn of a friendly card (instrumental)https://donsz.nl/blog/turn-of-a-friendly-cardhttps://donsz.nl/blog/turn-of-a-friendly-cardThu, 22 Jan 2026 00:00:00 +0000 The turn of a friendly card (instrumental)

My guitar tab of "The turn of a friendly card", by the Alan Parsons Project

[verse]

                                          v use left thumb on E string
e|----0---|0-------|--------|--------|--------|--------|----0---|--------|
b|--13----|--3-----|3-1-----|1-0-----|0-------|--------|--------|-3---1--|
g|------2-|------1-|--------|--------|--2-----|-2-0----|------0-|---0----|
d|--------|--------|------3-|------2-|------0-|------2-|0-2-----|--------|
a|----0---|----2---|----3---|--------|--------|--------|--------|-0---0--|
E|--------|--------|--------|----0---|----1---|-----0--|----0---|--------|

                                          v use left thumb on E string
e|----0---|0-------|--------|--------|--------|--------|--------|--------|
b|--13----|--3-----|3-1-----|1-0-----|0-------|--------|----1---|-0------|
g|------2-|------1-|--------|--------|--2-----|-2-0----|------0-|---0-2--|
d|--------|--------|------3-|------2-|------0-|------2-|0-2-----|-----2--|
a|----0---|----2---|----3---|--------|--------|--------|----0---|-0---0--|
E|--------|--------|--------|----0---|----1---|-----0--|--------|--------|

[chorus]
From here, fingers mostly in the shapes of an Am and Fmaj7 chord

           v 010 hammer   v use left thumb on E string
e|--------|--------|--------|--------|--------|--------|--------|--------|
b|--0-1---|1H------|--------|--------|--------|--------|--------|--0---1-|
g|2-------|--2-0---|--0-2---|0-------|--2-----|2-------|2-----2-|----0---|
d|--------|--------|2-------|--3-2---|2-----2-|--2-2---|--2-0---|2-0-----|
a|------0-|--------|--------|--------|----35--|--------|------0-|------0-|
E|--------|------0-|------1-|------0-|--------|------0-|--------|--------|

           v 010 hammer   v use left thumb on E string
e|--------|--------|--------|--------|--------|--------|--------|--------|
b|--0-1---|1H------|--------|--------|--------|--------|--------|--0---1-|
g|2-------|--2-0---|--0-2---|0-------|--2-----|2-------|2-----2-|----0---|
d|--------|--------|2-------|--3-2---|2-----2-|--2-2---|--2-0---|2-0-----|
a|------0-|--------|--------|--------|----35--|--------|------0-|------0-|
E|--------|------0-|------1-|------0-|--------|------0-|--------|--------|


[bridge]

    v---v-hammer on------v    v 010 hammer                        Am chord
e|--------|--------|--------|--------|--------|--------|--------|------0-|
b|--------|--------|--0-1H--|0H------|--------|--------|--0-1---|----1---|
g|--------|--------|2-------|--2-0---|--------|--------|2-------|--2-----|
d|--0-2H--|2-2-2---|--------|--------|0-0-2H--|2-2-2---|--------|2-------|
a|03H-----|------0-|------0-|--------|-3H-----|------0-|------0-|--------|
E|------0-|--------|--------|------0-|------0-|--------|--------|--------|

[chorus]
Start with 2 beat of rest, this makes the song feel like it pauses and then has to catch up with itself
even though no beats were actually lost

  vvvv rest                              vvv 202 is hold - release - hammer on
        vvvv 1 to 0 pull off      v use left thumb on E string
e|--------|--------|--------|--------|--------|--------|--------|--------|
b|------1-|0-------|--------|--------|--------|--------|--------|--0---1-|
g|----24H-|--2-0---|--0-3---|2-0-----|--------|2-------|2-----2-|----0---|
d|--------|--------|2-------|----3---|--0-202-|--2-2---|--2-0---|2-0-----|
a|--------|--------|--------|--------|0-------|--------|------0-|------0-|
E|--------|------0-|------3-|------1-|--------|------0-|--------|--------|
                        ^ In previous repetitions this 3 was a 2, i.e. we go out of key here 
                          for a few notes following.
                          This is actually what makes this song so pretty to me.

                                          vvv 202 is hold - release - hammer on
e|--------|--------|--------|--------|--------|--------|--------|--------|--------|
b|--0-2---|3-1-0---|--------|--------|--------|--------|--------|--0---1-|--0-----|
g|2-------|--------|--0-3---|2-0-----|--------|2-------|2-----2-|----0---|2---0-2-|
d|--------|--------|2-------|----3---|--0-202-|--2-2---|--2-0---|2-0-----|--0-----|
a|------0-|--------|--------|--------|0-------|--------|------0-|------0-|------0-|
E|--------|------0-|------3-|------1-|--------|------0-|--------|--------|--------|
                                       slow down at the end, hold the last note ^
                                       the last bar is outro

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Pipes in Pythonhttps://donsz.nl/blog/pipethonhttps://donsz.nl/blog/pipethonTue, 06 Jan 2026 00:00:00 +0000 Pipes in Python
Press for table of Contents

A feature I've always liked in languages, is the pipe operator. Some version of this can be achieved in haskell, elixir has it, and nix has a preview feature for it. And, most well known I think, bash has a pipe operator, though the way it operates on io streams is somewhat unique. Especially in interactive REPLs, like bash, the operator is super useful. So, this week I created a patch to cpython, that you can try out, that actually adds that! Though, to explain this, I think it's cool to talk a little about good syntax design, and what I call syntax that "goes with the direction of thought"!

Postfix await is just better

(I think, don't @me)

To start, I'd like to consider the syntax design of the await operator. Many languages that support async, have a natural counterpart called await. This is often used as a prefix to some expressions. You might recognize this from javascript and python.

To quote the first example from the asyncio docs:

import asyncio

async def main():
    print('Hello ...')
    await asyncio.sleep(1) # <-- see! await used here!
    print('... World!')

asyncio.run(main())

And I think this is a terrible choice! To show why, let's look at an alternative await syntax used by Rust. I remember that when learning Rust myself, around 2018, async and await just got added to the language. There was quite some debate over how to spell this operator. In the end, Rust got a postfix await operator. In other words:

#[tokio::main]
async fn main() {
    // await at the end!
    tokio::time::sleep(Duration::from_secs(1)).await;
}

At the time, it was already recognized that this is quite convenient when chaining awaits together with method calls and field accesses. Though that's not the only reason I think it makes sense. It is very natural to write some expression, like the tokio::time::sleep(...), and then at the end of writing that expression, decide that you should await it. If only for the fact that you might have been writing tokio::time::sl in your editor, and it pops up the documentation for sleep that says it's asynchronous so it makes sense then to await it.

In javascript, I've often found myself annoyed by this. You write an expression that ends with an async function call, so you have to jump your cursor to the start of the line to add an await. Then you can go back to the end of the expression to add another method call to the result. Oh wait! You also have to add parentheses to make the await bind correctly. Back to the start, add the parentheses, go back to the end. Ugh.

Syntax with the direction of thought

This process of having to jump to the start of the line often breaks the flow of writing. When you're writing an expression, from left to right (at least in english), you're also constantly thinking about what to write next. Some editors recognize that not being able to do so is frustrating, so they have a feature to autocomplete little code transformations, often called snippets. For example, maybe you can suffix a javascript expression with foo().await, and then press autocomplete to do the transformation to await foo().

This might help with writing, though I'd argue that reading code has exactly the same problem. You read (in english) a line of code from left to right, and then you don't want to check back at the start whether it was awaited. Instead, if it's so much easier to autocomplete a snippet by writing a suffix, we can just make that the syntax. And that's the choice Rust made.

When syntax works like this, from left to right, I like to describe it as going "with the direction of thought". Another example of syntax that in most languages goes with the direction of thought is field accesses. a.b.c.d means, start from some object a, and then walk your way through the object to d, in order.

I don't think this is just a preference. From people I've talked to, or students I have taught programming to in the past, I think this works like this for a lot of people. Just for fun, imagine what field access would look like in the style of python and javascript's await syntax. It would become d (c (b a)), which I think most people would agree is much worse. I think that's an example of going against the direction of thought D:

In zig, even the deference operator which is spelled some_pointer.* goes with the direction of thought. I don't agree with a lot of choices zig made in language design, but that one is great!! The alternative in C, C++ and Rust is a prefix. Of course, in Rust, the . operator performs automatic dereferencing, and both C and C++, have a -> operator that performs a dereference. I would argue that those "hacks", however useful they are, exist to make the syntax follow the direction of thought.

Something similar goes for Scala's postfix match which I learned today is also experimentally proposed in Rust.

Syntax against the direction of thought

Let's look at some more examples of where I think syntax doesn't follow the direction of thought. In Rust, there's if let syntax. It works like this:

fn maybe_foo(opt: Option<u32>) {
    if let Some(inner) = opt {
        // do something with inner
    }
}

You can read this as a conditional let binding. Writing let Some(inner) = opt without the if, would mean "destructure opt into Some(...), and bind the contents". However, that destructuring could fail if opt is None. So, I like reading the if part in if let as, if the let binding succeeds, run some code on the result.

Anyway, after years of trying to get used to this, I regularly find myself frustrated by it. I've seen that it ends up being very natural to first write some expression, and then decide what pattern it has to match it against. And I'm not alone! There's an open RFC about the possibility of an is operator which would in theory allow you to write if opt is Some(inner) {}. And though there are some real concerns around this syntax, what I think it does right is to follow the direction of thought.

Let's finish this section around syntax design with an example from Python. Though technically this example isn't even syntax, since it's a consequence of the design of the standard library. In theory, python could've done this differently! Let's look at what it looks like to compose iterators in python:

from functools import reduce

res = reduce(
    lambda a, b: a + b, 
    filter(
        lambda x % 3 == 0, 
        map(
            lambda x: x * 2, 
            range(100)
        )
    )
)

And I know sum and list comprehensions exist, this is just an example. A good explanation of what it does, I think would be: It finds the first 100 integers, doubles them to get the first 100 even integers, takes only hose that are divisible by 3, and calculates their sum.

However, the direction I explained that in doesn't match the direction it was written in, does it! It's written as the sum of (the elements that are divisble by 3 of (the doubles of (the first 100 numbers))). The equivalent in Rust would be:

0..100
    .map(|i| i * 2)
    .filter(|x| x % 3 == 0)
    .reduce(|a, b| a + b)

In my opinion: much clearer. That actually matches how I like to explain what the code does! It follows the direction of thought.

Pipes in Python

I don't often use python for actual programs, but I almost permanently have one or two terminals open somewhere with a python REPL to use as a calculator. To make that experience better, I've made some modifications to my python REPL. For example, it automatically imports numpy and all math functions, as well as define some constants for unit conversion.

In a REPL, direction-of-thought syntax matters even more. Ideally, you want to get an expression right in one go, press enter, and see the outcome. Scrolling left and right (with arrow keys) in what you just wrote is annoying.

So, because map and filter and other iterator combinators from itertools go against this direction of thought so much, I feel like they're esentially useless to me. List comprehensions often work a bit better, but I think I would use map if it was easier to write.

So this is where pipe operators come in. Imagine you could write:

range(100) | map(lambda x: x * 2) | filter(lambda x: x * 3 == 0) | reduce(lambda a, b: a + b)

The pipe operator would implicitly supply the last parameter of each function, such that map gets passed the range, filter gets passed the mapped range, etc. I think that would completely reverse the way you think about these chains, letting them go with the direction of thought.

I'm deliberately using the | operator here, since it already exists in python! It just means bitwise or...or does it? Python has super rich operator overloading available, so last year I realized that it might actually be possible to implement pipes like this inside normal Python.

The only problem is that it'd require modifying every built-in function to accept fewer parameters than normal, so that the missing parameter could be piped in. So.... that's what I did!

If you're ready for it, and I warn you: think carefully whether you are, open this section...
import sys
import ctypes
import inspect

def maybe_defer(orig, *args, **kwargs):
    try:
        return orig(*args, **kwargs)
    except TypeError as e:
        if "argument" in str(e):
            return lambda *new_args, **new_kwargs: maybe_defer(orig, *args, *new_args, **kwargs, **new_kwargs)
        else:
            raise

def make_init(cls):
    def __init__(self, *args, **kwargs):
        self._old = maybe_defer(cls, *args, **kwargs)
    return __init__

class Wrapper(BaseException):
    def __init__(self, old):
        self._old = old
    def __getattribute__(self, attr):
        if attr == "__call__":
            return object.__getattribute__(self, attr)
        elif attr == "__or__":
            return object.__getattribute__(self, attr)
        else:
            return object.__getattribute__(object.__getattribute__(self, "_old"), attr)
    def __len__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__len__")(*args, **kwargs)
    def __iter__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__iter__")(*args, **kwargs)
    def __repr__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__repr__")(*args, **kwargs)
    def __contains__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__contains__")(*args, **kwargs)
    def __hasattr__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__hasattr__")(*args, **kwargs)
    def __getitem__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__getitem__")(*args, **kwargs)
    def __setitem__(self, *args, **kwargs):
        return object.__getattribute__(object.__getattribute__(self, "_old"), "__setitem__")(*args, **kwargs)
    def __or__(self, other):
        return Wrapper(other(self))
    def __ror__(self, other):
        return Wrapper(Wrapper(self)(other))
    def __call__(self, *args, **kwargs):
        return maybe_defer(object.__getattribute__(object.__getattribute__(self, "_old"), "__call__"), *args, **kwargs)

def modify_iterator_functions(item_dict):
    if item_dict is None:
        return
    for k in item_dict:
        if not (k in ["object", "type", "StopIteration"] or "Exception" in k or "Error" in k):
            if inspect.isclass(item_dict[k]) or repr(item_dict[k]).startswith("<class"):
                (lambda o: ctypes.cast(id(o)+type(o).__dictoffset__, ctypes.POINTER(ctypes.py_object)).contents.value)(sys.modules['builtins'])[k] = item_dict[k].__class__(
                    f"Wrapper of {k}",
                    (),
                    Wrapper.__dict__ | {"__init__": make_init(item_dict[k])}
                )
            else:
                item_dict[k] = Wrapper(item_dict[k])


modify_iterator_functions(__builtins__.__dict__)
modify_iterator_functions(locals())

x = [1, 2, 3]
res = x | map(lambda x: x + 3) | max()
print(res) # actually prints 4, 5, 6 🤯

Told ya it's terrible ...

Anyway, that was just a joke from half a year ago, oh wait, apparently almost a year and a half ago. It barely hangs together, and if you look at it wrong it segfaults the interpreter (that part is not a joke). The only way to actually do this, would be to modify python itself.

Which, I am delighted to tell you, is not that hard, as I found out this morning! Apparently, you can just do things. So, I wrote a small patch file, in total less than 100 lines changed, that actually implements this, to try out what it'd be like. And honestly, you do you, but I think I'll be keeping this for my calculator. ITS SO GOOD!

Instead of a |, I chose to add a new operator that didn't previously exist in python. That way it couldn't conflict with anything.

I added this new grammar rule, just below the bitwise | operator's precedence level:

pipe[expr_ty]:
    | lhs=pipe '|>' rhs=primary_nocall b=genexp {
      ...
    }
    | lhs=pipe '|>' rhs=primary_nocall '(' arg=[arguments] ')' {
      ...
    }
    | bitwise_or

This means, to parse a pipe, first parse a pipe (it's left recursive), and then, look for a segment in the shape of a call. That is, first a primary, except exclude those that could be themselves function calls. And then, parse the argument list.

And then, the only logic we do in the place of the ... above is to effectively swap the order around. So, foo |> bar(a, b, c) is turned into bar(a, b, c, foo). That's all!

But with that you can already write the following:

[1, 2, 3] |> map(lambda x: x+1) |> filter(lambda x: x>2) |> list()
#> [3, 4]

Playing around with it though, I realized that since the parentheses of the argument list are always required when piping, it isn't ambiguous to instead put a variable name there instead. Wouldn't it be cool if you could also put a variable name there, to write to it? Well, trying it, I think it is!

[1, 2, 3] |> map(lambda x: x+1) |> list() |> mapped |> filter(lambda x: x>2) |> list()
#> [3, 4]

print(mapped) # prints [2, 3, 4]

The transformation that's happening here is that foo |> NAME is turned into NAME := foo.

Finally, I added a tiny bit more logic to recognize when you use an underscore in the right hand side. In that case, that argument is replaced. So, with that, you can write:

def sub(x, y):
    return x - y

3 |> sub(_, 5) # prints -2
3 |> sub(5, _) # prints 2

Earlier I posted about it on mastodon too with some examples.

Conclusion

If you're interested in trying it out, I've published the patch here, together with a nix package to easily get a build of it. In that let me know, if it also helps you write code "with the direction of thought".

I've found some small limitations of this approach, like the way _ is technically a variable name you could want to write, so this is by no means a (ready) proposal for the python language.

However, I've been using this for a little bit now, and I've been loving it. For me this really solves the problem with using map and filter. Well enough that I think I'll keep using this daily.

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
A small adventure in the standard libraryhttps://donsz.nl/blog/exactly-oncehttps://donsz.nl/blog/exactly-onceMon, 24 Nov 2025 00:00:00 +0000 A small adventure in the standard library
Press for table of Contents

While writing some machinery for dealing with spans on built-in attributes last month, I was slightly bothered by a missing method on Iterator. I usually work on the compiler, not the library, but I decided to write a little proposal, which was then accepted, so today it was time to try and implement it. In fairness, that implementation itself was mostly trivial, and yet right now there's no way to stabilize it. What I actually want to share is the incredible compiler work going on as we speak that will unblock it.

The feature I'd like

This is not the first time I ran into this: let's say you have an api that returns a vector of elements:

fn get_all_spans() -> Vec<Span> { ... }

Then sometimes you want to do something when exactly one element is returned. You could write:

let mut elems = get_all_spans();
if elems.len() != 1 {
    return None
} else {
    return Some(elems.pop().expect("we just checked the length"))
}

The expect is a bit unfortunate though. Maybe this is nicer?

let mut elems = get_all_spans();
elems.pop().filter(|| elems.is_empty())

There are certainly ways to remove the mutable variable too, maybe with iterators. Still, none of those solutions are particularly pretty. I think this is common enough that it's something the standard library could provide. Maybe not on Vec, or &[], but on iterators so it's widely applicable. That's what I proposed in rust-lang/libs-team#676 , with which we could write the solution as:

get_all_spans().into_iter().exactly_one()

Stabilizing methods on traits

The implementation is rather trivial, this is the PR if you're interested anyway: rust-lang/rust#149270 . The problem is that itertools already has a method named exactly_one. With a different signature. And a blanket implementation for T: Iterator.

So, there exists perfectly valid Rust code, in production right now, that looks like this:

// import the trait with a blanket implementation
use itertools::Itertools;

match foo.into_iter().exactly_one() {
    Ok(_) => ...,
    Err(_) => ...,
}

Which will break as soon as we stabilize the method on Iterator which instead returns an Option. And that's not just a problem with Iterator. Any trait method we stabilize, that someone already added themselves through an extension trait with a blanket implementation, is a breaking change. Though the problem with methods we want on Iterator, which already existing in itertools is apparently a common one.

Not that this clueless girl knew that this morning. I only found out when CI failed on the implementation PR: the compiler itself couldn't compile with the updated standard library containing exactly_one because of a warning about a name resolution ambiguity. We use itertools and its existing exactly_one method too, causing the exact same problem users will when we stabilize it.

The good news is: this is being fixed! And if like me, you didn't already know about this: you should! It's cool! These internal compiler changes aren't always super visible, but the result is that we can stabilize parts of the standard library which are things you might notice.

Only 3 weeks ago amanieu's github avatar amanieu opened the stabilization PR for a feature called supertrait_item_shadowing rust-lang/rust#148605 which makes changes rust's name resolution rules to:

  • When a type implements a trait, like trait Iterator {...},
  • as well as another trait which is a subtrait, like trait Itertools: Iterator {...},
  • and when name resolution finds a reference to a name that exists in both, like exactly_one,
  • prefer the one from the subtrait, so the existing one from Itertools.

And guess what, no less than 3 blocked PRs are mentioned about adding methods to Iterator!

This whole issue reminds me of the issue we have with built-in attributes. Technically, every built-in attribute we add to rustc is a breaking change... I expect some day we need a similar change in name resolution there.

So, to end off, let me repeat the acknowledgements from under that stabilization PR, by thanking compiler-errors's github avatar compiler-errors and lcnr's github avatar lcnr for making this possible.

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Maintaining the Rust compilerhttps://donsz.nl/blog/maintenancehttps://donsz.nl/blog/maintenanceTue, 28 Oct 2025 00:00:00 +0000 Maintaining the Rust compiler
Press for table of Contents

The rust compiler is a giant project. Hundreds of contributors help maintain it. I consider myself quite lucky that this maintenance work has become my full time job. Most of that time though, isn't spent to work on new language features, though there is a bit of that. In fact, most maintenance is barely visible to users of the language. So what does maintaining the compiler look like?

I was talking to Mara ( m-ou-se's github avatar m-ou-se ) today about what I was busy working on. I said that I was fixing weird behavior of #[deprecated] attributes. Last week WaffleLapkin's github avatar WaffleLapkin and I discovered some subtle bugs in that. Yesterday, I finished up a pull request that fixed unsizing coercions in the new trait solver.

Both of these tiny projects I quite enjoyed, and they represent what most of my days are filled with. Subtle bugs, small inconveniences in the compiler's source, some slightly confusing error messages. And not just my days; there are hundreds, if not thousands of these small tasks that, one by one, improve the rust compiler just a bit. And so a large group of people work on these kinds of tasks, every day.

When many small tasks, together result in new compiler features, then those are easily visible to end users. But the bug fixes and internal refactors required for them are not. We do often share those among other maintainers. For example, when a bug causes a small snippet of trivial code not to compile, we all laugh. How did we miss that? Why are there no tests for that? How did nobody notice that before?

So, Mara convinced me to share some of that. From now on, I intend to write about some of the things I work on in the compiler. I'll tag them with maintenance .

I've already written a couple!

Note that these blog posts won't be super exciting. They won't fundamentally change Rust. There's the rust blog for that already. However, sometimes they might show some interesting bugs, or show progress in a larger project. That's what compiler maintenance is like!

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Testing the crate roothttps://donsz.nl/blog/test-cratehttps://donsz.nl/blog/test-crateTue, 28 Oct 2025 00:00:00 +0000 Testing the crate root
Press for table of Contents

I woke up last weekend, and someone assigned me to a PR to review. That's quite common. It fixed a crash with attributes, something I've worked a lot on. Some code crashed the compiler, but the code to reproduce it used a specific nightly feature called "contract attributes", and I didn't believe that that was the only way to reproduce the crash. So, I tried to see if any currently stable attribute would the same so the reproducer was more general. It turned out: I was wrong! But playing with it I found a complely new way to crash the compiler in a single line of code.

So the attribute I tried was #[test]. The compiler (broadly) provides two kinds of built-in attributes:

  1. Built-in attribute macros, they're like normal attribute macros but defined in the compiler's source code and they can do a few magic things user-defined attributes can't. For example, they don't need syn and quote since they can use the compiler's own parser.
  2. Built-in attributes which are not macros at all, and are instead hints to the compiler. #[inline] for example! Not a macro, there's nothing it could expand to do do the inlining. Instead it annotates a function to hint how code generation should work.

What's noteworthy for this bug: built-in attributes (1) play no role in name resolution, while built-in attribute macros (2) do. The latter you could for example shadow, and thus redefine. And, in core, each built-in attribute macro has a stub definition. For test it looks like this:

#[allow_internal_unstable(test, rustc_attrs, coverage_attribute)]
#[rustc_builtin_macro]
pub macro test($item:item) {
    /* compiler built-in */
}

(yes, that comment is really there, and the fact that it's empty is also correct. The "real" definition of it lives in compiler/rustc_builtin_macros)

However, #[test] is interesting: it's treated as both categories. It's both listed as a built-in attribute and a macro. I'm not 100% sure why, this may be because #[test] already existed before macros did? Though don't quote me on that.

Additionally, the fact that it's both mostly doesn't matter. Macros expand during name resolution. So, after name resolution, attribute macros mostly disappear from the source code and are replaced by their expansion. So, the fact that it's also a built-in attribute never really ends up mattering later on.

One way I found it does matter though is a specific diagnostic to tell you that an attribute is not valid on the crate root. In other words, the compiler errors when you do this:

#![inline] // <-- not valid
#![test] // <-- not valid

fn main() {}

This check happens before expansion, so also occurs for #[test].

So, the code I accidentally stubled onto that ICEs the compiler is the following:

#![core::prelude::v1::test]

That's all! Try it out, it's going to be fixed soon with rust-lang/rust#147841 but on for example rust stable 1.90.0 this still crashes the compiler. So what's happening here? Because we're explicitly referring to the macro stub in core by its path, and macros are name resolved, this still acts as a test attribute macro. However, built-in attributes are matched by name. In other words, we never error out because you used #![test] on the root of a crate, since the name of the attribute isn't exactly the word test. It's a whole path. So we skip that check and blindly expand the macro.

Next, the #[test] macro expands. It however does something funny. When you compile with --tests (like what happens when you run cargo test), tests should be compiled. But if you just run cargo build for example, it saves some time to not compile tests at all. So, the very first thing the #[test] macro does is to check whether we're in testing mode. If we're not, it replaces the entire item #[test] was used on from the syntax so we don't spend any further time compiling it.

Try it out! This code doesn't raise a type error when running cargo build. It does when you run cargo test.

#[test]
fn foo() -> u32 {
    "not a u32"
}

So, now you can maybe see where I'm going with this. We applied #![test] to the crate, and we managed to avoid the check whether the attribute was valid on a crate. We've just: deleted the entire crate.

That seems like it could cause problems...

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Deprecated on re-exportshttps://donsz.nl/blog/deprecatedhttps://donsz.nl/blog/deprecatedTue, 28 Oct 2025 00:00:00 +0000 Deprecated on re-exports
Press for table of Contents

Deprecated is an attribute you can put on items to mark them as, well, deprecated. Any use of that item will give a warning, telling you that you're using something that's deprecated. The thing is, it doesn't always work...

The deprecated attribute

Let's start with some examples that work. Here, foo is clearly deprecated:

#[deprecated]
fn foo() {}

fn main() {
    foo();
    //~^ WARNING use of deprecated function `foo`
}

The attribute is slightly more flexible; You can also give a version and a reason like so:

#[deprecated(
    since = "5.2.0",
    note = "foo was rarely used. Users should instead use bar"
)]
fn foo() {}

Or, just a reason:

#[deprecated = "foo was rarely used. Users should instead use bar"]
fn foo() {}

Alright, makes sense.

Now, the reference says one more thing about the attribute:

When applied to an item containing other items, such as a module or implementation, all child items inherit the deprecation attribute.

Let's take a look:

#[deprecated]
mod a {
  pub struct Foo;
}

fn main() {
    let _x = a::Foo;
    //~^ ERROR use of deprecated unit struct `a::Foo`
}

The struct Foo is still deprecated, while the attribute only applies to a. And this doesn't just work for functions or structs. pretty much any kind of item works. I say prety much, since use items are not supported. However, if they were that'd be pretty useful. Just look at this example:

fn foo() {}

#[deprecated]
use foo as bar;

fn main() {
    bar();
}

You might expect this to mean thattThe use of the name bar is deprecated, but foo isn't. But what does the rust compiler say? Make your best guess now!

Turns out: it doesn't work

Believe it or not, the answer is: absolutely nothing. Nothing is deprecated, but the compiler is absolutely fine with the attribute being there. The reason is that after name resolution, the use of bar doesn't pass through the re-export anymore. It just resolves directly to foo. So, the fact that the use item is deprecated isn't noticed.

WaffleLapkin's github avatar WaffleLapkin were planning to fix that. It seems reasonable that this should work, or otherwise at least warn that it doesn't do anything. What happened is that we found multiple other bugs with #[deprecated].

So, it's quiz time. Based on what you read sofar, and what you maybe already knew. How many deprecation warnings does this code produce? I've annotated lines that potentially use a deprecated item.

#[deprecated]
pub mod a {
    #[macro_export]
    macro_rules! foo {() => {};}

    pub fn bar() {}

    macro_rules! foo_no_export {() => {};}
    foo_no_export! {} // <-- 1
}

#[deprecated]
macro_rules! baz {() => {};}
baz! {} // <-- 2

use a::bar as bar1; // <-- 3
use foo as foo1; // <-- 4

fn main() {
    foo! {} // <-- 5
    a::bar(); // <-- 6
}

You might think: 6. There are 6 usages, so that's what I expected as well. Except, the answer is only 4. It turns out, the inheriting of #[deprecated] through modules doesn't work, so 1 and 5 don't produce a warning. Though what surprised us was that 4 does give a warning!

What's going on here is that the reason inheriting doesn't work on macros, is that when a macro expands, we pretty much remove any trace of it from the AST. The macro is replaced by whatever the macro expanded to. Checking deprecation only happens later, once the macros are already gone, so their uses go unnoticed.

So why does 2 work then? Turns out, there's special code to handle deprecated on macros. To make sure that in most cases it works as it is supposed to. But, that codepath only works when #[deprecated] is used directly on a macro. It doesn't look up.

There's more???

While playing around with what does and doesn't work with #[deprecated], we tried the following code. Again, I've annotated lines that potentially use a deprecated item. How many deprecation warnings do you think this produces?

#[deprecated]
pub mod a {
    pub struct Foo;
    pub struct Bar();
    pub struct Baz {}
}


use a::Foo; // <-- 1
use a::Bar; // <-- 2
use a::Baz; // <-- 3

fn main() {
  a::Foo; // <-- 4
  a::Bar(); // <-- 5
  a::Baz {}; // <-- 6
}

This time, it's not fewer, it's more! Not 6, but 8. 1 and 2 count twice, since in a way they're exporting two things. Unit and tuple structs can be constructed without braces. The way that works is that the compiler effectively does the following transformation:

struct Foo
// roughly expands to
struct Foo {}
const Foo: Foo = Foo{};

And use imports both definitions of Foo. Hence, two warnings.

Structs with braces don't get this treatment, and 3 only produces one warning.

This last bug is fixed with rust-lang/rust#147994 .

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Arenashttps://donsz.nl/blog/arenashttps://donsz.nl/blog/arenasThu, 15 Aug 2024 00:00:00 +0000 Arenas
Press for table of Contents

Sometimes you just really need an arena. Sometimes for performance reasons, other times for lifetime-related reasons. In their most basic forms, they're just a vec with some extra guarantees. However, it's those extra guarantees that matter. I've found myself looking for the right kind of arena too many times, so here's an overview of literally everything there is. I think, let me know if I forgot something.

What's an arena?

Very basically, an arena is a way to store your data somewhere without directly going through the system allocator. If you have a lot of small objects which you don't mind to deallocate together instead of individually, this can be a lot faster. You could use a Vec for this. However, if you store data in a vec its address might change all the time.

When the vector grows, its contents move around memory. That's not actually as inefficient as you think, it happens less and less often as the vec grows. The real problem with this moving around is that you can't depend on the address of your elements staying the same. It's common that you want to hold on to those addresses in a datastructure while also adding new objects.

The simplest kind of arena is built to solve that exact problem, by allocating in large chunks, and promising never to deallocate or move those. To grow, such an arena would simply allocate a whole new chunk of memory. However, there are so many more ways to do this which give you arenas with slightly different properties.

properties of arenas

In the table below, I talk about various properties arena do and don't have. Sometimes, you might not need an arena at all. Libraries such as elsa or rpds can help you write efficient datastructures, that are immutable, or provide quick clones because they are mostly based on pointers internally. That could be what you're actually looking for.

In the table, I've abbreviated some things a little, so here is the full explanation:

Type

Some arena implementations only allow you to store a single datatype in them. This can be good for performance, and might make it possible to iterate over the full list of allocations. However, it can be a limitation too. If the arena is supposed to work a little like an alternative to the built-in memory allocator, then you might want to allocate arbitrary elements of mixed types. This makes element iteration harder, since elements aren't stored at well-known offsets in the arena.

Interestingly, bumpalo supports both kinds. By default, the arena is mixed-type, but you can allocate vectors of a single type in their arena.

Requires

This indicates what kind of reference to the arena you need to allocate something in it.

Gives Out, and Deref Key

Some arenas directly give you references, but some are based on indices, and you might even need to have access to the arena to use these indices. "Gives Out" documents what kind of type you get to refer to an allocation. Sometimes it has a 'a lifetime, refering to the fact that it is bound to the lifetime of the arena, or T: 'static, meaning the type given out must be 'static. If no such lifetime is given, the index type is completely free from the arena's lifetime. "Deref Key" documents whether you need access to the arena to use the key, or whether the key directly gives access to the element.

Reuse Memory

Some arena allocators assume you only allocate, never free. That might be what you want, but sometimes it might not be. Most freeing arenas use a linked list of free spots, a freelist, though there are other options like garbage collection (GC), and I found one library that can actually compact itself and shrink.

Note, that for performance reasons, shrinking usually isn't often a very good idea.

Runs Drop

Especially the mixed-type arenas sometimes don't run Drop of stored elements. It's hard to call Drop if you don't know where in the arena elements are stored. This is a problem if you, for example, want to store an Rc in an arena, though there are many more types for which this is a problem.

Iteration

Although an arena isn't a vec, the items are often stored prety much in-order. So this column is about whether the arena supports this. The type behind it indicates whether to iterate you need a reference or mutable reference to the arena.

Collections

A few arena libraries also provide an alternative set of collections like Vec, String, and pointers like Box and Rc.

no_std and no unsafe

These should be pretty obvious.

ABA mitigation

If your arena supports deletions, the arena must be careful that on re-allocation it doesn't accidentally reuse an ID it previously gave to you. If you had still stored that old ID, you could now use it to access a new allocation. Or not, some arenas don't care and let you deal with this.

Concurrent

This documents whether the arena can be used concurrently. For some, I documented whether they require locking.

Dedup

Interners often deduplicate keys, to gain pointer equality. So far I only investigated one library doing this.

Approach

This roughly documentes how the library works.

  • Linked Arena Chunks means that at a basic level, big chunks of contiguous memory are allocated, slowly filled, and linked together to form one big arena built up from smaller chunks.
  • Vec (with freelist) refers to the backing storage. An Arena is a Vec, and you get indices to later index the Vec to get the element you stored.
  • Linked List means every element in the arena gets its own link in a linked list.

The others I think mostly make sense.

Note that there are instances where I left a cell blank. That's on some new columns, for which I've not gone through all the crates yet, so I don't have all the data yet. Feel free to help out!

Overview

Ordered by number of downloads, not by what you should use!! That only depends on your situation.

Crate Type Takes Gives Out Idx Size Deref Key Reuse Mem Runs Drop Iteration Collections no_std no unsafe ABA mitigation concurrent dedup Approach Downloads Last Update Version
slab Single &mut usize usize ❌ ✅ compact1 ✅ ✅ & ❌ ✅ ✅ ❌ ❌ ❌ Vec with Freelist
bumpalo Both & &'a mut NonZero<usize> ✅ ❌ ❌ ❌ ✅ ✅ ❌ ❌ ❌ Linked Arena Chunks
sharded-slab Single & usize usize ❌ ✅ freelist ✅ ✅ & ❌ ❌ ❌ ✅ ❌ Linked Arena Chunks
typed-arena Single & &'a mut NonZero<usize> ✅ ❌ ✅ ✅ &mut ❌ ❌ ❌ ❌ ❌ Linked Arena Chunks
slotmap Single &mut K: Key NonZero<u64> ❌ ✅ freelist ✅ ✅ & ❌ ✅ ❌ ✅ ❌ ❌ Vec with Freelist
id-arena Single &mut Id usize+u32 ❌ ❌ ✅ ✅ & ❌ ✅ ✅ ✅ ❌ Indexed Vec
generational-arena Single &mut Index usize+u64 ❌ ✅ freelist ✅ ✅ & ❌ ✅ ✅ ❌ ❌ Vec with Freelist
internment Single & ArenaIntern<'a> usize ✅ ❌ ✅ ❌ ❌ ❌ ❌ 🔒 ✅ Hashset of Boxes
concurrent_arena Single & ArenaArc 2*u32+Arc ✅ ✅ ✅ ❌ ❌ ❌ ❌ ✅ alloc may 🔒 ❌ Reference Counted Buckets
thunderdome Single &mut Index NonZero<u64> ❌ ✅ freelist ✅ ❌ ❌ ✅ ❌ ✅ ❌ ❌ Memory efficient generational-arena
atree Single &mut Token NonZero<usize> ❌ ✅ freelist ✅ ✅ & ❌ ❌ ✅ ❌ ❌ Vec-Backed Linked Tree
multi-stash Single &mut Key usize ✅ ❌ ✅ ✅ & ❌ ✅ ❌ ❌ ❌ Indexed Vec
colosseum Single & &'a mut NonZero<usize> ✅ ❌ ✅ ❌ ❌ ❌ ❌ 🔒 ❌ Mutexed typed-arena
gc Mixed glob Gc usize ✅ ✅ GC ✅ ❌ ❌ ❌ ❌ ❌ ❌ Garbage Collector
atomic_arena Single &mut?? Key 2*usize ❌ ✅ freelist ✅ ✅ & ❌ ❌ ❌ 🔒 ❌ Vec2
gc-arena Single & Gc<'a> NonZero<usize> ✅ ✅ GC ✅ ❌ ❌ ✅ ❌ ❌ ❌ Garbage Collector
typed-arena-nomut Single & &'a usize ✅ ❌ ✅ ✅ & ❌ ❌ ❌ ❌ ❌ Linked Arena Chunks
typed-generational-arena Single &mut Index ~usize+u64 ❌ ✅ freelist ✅ ✅ & ❌ ✅ ✅ ✅ ❌ ❌ Various backings with Freelist3
compact_arena Single &mut IdxN<'a> u32/u16/u8 ❌ ❌ ✅ ❌ ❌ ✅ ❌ ❌ ❌ Vec with "branded" indices
blink-alloc Mixed & &mut (T: 'static) NonZero<usize> ✅ ❌ ✅ ❌ ✅ ✅ ❌ 🔒 ❌ Linked Arena Chunks
bumpalo-herd Mixed & &'a mut NonZero<usize> ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ Per-thread bumpalo arena
bump_scope Both & BumpBox<'a> NonZero<usize> ✅ ❌ ✅ ❌ ✅ ✅ ❌ ✅ ❌ Linked Arena Chunks4
shredder Mixed glob Gc<'a> Arc+2*usize ✅ ✅ GC ✅ ❌ ❌ ❌ ❌ 🔒 (spinlock) ❌ Single Global GC
erased-type-arena Mixed & AllocMut<'a> NonZero<usize>+Arc ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ ❌ Linked List
riddance Single &mut Id<T> u32/u64 ❌ ✅ freelist ✅ ✅ ❌ ❌ ❌ ❌ ❌ Vec with Freelist
drop-arena Single & DropBox<'a> NonZero<usize> ✅ ✅ freelist ✅ ❌ ❌ ❌ ❌ ❌ ❌ typed-arena + free list
elise Mixed glob Gc<'a> usize ✅ ✅ GC ✅ ❌ ❌ ❌ ❌ 🔒 ❌ Single Global GC
hato Mixed &mut Handle u32+u32 ❌ ✅ freelist ❌ ❌ ❌ ❌ ❌ ❌ 🔒 ❌ Vecs of bitwise-copyable objects grouped by type

Footnotes

  1. Also uses a freelist, but on request can actually reduce its memory size. ↩

  2. Allocating slots in the arena is atomic, but using and storing data into the arena requires &mut and the only useful way to use this library across threads is by putting the Arena in a Mutex ↩

  3. Seems to be a fork of sorts of generational-arena . Supports many more types of backing storage including more space-efficient ones. ↩

  4. Surprisingly well-made and documented library compared to its number of downloads. Super well tested too. Also has arena-per-thread. ↩

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Incremental Compilationhttps://donsz.nl/blog/incremental-compilationhttps://donsz.nl/blog/incremental-compilationMon, 12 Aug 2024 00:00:00 +0000 Incremental Compilation
Press for table of Contents

Over the past week I've been trying to understand rust's incremental compilation model by implementing it myself. The one inside rustc is already very well documented, but the implementation lives inside of a giant compiler. Maybe this is a little more accessible to you!

This blog post is going to be a short one, since most of the things I wanted to say I ended up writing in the documentation of the crate I published containing the implementation. As I write more there, I might decide I have more to talk about, but for now the documentation itself should be a pretty interesting read, one I'd otherwise put in a blog post. If you're interested, you can find it here!

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>
Global Registrationhttps://donsz.nl/blog/global-registrationhttps://donsz.nl/blog/global-registrationWed, 10 Jul 2024 00:00:00 +0000 Global Registration
Press for table of Contents

You might not have considered this before, but tests in Rust are rather magical. Anywhere in your project you can slap #[test] on a function and the compiler makes sure that they're all automatically run. This pattern, of wanting access to items that are distributed over a crate and possibly even multiple crates, is something that projects like bevy, tracing, and dioxus have all expressed interest in but it's not something that Rust supports except for tests specifically.

Criterion is a custom benchmarking framework which provides great statistical analyses of performance. If you've ever used it before, you may have seen this pattern:

fn criterion_benchmark(c: &mut Criterion) {
    c.bench_function("benchmark_1", benchmark_1);
    c.bench_function("benchmark_2", benchmark_2);
    c.bench_function("benchmark_3", benchmark_3);
    // ...
    c.bench_function("benchmark_n", benchmark_n);
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

Being explicit has upsides, but it would be very cool if you could instead write:

#[bench]
fn benchmark_1(b: &mut Bencher) {}

#[bench]
fn benchmark_2(b: &mut Bencher) {}

#[bench]
fn benchmark_3(b: &mut Bencher) {}

// ...

#[bench]
fn benchmark_n(b: &mut Bencher) {}

// magically finds all the #[bench] functions
// even when they're spread over multiple files
criterion_main!();

I call this pattern global registration: collecting marked items across a crate, or even across all the crates in an executable.

We see it in many places:

Personally, I needed this functionality for implementing unit tests in embedded code. There, you don't have access to rust's built-in test framework. Interestingly, Rust does have a weird kind of support for this through #![feature(custom_test_frameworks)]. I've even recommended it to my students once or twice, who wanted to test their embedded software but it always felt a bit painful as this feature will, at least in its current form, never be stable.

Various people have expressed interest in having a generic system built into the compiler that can provide this behavior. At RustNL, I talked to epage's github avatar epage who wants this for the Testing Devex team, exactly to make tests less magical and to provide users with the option to define their own test frameworks. Since then, I've been thinking about this feature. It started with a pre-rfc, and at this point I've written most of an implementation. I'm just not 100% sure that that's the implementation we should want anymore.

Why? Well that's what the rest of this blog post is about.

Library Solutions

While direct support from the compiler is the ideal way to implement this feature, as always, people have come up with interesting workarounds. There are currently two libraries that can help you achieve global registration, both of which are maintained by dtolnay's github avatar dtolnay Let's discuss them briefly.

Inventory

Inventory works using global constructors. These are special functions that are called before main by the operating system - the exact mechanism differs a bit from platform to platform. C++ uses these to run constructors for global variables, and in rust you can use them using the ctor. If you would like to know more, I rather liked this blog post about them.

What happens, is that before main, a small bit of code runs for each element that needs to be registered, to atomically add itself to a global linked list. Collecting happens based on the type of the item that's being collected. I adapted the example from their docs to show what's going on:

pub struct Flag {
    short: char, name: &'static str,
}

inventory::submit! {
    Flag::new('v', "verbose")
}
// ====== generates rougly ========
// linked list node
static NODE: ... = Node::new(/* the flag */);
// runs when the program starts before main
#[cfg(link_section = ".text.startup")]
unsafe extern "C" fn __ctor() {
    // where T: Collect
    unsafe { add_to_registry::<T>(NODE); }
}
// ================================

inventory::collect!(Flag);
// ====== generates rougly ========
impl Collect for Flag {
    fn registry() -> Registry {
        // A registry is a linked list
        // this is what the add_to_registry function adds to 
        static REGISTRY: ... = Registry::new();
        &REGISTRY
    }
}
// ================================

fn main() {
    // iterate through the linked list 
    // which was built just before main started
    for flag in inventory::iter::<Flag> {
        println!("-{}, --{}", flag.short, flag.name);
    }
}

Inventory works as promised, and can be quite useful. Also, it deals well with dynamic library loading. However, it requires running code before main, and it doesn't work on all platforms; notably embedded platforms don't really work.

Using global constructors in Rust code is quite tricky. The standard library does a lot of work before main(), such as preparing the threading infrastructure, configuring standard input and output, and collecting the command-line arguments. When global constructors are run, none of these are initialized yet. Thus, Inventory does the bare minimum within its constructor functions.

Linkme

An alternative to inventory is linkme. It comes with slightly different platform support, there are even some test for a cortex-m target, and does not involve running code before main. It works all at compile time, though most of the magic happens during linking.

Object files contain different kinds of data (code, global variables, etc.), organized into sections. When a linker processes object files, it collects the data for each section together. Linkme uses some tricks that instruct the linker to create a section that contains all the registered elements. Again, adapting the example from the docs:

use linkme::distributed_slice;

// all the elements ultimately "appear" here
#[distributed_slice]
pub static BENCHMARKS: [fn(&mut Bencher)];

// adds to the static above
// by placing this in a specific linker section
#[distributed_slice(BENCHMARKS)]
static BENCH_DESERIALIZE: fn(&mut Bencher) = bench_deserialize;
fn bench_deserialize(b: &mut Bencher) {}

// tries to generate a linker section that contains:
// __SPECIAL_START_SYMBOL
// &BENCH_DESERIALIZE
// ... more elements
// __SPECIAL_END_SYMBOL

Now, the memory between the special start and end symbol form a contiguous range of memory, a slice, containing each of the elements that were added. This works without running code before main, though dynamic library loading isn't really supported.

At this point, an obvious reaction is:

Why don't we put this in the compiler?

If the compiler could generate this kind of distributed slice, the issue of platform support disappears. This is basically how libtest works. So, after talking about it on internals.rust-lang.org I enthusiasicaly added what is essentially linkme to the compiler. The implementation even considers the possibility of some day supporting dynamic linking if Rust ever starts properly doing that.

Note, in past discussions this feature was often called "distributed slice". That's also the name of the version from the linkme crate. I renamed it because it being specifically a slice exposes too much of the internal working of the feature, and it removes a lot of flexibility to change the design later.

#![feature(global_registration)]

use core::global_registration::{global_registry, register, Registry};

#[global_registry]
static ERROR_MSGS: Registry<&str>;

register!(ERROR_MSGS, "a");
register!(ERROR_MSGS, "b");

fn main() {
    for msg in ERROR_MSGS {
        println!("{}", msg);
    }
}

One crate defines a registry, with #[global_registry]; anyone can add to it and in the final binary a static appears that, when iterated over, contains all the elements. Neat!

Of course, I acted first and only then thought about it properly... After lots of incredibly helpful discussion with m-ou-se's github avatar m-ou-se , we realised that this has a lot of implications. Let's do some thought experiments, and discuss what they mean.

Visibility

If a registry is public, should anyone be able to add to it? Does pub mean read or write access? On a related note, does pub use forward this read and/or write access?

An option that we considered is somehow applying two visibilities to a registry definition, or splitting it up in two parts:

#[global_registry(pub REGISTRY_ADDER)]
static ERROR_MSGS: Registry<&str>;

// in another crate:

// the adder is public so this is ok
register!(REGISTRY_ADDER, "b");

I think the only design that makes sense is that pub forwards both read and write access, if you want a read-only registry you can make it not public but provide a public getter function that returns the elements:

// private
#[global_registry]
static ERROR_MSGS: Registry<&str>;

// public getter
pub fn get_error_msgs() -> &Registry<&str> {
    &ERROR_MSGS
}

If you somehow want a write-only registry, you're out of luck.

Versioning

What happens if there are two different versions of a crate that defines a global registry in the dependency tree.

Imagine a crate graph like this:

cargo tree
a v0.1.0
├── b v0.1.0
│   └── c v0.1.0
└── c v0.2.0

c defines a global registry ERROR_MSGS. b adds some messages, and a reads them, getting access to the registy by importing a different version of c. The only possibility is that a does not see the items added by b.

You can already encounter this problem with the log crate right now. They did employ the semver trick to make it somewhat complicated to construct.

Semver

You've published a crate with a global registry definition collecting u32. You've made a mistake though, and would actually like it to collect u64 instead. Is there any way you can upgrade without it being a breaking change?

With global registration, everyone in the dependency tree of a crate has to agree on the type of the element in the registry. You could define a second registry, and deprecate the old one, and when you need the elements iterate over both the new old registry. However, existing crates that are already reading the global registry won't know that they now have to read two registries to make sure they get all the elements.

What you'd want is some way to communicate that all the elements in the old registry should be converted using some function into elements in the new registry. Later on in this blog post I briefly touch on Externally Implementable Items, for which m-ou-se's github avatar m-ou-se and I think we have solved this problem, but it won't easily transfer to global registration.

Compile time access

If the compiler implements this feature, why is the information only available at runtime? What stops us from collecting registered elements to a const like this?

// private
#[global_registry]
const ERROR_MSGS: Registry<&str>;

register!(ERROR_MSGS, "a");
register!(ERROR_MSGS, "b");

It would be rather convenient if you could for example sort the elements at compile time (ignoring problems with us not yet having traits in const fns). In fact, it seems rather likely that most uses of global registries will be hidden in macros. Like a #[test] attribute, or #[get("/")] on a server route. So why couldn't registries be a part of the macro machinery?

The answer to all this is quite simple, we can only know all the items in a registry once all crates in a dependency tree are compiled. At that point we can't go back and rerun const fns in other crates with the final list of items, and we definitely can't expand any macros in dependent crates anymore.

Registration in dependencies

Let's think of a usecase. A custom testing framework. The framework defines a registry of tests, and in your crate you add to it. Some dependency of yours uses the same version of the same test framework. When you run your tests, should the tests of the dependency also run?

If the registry is truly global, that's exactly what'd happen, but it's not at all how tests behave right now. Obviously, each crate should get its own crate-local registry of tests. In fact, I'm not sure there's ever a usecase where you genuinely want global registration. A benchmarking framework will also be crate-local, and the routes for a webserver probably as well. It can even be pretty confusing if some far far dependency accidentally (or even maliciously) injects some extra routes in your webserver.

So why did I initially implement inter-crate global registration? I thought that was the thing people wanted. For a little while I was afraid I'd misunderstood what people thought this feature meant, or implemented the wrong thing. But this is the design I sketched in my pre-rfc, where everyone generally agreed with it. Maybe that's because of the way I framed it. However, it's also just the version of global registration people are used to. That's what linkme and inventory provide.

In any case, I now believe that actually global registration is not something we should want.

An alternative design

A fact is that testing devex wants to make custom test frameworks a thing ( rust-lang/testing-devex-team#2 ), and I think it's safe to conclude that this same pattern is useful in other places too. Let's start from the beginning.

To support custom test frameworks, we at least want a way to register items from various modules within a single crate. So lets start with single-crate registries. That also resolves some problems with registry visibility, within a single crate it's less important who's allowed to add to a registry since you control all the code. In fact, now a registry could actually be a const and available during const evaluation, since within a crate that shouldn't really be an issue. Neat!

At this point we can also drop any issues related to registries and dynamic linking. Everything happens before and during const evaluation and dynamic linking isn't yet relevant.

Intercrate sometimes?

It's not true that there are no applications for inter-crate registries. Talking to some friends, we came up with some examples

  • A dependency might define some routes that you can just import.
  • epage's github avatar epage talked about wanting a dependency to register test fixtures.
  • Sometimes in embedded programming, the only actually runnable crate is your main crate (it contains the code to boot the processor). However, this main crate might then actually want to run test from dependencies as they wouldn't be able to boot on their own.
  • A dependency of yours defines some metrics, you want to register those, and some more of your own. You might also want to combine several lists of metrics from several dependencies.

Still, you want to limit the number of crates these items can come from. You don't want a random dependency's metrics, or server routes, or tests. A way to solve this, is to explicitly register them.

use other_crate::OTHER_ROUTES;

#[global_registry]
const MY_ROUTES: &[Route];

register_many!(MY_ROUTES, OTHER_ROUTES);

I think having this is pretty neat, as you can now register any const slice of elements.

Unsolved questions

I think that designing registries like this solves most of the complicated questions associated with the magic intercrate global registration that I implemented. Adding elements from different crates is possible, but much more explicit now. However, there are also some things I haven't completely worked out yet.

In truth, that's why I began writing this blogpost. This is roughly where I got to, and maybe someone else has a brilliant idea.

Stable identifier

I think it will be common to hide global registries in macros. From a user's perspective, it looks like tests are somewhat magically collected, and I actually think that's alright. So let's look at an example of what a custom test framework could look like:

use custom_test_framework::test_main;

fn main() {
    #[cfg(test)]
    test_main!()
}

#[cfg(test)]
mod tests {
    use custom_test_framework::custom_test;

    #[custom_test]
    fn test_a() {}

    #[custom_test]
    fn test_b() {}
}

I think this looks pretty neat, if this worked. Using #![feature(custom_test_frameworks)] I've written tests for embedded systems that look pretty much exactly like this. But where, in this example is the global registry defined? If test_main! defines it, with some name, how does #[custom_test] refer to it? test_main! is invoked in a function scope. Maybe you'd have a 3rd macro, that you'd have to put above main to define the registry.

use custom_test_framework::{test_main, init};

init!()

fn main() {
    #[cfg(test)]
    test_main!()
}

#[cfg(test)]
mod tests {
    use custom_test_framework::custom_test;

    #[custom_test]
    fn test_a() {}

    #[custom_test]
    fn test_b() {}
}

Actually, nowhere here we use the name of the global registry. That's actually fine hygiene-wise because top level items don't really have hygiene in Rust, but it'd probably give some weird errors when you call init! twice, or not at all. It's all a little unfortunate.

NonLocal DefIDs

Under this crate-local version of global registration, an implementation would probably look like a hashmap that maps from the DefId of the registry definition to a list of all the elements. This DefId key could in theory be any DefId. Also one that lives outside the current crate. In theory, this could be valid:

register!(custom_test_framework, some_test);

The identifier custom_test_framework, the name of our custom test framework crate is easy to refer to from anywhere in the program. Alternatively you could refer to some identifier in custom_test_framework. I think this is incredibly complicated to teach the community. Also, it can be confusing because if you bind a registry to an identifier in a different crate, that crate itself won't get access to the elements as registration will be crate-local.

Compile time collections

At this point, we're inventing a lot of new features that together gives users what's essentially a compile-time growable vector. You can add single const elements, concat const slices from different crates into one larger slice, and iterate over them. However, that's a completely new thing to Rust. Rust doesn't really have a compile-time only collection type, nor the ability to express that. Maybe that's an abstraction that we'd need first in Rust before implementing this feature, because right now there's a lot of compiler magic going on that's likely hard to teach to users of the language.

Externally Implementable Items

Many of the things I've written about here come from discussions with m-ou-se's github avatar m-ou-se , as we thought that a feature she was working on (Externally Implementable Items; EII rust-lang/rust#125418 ) would have a very similar implementation to global registration.

Basically, externally implementable items are like registering a single element in a global registry. Or worded differently, a global registry is like having the possibility to provide more than one externally implemented element.

In the end I think we worked out a rather neat design for EII (more info probably coming soon), but one that does not help implement global registration.

Conclusion

Well, that's where global registration stands now. I'm pretty sure we should not want the kind of inter-crate registration that I originally implemented. A more explicit importing of elements from other crates leads to fewer surprises. However, as you can see there are still some details that I'm not entirely sure about how to solve. Let me know if you have any ideas! Also, if you do have strong arguments for usecases which need truly global registration, I'd love to hear.

NO large language models were or are used in any of my open source contributions or anything I do. Not for proof-reading, not for trying out random ideas, not for generating code or text. LLMs and the companies that create and sell them destroy communities and the world around us. If you see something that I made online: that is me, and I take full responsibility for it.
]]>