Macro not recursively expanding (macro_rules)

112 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 best solutions below

0
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