[Pre-RFC] assert macros simplified


#1

Problem

We have many types of assert macro just for printing more helpful messages. We only need one type of them. (Of course, we won’t get rid of debug_assert since it’s completely a different variant.)

Proposal

Make everything fit within the assert macro. For example, if the final operation is “==”, make it like assert_eq. We should also expand this to ordering operators like “<”. There are many frameworks mostly in C++ like Catch and Boost.Test already achieves that using macros. The problem with C++ macros is that they’re hard to write and they doesn’t always yield the desired behavior. However, Rust’s compiler (procedural) macros are very powerful, we can access the AST for precise parsing. We may also print out the immediate expressions, like [1 + 2 != 3]. This needs further discussion.

Unresolved questions

  • should we make assert! a compiler built-in? Can this done with macro_rules?
  • should we print out all values contained in the expression?

#2

It can, but it’s not pretty. macro_rules doesn’t provide access to AST, so you have to parse those things yourself.

And because this is a heavily recursive macro (I cannot really think of a way to do it without tt-munching, but perhaps there is one), it may as well reach the recursion limit. So it’s probably not really an option for a standard library macro (macro_rules! implementation that is).

macro_rules! assert_internal {
    (normal [$($left:tt)*] && $($tail:tt)*) => {
        assert_internal!(nope $($left)* && $($tail)*)
    };
    (normal [$($left:tt)*] || $($tail:tt)*) => {
        assert_internal!(nope $($left)* || $($tail)*)
    };
    (normal [$($left:tt)*] == $($tail:tt)*) => {
        assert_internal!(eq [$($left)*] [] $($tail)*);
    };
    (normal [$($left:tt)*] $t:tt $($tail:tt)*) => {
        assert_internal!(normal [$($left)* $t] $($tail)*)
    };
    
    (eq [$($left:tt)*] [$($right:tt)*] && $($tail:tt)*) => {
        assert_internal!(nope $($left)* == $($right)* && $($tail)*)
    };
    (eq [$($left:tt)*] [$($right:tt)*] || $($tail:tt)*) => {
        assert_internal!(nope $($left)* == $($right)* || $($tail)*)
    };
    (eq $left:tt [$($right:tt)*] $t:tt $($tail:tt)*) => {
        assert_internal!(eq $left [$($right)* $t] $($tail)*)
    };

    (normal [$all:expr]) => {
        assert_internal!(nope $all)
    };
    (nope $all:expr) => {
        println!("Regular assert: {}", stringify!($all));
    };
    (eq [$left:expr] [$right:expr]) => {
        println!("Assert equal, left: {}, right: {}", stringify!($left), stringify!($right));
    };
}

macro_rules! assert_demo {
    ($($tail:tt)*) => {
        assert_internal!(normal [] $($tail)*)
    }; 
}

fn main() {
    assert_demo!(a);
    assert_demo!(a == b);
    assert_demo!(a && b == c);
    assert_demo!(a == b || c);
}

And all that to determine if the top AST node is equality node (yes, I know there are more operators than && and ||, but it would be just adding additional rules which would distract from the code).


#3

The Catch testing framework is just beautiful. Its assertion errors are very clear:

test.cc:100: FAILED:
  REQUIRE(foo(x) >= y)
with expansion:
  3 >= 4

Compare this to:

---- test::foo stdout ----
    thread 'test::foo' panicked at 'assertion failed: `(left >= right)`
(left: `[3]`, right: `[4]`)', test.rs:100
note: Run with `RUST_BACKTRACE=1` for a backtrace.

I don’t think that immediate technical reasons must limit aesthetic aspirations.


#4

I love this. It doesn’t necessarily need to be in stdlib, though. Although a really good implementation would probably require procedural macros.

should we print out all values contained in the expression?

I think so. At least at the top level. With the current assert macros I find I frequently need to add println! statements to figure out what the actual value is.


#5

I don’t think procedural macros are needed for a good implementation (macro_rules! will do). The task is essentially to figure out if there is ==/!= token, and no tokens of higher precedence (I suppose you may also add an extension allowing expressions like ((a == b)), but it’s really easy to handle). Note that parenthesis/blocks don’t need to be considered, as they are a single token (matched fully by :tt) as far language is concerned. The only issue is recursion limit, which is decided by user of a macro, and not by macro itself


#6

IMO macro_rules! is limited and slow. As long as we’re bringing the feature into standard library, there’s no problem using a compiler macro, which express the things more directly.

I think we can apply this on all operators except . (function call or struct field reference). That way we can display the most helpful debug dump as possible. Expression block ({ sth }) can be used to not be dump out the block.


#7

I’d be happy even if it only supported basic cases (falling back to expecting true in hard to parse expressions), since most of the time my assert expressions are pretty simple, such as result > 0.5.


#8

I experimented with trying to implement something like this with macro_rules!.

With an implementation like:

macro_rules! assert2 {
    ($left:tt == $right:tt) => ({
        match (&$left, &$right) {
            (left_val, right_val) => if !(*left_val == *right_val) {
                panic!("{0} == {1} was false\n\t{0} = {2:?}, {1} = {3:?}",
                       stringify!($left), stringify!($right),
                       left_val, right_val);
            }
        }
    })
}

Then assert2!( 5 == 2 + 3) fails to compile with “no rules expected the token +”. If I change left and right to be expr instead of tt, then it still fails with "$left:expr is followed by ==, which is not allowed for expr fragments".

Wrapping the 2 + 3 in parenthesis does work with with the tt version, but that is a little awkward and non-intuitive to use.

I suppose it is possible to build something that would handle arbitrary expressions, but as far as I understand it, it would have to essentially parse the expressions themselves, duplicating a significant portion of rust’s expression grammar.


#9

You can have ($left:tt == $right:expr), which matches 5 == 2 + 3.

Of course it won’t match the other way, 2 + 3 == 5, since expressions can contain ==, so the parser has no way of knowing when to stop parsing expr and when to parse == in the macro. But instead of an error, there can be a generic fallback in such case:

macro_rules! assert2 {
    ($left:tt == $right:expr) => {
        match (&$left, &$right) {
            (left_val, right_val) => if !(*left_val == *right_val) {
                panic!("{0} == {1} was false\n\t{0} = {2:?}, {1} = {3:?}",
                       stringify!($left), stringify!($right),
                       left_val, right_val);
            }
        }
    };
    ($right:expr) => {assert!($right)};
}

fn main() {
    assert2!(5 == 2 + 3);
    assert2!(2 + 3 == 5);
}