Perplexing macro issue

#1

I have the following macro defined in a library my application uses:

#[macro_export]
macro_rules! cache_statement {
    ($sql:expr, $stmt:ident) => (
        {
            let mut needs_preparing:bool = false;
            if globals.statements.$stmt.is_none() {
                needs_preparing = true;
            };
            if needs_preparing {
                globals.statements.$stmt.replace(globals.db.prepare($sql).unwrap());
            };
            globals.statements.$stmt.as_mut().unwrap()
        }
    );
}

In one of the application’s source files, I call the macro like so:

170 fn display_reconciled_balance(account_register:&AccountRegister, globals:&Globals) {
171     let stmt:&mut Statement = cache_statement!(RECONCILED_BALANCE_SQL, reconciled_balance_stmt);
172     let guid = &(*account_register.guid);
173     let reconciled_balance:f64 = stmt.query_row(&[guid], get_result!(f64)).unwrap();
174     let reconciled_balance_string = format!("Reconciled balance for\n{}\n\n${:.*}",
175                                                 guid_to_path(cache_library_statement!(guid_to_path_init, guid_to_path_stmt), guid),
176                                                 2, reconciled_balance);
177     display_message_dialog(&reconciled_balance_string, globals);
178 }

When I compile this, I get the following error:

error[E0425]: cannot find value `globals` in this scope
   --> src/account.rs:171:31
    |
171 |     let stmt:&mut Statement = cache_statement!(RECONCILED_BALANCE_SQL, reconciled_balance_stmt);
    |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
    |
    = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

This is odd, since ‘globals’ is defined. In fact, this error occurs four times. I suspect this is once for each reference to ‘globals’ in the macro definition. This occurs with both the stable and nightly compilers. If, using the nightly compiler, I enable macro tracing around this call, I get the following expansion:

{
            let mut needs_preparing : bool = false ; if globals . statements .
            reconciled_balance_stmt . is_none (  ) { needs_preparing = true ; } ; if
            needs_preparing {
            globals . statements . reconciled_balance_stmt . replace (
            globals . db . prepare ( RECONCILED_BALANCE_SQL ) . unwrap (  ) ) ; } ;
            globals . statements . reconciled_balance_stmt . as_mut (  ) . unwrap (  ) }

If I then manually substitute this expansion for the macro call in the source code and recompile, no error.

This feels like a bug to me, but maybe I’m missing something. Perhaps someone has some insight into what’s going on here?

Thanks.

#2

This is the intended behavior. The expanded code will only be able to touch local variables that are passed in to the macro invocation, so you will need to pass in globals and refer to it that way.

Macros do not operate as simple text substitution for many reasons. Check out the macro hygiene section in the old Rust book for some examples.

1 Like
#3

It seems that Rust macros are heavily influenced by Scheme’s hygienic macros. While I’ve been writing Scheme code for many years (decades!), I am not an expert on define-syntax and friends, though I have used it/them. I just cooked up a little analogous example Scheme code and tested it with Chez Scheme. The behavior is the same as Rust’s.

Thanks very much for clearing this up for me.

1 Like