Search code examples
rustrust-macros

Generating a combination tuple vec using a macro


Suppose I have two sets. (a, b, c) and (p, q) I want to generate a vector like this using a macro.

let v = vec![(a,p), (a, q), (b, p), (b, q), (c, p), (c, q)];

So this is the macro I was writing. There may be other ways to write this, but I want to understand why this does not work. I want to use this for something more complicated. I have used this pattern in the past (not in the same context) and not sure why this does not work.

fn main() {
    macro_rules! vec_gen {
        ([$($left_item:ident),*], $right:tt) => {
            vec![$(vec_gen!(@call $right, $left_item)),*]
        };
        (@call ($($right_item:ident),*), $left_item:ident) => {
            $(($left_item, $right_item),)*
        };
    }

    let a = 1;
    let b = 2;
    let c = 3;

    let p = 1;
    let q = 2;
    let r = 3;

    let data = vec_gen!([a, b, c], (p, q, r));
    println!("{:?}", data);
}

rust-analyzer macro expansion shows this: (Not exactly what I want - it has extra set of parenthesis - at least it gives some results).

// Recursive expansion of vec_gen! macro
// ======================================

(<[_]>::into_vec(
    #[rustc_box]
    $crate::boxed::Box::new([
        ((a, p), (a, q), (a, r)),
        ((b, p), (b, q), (b, r)),
        ((c, p), (c, q), (c, r)),
    ]),
))

Even though this generates some code in rust-analyzer macro expansion, it gives this error message when building.

error: macro expansion ignores token `,` and any following
 --> src\main.rs:7:40
  |
4 |             vec![$(vec_gen!(@call $right, $left_item)),*]
  |                    ----------------------------------- help: you might be missing a semicolon here: `;`
  |                    |
  |                    caused by the macro expansion here
...
7 |             $(($left_item, $right_item),)*
  |                                        ^
  |
  = note: the usage of `vec_gen!` is likely invalid in expression context

Now if I removed the ',' and add a ';' whole thing breaks down and rust-analyzer does not produce any output.

Also I am trying to understand this: "note: the usage of vec_gen! is likely invalid in expression context". Why?

How to get this macro pattern right?


Solution

  • Macros has to expand to whole items. You cannot create a macro that will expand to multiple elements in vec![].

    To solve that, you have to use push-down accumulation using one macro expansion that will expand to another and build the expression over time:

    macro_rules! vec_gen {
        (@expand [ $($expanded:tt)* ] [ $left_item:ident, $($rest:ident,)* ] [ $($right:ident,)* ]) => {
            vec_gen!(@expand [ $($expanded)* $( ( $left_item, $right ), )* ] [ $($rest,)* ] [ $($right,)* ])
        };
        (@expand [ $($expanded:tt)* ] [] [ $($right:ident,)* ]) => {
            vec![$($expanded)*]
        };
        ([ $($left_item:ident),* $(,)? ], ( $($right_item:tt),* $(,)? )) => {
            vec_gen!(@expand [ ] [ $($left_item,)* ] [ $($right_item,)* ])
        };
    }
    

    But note that what you're building is just itertools::iproduct!():

    let data = Vec::from_iter(itertools::iproduct!([a, b, c], [p, q, r]));