Is this a compiler bug? (Macro that uses loop label, which is defined outside of loop)

This code (Playground):


macro_rules! skip1 {
    () => { break 'label } 
}

fn main() {
    'label: loop {
        macro_rules! skip2 {
            () => { break 'label }
        }
        skip2!(); // works
        skip1!(); // doesn't work
    }
}

...fails with:

error[E0426]: use of undeclared label `'label`
  --> src/main.rs:2:36
   |
2  | macro_rules! skip1 { () => { break 'label } }
   |                                    ^^^^^^ undeclared label `'label`
...
10 |         skip1!(); // doesn't work
   |         --------- in this macro invocation

Shouldn't that work?

No, because Rust macros are hygienic; they can’t access names declared outside the scope of the macro definition. Unlike C macros, they’re not totally naive textual substitutions.

5 Likes

In addition to what @jdahlstrom said, macro_rules! macros can only expand to complete expressions, items, patterns etc, not partial ones.
That's where the skip1 macro runs into trouble.

1 Like

@jdahlstrom: Ah, that makes sense, thank you!

The following works:

macro_rules! skip { ($label:lifetime) => { break $label } }

fn main() {
    'label: loop {
        skip!('label);
    }
}

That's where the skip1 macro runs into trouble.

@jjpe: Thank you, can you tell me what exactly the problem is with skip1? To me, it looks complete, and with the above change, it works.

Wait a second. If macros can't use outside labels, how is it possible that skip2 works?

fn main() {
    'label: loop {
        macro_rules! skip2 {
            () => { break 'label }
        }
        skip2!(); // works
    }
}

Shouldn't that be forbidden as well?

The issue is that the break 'label expansion can be called outside of a loop if the macro is called outside a loop, and on top of that the label in your skip1 macro isn't defined. Your revised macro fixes the 2nd issue.

I'm kind of surprised that the revised version works though, because of the 1st issue.

That makes sense. However, I'd expect that Rust expands the macro, and then checks if the resulting code is valid, wouldn't that make more sense?

Upon further reflection, the truth for issue 1 is somewhere in the middle: macro_rules macros can also expand to patterns for example, which are never valid outside of the appropriate expressions i.e. match and anything that desugars to it, as well as let and fn parameter bindings.

1 Like

In the revised version, 'label1 is in scope where the macro is defined, so it remains hygienic: Any use of 'label1 inside the macro refers to that label, regardless of where the macro is used.

5 Likes

I must've read past that a bit too quickly, that indeed explains it.

1 Like

break 'label is a complete expression though, so while the argument about expanding to full expressions, types, patterns, or items, is true, it is not the issue here 🙂

1 Like

I guess technically it is from a grammatical POV.

But it can't be used outside of a loop (are there other contexts it can be used in?) and so functionally it's incomplete on its own.