Macro to generate combinations for test cases

Hi folks!

I have a rather interesting macro problem.

I've created a system similar to axum's magic functions, but instead of creating a Context to accept functions, I needed to create the Context! So, two traits later and a bunch of impl blocks, I got it.

For my crate, this is the optional Repr object, which tells how some number should be formatted with: what unit, what metric system, what precision, and with a separator or not. And the user can simply send whatever options he wants, in any order! Just a System, a ("Unit", Sep), a (Sep, System, "Unit"), etc.

And now, I'm trying to generate the test cases. A macro that can take arguments once, and generate all possible combinations of them.

  • Types supported:
    • I support 3 types of units: &'static str, String, and Cow.
    • I support 2 types of precision: enum Precision and i8.
    • I support 2 types of separator: enum Separator and bool.
  • And, on top of that, single arguments are accepted both directly and within a 1-tuple.

Well, I'm no expert in macros, but it does work for a single argument. How could I make it work for two, three, and four arguments, i.e. the cases that need changing the order of elements (on top of the supported types above)?

fn main() {
    macro_rules! base {
        ($repr:expr => $unit:expr, $sys:expr, $prec:expr, $sep:expr) => {
            println!("assert_eq!(Repr{{ {}, {}, {}, {} }}, {})", 
                stringify!($unit), stringify!($sys), stringify!($prec), stringify!($sep), stringify!($repr)
            );
        };
    }
    macro_rules! case {
        // (($a:expr, $b:expr) => $($p:expr),*) => {
        //     base!(($a, $b) => $($p),*);
        //     base!(($b, $a) => $($p),*);
        // };
        ($a:expr => $($p:expr),*) => {
            base!($a => $($p),*);
            base!(($a,) => $($p),*);
        };
        (@unit $a:expr => $($p:expr),*) => {
            case!($a => $($p),*);
            case!(String::from($a) => $($p),*);
            case!(Cow::from($a) => $($p),*);
        };
        (@prec $a:expr => $($p:expr),*) => {
            case!($a => $($p),*);
            case!(Precision::from($a) => $($p),*);
        };
        (@sep $a:expr => $($p:expr),*) => {
            case!($a => $($p),*);
            case!(Separator::from($a) => $($p),*);
        };
    }

    // --- one argument.
    case!(@unit "U" => "U", None, None, None);
    case!(System::SI2 => "", Some(System::SI2), None, None);
    case!(@prec -5i8 => "", None, Some(Precision::Fixed(-5)), None);
    case!(@sep false => "", None, None, Some(Separator::No));

    // // --- two arguments.
    // case!(("U", System::SI2) => "U", Some(System::SI2), None, None);
    // case!(("U", -5) => "U", None, Some(Precision::Fixed(-5)), None);
    // case!(("U", Separator::No) => "U", None, None, Some(Separator::No));
    // case!((System::SI2, -5) => "", Some(System::SI2), Some(Precision::Fixed(-5)), None);
    // case!((System::SI2, Separator::No) => "", Some(System::SI2), None, Some(Separator::No));
    // case!((-5, Separator::No) => "", None, Some(Precision::Fixed(-5)), Some(Separator::No));
    // 
    // // --- three arguments.
    // case!(("U", System::SI2, -5) => "U", System::SI2, -5, None);
    // case!(("U", System::SI2, Separator::No) => "U", System::SI2, None, Separator::No);
    // case!((System::SI2, -5, Separator::No) => "", System::SI2, -5, Separator::No);
    // 
    // // --- four arguments.
    // case!(("U", System::SI2, -5, Separator::No) => "U", System::SI2, -5, Separator::No);

}

It prints this, i.e. 16 cases for only 4 case!() calls:

assert_eq!(Repr{ "U", None, None, None }, "U")
assert_eq!(Repr{ "U", None, None, None }, ("U",))
assert_eq!(Repr{ "U", None, None, None }, String::from("U"))
assert_eq!(Repr{ "U", None, None, None }, (String::from("U"),))
assert_eq!(Repr{ "U", None, None, None }, Cow::from("U"))
assert_eq!(Repr{ "U", None, None, None }, (Cow::from("U"),))
assert_eq!(Repr{ "", Some(System::SI2), None, None }, System::SI2)
assert_eq!(Repr{ "", Some(System::SI2), None, None }, (System::SI2,))
assert_eq!(Repr{ "", None, Some(Precision::Fixed(-5)), None }, -5i8)
assert_eq!(Repr{ "", None, Some(Precision::Fixed(-5)), None }, (-5i8,))
assert_eq!(Repr{ "", None, Some(Precision::Fixed(-5)), None }, Precision::from(-5i8))
assert_eq!(Repr{ "", None, Some(Precision::Fixed(-5)), None }, (Precision::from(-5i8),))
assert_eq!(Repr{ "", None, None, Some(Separator::No) }, false)
assert_eq!(Repr{ "", None, None, Some(Separator::No) }, (false,))
assert_eq!(Repr{ "", None, None, Some(Separator::No) }, Separator::from(false))
assert_eq!(Repr{ "", None, None, Some(Separator::No) }, (Separator::from(false),))

The case! macro, even though it appears to have one entry point and some helpers, does in fact have four entry points. So, when I uncomment the two-tuple case, even though it does generate two reversed order cases, I don't know how to include the supported types...

Thank you very much.

I’m not 100% sure I understood your explanations of what you’re after correctly. But maybe you meant something like this?

