Lesson 8 — macro_rules! basics
Question
Rust macros = ! calls like println! / vec! / format!. Compile-time code expansion; more flexible than functions. Reth / Revm source uses popn_top! / gas! etc constantly. How to read macro_rules!.
Principle (minimum model)
- Macro = compile-time code expansion. Expands to AST at the call site; more flexible than a function (variable arity / arbitrary syntax); trade-off is debuggability.
macro_rules!syntax.macro_rules! name { ($x:expr) => { ... } }— pattern → expansion rules. Fragment specifiers:expr/ident/ty/pat/stmt...- Variable arity.
$($x:expr),*matches a comma-separated list;$( ... )*expands once per element. Implements variadic args likeprintln!("{} {}", a, b). - Hygiene. Variables defined inside a macro don't leak into the surrounding scope. Safe abstraction.
- Revm's
popn_top!. Combines stack pop + underflow pre-check +unwrap_uncheckedinto one macro. Reused across 30+ binary opcodes (add / mul / sub / ...). gas!macro. Combines gas charging + cold_path hint + early return. 5-line; eliminates hotpath boilerplate.- Procedural macros (proc-macro).
#[derive(...)]/#[tokio::main]/sol!etc. More powerful thanmacro_rules!; implemented as Rust functions. Deep-dive is in Expert. - Reading a macro.
cargo expandshows the expanded code; reading the expansion is the right approach.macro_rules!syntax itself is just practice.
Worked example + steps
macro_rules! basics
The other of two areas where the standard Rust book is thin. The previous lesson covered unsafe; this one covers macro_rules!. Together they're what you need to read Revm's hot-path source — popn_top!, gas!, and the rest.
Revm's interpreter is dense with macros. popn_top!, gas!, push!, as_usize_or_fail! — these aren't function calls, they're compile-time text expansions. Reading them requires knowing the syntax.
The basic shape
A macro_rules! macro is pattern → expansion. The pattern matches caller syntax; the expansion produces code:
macro_rules! square {
($x:expr) => {
$x * $x
};
}
let n = square!(3 + 4); // expands to: (3 + 4) * (3 + 4) → 49
The $x:expr part declares a metavariable $x that matches any expression. expr is a fragment specifier telling the parser what kind of syntax to expect.
Common fragment specifiers
| Specifier | Matches |
|---|---|
expr | Any Rust expression |
ident | An identifier (variable name, function name) |
tt | A single token tree (the most flexible) |
stmt | A statement |
pat | A pattern (in match arms, let bindings) |
ty | A type |
block | A { ... } block |
For reading source, expr and ident and tt cover most cases.
Repetition: $( ... ),*
Macros can match lists with repetition syntax:
macro_rules! print_all {
( $( $x:expr ),* ) => {
$(
println!("{}", $x);
)*
};
}
print_all!(1, "hello", 3.14);
// expands to:
// println!("{}", 1);
// println!("{}", "hello");
// println!("{}", 3.14);
Reading the syntax:
$( ... )declares a repetition group,*says "separated by commas, zero or more times" (use,+for one or more)- Inside the expansion,
$( ... )*repeats the body once per match
Reading Revm's popn_top!
Now armed with the basics:
macro_rules! popn_top {
([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
// ... body
};
}
Translation:
- The pattern starts with
[, then$($x:ident),*(a comma-separated list of identifiers), then] - Then a comma, then
$top:ident(one identifier) - Then a comma, then
$interpreter:expr(an expression)
So calling popn_top!([op1], op2, ctx.interpreter) matches with:
$x= list of one identifier:op1$top=op2$interpreter=ctx.interpreter
Calling popn_top!([a, b, c], top, ctx.interpreter) would have $x as a list of three.
The expansion uses $($x),* to expand the same list of identifiers into the destructuring pattern.
Macro hygiene — why a macro can't pollute your scope
macro_rules! macros are hygienic: variable names introduced inside a macro don't conflict with variables in the caller's scope.
macro_rules! double {
($e:expr) => {{
let temp = $e; // 'temp' inside macro
temp * 2
}};
}
let temp = "important"; // 'temp' in caller
let n = double!(5); // expands without breaking the caller's 'temp'
println!("{}", temp); // still "important"
Hygiene is a feature of macro_rules! (and a major reason it's preferred over C-style macros).
macro_rules! vs proc_macro
You'll meet both. Quick distinction:
macro_rules! | proc-macro | |
|---|---|---|
| Defined as | Pattern-matching rules | Rust function in a separate proc-macro crate |
| Operates on | Token trees (literal syntax) | TokenStream (compiler's parsed representation) |
| Power | Limited but enough for most things | Arbitrary code generation |
| Examples | vec!, println!, popn_top!, gas! | #[derive(Serialize)], sol!, address!'s underlying hex! |
macro_rules! covers most of what Revm's interpreter uses. sol! and the heavy proc-macros are an Expert-tier topic.
Putting it together
When you open revm/crates/interpreter/src/instructions/macros.rs in Intermediate lesson 1, you'll see:
macro_rules! popn_top {
([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
$crate::primitives::hints_util::cold_path();
return Err($crate::InstructionResult::StackUnderflow);
}
let ([$( $x ),*], $top) = unsafe {
$crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
.unwrap_unchecked()
};
};
}
You can now read it word by word:
macro_rules!— declarative macro definition- Pattern
([ $($x:ident),* ], $top:ident, $interpreter:expr)— destructure caller arguments - Body: a length check, then an
unsafe { ... }block callingunwrap_unchecked()because the length was just verified - The
SAFETYreasoning is the length check — the comment is implicit (Revm sometimes elides them on tight code)
You're not reading magic. You're reading patterns you now know.
Reading list
- Rust Book chapter 19.5 (Macros) — concise. Read once, refer back as needed.
- The Little Book of Rust Macros (danielkeep.github.io) — free, the best macro-by-example reference.
What you should walk away with
macro_rules!matches token patterns and expands to code at compile time- Fragment specifiers (
$x:expr,$x:ident,$x:tt) declare what kind of syntax each metavariable matches - Repetition syntax
$( ... ),*lets one pattern match a list of arguments - Hygiene prevents macros from polluting caller scope
- proc-macros are the heavier sibling — separate crate, operates on TokenStream — covered in Expert
Technical prereqs complete
You've now finished the technical prereqs of Reading the Stack — Bridge to Intermediate:
- ✓ EVM at the bytes level (dispatch loop, stores, gas, call frames)
- ✓ Block-level Ethereum (blocks, receipts, reorgs)
- ✓ Rust for source-reading (generics, dyn, Arc, unsafe, macros)
A quick gate-check before you go on. When you open the first Intermediate lesson, you'll see:
pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
popn_top!([op1], op2, context.interpreter);
*op2 = op1.wrapping_add(*op2);
Ok(())
}
Every piece should now read like one connected sentence:
- The signature: two type parameters, one of them
?Sizedso it can bedyn Host - The macro call: pop 1, get a mutable ref to the new top, with the unsafe
unwrap_uncheckedjustified by an internal length check - The arithmetic:
wrapping_addbecause EVMADDis mod 2²⁵⁶
If those three sentences fit, the technical prereqs are absorbed.
One more lesson — how Intermediate courses work
The Intermediate tier is three independent courses (Revm, Reth, Alloy), and they all share the same editorial style — Predict prompts, Quiz gates, the build-up → walkthrough → quiz → drill chain shape. Read "How Intermediate courses work" next; it's the meta-orientation that applies to all three Intermediate courses, so you only read it once.
After that, pick a course and start.
Summary (3 lines)
macro_rules!= compile-time code expansion. Variable arity + hygiene + fragment specifiers (expr / ident / ty ...). More flexible than functions.- Revm's
popn_top!andgas!collapse hotpath boilerplate across 30+ opcodes. - Use
cargo expandto read the expanded code. Procedural macros (#[derive]/sol!etc) are deeper — Expert covers them. Next lesson: how the source-reading courses work.