Compile-time assertions

Sometimes, you want to add checks to ensure that your constants are protected from typos or other logic errors. Here is how to implement compile-time assertions in Rust.

An example

Here’s a snippet from the smol_str crate that demonstrates a neat trick:

				
					const _: () = {
    assert!(WS.len() == N_NEWLINES + N_SPACES);
    assert!(WS.as_bytes()[N_NEWLINES - 1] == b'\n');
    assert!(WS.as_bytes()[N_NEWLINES] == b' ');
};
				
			

These 5 lines of code define some assertions within a block, then assign the result of that block to a constant. They’re checking that the three constants that are defined earlier in the code, WS (short for whitespace), N_NEWLINES, and N_SPACES interact in the intended way.

While it’s syntactically noisy, using an empty constant like this forces the compiler to invoke the assertions at compile time.

What's going on

How does it work? Defining a constant has property of triggering evaluation at compile time. Because blocks return values, a constant can be defined as the result of a block. Assertions suffixed with the semi-colon return unit, which means the constant itself is of type unit.

But why all of the syntactic noise? const _: () = { ... }; is quite busy just to define a few assertions.

In line 1, a constant is defined with the name _ (underscore). This constant is assigned the value of the block that follows.

All constants require type hints, which explains why : () is added to the right-hand side of th underscore.

The block contains assertions, but because the last expression is finished with a semi-colon, the block itself returns unit.

Why an underscore? Defining a variable or constant with a leading underscore tells the Rust compiler that we’ll never actually need the value, which silences any unused variable warnings. However, Rust will go to the trouble of creating it. It assumes that you really intended to create something that you don’t end up actually use.

What it looks like when assertions fail

One of the issues that you may encounter with this pattern is that the error messages can be slightly obtuse. If you run it in the playground, you’ll receive error[E0080]: evaluation of constant value failed, rather than anything about asserts.

				
					const _: () = {
    assert!("assertions go here".is_empty())
};

fn main() {
    println!("");
}

				
			
Here is the full error message:
				
					  Compiling playground v0.0.1 (/playground)
error[E0080]: evaluation of constant value failed
 --> src/main.rs:2:5
  |
2 |     assert!("assertions go here".is_empty())
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'assertion failed: "assertions go here".is_empty()', src/main.rs:2:5
  |
  = note: this error originates in the macro `assert` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0080`.
error: could not compile `playground` (bin "playground") due to 1 previous error

				
			

Further reading

The Rust Reference’s chapter on Constant Evaluation provides a detailed discussion of the topic.

Once you’re familiar with the approach suggested here, the Oro OS kernel takes this idea much further. In its oro-common-assertions module, you’ll see very advanced used of unsafe traits, associated constants, and functions that implement those traits (Thanks to Oro OS creator Josh Junon for the link).

Oh, if you like watching videos, you may also enjoy Oli Schneider’s talk from Rust Conf 2019:

Contents