Skip to content

← Macros, FFI and Type-Driven Design step 11 of 28

Medium Primitives

Macro hygiene: why your variable didn't leak

Fix let_named! so it declares the variable named by its caller. Keep the other macros, functions and modules unchanged. The starter always declares value, leaving total and extra undefined in introduced().

The supplied code demonstrates three kinds of name lookup:

Call Expected result What it demonstrates
observe() (42, 1) A macro's local binding does not capture the caller's expression.
introduced() 12 A captured identifier can name a binding the caller uses.
alpha::run() 10 helper() resolves in alpha.
beta::run() 20 The same macro resolves helper() in beta.

Two variables with the same spelling

macro_rules! using_a {
    ($e:expr) => {{
        let a = 42;
        (a, $e)
    }};
}

pub fn observe() -> (i32, i32) {
    let a = 1;
    using_a!(a)
}

The first tuple element refers to the a declared inside the expansion. The second comes from $e, which preserves the caller's reference to its own a. That is why the result is (42, 1).

Reading the expansion as plain text can be misleading. If you manually wrote { let a = 42; (a, a) } inside observe, ordinary shadowing would make both tuple elements 42. Macro hygiene preserves information about where names came from, so substituting text is not enough to predict name lookup.

Function names follow a different rule

macro_rules! call_helper {
    () => { helper() };
}

The supplied alpha and beta modules each define a helper function and invoke this macro inside run. The unqualified function name is resolved where the macro is invoked, so the two calls return different values.

This distinction is called mixed-site hygiene. For names written in a macro_rules! expansion, local variables and loop/block labels use the definition context; other symbols use the invocation context. Captured input retains its original context, as $e does above.

A local reference in a macro can also refer to a variable already in scope where the macro was defined. Hygiene does not mean every name written in a macro must refer to a binding declared inside that macro.

Introduce the requested name

let_named! already captures $n:ident and $v:expr. Use the captured identifier as the binding name instead of hardcoding value. The two supplied calls should then work together:

let_named!(total = 7);
let_named!(extra = 5);
assert_eq!(total + extra, 12);

This works with hygiene: the declaration and the later use of total both use the caller's identifier context.

Lexical scope still matters. The braces surrounding a macro arm's output delimit that output; they are not themselves an emitted block. Keep the declaration directly in that output. Adding another pair of braces, as in => {{ let $n = $v; }}, creates a block whose local variable is unavailable after the invocation.

The harness selects one of the supplied results using its want field. It tests observable values; preserve the macro structure so the exercise also demonstrates the intended name-resolution behavior.