Use `$t::method()` in macros cause error

To expand on this: the Rust grammar is very strict about what "kinds" of stuff can be encountered where; and …::to_be_bytes(…), for instance, will only parse if …::to_be_bytes is a path to a value. And here is what the Rust reference regarding the Rust grammar has to say about this:

      • e.g.
        u32::to_be_bytes(…) :white_check_mark:
      • e.g.
        <u32>::to_be_bytes(…) :white_check_mark:
        <(u32)>::to_be_bytes(…) :white_check_mark:

So, basically, when writing …::to_u32_bytes(<some expr>), the before the :: can only be one of two things:

  • either a (::-delimited sequence of) PathExprSegment(s), i.e., a (sequence of) identifier(s) (with optional turbofish args), such as the u32 identifier,

  • Or a QualifiedPathInExpression, such as <u32> (more generally, a bracketed-delimited type (with an optional as Trait).


The surprise here is that when capturing a u32 within a macro metavariable/transcriber of the :ty kind, to then use it to emit the call expression:

let some_expression = …;
macro_rules! with_u32 {( $u32:ty ) => (
    $u32::to_be_bytes(some_expression)
)}
let bytes = with_u32!( u32 );

This fails, even though, it "ought" to be simply emitting a u32 :: to_be_bytes ( some_expression ), right?

Wrong! :x:

The subtle thing here is that the macro metavariable / transcriber captures which are not :tt, :ident, or :lifetime actually wrap the captured syntax within "invisible parenthesis" that make it no longer be something as simple as an identifier: the "real" expansion of that with_u32!( u32 ) call, with our invisible parenthesis goggles on, is:

   ⁽u32⁾ :: to_be_bytes ( some_expression )
// ^^^^^
// a type

which does not match the syntax PathInExpression syntax at all, and doesn't match the QualifiedPathInExpression syntax by that tiny detail of there not being the necessary angle brackets.

Hence why adding the angle brackets was necessary (a :ty macro metavariable/transcriber (such as ⁽u32⁾) is obviously a valid Type within Rust grammar).

Note that these "invisible" parenthesis are not shown on cargo expand / rustc -- … {un,}pretty=expanded output, hence why this error message may look especially confusing given that one can copy-paste the output of such expansion and the code will work.


In some cases, this is a reason to use $($T:tt)* to capture the $T as an arbitrary sequence of arbitrary tokens which are later on emitted verbatim, causing no issues except when generics parameters appear (in which case @Hyeonu's remark about a necessary turbofish syntax or <…> wrapping applies). But then we have the issue of it being quite hard to parse multiple such $Ts, since nothing can follow a :tt)* repetition, and thus each type needs to be grouped within parenthesis, brackets, or braces, to avoid ambiguity errors:

macro_rules! with_types {(
    $( ($($T:tt)*) )*
) => (
    $(
        let _ = $($T)* :: to_be_bytes ( some_expression );
    )*
)}
with_types![
    (u32)
    (::core::primitive::u64)
    (Vec<String>) // Error, `Vec < String` expression followed by extraneous code
];

A more general solution to this issue is, since proc-macros can see and interact with those invisible parenthesis, to use a helper proc-macro which can strip them, such as:

  • Disclaimer: authored by me.
1 Like