Constant Assertions
Have you ever wanted to assert things even before runtime? We all have, of course. Particularly when the runtime machine is a poor little GBA, we'd like to have the machine doing the compile handle as much checking as possible.
Enter the static assertions crate, which
provides a way to let you assert on a const
expression.
This is an amazing crate that you should definitely use when you can.
It's written by Nikolai Vazquez, and they kindly wrote up a blog post that explains the thinking behind it.
However, I promised that each example would be single file, and I also promised to explain what's going on as we go, so we'll briefly touch upon giving an explanation here.
How We Const Assert
Alright, as it stands (2018-12-15), we can't use if
in a const
context.
Since we can't use if
, we can't use a normal assert!
. Some day it will be
possible, and a failed assert at compile time will be a compile error and a
failed assert at run time will be a panic and we'll have a nice unified
programming experience. We can add runtime-only assertions by being a little
tricky with the compiler.
If we write
# #![allow(unused_variables)] #fn main() { const ASSERT: usize = 0 - 1; #}
that gives a warning, since the math would underflow. We can upgrade that warning to a hard error:
# #![allow(unused_variables)] #fn main() { #[deny(const_err)] const ASSERT: usize = 0 - 1; #}
And to make our construction reusable we can enable the underscore_const_names feature in our program (or library) and then give each such const an underscore for a name.
# #![allow(unused_variables)] #![feature(underscore_const_names)] #fn main() { #[deny(const_err)] const _: usize = 0 - 1; #}
Now we wrap this in a macro where we give a bool
expression as input. We
negate the bool then cast it to a usize
, meaning that true
negates into
false
, which becomes 0usize
, and then there's no underflow error. Or if the
input was false
, it negates into true
, then becomes 1usize
, and then the
underflow error fires.
# #![allow(unused_variables)] #fn main() { macro_rules! const_assert { ($condition:expr) => { #[deny(const_err)] #[allow(dead_code)] const ASSERT: usize = 0 - !$condition as usize; } } #}
Technically, written like this, the expression can be anything with a
core::ops::Not
implementation that can also be as
cast into usize
. That's
bool
, but also basically all the other number types. Since we want to ensure
that we get proper looking type errors when things go wrong, we can use
($condition && true)
to enforce that we get a bool
(thanks to Talchas
for
that particular suggestion).
# #![allow(unused_variables)] #fn main() { macro_rules! const_assert { ($condition:expr) => { #[deny(const_err)] #[allow(dead_code)] const _: usize = 0 - !($condition && true) as usize; } } #}
Asserting Something
As an example of how we might use a const_assert
, we'll do a demo with colors.
There's a red, blue, and green channel. We store colors in a u16
with 5 bits
for each channel.
# #![allow(unused_variables)] #fn main() { newtype! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] Color, u16 } #}
And when we're building a color, we're passing in u16
values, but they could
be using more than just 5 bits of space. We want to make sure that each channel
is 31 or less, so we can make a color builder that does a const_assert!
on the
value of each channel.
# #![allow(unused_variables)] #fn main() { macro_rules! rgb { ($r:expr, $g:expr, $b:expr) => { { const_assert!($r <= 31); const_assert!($g <= 31); const_assert!($b <= 31); Color($b << 10 | $g << 5 | $r) } } } #}
And then we can declare some colors
# #![allow(unused_variables)] #fn main() { const RED: Color = rgb!(31, 0, 0); const BLUE: Color = rgb!(31, 500, 0); #}
The second one is clearly out of bounds and it fires an error just like we wanted.