Capturing variables into macro_rules

I'm trying to understand the rules for variables being implicitly captured in macros. Specifically, I'd like to know the formal difference between the following two cases:

#![allow(unused_macros)]

macro_rules! hack1 {
    () => { println!("{}", i); }
}

fn main() {
    let i = 1i32;
    {
        macro_rules! hack2 {
            () => { println!("{}", i); }
        }
        // uncomment next line to get an error
        // hack1!();
        // this, however, works
        hack2!();
    }
}

(Playground)

(In fact, I'm trying to understand how the proc_macro_hack works, but for now most of the code is more or less clear, and this is the blocker)
Does the macro definition always capture everything that is in scope at that moment? Or there are some restrictions for that?

2 Likes

I don't know exactly how this capturing works, but my guess is that it does the following:

  1. when a macro is called / expanded, it may use variables / identifiers that weren't given as input to the macro expansion (e.g., i here);

  2. It thus uses the definition site span / location information (instead of the one at the call site, since that would be unhygienic) to "guess" what these identifiers may refer to. For instance, it will look if / what i was defined / in scope when the macro was defined. For hack1! there was none, hence the error;

  3. (This is the part that I am less sure about)
    If there was such a variable in scope, then Rust will assume it was & [mut] borrowed / moved from that definition point until the usage point (and which of these three capture modes is chosen will depend on the usage that the macro expansion does, I imagine).
    So, in your example it will assume that i was share-borrowed when hack2! was defined, up until the println! where it is used. In other words, mut-borrowing i in between these two points should result in an error (can't test atm).

    EDIT: If there was such a variable in scope, then it will be & [mut] borrowed / moved (depending on the usage of the expanded code) starting at the usage / expansion point, regardless of where the definition point was located. In other words, it does not work like closure capture (which borrows from the definition point up to the usage point).

Just checked - yes, this seems to be the case:

fn main() {
    let mut i = 1i32;
    {
        macro_rules! hack2 {
            () => { println!("{}", i); }
        }
        let ref_i = &mut i;
        // this doesn't work, since at the call site the "i" is held
        // by the mutable borrow
        hack2!();
        drop(ref_i);
    }
}

Playground

Actually, to be tested we do not have to hold the &mut borrow during the "usage" point (as there was no chance that that could work), on the contrary I just wanted to test an ephemeral &mut between the definition and usage point (which suffices to trigger an error when using a closure to capture &i): it turns out I was wrong, there is no borrow held from the definition point; the borrow only starts when the macro is used:

fn main ()
{
    let mut i = 1i32;
    {
        macro_rules! hack {() => ({
            println!("{}", i);
        })}
        let _ = &mut i; // this does work, since the borrow `&i` has not started yet
        hack!(); // borrow starts (and ends) here
    }
    {
        let hack = || { // borrow starts here (captures `&i`)
            println!("{}", i);
        };
        let _ = &mut i; // Error!
        // this doesn't work, since at the call site the "i" is held
        // by the mutable borrow
        hack(); // borrow ends here
    }
}
1 Like