Macro not recursively expanding (macro_rules)

90 views Asked by At

I'm trying to create a macro to help build structs but the macro does not seem to be fully expanding. Rather, it has one iteration left.

Code

struct Hello {
    name: String,
    age: Option<u32>,
}

macro_rules! __hello_field {
    (name: $value:expr) => {
        name: $value
    };
    (age: $value:expr) => {
        age: Some($value)
    };
    ($field:ident: $value:expr, $($tail:tt)*) => {
        __hello_field!($field: $value),
        __hello_field!($($tail)*)
    };
    () => {};
    (,) => {};
}

macro_rules! hello {
    ($($tail:tt)*) => {
        Hello {
            __hello_field!($($tail)*)
        }
    };
}

fn main() {
    let hello_item = hello!(name: String::from("hello"), age: 10);
}

Expected output

let hello_item = Hello {
    name: (String::from("hello")),
    age: Some(10),
};

Whitespace is not important here.

Compile error produced

error: expected one of `,`, `:`, or `}`, found `!`
  --> src/main.rs:24:26
   |
23 |         Hello {
   |         ----- while parsing this struct
24 |             __hello_field!($($tail)*)
   |                          ^ expected one of `,`, `:`, or `}`
...
30 |     let hello_item = hello!(name: String::from("hello"), age: 10);
   |                      -------------------------------------------- in this macro invocation
   |
   = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info)

error: expected one of `,`, `.`, `?`, `}`, or an operator, found `)`
  --> src/main.rs:24:37
   |
23 |         Hello {
   |         ----- while parsing this struct
24 |             __hello_field!($($tail)*)
   |                                     ^ expected one of `,`, `.`, `?`, `}`, or an operator
...
30 |     let hello_item = hello!(name: String::from("hello"), age: 10);
   |                      --------------------------------------------
   |                      |                                          |
   |                      |                                          help: try adding a comma: `,`
   |                      in this macro invocation
   |
   = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0063]: missing field `name` in initializer of `Hello`
  --> src/main.rs:23:9
   |
23 |         Hello {
   |         ^^^^^ missing `name`
...
30 |     let hello_item = hello!(name: String::from("hello"), age: 10);
   |                      -------------------------------------------- in this macro invocation
   |
   = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info)

Output which rust-analyzer thinks this results in

Notice how there's still one more instance of the __hello_field! macro.

Hello {
  name:(String::from("hello")),__hello_field!(age:10)
}

Actual output is generated by the "Expand macro recursively at caret" feature in the VS Code rust-analyzer extension.

See also

1

There are 1 answers

0
Nick On

Root issue

The root issue is that the macro was evaluated from the outside inwards. This meant the last iteration of the macro produced invalid code (i.e., it produced age: Some(10)). Macros must always produce complete, valid Rust code (such as an entire expression).

Solution

Using push-down accumulation fixed the issue. This forces the macro to be evaluated from the inside out.

macro_rules! __hello_impl {
    (() -> ($($output:tt)*)) => {
        Hello {
            $($output)*
        }
    };
    ((name: $value:expr, $($rest:tt)*) -> ($($output:tt)*)) => {
        __hello_impl!(($($rest)*) -> ($($output)* name: $value, ))
    };
    ((name: $value:expr) -> ($($output:tt)*)) => {
        __hello_impl!(() -> ($($output)* name: $value ))
    };
    ((age: $value:expr, $($rest:tt)*) -> ($($output:tt)*)) => {
        __hello_impl!(($($rest)*) -> ($($output)* age: Some($value), ))
    };
    ((age: $value:expr) -> ($($output:tt)*)) => {
        __hello_impl!(() -> ($($output)* age: Some($value) ))
    };
}
macro_rules! hello {
    ($($input:tt)*) => {
        {
            __hello_impl!(($($input)*) -> ())
        }
    };
}

Resources