Assumed Audience: Hackers and anyone in the software industry who cares.

Discuss on Hacker News and Reddit.

Epistemic Status: Confident that Rust isn’t good everywhere, but not confident that people won’t try to use it where it does not fit.

So let me say upfront that I think Rust is a great step forward for the industry in general. It’s great, and if you like it, great. If it fits the job at hand, great. Keep using Rust.

This post is about everything else. Because I hope Rust isn’t used in everything.

Okay, obviously, it won’t be used in everything, but what I mean is that I hope Rust doesn’t become the only choice for systems programming and/or application programming because I hate writing in Rust, but I love application and systems programming.

Sure, I’m okay if people write stuff in Rust. But if you force me to use Rust, either by law or shame or cancellation or starving other languages, then I will go firebird. Dishonor on you! Dishonor on your cow!

Don’t let Rust infect everything. Don’t let the culture of the programming industry shift towards Rust or bust. If you make your choice between alternatives purely by if one was written in Rust, you are the problem.

Rust does not automatically make things better. It should be used where it can make things better, but not any more than that.

In other words, a programming language monoculture would be disastrous for our industry. Don’t let it happen.


Okay, post is over. I have said the important bit.

It’s time to rant. Leave now if you wish.

So why do I hate Rust?

First, the syntax. It’s ugly. TO MY EYES! Put down the tar! I’m not the only one who thinks so!

Syntax is the UI of a programming language, and I would prefer something that is less sigil-heavy.

Second, the async fracture. Non-async Rust is essentially a second-class citizen. This is bad for me because I don’t like async. I just don’t; my head is not built for it.

Third, the second async fracture. If you are using async, you’re probably using tokio. Crates support tokio, but not other executors like smol, despite the promise that Rust async would be agnostic to executors.

Rust’s designers forgot about Hyrum’s Law, the destroyer of library agnoticism.

I like small programs with few dependencies. I will write my own I/O to avoid dependencies. And tokio is to dependencies what god objects are to object-oriented programming.

Fourth, Rust is complicated! And this is mostly because of async.

Despite my lack of Rustacean credentials, I read without.boats and Baby Steps, and whenever they start talking about async Rust, I just can’t understand it. And remember, I picked apart async Zig!

People say C is complicated, but actually, C is very simple. People say it is hard to avoid UB in C, and that is true, but there is an incomplete list (and C23 will get a more comprehensive one). And I can actually read the entire standard and understand it.

With async Rust, it’s essentially “whatever rustc does,” and that is no better than a black box. Even if the Rust specification happens, I suspect that it will be about as long as the C++ spec, and most of it will be about async in some way (minus the stuff defining the standard library).

You can quote me on that prediction, and I’ll eat my words if it turns out otherwise.

Look, I get lifetimes and shared^mut; I implemented a form of them in C. But I just don’t get async Rust.

This could have been avoided if Rust’s designers hadn’t tried to make a language that could be everything to everybody. They tried to allow multiple executors, and they tried to get maximum performance. Yes, they succeeded at those two goals, but when those goals come at the cost of having an unreasonable programming language, I think they become false goals.

Fifth, despite all that complexity, async Rust is still viral.

Sixth, Rust doesn’t go far enough in static analysis. Rust had a chance to go much further, and the team blew it.

Seventh, Rust compile times are a joke. Yeah, I’ll be that blunt.

I run Gentoo. Gentoo builds from source.

When I see a Rust project in the list of out-of-date packages, I sigh and only send off the build when I’m about to go to bed.

This is acceptable for me when it comes to my browser. Yes, that behemoth should use Rust, and I’m glad it does. There are others too.

But there are some Rust packages that convince me that the devil exists.

I’ll call out one in particular: Starship.

Starship is nice. I love it when it’s running.

To see it in action on my machine, look at the asciinema examples here.

But when it’s building, I feel like Dr. House without his pills.

Gentoo runs a release build. Guess how long that takes?

If you said 15 minutes, you are right, but only for building just the main.rs file! There’s another 10 or so minutes for lib.rs, not to mention everything else.

In contrast, my monorepo does a full release build in less than 30 seconds.

“That doesn’t sound too good, Gavin.”

Ah, my bad; I meant that it does a full bootstrap in less than 30 seconds.

The monorepo contains its own build system, so it needs to bootstrap that. And it doesn’t just do the minimal bootstrap, either; it builds the full thing once in a development build with “debug code,” then again using a release build, and then for a third time with a release build.

That first build is easy; it should be fast, and it is. It uses a simple C program with hardcoded file lists to build from scratch.

But the version it builds has what I call “debug code.”

“Is that just asserts, Gavin?”

It includes asserts, yes, but that’s not all it is.

