FABRKNT
Reading the Stack — Bridge to Intermediate
Rust for source-reading
Lesson 9 of 10·CONTENT10 min20 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Reading the Stack — Bridge to Intermediate
Lesson role
CONTENT
Sequence
9 / 10

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 like println!("{} {}", 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_unchecked into 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 than macro_rules!; implemented as Rust functions. Deep-dive is in Expert.
  • Reading a macro. cargo expand shows 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

SpecifierMatches
exprAny Rust expression
identAn identifier (variable name, function name)
ttA single token tree (the most flexible)
stmtA statement
patA pattern (in match arms, let bindings)
tyA type
blockA { ... } 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 asPattern-matching rulesRust function in a separate proc-macro crate
Operates onToken trees (literal syntax)TokenStream (compiler's parsed representation)
PowerLimited but enough for most thingsArbitrary code generation
Examplesvec!, 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 calling unwrap_unchecked() because the length was just verified
  • The SAFETY reasoning 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

  1. Rust Book chapter 19.5 (Macros) — concise. Read once, refer back as needed.
  2. 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 ?Sized so it can be dyn Host
  • The macro call: pop 1, get a mutable ref to the new top, with the unsafe unwrap_unchecked justified by an internal length check
  • The arithmetic: wrapping_add because EVM ADD is 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! and gas! collapse hotpath boilerplate across 30+ opcodes.
  • Use cargo expand to read the expanded code. Procedural macros (#[derive] / sol! etc) are deeper — Expert covers them. Next lesson: how the source-reading courses work.