Don’t get me wrong, I love Rust, but what’s wrong with it? Let’s find out together. A journey into compile-time execution.

Szymon Walter
Nov 7 · 4 min read
The rust logo
The rust logo
The Rust language logo

My motivation for this article was an article I stumbled upon. It’s about making a cerrtain low-level hardware interface statically safe by exploiting the Rust type system to the fullest. Here’s a link to that article:

The article covers how one can use the typenum crate to use compile-time integers to generate more compile-time integers and validate that they have certain properties, like “only a 1 is used as this parameter”, or “only 1 or 3 is used as this parameter”. I don’t mean to tell you that validating things like that is wrong, and if you can validate this at compile time, it’s even better! Compile-time errors are always better than runtime-errors.

In this article I will go step by step through the same steps as dan pittman (the author of the article linked above), @pittma_ on Twitter, did and show how I would do them in a theoretical language that is similar to Rust, but is capable of some things that Rust isn’t.

Important: I don’t mean to bash on Dan or Rust. What he implemented using the Rust typesystem is incredibly clever and useful. This is simply my take on the same problem.


Imagine the following 8-bit register:

+----------+------+-----------+---------+
| (unused) | Kind | Interrupt | Enabled |
+----------+------+-----------+---------+
5-8(sic!) 2-4 1 0

So we want a function of three parameters that returns an 8-bit register with the top 3-bits unused (bits 5–7 inclusive), the next 3 set to the a 3-bit value (bits 2–4 inclusive), bit 1 set to the boolean value Interrupt and bit 0 set to the boolean value Enabled .

A solution immediately presents itself using bitfields. This way the compiler aids us at compile in setting the fields of the 8-bit register.

struct Flags {
unsigned enabled : 1;
unsigned interrupt: 1;
unsigned kind : 3;
unsigned unused : 3;
};

Using this approach we get the ability to set the flags individually or query the register as a whole.

But wait… Rust doesn’t have bitfields. And unions are unsafe . So we do need a function to do this after all. Here’s my function that calculates that register in Rust.

fn register(kind: u8, interrupt: u8, enabled: u8) -> u8 {
((kind << 2) & (7 << 2))
| ((interrupt << 1) & (1 << 1))
| (enabled & 1)
}

Although it guarantees that the result will be correct, it doesn’t guarantee that the parameters passed are correct. It simply ignores any erroneous arguments! If only Rust had a u3and a u1type… It does have the bool type, which we could use to substitute u1. And we could make our own u3 type.

struct u3(u8);

This guarantees at runtime that u3 only ever contains a 3-bit value. But using Option suddenly forces us to handle errors, at runtime! Isn’t this something we can handle at compile time? Imagine we could write a function that validates it’s arguments at compile time. Kind of like this:

fn register(
kind: lit<u8>,
interrupt: lit<u8>,
enabled: lit<u8>,
) -> u8 {
#[comptime]
assert!(kind & !7 == 0);
#[comptime]
assert!(interrupt & !1 == 0);
#[comptime]
assert!(enabled & !1 == 0);

By introducing the concept of a literals lit<u8> and compile-time #[comptime] statement, we achieved our goal with way less code than needed! Alas, Rust has neither of these features. The closest Rust comes to this is using const fn which could achieve similar results, however this features is still unfinished. This is where my theoretical language comes in. In fact, it’s not just theoretical. It’s a real language that is in development right now and has things like this planned or already started. Here’s how the register function would look like in &, the language that I’m developing right now.

#[comptime]
fn register: (
kind: Lit(T: uchar),
interrupt: Lit(T: uchar),
enabled: Lit(T: uchar),
) -> uchar = {
if kind.value & ~7 != 0 {
error! "`kind` must be a 3-bit literal";
}
if interrupt.value & ~1 != 0 {
error! "`interrupt` must be a 1-bit literal";
}
if enabled.value & ~1 != 0 {
error! "`enabled` must be a 1-bit literal";
}

register is marked as a #[comptime] function, which means it will execute at compile time and never get translated into machine-code. The generic type Lit is a special type that restricts the type of a parameter to a literal (like the integer 42 or the string literal "hello, world" ) of a certain type. The values of the literals are asserted to have certain properties and only after these pass, a value (calculated at compile-time) is returned.

The first ever release of & is currently planned on Tuesday the 12th of November. You can follow the development of the compiler on Gitlab at https://gitlab.com/ampersand-lang/etc. Do note that the comptime and other features are unfinished and the language as a whole is still in a highly experimental phase.

Make sure to also follow Dan Pittman on Twitter and read his blog entries at https://blog.auxon.io. They’re quite fascinating.

Szymon Walter

Written by

Computer Science student | Systems programming enthusiast | Functional freak

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade