From 51d3915dea6e8eae1b09576c859597ce7342d246 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Sat, 15 Dec 2018 20:35:57 -0700 Subject: [PATCH] static asserts --- book/src/01-quirks/04-newtype.md | 125 +++++++++++++----------- book/src/01-quirks/05-static_asserts.md | 114 +++++++++++++++++++++ book/src/SUMMARY.md | 1 + examples/bg_demo.rs | 2 +- examples/hello_world.rs | 72 ++++++++++---- examples/light_cycle.rs | 2 +- 6 files changed, 239 insertions(+), 77 deletions(-) create mode 100644 book/src/01-quirks/05-static_asserts.md diff --git a/book/src/01-quirks/04-newtype.md b/book/src/01-quirks/04-newtype.md index 07244c9..86b2916 100644 --- a/book/src/01-quirks/04-newtype.md +++ b/book/src/01-quirks/04-newtype.md @@ -40,19 +40,6 @@ with our newtypes. 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 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_. 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 -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 #[macro_export] @@ -74,8 +66,39 @@ macro_rules! newtype { } ``` -Except we also want to be able to add attributes (which includes doc comments), -so we upgrade our macro a bit: +The `#[macro_export]` makes it exported by the current module (like `pub` +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 #[macro_export] @@ -88,52 +111,44 @@ macro_rules! newtype { } ``` -And we want to automatically add the ability to turn the wrapper type back into -the wrapped type. +So now we can write ```rust -#[macro_export] -macro_rules! newtype { - ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { - $(#[$attr])* - #[repr(transparent)] - pub struct $new_name($old_name); - - impl From<$new_name> for $old_name { - fn from(x: $new_name) -> $old_name { - x.0 - } - } - }; +newtype! { + /// Color on the GBA gives 5 bits for each channel, the highest bit is ignored. + #[derive(Debug, Clone, Copy)] + PixelColor, u16 } ``` -That seems like enough for all of our examples, so we'll stop there. We could -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. +And that's about all we'll need for the examples. **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 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. + +## 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. diff --git a/book/src/01-quirks/05-static_asserts.md b/book/src/01-quirks/05-static_asserts.md new file mode 100644 index 0000000..845ede1 --- /dev/null +++ b/book/src/01-quirks/05-static_asserts.md @@ -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. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 6de4af7..08bf812 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -12,6 +12,7 @@ * [Fixed Only](01-quirks/02-fixed_only.md) * [Volatile Destination](01-quirks/03-volatile_destination.md) * [Newtype](01-quirks/04-newtype.md) + * [Static Asserts](01-quirks/05-static_asserts.md) * [Concepts](02-concepts/00-index.md) * [CPU](02-concepts/01-cpu.md) * [BIOS](02-concepts/02-bios.md) diff --git a/examples/bg_demo.rs b/examples/bg_demo.rs index c18a00c..3fa219f 100644 --- a/examples/bg_demo.rs +++ b/examples/bg_demo.rs @@ -1,5 +1,5 @@ -#![feature(start)] #![no_std] +#![feature(start)] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 8cd4270..87e6a1c 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,18 +1,65 @@ -#![feature(start)] #![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] fn panic(_info: &core::panic::PanicInfo) -> ! { 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 = 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 = 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] fn main(_argc: isize, _argv: *const *const u8) -> isize { unsafe { - DISPCNT.write(MODE3 | BG2); - mode3_pixel(120, 80, rgb16(31, 0, 0)); - mode3_pixel(136, 80, rgb16(0, 31, 0)); - mode3_pixel(120, 96, rgb16(0, 0, 31)); + DISPLAY_CONTROL.write(JUST_MODE3_AND_BG2); + Mode3::draw_pixel_unchecked(120, 80, rgb(31, 0, 0)); + Mode3::draw_pixel_unchecked(136, 80, rgb(0, 31, 0)); + Mode3::draw_pixel_unchecked(120, 96, rgb(0, 0, 31)); loop {} } } @@ -31,18 +78,3 @@ impl VolatilePtr { VolatilePtr(self.0.wrapping_offset(count)) } } - -pub const DISPCNT: VolatilePtr = 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); -} diff --git a/examples/light_cycle.rs b/examples/light_cycle.rs index c200663..c593c19 100644 --- a/examples/light_cycle.rs +++ b/examples/light_cycle.rs @@ -1,5 +1,5 @@ -#![feature(start)] #![no_std] +#![feature(start)] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! {