Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I write a wrapper for a macro without repeating the rules?

Tags:

macros

rust

I am trying to make a wrapper for a macro. The trouble is that I don't want to repeat the same rules in both macro. Is there a way to do that?

Here is what I tried:

macro_rules! inner {
    ($test:ident) => { stringify!($test) };
    ($test:ident.run()) => { format!("{}.run()", stringify!($test)) };
}

macro_rules! outer {
    ($expression:expr) => {
        println!("{}", inner!($expression));
    }
}

fn main() {
    println!("{}", inner!(test));
    println!("{}", inner!(test.run()));
    outer!(test);
    outer!(test.run());
}

but I get the following error:

src/main.rs:8:31: 8:42 error: expected ident, found test
src/main.rs:8         println!("{}", inner!($expression));
                                            ^~~~~~~~~~~

If I change the outer macro for this, the code compile:

macro_rules! outer {
    ($expression:expr) => {
        println!("{}", stringify!($expression));
    }
}

What am I doing wrong?

like image 524
antoyo Avatar asked Aug 29 '15 18:08

antoyo


1 Answers

macro_rules! is both cleverer and dumber than you might realise.

Initially, all input to a macro begins life as undifferentiated token soup. An Ident here, StrLit there, etc. However, when you match and capture a bit of the input, generally the input will be parsed in an Abstract Syntax Tree node; this is the case with expr.

The "clever" bit is that when you substitute this capture (for example, $expression), you don't just substitute the tokens that were originally matched: you substitute the entire AST node as a single token. So there's now this weird not-really-a-token in the output that's an entire syntax element.

The "dumb" bit is that this process is basically irreversible and mostly totally invisible. So let's take your example:

outer!(test);

We run this through one level of expansion, and it becomes this:

println!("{}", inner!(test));

Except, that's not what it looks like. To make things clearer, I'm going to invent some non-standard syntax:

println!("{}", inner!( $(test):expr ));

Pretend that $(test):expr is a single token: it's an expression which can be represented by the token sequence test. It is not simply the token sequence test. This is important, because when the macro interpreter goes to expand that inner! macro, it checks the first rule:

    ($test:ident) => { stringify!($test) };

The problem is that $(test):expr is an expression, not an identifier. Yes, it contains an identifier, but the macro interpreter doesn't look that deep. It sees an expression and just gives up.

It fails to match the second rule for the same reason.

So what do you do? ... Well, that depends. If outer! doesn't do any sort of processing on its input, you can use a tt matcher instead:

macro_rules! outer {
    ($($tts:tt)*) => {
        println!("{}", inner!($($tts)*));
    }
}

tt will match any token tree (see the Macros chapter of the Rust Book). $($tts:tt)* will match any sequence of tokens, without changing them. This of this as a way to safely forward a bunch of tokens to another macro.

If you need to do processing on the input and forward it on to the inner! macro... you're probably going to have to repeat the rules.

like image 131
DK. Avatar answered Sep 22 '22 18:09

DK.