3

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

vallentin
  • 23,478
  • 6
  • 59
  • 81
Big Monday
  • 590
  • 1
  • 5
  • 15
  • Does this answer your question? [Why can I not access a variable declared in a macro unless I pass in the name of the variable?](https://stackoverflow.com/questions/53716044/why-can-i-not-access-a-variable-declared-in-a-macro-unless-i-pass-in-the-name-of) – pretzelhammer Dec 25 '20 at 20:33
  • @pretzelhammer correct me if I'm wrong, but if you switch `$field_path` type to `ident` and run the `my_macro!(inner, Inner { value: 42 });` line from my demo, it compiles and properly modifies `x`. I think the issue lies in the fact that `inner.value` contains the dot operator. – Big Monday Dec 25 '20 at 20:43

2 Answers2

8

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 });
vallentin
  • 23,478
  • 6
  • 59
  • 81
3

The reason it doesn't work is because Rust macros operate on AST nodes, instead of tokens.

The AST for accessing a nested field looks like this (with a very loose syntax where parentheses denote an AST node): (((x).outer).inner)

You've set $field_path to be an expr, so it gets parsed like this: ((inner).outer). This is a valid expression that accesses the outer field of the inner variable.

Then, evaluating the macro produces something like: ((x).((inner).outer)), which doesnt make sense.

Now, how to fix it?

The key is that the variable name (like x in this case) needs to end up "deep within" the AST of the expression that accesses the field. This makes it impossible to do it with any way of "passing in" a path as a macro argument.

What you can do is pass in a macro that builds the path expression:

macro_rules! my_macro {
    ($path:ident, $v:expr) => {
        $path!(x) = $v;
    }
}
macro_rules! inner_value {
    ($x:ident) => {
        $x.inner.value
    }
}

my_macro!(inner_value, 42); // expected output: x.inner.value = 42;

(playground)

I'd consider doing this without macros though, for better readability. Depending on what you're doing, using a closure might work:

fn do_things(x: &mut Outer, path: impl Fn(&mut Outer) -> &mut u8){
    *(path(x)) = 42;
}

do_things(&mut x, |x| &mut x.inner.value);

(playground)

vallentin
  • 23,478
  • 6
  • 59
  • 81
Dirbaio
  • 2,921
  • 16
  • 15
  • 2
    I appreciate the explanations, however it seems like macro repetitions that were suggested by @vallentin work better for my case. – Big Monday Dec 25 '20 at 21:34