Search code examples
rustmacrostoken

Rust Declarative Macros and Scoping of tt


I encountered this problem while trying to apply a function to expressions inside a macro. I condensed it down to this somewhat silly minimal working example, where we have an expression inside curly braces and apply a function to it like so:

macro_rules! foo {
    ( {$expression:expr} .apply($($f:tt)+)) => {
        {
            let apply_func = $($f)+;
            apply_func($expression)
        }
    }
}

For my use case I am actually applying the function to multiple expressions, but that is not important here. The thing is that I bind a local variable apply_func to the tokens inside the apply(...). I was pretty sure that the local let binding would do exactly what I want and I am happy to say that the following expressions all work as intended:

//WORKS: simple use case
println!("{}", foo!({10}.apply(|x|x*x)));
//WORKS: nested use case
println!("{}", foo!({ foo!({10}.apply(|x|x*x)) }.apply(|x|x+3)));
//WORKS: using some function to pass to the macro
let some_func = |x|x+10;
println!("{}", foo!({ foo!({10}.apply(some_func)) }.apply(|x|x+3)));
//WORKS: calling a function in this scope `apply_func` and using it
//from the inner macro invocation
{
    let apply_func = |x|x*3;
    println!("{}", foo!({ foo!({10}.apply(apply_func)) }.apply(|x|x+3)));
}

The resulting output is

100
103
53
33

as expected.

However, the problem manifests if I give apply_func as the argument of the inner macro invocation like so:

// FAILS: trying to access the apply_func of the outer macro from
// the inner macro without defining it in this scope
println!("{}", foo!({ foo!({10}.apply(apply_func)) }.apply(|x|x+3)));

I feared that this could expand to

{
    let apply_func = |x| x + 3;
    apply_func({
        let apply_func = apply_func;
        apply_func(10)
    })
}

which I would not have liked one bit, because I want no interaction between apply_func instances of the nested scopes. But on my machine (with rustc 1.48.0 (7eac88abb 2020-11-16)) it gives a compile error

 |     println!("{}", foo!({ foo!({10}.apply(apply_func)) }.apply(|x|x+3)));
 |                                           ^^^^^^^^^^ not found in this scope

which is fantastic. Try it on the Rust Playground.

My questions are: Can I rely on that behavior? Is that actually expected and can someone explain that to me?

I know that Rust macros are not C-style text replacement. Is it the case that the tokens passed to the macro need to be valid code before being "pasted" into the macro expansion?


Solution

  • I would say yes, you can rely on that behaviour.

    Rust macros are hygienic, which means the compiler takes care of identifiers created by macros not clashing with identifiers created outside macro invocations.

    In your case, the apply_func created by the macro will be seen as a completely different identifier than the one you declared outside the macro and then gave to the macro.

    You can read more about it here.