Search code examples
rustmacros

How do I generate an array with macro! by applying a function to each element?


I want to generate a static array (doing this at runtime is not an option) with help of a macro.

My attempts are

macro_rules! test {
    ($($data:expr),*)   => {
    [ test!(@xform $($data),*) ]
    };

    (@xform)            => { };
    (@xform $a:expr)        => { be32($a) };
    (@xform $a:expr, $($data:expr),*)   => { be32($a), test!(@xform $($data),*) };
}

// just for simplicity...
const fn be32(v: u32) -> u32 { v + 1 }

static FILE_HEADER_0: [u32;2] = test!(1, 2);
static FILE_HEADER_1: [u32;2] = [be32(1), be32(2)];

but this fails with

error: macro expansion ignores token `,` and any following
 --> src/lib.rs:8:52
  |
3 |     [ test!(@xform $($data),*) ]
  |       ------------------------- help: you might be missing a semicolon here: `;`
  |       |
  |       caused by the macro expansion here
...
8 |     (@xform $a:expr, $($data:expr),*)    => { be32($a), test!(@xform $($data),*) };
  |                                                       ^
  |
  = note: the usage of `test!` is likely invalid in expression context

I expect that FILE_HEADER_0 is generated like FILE_HEADER_1

Is this possible with normal macro_rules! or do I have to use proc_macro?


Solution

  • Your macro expands to multiple comma-separated expressions, and this is invalid. It has to generate only one.

    The usual solution to that is push-down accumulation: accumulate the resulting array in the macro and keep adding elements to it:

    macro_rules! test {
        ($($data:expr),*) => {
            test!(@xform [] $($data),*)
        };
    
        (@xform $arr:tt) => { $arr };
        (@xform [ $($arr:tt)* ] $a:expr) => {
            test!(@xform [ $($arr)* be32($a) ])
        };
        (@xform [ $($arr:tt)* ] $a:expr, $($data:expr),*) => {
            test!(@xform [ $($arr)* be32($a), ] $($data),*)
        };
    }
    

    In the first expansion we will have test!(@xform [] 1, 2). This will reach the last arm and expand to test!(@xform [ be(1), ] 2). This will expand (by the third arm) to test!(@xform [ be(1), be(2) ]) which will finally expand to [be(1), be(2)] by the second arm.

    In this case, however, you don't need complex solutions. A simple macro will do:

    macro_rules! test {
        ($($data:expr),*) => {
            [ $( be32($data) ),* ]
        };
    }