[macros] Using path tokens with format_args()


#1

I’m trying to define a macro that takes a module name as input, as :path, and access its items.

Unfortunately, format_args macro, and therefore print/println, do not let me use a :path metavariable in their args, expecting :tt.

macro_rules! something {
    ( $path:path ) => (
        //println!("Say: {}", $path::say);
        format_args!("Say: {}", $path::say);
    );
}

mod foo {
    const say: &str = "Hello";
}

mod bar {
    const say: &str = "World";

    mod baz {
        const say: &str = "Universe";
    }
}

fn main() {
    something!(foo);
    something!(bar);
    something!(bar::baz);
}

It fails with three instances of this error (with RUSTFLAGS='-Z external-macro-backtrace'):

error: expected token: `,`
  --> src/main.rs:4:9
   |
1  | / macro_rules! talk {
2  | |     ( $mod:path ) => (
3  | |         // print!("Say: {}", $mod::say);
4  | |         format_args!("Say: {}", $mod::say);
   | |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5  | |     );
6  | | }
   | |_- in this expansion of `talk!`
...
21 |       talk!(foo);
   |       ----------- in this macro invocation

I’m surprised to see that $path::say doesn’t resolve into a tt before being passed to the internal macro call. Isn’t this how path metavariables are expected to be used?

Or maybe the limit here is the format_args implementation? If so, what input token type can format_args use to address this limit?

I couldn’t find a answer looking at the Macros docs. Any ideas?


#2

I believe this is a limitation in the expression parser – it doesn’t know how to combine a previously parsed $:path metavariable with a suffix like ::say to come up with a single joined path. You can reproduce it with a macro macro_rules! e { ($e:expr) => {} } and calling e!($path::say). File an issue if you can’t find one already open about this, because it should be reasonably easy to support $path::say.

For now two possible workarounds would be parsing the joined path all at once from tokens by matching $($path:tt)+ and passing $($path)+::say, or continuing to match a $:path but then using it indirectly through an import like { use $path as base; base::say }.


#3

Thanks, @dtolnay. Yeah, makes sense. I’ll make sure it’s reported, then!

I was going for the first workaround you mentioned, but I think the second one is even easier! Thanks for the hint! :slight_smile:


#4

Couldn’t find any related open issue. Filed this: https://github.com/rust-lang/rust/issues/48067.