static asserts

This commit is contained in:
Lokathor 2018-12-15 20:35:57 -07:00
parent 7cdcc02aaf
commit 51d3915dea
6 changed files with 239 additions and 77 deletions

View file

@ -40,19 +40,6 @@ with our newtypes.
pub struct PixelColor(u16); pub struct PixelColor(u16);
``` ```
Ah, and of course we'll need to make it so you can unwrap the value:
```rust
#[repr(transparent)]
pub struct PixelColor(u16);
impl From<PixelColor> for u16 {
fn from(color: PixelColor) -> u16 {
color.0
}
}
```
And then we'll need to do that same thing for _every other newtype we want_. And then we'll need to do that same thing for _every other newtype we want_.
Except there's only two tiny parts that actually differ between newtype Except there's only two tiny parts that actually differ between newtype
@ -62,7 +49,12 @@ a job for a macro to me!
## Making It A Macro ## Making It A Macro
The most basic version of the macro we want goes like this: If you're going to do much with macros you should definitely read through [The
Little Book of Rust
Macros](https://danielkeep.github.io/tlborm/book/index.html), but we won't be
doing too much so you can just follow along here a bit if you like.
The most basic version of a newtype macro starts like this:
```rust ```rust
#[macro_export] #[macro_export]
@ -74,8 +66,39 @@ macro_rules! newtype {
} }
``` ```
Except we also want to be able to add attributes (which includes doc comments), The `#[macro_export]` makes it exported by the current module (like `pub`
so we upgrade our macro a bit: kinda), and then we have one expansion option that takes an identifier, a `,`,
and then a second identifier. The new name is the outer type we'll be using, and
the old name is the inner type that's being wrapped. You'd use our new macro
something like this:
```rust
newtype! {PixelColorCurly, u16}
newtype!(PixelColorParens, u16);
newtype![PixelColorBrackets, u16];
```
Note that you can invoke the macro with the outermost grouping as any of `()`,
`[]`, or `{}`. It makes no particular difference to the macro. Also, that space
in the first version is kinda to show off that you can put white space in
between the macro name and the grouping if you want. The difference is mostly
style, but there are some rules and considerations here:
* If you use curly braces then you _must not_ put a `;` after the invocation.
* If you use parentheses or brackets then you _must_ put the `;` at the end.
* Rustfmt cares which you use and formats accordingly:
* Curly brace macro use mostly gets treated like a code block.
* Parentheses macro use mostly gets treated like a function call.
* Bracket macro use mostly gets treated like an array declaration.
## Upgrade That Macro!
We also want to be able to add `derive` stuff and doc comments to our newtype.
Within the context of `macro_rules!` definitions these are called "meta". Since
we can have any number of them we wrap it all up in a "zero or more" matcher.
Then our macro looks like this:
```rust ```rust
#[macro_export] #[macro_export]
@ -88,52 +111,44 @@ macro_rules! newtype {
} }
``` ```
And we want to automatically add the ability to turn the wrapper type back into So now we can write
the wrapped type.
```rust ```rust
#[macro_export] newtype! {
macro_rules! newtype { /// Color on the GBA gives 5 bits for each channel, the highest bit is ignored.
($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { #[derive(Debug, Clone, Copy)]
$(#[$attr])* PixelColor, u16
#[repr(transparent)]
pub struct $new_name($old_name);
impl From<$new_name> for $old_name {
fn from(x: $new_name) -> $old_name {
x.0
}
}
};
} }
``` ```
That seems like enough for all of our examples, so we'll stop there. We could And that's about all we'll need for the examples.
add more things:
* Making the `From` impl being optional. We'd have to make the newtype
invocation be more complicated somehow, the user puts ", no-unwrap" after the
inner type declaration or something, or something like that.
* Allowing for more precise visibility controls on the wrapping type and on the
inner field. This would add a lot of line noise, so we'll just always have our
newtypes be `pub`.
* Allowing for generic newtypes, which might sound silly but that we'll actually
see an example of soon enough. To do this you might _think_ that we can change
the `:ident` declarations to `:ty`, but since we're declaring a fresh type not
using an existing type we have to accept it as an `:ident`. The way you get
around this is with a proc-macro, which is a lot more powerful but which also
requires that you write the proc-macro in an entirely other crate that gets
compiled first. We don't need that much power, so for our examples we'll go
with the macro_rules version and just do it by hand in the few cases where we
need a generic newtype.
* Allowing for `Deref` and `DerefMut`, which usually defeats the point of doing
the newtype, but maybe sometimes it's the right thing, so if you were going
for the full industrial strength version with a proc-macro and all you might
want to make that part of your optional add-ons as well the same way you might
want optional `From`. You'd probably want `From` to be "on by default" and
`Deref`/`DerefMut` to be "off by default", but whatever.
**As a reminder:** remember that `macro_rules` macros have to appear _before_ **As a reminder:** remember that `macro_rules` macros have to appear _before_
they're invoked in your source, so the `newtype` macro will always have to be at they're invoked in your source, so the `newtype` macro will always have to be at
the very top of your file, or if you put it in a module within your project the very top of your file, or if you put it in a module within your project
you'll need to declare the module before anything that uses it. you'll need to declare the module before anything that uses it.
## Potential Homework
If you wanted to keep going and get really fancy with it, you could potentially
add a lot more:
* Make a `pub const fn new() -> Self` method that outputs the base value in a
const way. Combine this with builder style "setter" methods that are also
const and you can get the compiler to do quite a bit of the value building
work at compile time.
* Making the macro optionally emit a `From` impl to unwrap it back into the base
type.
* Allow for visibility modifiers to be applied to the inner field and the newly
generated type.
* Allowing for generic newtypes. You already saw the need for this once in the
volatile section. Unfortunately, this particular part gets really tricky if
you're using `macro_rules!`, so you might need to move up to a full
`proc_macro`. Having a `proc_macro` isn't bad except that they have to be
defined in a crate of their own and they're compiled before use. You can't
ever use them in the crate that defines them, so we won't be using them in any
of our single file examples.
* Allowing for optional `Deref` and `DerefMut` of the inner value. This takes
away most all the safety aspect of doing the newtype, but there may be times
for it. As an example, you could make a newtype with a different form of
Display impl that you want to otherwise treat as the base type in all places.

View file

@ -0,0 +1,114 @@
# Static Asserts
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 [static assertions](https://docs.rs/static_assertions/).
This is an amazing crate that you should definitely use when you can.
It's written by [nvzqz](https://github.com/nvzqz), and they kindly wrote up a
[blog
post](https://nikolaivazquez.com/posts/programming/rust-static-assertions/) 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
```rust
const ASSERT: usize = 0 - 1;
```
that gives a warning, since the math would underflow. We can upgrade that
warning to a hard error:
```rust
#[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 give each such const an underscore for a
name.
```rust
#![feature(underscore_const_names)]
#[deny(const_err)]
const _: usize = 0 - 1;
```
Now we wrap this in a macro where we give an expression for a bool. 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.
```rust
macro_rules! const_assert {
($condition:expr) => {
#[deny(const_err)]
#[allow(dead_code)]
const ASSERT: usize = 0 - !$condition as usize;
}
}
```
This allows anything which supports `core::ops::Not` and can then can cast into
`usize`, which technically isn't just `bool` values, but close enough.
## 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.
```rust
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.
```rust
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
```rust
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.

View file

@ -12,6 +12,7 @@
* [Fixed Only](01-quirks/02-fixed_only.md) * [Fixed Only](01-quirks/02-fixed_only.md)
* [Volatile Destination](01-quirks/03-volatile_destination.md) * [Volatile Destination](01-quirks/03-volatile_destination.md)
* [Newtype](01-quirks/04-newtype.md) * [Newtype](01-quirks/04-newtype.md)
* [Static Asserts](01-quirks/05-static_asserts.md)
* [Concepts](02-concepts/00-index.md) * [Concepts](02-concepts/00-index.md)
* [CPU](02-concepts/01-cpu.md) * [CPU](02-concepts/01-cpu.md)
* [BIOS](02-concepts/02-bios.md) * [BIOS](02-concepts/02-bios.md)

View file

@ -1,5 +1,5 @@
#![feature(start)]
#![no_std] #![no_std]
#![feature(start)]
#[panic_handler] #[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { fn panic(_info: &core::panic::PanicInfo) -> ! {

View file

@ -1,18 +1,65 @@
#![feature(start)]
#![no_std] #![no_std]
#![feature(start)]
#![feature(underscore_const_names)]
#[macro_export]
macro_rules! newtype {
($(#[$attr:meta])* $new_name:ident, $old_name:ident) => {
$(#[$attr])*
#[repr(transparent)]
pub struct $new_name($old_name);
};
}
#[macro_export]
macro_rules! const_assert {
($condition:expr) => {
#[deny(const_err)]
#[allow(dead_code)]
const _: usize = 0 - !$condition as usize;
};
}
#[panic_handler] #[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {} loop {}
} }
newtype! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Color, u16
}
pub const fn rgb(red: u16, green: u16, blue: u16) -> Color {
Color(blue << 10 | green << 5 | red)
}
newtype! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
DisplayControlSetting, u16
}
pub const DISPLAY_CONTROL: VolatilePtr<DisplayControlSetting> = VolatilePtr(0x04000000 as *mut DisplayControlSetting);
pub const JUST_MODE3_AND_BG2: DisplayControlSetting = DisplayControlSetting(3 + 0b100_0000_0000);
pub struct Mode3;
impl Mode3 {
const SCREEN_WIDTH: isize = 240;
const PIXELS: VolatilePtr<Color> = VolatilePtr(0x600_0000 as *mut Color);
pub unsafe fn draw_pixel_unchecked(col: isize, row: isize, color: Color) {
Self::PIXELS.offset(col + row * Self::SCREEN_WIDTH).write(color);
}
}
#[start] #[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize { fn main(_argc: isize, _argv: *const *const u8) -> isize {
unsafe { unsafe {
DISPCNT.write(MODE3 | BG2); DISPLAY_CONTROL.write(JUST_MODE3_AND_BG2);
mode3_pixel(120, 80, rgb16(31, 0, 0)); Mode3::draw_pixel_unchecked(120, 80, rgb(31, 0, 0));
mode3_pixel(136, 80, rgb16(0, 31, 0)); Mode3::draw_pixel_unchecked(136, 80, rgb(0, 31, 0));
mode3_pixel(120, 96, rgb16(0, 0, 31)); Mode3::draw_pixel_unchecked(120, 96, rgb(0, 0, 31));
loop {} loop {}
} }
} }
@ -31,18 +78,3 @@ impl<T> VolatilePtr<T> {
VolatilePtr(self.0.wrapping_offset(count)) VolatilePtr(self.0.wrapping_offset(count))
} }
} }
pub const DISPCNT: VolatilePtr<u16> = VolatilePtr(0x04000000 as *mut u16);
pub const MODE3: u16 = 3;
pub const BG2: u16 = 0b100_0000_0000;
pub const VRAM: usize = 0x06000000;
pub const SCREEN_WIDTH: isize = 240;
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
blue << 10 | green << 5 | red
}
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
VolatilePtr(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write(color);
}

View file

@ -1,5 +1,5 @@
#![feature(start)]
#![no_std] #![no_std]
#![feature(start)]
#[panic_handler] #[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { fn panic(_info: &core::panic::PanicInfo) -> ! {