Search code examples
rustrust-macros

Passing nested struct field path as macro parameter


I am fairly new to Rust, and I am having trouble getting the following code to compile:

#![feature(trace_macros)]

fn main() {
    #[derive(Debug)]
    struct Inner {
      value: u8
    }
    
    #[derive(Debug)]
    struct Outer {
      inner: Inner
    }
    
    let mut x  = Outer { inner: Inner { value: 64 } };
    
    /********/
    
    macro_rules! my_macro {
        ($field_path:expr, $v:expr) => {
            x.$field_path = $v;
        }
    }

    trace_macros!(true);    
    // my_macro!(inner, Inner { value: 42 }); // only works with $field_path:ident
    my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
    trace_macros!(false);
    
    x . inner.value = 42; // works fine
    
    assert_eq!(42, x.inner.value);
}

I am getting the following errors:

error: unexpected token: `inner.value`
  --> src/main.rs:20:15
   |
20 |             x.$field_path = $v;
   |               ^^^^^^^^^^^
...
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     --------------------------- in this macro invocation
   |

...

error: expected one of `.`, `;`, `?`, `}`, or an operator, found `inner.value`
  --> src/main.rs:20:15
   |
20 |             x.$field_path = $v;
   |               ^^^^^^^^^^^ expected one of `.`, `;`, `?`, `}`, or an operator
...
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     --------------------------- in this macro invocation
   |

...

However, trace_macro seems to be able to expand my_macro!:

note: trace_macro
  --> src/main.rs:26:5
   |
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: expanding `my_macro! { inner . value, 42 }`
   = note: to `x . inner.value = 42 ;` <<< exactly what I am looking for

If I keep $field_path parameter as ident, I am simply getting no rules expected the token `.` , which I guess makes sense, because . is an operator. What am I missing?

Playground link


Solution

  • I think the issue lies in the fact that inner.value contains the dot operator.

    That's exactly your issue. There isn't a single fragment specifiers that allows you to match ident and/or a field access expression. The issue with using expr (expressions) is that when expanded it essentially wraps in parenthesis, i.e. x.(inner.value), which doesn't make sense and thus why you're getting an "unexpected token `inner.value`".

    However, you can indeed use ident, you just need to use repetition as well.

    In short, instead of $field_path:ident then you do $( $field_path:ident ).+. Then to expand it, instead of x.$field_path then you do x. $( $field_path ).+.

    macro_rules! my_macro {
        ($($field_path:ident).+, $v:expr) => {
            x.$($field_path).+ = $v;
        };
    }
    
    // Now both are allowed
    my_macro!(inner.value, 42);
    my_macro!(inner, Inner { value: 64 });