13

I'm playing with macros in Rust and want to do nested expansion, i.e. combinatorics.

This is the code I've written:

macro_rules! nested {
    (
        $(arg $arg:ident;)*
        $(fun $fun:ident;)*
    ) => {
        $(
            $fun($($arg),*);
        )*
    }
}

fn show1(a: i32, b: i32, c: i32) {
    println!("show1: {} {} {}", a, b, c);
}
fn show2(a: i32, b: i32, c: i32) {
    println!("show2: {} {} {}", a, b, c);
}

fn main() {
    let a = 1;
    let b = 2;
    let c = 3;
    nested! {
        arg a;
        arg b;
        arg c;
        fun show1;
        fun show2;
    }
}

Playground

I want this to expand to

fn main() {
    let a = 1;
    let b = 2;
    let c = 3;
    // iteration over $fun
    show1(/* iteration over $arg */a, b, c);
    show2(/* iteration over $arg */a, b, c);
}

However, it seems that Rust doesn't support this and instead complains:

error: inconsistent lockstep iteration: 'fun' has 2 items, but 'arg' has 3

So apparently it ignores the inner iteration.

However, if I remove one of the args, to make it 2 items for both, it still complains:

<anon>:7:18: 7:25 error: attempted to repeat an expression containing no
                  syntax variables matched as repeating at this depth
<anon>:7             $fun($($arg),*);

Is there a way to do what I want?

malbarbo
  • 10,717
  • 1
  • 42
  • 57
Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157

3 Answers3

15

It seems that it is not possible to do this kind of expansion. Here is a workaround:

macro_rules! nested {
    ($(arg $arg:ident;)* $(fun $fun:ident;)*) => {
        // expand arg to a tuple that will be matched as tt
        // in @call_tuple an will be decomposed back to
        // a list of args in @call
        nested!(@call_tuple $($fun),* @ ($($arg),*))
    };
    (@call_tuple $($fun:ident),* @ $tuple:tt) => {
        $(nested!(@call $fun $tuple))*
    };
    (@call $fun:ident ($($arg:expr),*)) => {
        $fun($($arg),*);
    };
}

The @id is only used to keep the rules internal to the macro.

malbarbo
  • 10,717
  • 1
  • 42
  • 57
3

Here is a slightly simpler approach.

macro_rules! nested {
    (($($f:ident),*) $args:tt) => {
        $(nested!(@call $f $args);)*
    };
    (@call $f:ident ($($arg:expr),*)) => {
        $f($($arg),*);
    };
}

nested! {
    (show1, show2)
    (a, b, c)
}

Same principle as @malbarbo's answer but change your macro to put args in parenthesis. Then you can accept the args as a single token so that only the function names are repeated in the first expansion.

cambunctious
  • 8,391
  • 5
  • 34
  • 53
0

Seems like you can't nest repetitions directly, but you can do it with a second macro invocation. Here I'm expanding all of the args with one repetition, calling back into an internal rule in my macro. The internal rule sees them all together as combined_args. It then does the second repetition over the functions.

macro_rules! nested {
    (@internal $combined_args:tt ; $(fun $fun:ident;)*) => {
        $( $fun$combined_args; )*
    };

    ( $(arg $arg:ident;)* $($funs:tt)* ) => {
        nested!(@internal ($($arg),*) ; $($funs)*)
    };
}

...except it doesn't compile because of ambiguity:

error: local ambiguity when calling macro `nested`: multiple parsing options: built-in NTs tt ('funs') or 1 other option.
  --> src/main.rs:23:9
   |
23 |         arg a;
   |         ^^^

error: could not compile `playground` due to previous error

Apparently macro_rules! isn't greedy like (e.g.) regular expressions. So now I'll modify it to look for the end or fun to disambiguate:

macro_rules! nested {
    ( @internal $combined_args:tt ; $(fun $fun:ident;)* ) => {
        $( $fun$combined_args; )*
    };

    ( $(arg $arg:ident;)* ) => { };

    ( $(arg $arg:ident;)* fun $($tail:tt)* ) => {
        nested!(@internal ($($arg),*) ; fun $($tail)*)
    };
}

This works.

That said, the form @cambunctious used is easier to handle (because the args are pre-combined) and looks more like a regular function call anyway. multi_fn_call! { (show1, show2)(a, b, c) } has a nice ring to it. If you aren't picky about the format of compile errors when the caller doesn't pass the args as expected, you can do it with one repetition in one rule:

macro_rules! multi_fn_call {
    ( ( $($fn:path),* ) $args:tt ) => { $($fn$args;)* };
}
Scott Lamb
  • 2,266
  • 1
  • 19
  • 21