Referencing local variables from include!'ed file within MBE

Hello!

I have the following code:

main.inc

{ println!("{}", x); }

main.rs

macro_rules! include_wrapper {
    () => {{
        let x = 5;
        include!("main.inc");
    }};
}

fn main() {
    include_wrapper!();
}

I would expect the program to compile successfully and print 5 when run. However, rustc emits an error:

error[E0425]: cannot find value `x` in this scope
 --> src\main.inc:1:18
  |
1 | { println!("{}", x); }
  |                  ^ not found in this scope

If I instead expand main.rs manually with cargo rustc -- -Zunpretty=expanded, rustc emits the following code:

#![feature(fmt_internals, prelude_import, print_internals)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
macro_rules! include_wrapper {
    () => { { let x = 5 ; include! ("main.inc") ; } } ;
}

fn main() {
    {
        let x = 5;
        {
            {
                ::std::io::_print(::core::fmt::Arguments::new_v1(&["", "\n"],
                        &[::core::fmt::ArgumentV1::new_display(&x)]));
            };
        };
    };
}

...which compiles successfully and prints the expected 5 when run.

I have a few questions:

  • Is this expected behavior of the include! macro?
  • Is there a workaround such that I do not need to call cargo rustc -- -Zunpretty=expanded to compile this code?

Thanks in advance!

Yes.

I'm less sure about the actual question, but expanded code having different semantic meaning (or in this case, ability to compile) is expected too.

Thanks for the response!

I am a little confused as to where exactly hygiene applies.

The include! macro does not obey typical macro hygiene; this is explicitly stated in the documentation. Furthermore, the include_wrapper! macro does not receive any arguments (the variable x is local to include_wrapper!), so hygiene seems to not apply there either. Or, am I misunderstanding how hygiene works?

As an example, if I rewrite main.rs as such:

fn main() {
    let x = 5;
    include!("main.inc");
}

...then the program prints the expected 5. The only change from the original code is that include! is now called in a function rather than in an MBE. This suggests to me that there is something special about calling include! from within any MBE.

Ah, sorry, I didn't realize behavior differed inside and outside the macro. I'm afraid I don't know the answer either. I tried to track down the documentation you cite before replying for details, actually, but it dates until 2016 or before and I figured I wasn't going to find a precise answer. Additionally include! is a built-in macro with potentially magic behavior. Seems like one of those areas that could really benefit from proper documentation / specification.

Hopefully someone more familiar with the implementation can provide a better answer.

The behavior seems explainable without special-case rules, by expansion order combined with macro_rules hygiene behavior: the include_wrapper macro is expanded first, converting

include_wrapper!();

into the token stream with hygiene-affected symbols

let invisible_because_from_a_macro#x = 5;
include!("main.inc");

and then the include is expanded into

let invisible_because_from_a_macro#x = 5;
{ println!("{}", x); }

which fails. The key thing is that since macros are expanded outside-in, the include!'s included tokens are not part of the macro_rules macro at all, and so cannot refer to its variables. (I entirely agree that this ought to have reference documentation.)

2 Likes

No worries at all!

That makes sense. Thanks!

The Reference seems to provide little information on macro hygiene. It begins with:

By default, all identifiers referred to in a macro are expanded as-is, and are looked up at the macro's invocation site. This can lead to issues if a macro refers to an item or macro which isn't in scope at the invocation site.

...and then proceeds to explain the $crate keyword. Unless there is another section in The Reference which discusses this in further detail, these statements do not seem to provide a full picture of macro hygiene. I recall reading somewhere in official Rust documentation that each MBE invocation opens a new scope disassociated from the current scope, which explains why MBEs (except include!, sometimes) cannot refer to local variables accessible at the invocation site. But, I cannot seem to find it.

By contrast, The Little Book of Rust Macros says this in its first four sentences on declarative macro hygiene:

macro_rules! macros in Rust are partially hygienic, also called mixed hygiene. Specifically, they are hygienic when it comes to local variables, labels and $crate, but nothing else.

Hygiene works by attaching an invisible "syntax context" value to all identifiers. When two identifiers are compared, both the identifiers' textual names and syntax contexts must be identical for the two to be considered equal.

Do you think the documentation for the include! macro could also be improved? Maybe a note that include!'ed code cannot reference local variables from an MBE because they are invisible due to hygiene?