In the world of systems programming where one may find themselves writing hardware drivers or interacting directly with memory-mapped devices, that interaction is almost always through memory-mapped registers provided by the hardware. We typically interact with these things through bitwise operations on some fixed-width numeric type.

For instance, imagine an 8-bit register with three fields:

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

The number below the field name prescribes the bits used by that field in the register. To enable this register, one would write the value 1 , represented in binary as 0000_0001 , to set the enabled field’s bit. Often, though, we also have an existing configuration in the register that we don’t want to disturb. Say we want to enable interrupts on our device above but also want to be sure that the device itself remains enabled. To do that, we must combine the Interrupt field’s value with the Enabled field’s value. We’d do that with bitwise operations:

1 | (1 << 1)

This gives us the binary value 0000_0011 by or -ing 1 with 2, which we get by shifting 1 left by 1. We can write this to our register, leaving it enabled but also enabling interrupts.

This is a lot to keep in our heads, especially when dealing with potentially 100s of registers for a complete system. In practice, we do this with mnemonics which track a field’s position in a register and how wide that field is—i.e. what’s its upper bound?

Here’s an example of one of these mnemonics, they’re C macros that replace their occurrences with the code on the right-hand side. This is our shorthand for the register we laid out above. The left-hand side of the & puts us in position for that field and the right-hand side limits us to only that field’s bits.

We’d then use these to abstract over the derivation of a register’s value with something like:

And this is the state of the art, really. In fact, this is how the bulk of the drivers appear in the Linux kernel.

Is there a better way? Consider the boon to safety and expressibility if our type system was one borne out of modern programming languages research. That is, what could we do with a richer, more expressive type system to make this process safer and more tenable?

Decoding Failure Depending on your personal pain threshold you may have noticed that the errors are nearly unintelligible. Let’s look at a not-so-subtle reminder of what I’m talking about: error[E0271]: type mismatch resolving `<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>, typenum::B0> as typenum::IsLessOrEqual<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>>>::Output == typenum::B1` --> src/main.rs:12:5 | 12 | less_than_ten::<U20>(); | ^^^^^^^^^^^^^^^^^^^^ expected struct `typenum::B0`, found struct `typenum::B1` | = note: expected type `typenum::B0` found type `typenum::B1` The expected typenum::B0 found typenum::B1 part kind of makes sense, but what on Earth is the typenum::UInt<typenum::UInt, typenum::UInt... nonsense? Well, typenum represents numbers as binary cons cells! Errors like this make it hard, especially when you have several of these type-level numbers confined to tight quarters, to know which number it’s even talking about. Unless, of course, it’s second nature for you to translate baroque binary representations to decimal ones. After the U100 th time attempting to decipher any meaning from this mess, a teammate got Mad As Hell And Wasn’t Going To Take It Anymore and made a little utility, tnfilt , to parse the meaning out from the misery that is namespaced binary cons cells. tnfilt takes the cons cell-style notation and replaces it with sensible decimal numbers. So, along with bounded-registers we’d like to also share tnfilt . You use it like this: $ cargo build 2>&1 | tnfilt It transforms the output above into something like this: error[E0271]: type mismatch resolving `<U20 as typenum::IsLessOrEqual<U10>>::Output == typenum::B1` Now that makes sense!