How to understand "the second macro will see an opaque AST" in the reference?

As the reference explains in section `Transcribing` about macros by example:

When forwarding a matched fragment to another macro-by-example, matchers in the second macro will see an opaque AST of the fragment type. The second macro can't use literal tokens to match the fragments in the matcher, only a fragment specifier of the same type. The ident, lifetime, and tt fragment types are an exception, and can be matched by literal tokens.

In my understanding, any fragment specifier except whose type is one of ident, lifetime, and tt can only be matched in the second macro using a fragment specifier of the same type.

But this code works (playground link):

macro_rules! foo {
    ($l:expr) => { bar!($l); }
}

macro_rules! bar {
    ($l:tt) => {}
}

foo!(3);

So I'm wondering if it works using ident or lifetime instead of tt, but run this code I got an error (playground link):

/*--- snip ---*/

macro_rules! bar {
    ($i:ident) => {}
}

foo!(a);  // Error!

To my surprise, the first code still works if I change the type of $l from tt to literal:

/*--- snip ---*/

macro_rules! bar {
    ($l:literal) => {};
}

foo!(3);

Did I misunderstand what has been explained in the reference? if I did, how to understand it correctly? Or the reference makes a mistake?

I believe the documentation is saying this will work

macro_rules! foo {
    (3) => {};
}

foo!(3);

But this will not

macro_rules! foo {
    ($l:expr) => { bar!($l); };
}

macro_rules! bar {
    (3) => {};
}

foo!(3);

The compiler seems to agree.

The documentation is saying that you can only match on the literal token 3 in the first macro. In the second macro, you must match it as a token tree, expression, code literal, etc. The second macro sees only an "opaque abstract syntax tree". It cannot see through the token type (expr, tt, literal) to see the actual contents of the token (in this case 3).

4 Likes

But how to explain why this code doesn't work? (playground link)

macro_rules! foo {
    ($e:expr) => { bar!($e); }
    // ERROR:           ^^ no rules expected this token in macro call
}

macro_rules! bar {
    ($i:ident) => {}
}

foo!(a);

The only match arm in the definition of bar requires an ident, but foo doesn’t have one of those. Instead, $e is an opaque expr which might be something other than a single identifier, such as a literal 42 or a compound expression a + b. The expansion rules forbid this regardless of what’s actually present at the callsite.

3 Likes

The main point of confusion is, I think, why didn't the compiler throw an error on the chain 3 -> $expr -> $literal (as in the third example by OP)? It probably is expected to fall under the same logic: $expr might not be $literal, so the inner macro call should fail, but it doesn't.

2 Likes

My guess is that was originally a compiler bug, which turned into a special case to avoid breaking some pre-existing Rust code.

I'm convinced there's a compiler bug. See the code below, it prints bar!(3 + 4) = 7, surprisingly! (playground link)

macro_rules! foo {
    ($e:expr) => { dbg!(bar!($e)) }
}

macro_rules! bar {
    ($($t:tt)?) => {
        $($t)?
    }
}

fn main() {
    foo!(3 + 4);
    // dbg!(bar!(3 + 4)); // error
}

Well, the last example is not very surprising, in fact, if you consider that the following compiles too:

macro_rules! bar {
    ($($t:tt)?) => {
        $($t)?
    }
}

fn main() {
    dbg!(bar!{ (3 + 4) });
}

(This, however, throws a warning about unnecessary parentheses, which is a false-positive - without them, as you've already found, the code would not compile. I'd probably search for the issue a little later and create it if it doesn't exist yet)

The point is that (3 + 4) is a single token tree. In procedural macros, it would be represented as TokenTree::Group. So the whole parenthesised expression is matched as $tt and passed as-is into the expansion.

The same thing applies to the already-matched items, such as $exprs: they are treated "as if" they are wrapped in the invisible parentheses - in procedural macros, this correspond to the Group with Delimiter::None; that's what allows this code to provide expected result:

macro_rules! double {
    ($x:expr) => {
        2 * $x
    };
}

fn main() {
    dbg!(double!(1 + 2)); // prints 6
}

If not for these invisible groups, double!(1 + 2) would expand to 2 * 1 + 2 (as it does in C) and evaluate to 5, not 6.
So, if something in an $expr, it is again a single token tree, no matter what was matched as this $expr before; and so it can be matched by $tt.

On the other hand, 3 + 4 by itself is not a single token tree. It can be matched by $($t:tt)*, as a sequence of three token trees - 3, +, 4; but not by a single $tt.

5 Likes

There is no bug here, it's all working as intended. I've explained this phenomenon several times already, posting it here for reference:






From your first post, it looks like you got the :tt / :ident vs. matcher situation reversed:

  • If foo! were to capture a :tt, :ident (or :lifetime), then you'd be able to transparently match against the exact token capture within bar!.

  • But if foo! uses a high-level auto-grouped capture such as :expr, then that metavariable will thenceforward represent an invisibly-parenthesized group. It's thus a single token tree (quite handy for recursing, btw), but one which appears opaque to the second macro, in the same fashion that bar! { ( quux ) } will not be a valid call if bar! were to expect a quux argument.

    So, bar! can only handle a higher-level-grouped metavariable from foo! if and only if:

    • It takes a :tt, since all (parenthesized, braced, …) groups, including the "invisibly parenthesized" ones, are single token-trees each, or if it takes some other higher-level capture compatible with the first one: an :expr is compatible with an :expr (and more generally, for any kind, a :kind is always compatible with :kind), and then you can have a :path be compatible with :expr, or with :ty, or with :pat; an :item is compatible with a :stmt , a :block is compatible with an :expr, etc.

    See what the reference has to say about the Rust grammar to better figure out these compatibilities.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.