Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rust: improve static_assert! #269

Open
wants to merge 1 commit into
base: rust
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 166 additions & 3 deletions rust/kernel/static_assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,198 @@

/// Static assert (i.e. compile-time assert).
///
/// Similar to C11 [`_Static_assert`] and C++11 [`static_assert`].
/// There are several forms of this macro:
///
/// - Boolean assertion: `expr`.
/// - Set membership assertion: `(expr) is in {a0, ..., aN}`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pascal has an in operator. If we want to follow precedent, perhaps we could drop is and just have expr in {a0, ..., aN}.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Python too (as well as the not in). I am happy either way!

/// - Interval membership assertion: `(expr) is in [min, max]`.
/// - Fits-in-type assertion: `(expr) fits in type`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I saw this, the first thing I thought was whether a type could be 'transmuted' into another. For example, whether *const T fits in usize. Could we support something like this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a comparison of the sizes? Or something more?

I wonder if it is best to leave that to utilities on their own (e.g. like transmute checks for the size).

///
/// The expressions in all the forms are evaluated in [const context].
///
/// [const context]: https://doc.rust-lang.org/reference/const_eval.html
///
/// # Boolean assertion: `expr`
///
/// Statically asserts the given expression.
///
/// Similar to C11 [`_Static_assert`] and C++11 [`static_assert`].
/// The feature may be added to Rust in the future: see [RFC 2790].
///
/// [`_Static_assert`]: https://en.cppreference.com/w/c/language/_Static_assert
/// [`static_assert`]: https://en.cppreference.com/w/cpp/language/static_assert
/// [RFC 2790]: https://github.com/rust-lang/rfcs/issues/2790
///
/// # Examples
/// ## Examples
///
/// ```
/// // Trivial assert.
/// static_assert!(42 > 24);
///
/// // Assert on sizes, similar to C's `sizeof(T)`.
/// static_assert!(core::mem::size_of::<u8>() == 1);
///
/// // Assert on binary string.
/// const X: &[u8] = b"bar";
/// static_assert!(X[1] == 'a' as u8);
/// static_assert!(X[1] == b'a');
///
/// // Check we uphold some constraint from the C side by testing the bindings.
/// static_assert!(RUST_BUFFER_SIZE >= bindings::LOG_LINE_MAX);
///
/// // Calling `const fn`s is possible.
/// const fn f(x: i32) -> i32 {
/// x + 2
/// }
/// static_assert!(f(40) == 42);
/// ```
///
/// # Set membership assertion: `(expr) is in {a0, ..., aN}`
///
/// Statically asserts that the given expression (typically a `const` integer) is in a set.
/// The negated form (`is not in`) is also available.
///
/// ## Examples
///
/// ```
/// // Trivial usage.
/// static_assert!((-2) is not in {-1, 0, 2});
/// static_assert!((-1) is in {-1, 0, 2});
/// static_assert!(( 0) is in {-1, 0, 2});
/// static_assert!(( 1) is not in {-1, 0, 2});
/// static_assert!(( 2) is in {-1, 0, 2});
/// static_assert!(( 3) is not in {-1, 0, 2});
///
/// // Typical usage.
/// static_assert!((SOME_CONSTANT_DEPENDING_ON_ARCH) is in {FOO, BAR, BAZ});
/// static_assert!((core::mem::size_of::<usize>()) is in {4, 8});
/// ```
///
/// # Interval membership assertion: `(expr) is in [min, max]`
///
/// Statically asserts that the given expression (typically a `const` integer) is in a closed
/// interval (i.e. inclusive range). The negated form (`is not in`) is also available.
///
/// ## Examples
///
/// ```
/// // Trivial usage.
/// static_assert!((-2) is not in [-1, 2]);
/// static_assert!((-1) is in [-1, 2]);
/// static_assert!(( 0) is in [-1, 2]);
/// static_assert!(( 1) is in [-1, 2]);
/// static_assert!(( 2) is in [-1, 2]);
/// static_assert!(( 3) is not in [-1, 2]);
///
/// // Typical usage.
/// static_assert!((FOO) is in [MIN_FOO, MAX_FOO]);
/// ```
///
/// # Fits-in-type assertion: `(expr) fits in type`
///
/// Statically asserts that the given expression (typically a `const` integer) fits in the given
/// type (which must provide `T::MIN` and `T::MAX`). The negated form (`does not fit in`) is also
/// available.
///
/// Casting a "kernel integer" (i.e. up to [`i64`]/[`u64`]) to [`i128`] within the expression is
/// allowed to easily manipulate integers: no 128-bit code will be generated since it will be
/// evaluated in a const context.
///
/// ## Examples
///
/// ```
/// // Trivial usage.
/// static_assert!(( -1) does not fit in u8);
/// static_assert!(( 0) fits in u8);
/// static_assert!((255) fits in u8);
/// static_assert!((256) does not fit in u8);
///
/// // Two's complement.
/// static_assert!((-128) fits in i8);
/// static_assert!(( 127) fits in i8);
/// static_assert!(( 128) does not fit in i8);
///
/// // Using `i128` for easy manipulation of integers.
/// const MAX_ERRNO: u32 = 4095;
/// static_assert!((-(MAX_ERRNO as i128)) fits in i16);
/// ```
#[macro_export]
macro_rules! static_assert {
// Boolean assertion: `expr`.
($condition:expr) => {
// Based on the latest one in `rustc`'s one before it was [removed].
//
// [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d
#[allow(dead_code)]
const _: () = [()][!($condition) as usize];
};

// Set membership assertion: `(expr) is in {a0, ..., aN}`.
(($expression:expr) is in {$($a:expr),+}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could have a $(,)? after the ,+ to allow for trailing commas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the parenthesis around the expression?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, expressions can only be followed by some tokens. See my comments above.

static_assert!( $(($expression) == ($a))||* );
};
(($expression:expr) is not in {$($a:expr),+}) => {
static_assert!(!($(($expression) == ($a))||*));
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do like maths notation, I'd probably make these use the square brackets as that is generally how you represent a collection in Rust. This isn't as relevant as the "in [ range ]" one though as this one can't be misunderstood when read in in isolation.


// Interval membership assertion: `(expr) is in [min, max]`.
(($expression:expr) is in [$min:expr, $max:expr]) => {
static_assert!( ($expression) >= ($min) && ($expression) <= ($max) );
};
(($expression:expr) is not in [$min:expr, $max:expr]) => {
static_assert!(!(($expression) >= ($min) && ($expression) <= ($max)));
};
Copy link

@soruh soruh May 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be better to use regular Rust range syntax (and range.contains(...)) here. I think it's generally useful to have macros resemble normal syntax as closely as possible as it makes the macros easier to learn. Is there a reason we can't / don't want to do that here?


// Fits-in-type assertion: `(expr) fits in type`.
(($expression:expr) fits in $t:ty) => {
static_assert!(($expression) is in [<$t>::MIN as i128, <$t>::MAX as i128]);
};
(($expression:expr) does not fit in $t:ty) => {
static_assert!(($expression) is not in [<$t>::MIN as i128, <$t>::MAX as i128]);
};
}

// Tests.
//
// These should later on go into a proper test.

static_assert!(42 > 24);
static_assert!(core::mem::size_of::<u8>() == 1);

const X: &[u8] = b"bar";
static_assert!(X[1] == b'a');

const fn f(x: i32) -> i32 {
x + 2
}
static_assert!(f(40) == 42);

static_assert!((-2) is not in {-1, 0, 2});
static_assert!((-1) is in {-1, 0, 2});
static_assert!(( 0) is in {-1, 0, 2});
static_assert!(( 1) is not in {-1, 0, 2});
static_assert!(( 2) is in {-1, 0, 2});
static_assert!(( 3) is not in {-1, 0, 2});

static_assert!((core::mem::size_of::<usize>()) is in {4, 8});

static_assert!((-2) is not in [-1, 2]);
static_assert!((-1) is in [-1, 2]);
static_assert!(( 0) is in [-1, 2]);
static_assert!(( 1) is in [-1, 2]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: using ranges here:
Reading this line in isolation and without prior knowledge of this macro I would probably expect this to fail as "1 is neither -1 nor 2"

static_assert!(( 2) is in [-1, 2]);
static_assert!(( 3) is not in [-1, 2]);

static_assert!((-129) does not fit in i8);
static_assert!((-128) fits in i8);
static_assert!(( 127) fits in i8);
static_assert!(( 128) does not fit in i8);

static_assert!(( -1) does not fit in u8);
static_assert!(( 0) fits in u8);
static_assert!((255) fits in u8);
static_assert!((256) does not fit in u8);

const MAX_ERRNO: u32 = 4095;
static_assert!((-(MAX_ERRNO as i128)) fits in i16);
static_assert!((-(MAX_ERRNO as i128)) does not fit in i8);
static_assert!((-(MAX_ERRNO as i128)) does not fit in u16);