Search code examples
rustmacrosmetaprogramming

Expand macro into a tuple initialization


My goal is to make a macro that expands in such a way I can describe a tuple's initilization.

I made this toy example:

macro_rules! fill_tuple {
    ($_idx : expr) => {};
    ($idx : expr, $kind : ident) => {
        $idx
    };
    ($idx : expr, $head : ident, $($tail : ident),+) => {
        fill_tuple!($idx, $head),
        fill_tuple!($idx + 1usize, $($tail),+)
    };
}

fn main()
{
    (
    fill_tuple!(0usize, A,B,C)
    )
}

The idea here is, there are 3 parameters, so after expanding we should get (0, 1, 2). This is just an example, at the end I will actually use the A,B,C in a more sophisticated way to initialise the tuple.

My current hurdle is that the comma on the last block of the macro is not accepted by rust. I get the error:

Compiling playground v0.0.1 (/playground)
error: macro expansion ignores token `,` and any following
  --> src/main.rs:7:33
   |
7  |         fill_tuple!($idx, $head),
   |                                 ^
...
15 |     fill_tuple!(0usize, A,B,C)
   |     --------------------------- help: you might be missing a semicolon here: `;`
   |     |
   |     caused by the macro expansion here
   |
   = note: the usage of `fill_tuple!` is likely invalid in expression context

How do i get around this? I do absolutely need that comma, otherwise the whole generated code is wrong.


Solution

  • You can recursively go through the macro arguments and keep a list of your resulting numbers.

    macro_rules! fill_tuple {
        // Base cases
        (@step 0, (),) => {
            ()
        };
        (@step $i:expr, ($($result:expr),*), $head:expr) => {
            ($($result,)* foo($i, $head),)
        };
    
        // Recursive case
        (@step $i:expr, ($($result:expr),*), $head:expr, $($tail:expr),+) => {
            fill_tuple!(@step $i + 1, ($($result,)* foo($i, $head)), $($tail),+)
        };
    
        // Public interface
        ($($e:expr),*) => {
            fill_tuple!(@step 0, (), $($e),*)
        };
    }
    

    And if you don't need the index, it's much more simple:

    macro_rules! fill_tuple {
        ($($e:expr),*) => {
            ($(bar($e),)*)
        }
    }
    

    There's some subtle details regarding single-element tuples because you have to put the value in parentheses and have a comma immediately after the value--for example, (7,) instead of (7). So in the first macro, we need the comma after foo() on the line ($($result,)* foo($i, $head),). Similarly, in the second macro we need ($(bar($e),)*) instead of ($(bar($e)),*) so that there's a comma after each value rather than each value except for the last.