fn main() {
    macro_rules! base {
        ($repr:expr => $unit:expr, $sys:expr, $prec:expr, $sep:expr) => {
            println!("assert_eq!(Repr{{ {}, {}, {}, {} }}, {})", 
                stringify!($unit), stringify!($sys), stringify!($prec), stringify!($sep), stringify!($repr)
            );
        };
    }

    macro_rules! permute {
        // main recursion
        ([$current:expr, $($input:expr,)*] [$($prefix:expr,)*] /* insert here or later */ [$head:expr, $($tail:expr,)*] => $($p:expr),*) => {
            // "insert here"
            // leave 'prefix' empty for recursive call to allow all positions for next 'current'
            permute!([$($input,)*] [] /* next: insert here or later */ [$($prefix,)* $current, $head, $($tail,)*] => $($p),*);
            
            // "or later"
            // move 'head' over, allows all remaining positions recursively
            permute!([$current, $($input,)*] [$($prefix,)* $head,] /* next: insert here or later */ [$($tail,)*] => $($p),*);
        };
        // simplified case when tail is empty
        ([$current:expr, $($input:expr,)*] [$($prefix:expr,)*] /* insert here */ [] => $($p:expr),*) => {
            permute!([$($input,)*] [] [$($prefix,)* $current,] => $($p),*);
        };

        // when we're done
        ([][][$($a:expr,)*] => $($p:expr),*) => {
            base!(($($a),*) => $($p),*);
        }
    }

    macro_rules! case {
        // initial step
        (($($t:tt)*) => $($p:expr),*) => {
            case!(@[] ($($t)*) => $($p),*);
        };
        ($(@$i:ident)? $a:expr => $($p:expr),*) => {
            case!(@[] ($(@$i)?$a) => $($p),*);
        };

        // process each entry
        (@[$($processed:expr,)*] (@unit $a:expr $(, $($more:tt)*)?) => $($p:expr),*) => {
            case!(@[$($processed,)* $a              ,] ($($($more)*)?) => $($p),*);
            case!(@[$($processed,)* String::from($a),] ($($($more)*)?) => $($p),*);
            case!(@[$($processed,)* Cow::from($a)   ,] ($($($more)*)?) => $($p),*);
        };
        (@[$($processed:expr,)*] (@prec $a:expr $(, $($more:tt)*)?) => $($p:expr),*) => {
            case!(@[$($processed,)* $a                 ,] ($($($more)*)?) => $($p),*);
            case!(@[$($processed,)* Precision::from($a),] ($($($more)*)?) => $($p),*);
        };
        (@[$($processed:expr,)*] (@sep $a:expr $(, $($more:tt)*)?) => $($p:expr),*) => {
            case!(@[$($processed,)* $a                 ,] ($($($more)*)?) => $($p),*);
            case!(@[$($processed,)* Separator::from($a),] ($($($more)*)?) => $($p),*);
        };
        (@[$($processed:expr,)*] ($a:expr $(, $($more:tt)*)?) => $($p:expr),*) => {
            case!(@[$($processed,)* $a              ,] ($($($more)*)?) => $($p),*);
        };

        // final step
        (@[$a:expr,] () => $($p:expr),*) => {
            base!($a => $($p),*);
            base!(($a,) => $($p),*);
        };
        (@[$($a:expr,)*] () => $($p:expr),*) => {
            permute!([$($a,)*][][] => $($p),*);
        };
    }

    // // --- one argument.
    case!(@unit "U" => "U", None, None, None);
    case!(System::SI2 => "", Some(System::SI2), None, None);
    case!(@prec -5i8 => "", None, Some(Precision::Fixed(-5)), None);
    case!(@sep false => "", None, None, Some(Separator::No));

    println!("---");
    // // --- two arguments.
    case!((@unit "U", System::SI2) => "U", Some(System::SI2), None, None);
    case!((@unit "U", @prec -5i8) => "U", None, Some(Precision::Fixed(-5)), None);
    case!((@unit "U", @sep false) => "U", None, None, Some(Separator::No));
    case!((System::SI2, @prec -5i8) => "", Some(System::SI2), Some(Precision::Fixed(-5)), None);
    case!((System::SI2, @sep false) => "", Some(System::SI2), None, Some(Separator::No));
    case!((@prec -5i8, @sep false) => "", None, Some(Precision::Fixed(-5)), Some(Separator::No));
    
    println!("---");
    // --- three arguments.
    case!((@unit "U", System::SI2, @prec -5i8) => "U", System::SI2, -5, None);
    case!((@unit "U", System::SI2, @sep false) => "U", System::SI2, None, Separator::No);
    case!((System::SI2, @prec -5i8, @sep false) => "", System::SI2, -5, Separator::No);
    
    println!("---");
    // --- four arguments.
    case!((@unit "U", System::SI2, @prec -5i8, @sep false) => "U", System::SI2, -5, Separator::No);

}

Rust Playground

1 Like

:scream:

AMAZING!
Thank you very very much! That's perfect!

But, care to explain it a bit?
How did you do it?

Well… building it up step by step until it does the right thing. trace_macros!(true); can help a lot for debugging. The basic principles of the so-called “tt muncher” approach applies, though my personal usage of macros isn’t really built on any guides. I’ve left some high-level comments to explain the very rough structure.

I suppose… Feel free to look at trace_macros output to try and follow the step-by-step, if you want to. (Or one of the 3-input one for a more interesting case of permute!.)

1 Like

Thanks again, @steffahn.
I'll read it with great attention now, to try to make sense of it.
And I'm glad you've reused the same @ annotations I've started with, which means I was on the right track. I didn't like it before when I was exploding the units and precisions and separators by hand, so I came up with those.
You 100% understood what I was after. Beautiful work! :clap:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.