-
Notifications
You must be signed in to change notification settings - Fork 430
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
base: rust
Are you sure you want to change the base?
Conversation
This is an RFC/experiment. Let me know what you think. It is also likely I am missing a better way, so please feel free to submit another alternative. There are two main points to discuss here. First of all, whether this makes sense at all. I added the interval/set cases later (since the I tried a few alternatives without macros, but AFAIK there is no C++ So I ended up going with a macro, and thinking that since we enforce this to be always a
The second part of the experiment is going with custom syntax, all in
instead of going for different macros:
I do not know which is best. Part of me just wanted to see how it looked ;) |
Now `static_assert!` allows for several forms: - Boolean assertion: `expr`. - Set membership assertion: `(expr) is in {a0, ..., aN}`. - Interval membership assertion: `(expr) is in [min, max]`. - Fits-in-type assertion: `(expr) fits in type`. These are all `static_assert!`s instead of being split into a separate macro (e.g. `fits_in!`) because we want to ensure they are only within/as a `static_assert!` (e.g. for the `i128` trick). The documentation is also improved for the boolean assertion case. Signed-off-by: Miguel Ojeda <[email protected]>
8a4b153
to
dd89a2e
Compare
I tried this out on #268 and it's a great solution ! I wish I knew enough about Rust macros to help review. One small nit though: when the static assertion fails, the build error is very cryptic. It's not immediately obvious what has happened. const MY_ERRNO: u32 = 40000;
static_assert!((-(MY_ERRNO as i128)) fits in i16);
|
}; | ||
(($expression:expr) is not in [$min:expr, $max:expr]) => { | ||
static_assert!(!(($expression) >= ($min) && ($expression) <= ($max))); | ||
}; |
There was a problem hiding this comment.
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?
I don't know if there is much that can be done to address that as this is somewhat abusing the compiler safety checks to produce the error. One small way to improve this would be to replace the |
Ah of course this would probably make messages better when it becomes stable. |
}; | ||
(($expression:expr) is not in {$($a:expr),+}) => { | ||
static_assert!(!($(($expression) == ($a))||*)); | ||
}; |
There was a problem hiding this comment.
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.
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]); |
There was a problem hiding this comment.
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"
The required parathesis around the expressions seem unfortunate but are probably unavoidable. In general I like this quite a lot though btw. and the macros themselves (apart from my syntax nits :) ) look correct to me. |
($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),+}) => { |
There was a problem hiding this comment.
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.
@soruh Answering here to several of the comments in one. I agree that inventing new syntax is not ideal, in particular if it is close to the "real" one. On the ranges syntax ( On the
Then we could also use Concerning the parenthesis around
The last ones are closer to Rust, but then if we want to match
An advantage of the
It could be called the "asserting something about" operator when used inside our I am not particularly in favor of any particular way, so I am happy to hear opinions! I also tried to support open intervals etc., but we cannot use |
Yeah, that is a problem already for the current |
($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),+}) => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
/// - Boolean assertion: `expr`. | ||
/// - Set membership assertion: `(expr) is in {a0, ..., aN}`. | ||
/// - Interval membership assertion: `(expr) is in [min, max]`. | ||
/// - Fits-in-type assertion: `(expr) fits in type`. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
/// There are several forms of this macro: | ||
/// | ||
/// - Boolean assertion: `expr`. | ||
/// - Set membership assertion: `(expr) is in {a0, ..., aN}`. |
There was a problem hiding this comment.
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}
.
There was a problem hiding this comment.
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!
Here's a version of macro_rules! static_assert {
(@normal_assert $invert:literal ($condition:expr)) => {
const _: () = [()][(($condition) == $invert) as usize];
};
(@set_assert $invert:literal ($expr:expr) [$($a:expr),+]) => {
static_assert!(@normal_assert $invert ($(($expr) == ($a))||*));
};
(@range_assert $invert:literal ($expr:expr) ($min:expr) ($max:expr)) => {
static_assert!(@normal_assert $invert (($expr) >= ($min) && ($expr) < ($max)));
};
(@range_from_assert $invert:literal ($expr:expr) ($min:expr)) => {
static_assert!(@normal_assert $invert (($expr) >= ($min)));
};
(@range_inclusive_assert $invert:literal ($expr:expr) ($min:expr) ($max:expr)) => {
static_assert!(@normal_assert $invert (($expr) >= ($min) && ($expr) <= ($max)));
};
(@range_to_assert $invert:literal ($expr:expr) ($max:expr)) => {
static_assert!(@normal_assert $invert (($expr) < ($max)));
};
(@range_to_inclusive_assert $invert:literal ($expr:expr) ($max:expr)) => {
static_assert!(@normal_assert $invert (($expr) <= ($max)));
};
(@parse_expr ($($head:tt)*)) => {
static_assert!(@normal_assert false ($($head)*));
};
(@parse_expr ($($head:tt)*) in $($tail:tt)*) => {
static_assert!(@parse_range false ($($head)*) () $($tail)*);
};
(@parse_expr ($($head:tt)*) not in $($tail:tt)*) => {
static_assert!(@parse_range true ($($head)*) () $($tail)*);
};
(@parse_expr ($($head:tt)*) $current:tt $($tail:tt)*) => {
static_assert!(@parse_expr ($($head)* $current) $($tail)*);
};
(@parse_range $invert:literal ($expr:expr) () [$($tail:tt)*]) => {
static_assert!(@set_assert $invert ($expr) [$($tail)*]);
};
(@parse_range $invert:literal ($expr:expr) ($($head:tt)+) ..) => {
static_assert!(@range_from_assert $invert ($expr) ($($head)*));
};
(@parse_range $invert:literal ($expr:expr) ($($head:tt)+) .. $($tail:tt)+) => {
static_assert!(@range_assert $invert ($expr) ($($head)*) ($($tail)*));
};
(@parse_range $invert:literal ($expr:expr) ($($head:tt)+) ..= $($tail:tt)+) => {
static_assert!(@range_inclusive_assert $invert ($expr) ($($head)*) ($($tail)*));
};
(@parse_range $invert:literal ($expr:expr) () .. $($tail:tt)+) => {
static_assert!(@range_to_assert $invert ($expr) ($($tail)*));
};
(@parse_range $invert:literal ($expr:expr) () ..= $($tail:tt)+) => {
static_assert!(@range_to_inclusive_assert $invert ($expr) ($($tail)*));
};
(@parse_range $invert:literal ($expr:expr) ($($head:tt)*) $current:tt $($tail:tt)*) => {
static_assert!(@parse_range $invert ($expr) ($($head)* $current) $($tail)*);
};
(@ $($tail:tt)*) => { compile_error!("wrong syntax"); };
($($tail:tt)*) => { static_assert!(@parse_expr () $($tail)*); }
}
static_assert!(1 == 1);
static_assert!(2 in [1, 2]);
static_assert!(2 in 1 .. 100);
static_assert!(2 in 0..= 100);
static_assert!(2 in ..= 100);
static_assert!(2 in .. 100);
static_assert!(2 in 0..);
static_assert!(2 in [1, 2]);
static_assert!(2 not in 3 .. 100);
static_assert!(2 not in 3..= 100);
static_assert!(2 not in ..= 1);
static_assert!(2 not in .. 2);
static_assert!(2 not in 3..);
static_assert!(2 not in [1, 3]); |
@nbdd0121 Heh, taking tokens individually is indeed very cool. It defeats a bit the beauty of the "by example" part of macros by example that I wanted to show with this file, but of course opens up more possibilities. The obvious concern is how it will be received when people take a look at this file. But given we already have proc macros around, I guess it is fair to say we already crossed that bridge a long time ago... :P If we go with the token tree approach, then I would focus on telling people the same we do for |
Review of
|
Since rust-lang/rust#89508 has landed I don't see why this can't use |
Now
static_assert!
allows for several forms:expr
.(expr) is in {a0, ..., aN}
.(expr) is in [min, max]
.(expr) fits in type
.These are all
static_assert!
s instead of being split intoa separate macro (e.g.
fits_in!
) because we want to ensurethey are only within/as a
static_assert!
(e.g. for thei128
trick).
The documentation is also improved for the boolean assertion case.
Signed-off-by: Miguel Ojeda [email protected]