I have specific code that is meant to check preconditions, postconditions, and invariants. Some of that code is thorough, so thorough that it is often O(n), or even O(n^2).

My build system is basically O(n) in the number of targets, but when it has debug code compiled in, the whole thing becomes O(n^2).

That is what is running the second stage of the bootstrap. In a development build with no optimizations!

“Why would you do that, Gavin?

Because that is the best sanity check for my code: debug code with zero optimizations to avoid getting plastered by 00UB.

A full release build, just a release build, while using a release build, takes about 8 seconds. Even if we doubled that to 16 (because there is a release build and a development build), that middle build is only taking about 15 seconds.

For 193 files.

I’ve already said how bad Starship is for a release build, but what about development builds?

It takes 1:30 to run a clean development build on Starship.

“That’s not so bad…”

You say that after saying that a 30 second full bootstrap build isn’t too good?

But regardless, 90 seconds is not good when compared with a 4 second clean development build on my monorepo. That’s over 20 times as long!

What about incremental builds? Change just one Starship file, main.rs, and it takes 8.8 seconds. Change one file in my monorepo, and it takes 0.5 seconds. Still about 17 times longer.

And I haven’t even mentioned the worst part: I abuse the preprocessor.

I have one file, include/yc/arith.h, that uses macros to generate safe arithmetic functions.

I use those functions to avoid undefined behavior in arithmetic.

The file I changed, src/urdo/main.c, is about 2300 lines. But it includes arith.h, so it expands to over 27k LoC and nearly a megabyte. Starship’s main.rs, in contrast, is only 273 lines and 9k bytes.

Yes, C++ expands to so much more, and yes, I’m not using outside libraries.

“Gavin, Rust has macros and stuff to make it powerful. And macros expand to a lot of code.”

C’mon! Y’all should have taken C++ as a warning, not an example.

Okay, macros give you power, but just stop and think: is there really no way to have that kind of power without requiring xkcd compilation?

Here’s the truth: there is a way.

My build system uses a language of my own design called Yao. Yao has a way for clients to add keywords. It also has a way for clients to change how some code is lexed.

With that, I can implement special keywords. Rig uses one to define targets:

target stuff.o: stuff.c
{
	$ clang $CFLAGS -o stuff.o stuff.c;
}

Do you see that line beginning with $? That’s a shell invocation. That’s what changing the lexer gives you. I can even embed JSON:

j := @(json){
	"key1": "data",
	"key2": 1.0,
	"key3": true
};

So Yao is as powerful as Rust.

“I bet you’re not using many keywords/macros in your Yao code, Gavin.”

Actually, I am. Yao has zero primitives; even if, while, and foreach are implemented as user-defined keywords. Every keyword is, including return and fn (to define functions). I use tons of “macros” because there literally isn’t any way to avoid them.

And yet, Yao compiles fast.

How fast? A null build (nothing to do) on Starship takes cargo build 0.2 seconds. A null build on my monorepo takes 0.1 seconds. That compiles and runs 1800 lines of Yao code. And that doesn’t even include parsing another three config files that aren’t even in Yao.

“Three config files? Yikes!”

One actual config file, one immutable build database file, and one mutable build database file that is updated on every build.

And those numbers still include all asserts besides the ones in debug code.

My eventual goal is that Yao can compile 1M LoC per second. There are languages that can because computers are fast.

How fast is 1M LoC/s? You could compile every keystroke at 120 wpm (720 cpm) on a 10k LoC file, and still have a little spare time between each keystroke!

Hm…if it can compile on every keystroke, could you also add support for the Language Server Protocol (LSP) to the compiler and just use the compiler as the LSP server?

Why, yes, you can!

My Yao compiler already sets some data on every token; an LSP server could just iterate over the resulting token list and send that info to the client. And the compile errors would be correct as well because it was the compiler that generated them!

In other words, if compilation is fast, the tooling is easy.

Rust, on the other hand, has to have a separate lazy compiler to support LSP, a compiler so complicated that it needs a 21 video playlist to explain its architecture.

Rust: No, a compiler can't be an LSP server! Yao: Haha compiler go brrr

Even people who know better make this mistake!

Anders Hejlsberg, creator of Turbo Pascal, and who should know a thing or two about how to make fast compilers and how much people love them, made a video at Microsoft talking about how you have to make compilers lazy to make them responsive enough.

I think the problem is that he was working at Microsoft and forgot how fast computers are.

Well, if he had made C# compile at 1M LoC/s, there would have been no need for that.

“But Gavin! You need to make compilers lazy so that they can handle incomplete code!”

No, you don’t. Clang and GCC will return multiple errors, so they already need to be robust in the face of errors. Yao’s compiler also returns multiple errors.

The compiler is not robust yet, however.

Anyway, rant over.

Don’t let Rust oxidize everything. Monoculture bad.