We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 11 of 28
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.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.