Master Rust Declarative Macros

Build a macro pattern library covering every major macro_rules! concept.

Introduction

30 Second Summary

Think about every time you have copied and pasted a block of code, changed a couple of names, and pasted it again. What if the compiler could learn your pattern and write those variations for you?

In this project, you will build a zero-dependency library of declarative macros in Rust. Each macro demonstrates a specific macro_rules! pattern, from simple matching rules to recursive token processing, with tests proving how each macro expands.

What You'll Build

A single Rust library crate containing carefully crafted macros, each backed by tests that prove how macro expansion behaves.

By the end of this project, you'll have:

  • Multi-rule macros with literal token matching and fragment specifiers for capturing identifiers, expressions, types, and more.
  • Variadic constructors, a recursive counting macro, and nested repetition using repetition operators.
  • Macros demonstrating mixed-site hygiene, internal rules, TT munchers, and the callback pattern for composing macros together.
  • Secret Mission: a pipeline macro using push-down accumulation to chain function calls left-to-right.

Are there any prerequisites?

You should be comfortable writing Rust, including functions, structs, enums, traits, closures, and generics. No prior macro experience is needed.

Set Up the Project

Before writing any macros, you need a Rust project to hold them. Every macro you build in this project lives inside a single library crate.

You'll use Cargo to scaffold a library crate with zero external dependencies. All of macro_rules! is built into the Rust language, so no packages are needed.

In this step, get ready to:
  • Create the macro_patterns library crate.
  • Verify the toolchain compiles and runs tests.
Create the library crate and verify the toolchain
  • Move to your Desktop, scaffold the library crate, and enter the project by running these commands:
cd ~/Desktop
cargo new --lib macro_patterns
cd macro_patterns

What is a library crate?

The --lib flag tells Cargo to create a library crate instead of a binary crate. A library crate has src/lib.rs as its root file instead of src/main.rs.

Libraries expose code for other crates to use. Since you're building a collection of reusable macros (not a standalone program), a library crate is the right choice.

  • Confirm the project files exist by running:
ls

You should see Cargo.toml and a src directory in the output.

Not seeing the project files?

  • If you see an error about the directory already existing, a previous attempt left a macro_patterns folder behind. Delete it with rm -rf ~/Desktop/macro_patterns and run the commands again.
  • Make sure you ran cd ~/Desktop first. Without it, the project may have been created in a different directory.
  • Why can't Cargo create my project?
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your code editor (for example, VS Code) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the macro_patterns folder.
  • Click Open.
  • In the Explorer sidebar, click the arrow next to src to expand it.
  • Click lib.rs to open the file.

Cargo generates a default scaffold with a sample add function and test. You need to clear all of this out so you start with a blank file.

  • Select all the content by pressing Cmd+A (macOS) or Ctrl+A (Windows).
  • Press Delete or Backspace to clear the file.
  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Click Cargo.toml in the Explorer sidebar to open it.

Your Cargo.toml should already match the following. No changes are needed.

[package]
name = "macro_patterns"
version = "0.1.0"
edition = "2024"

[dependencies]

Why no dependencies?

The macro_rules! system is built directly into the Rust compiler. Every feature you'll use in this project, including stringify! for macro introspection, is part of the standard language.

The [dependencies] section stays empty for the entire project. Zero packages means zero build overhead.

  • Verify the project compiles by running this command in your terminal:
cargo test

You should see the project compile successfully. The test summary shows running 0 tests and test result: ok. This confirms your crate builds and the test harness works.

Still seeing tests run?

  • If you see running 1 test instead of zero, you did not fully clear lib.rs. Open the file, select all content, delete it, and save again.
  • If you see a compilation error, make sure lib.rs is completely empty. A stray character or partial deletion can cause a syntax error.
  • Make sure your terminal is inside the macro_patterns directory. Run pwd to check your current location.
  • Why is cargo test still failing for me?

✔️ Awesome, I've got everything!

Great. Double check you've saved lib.rs as a completely empty file and confirmed your Cargo.toml matches the reference above.

ⓧ I'd like to double check the full code

Your Cargo.toml should look exactly like this:

[package]
name = "macro_patterns"
version = "0.1.0"
edition = "2024"

[dependencies]

Your src/lib.rs should be completely empty. No functions, no tests, no comments.

Your project is scaffolded and the toolchain is working. Next up, you'll write your first macros and discover how macro_rules! matches patterns and generates code.

Write Macro Fundamentals

Your macro_patterns crate is scaffolded and compiling clean. Now it's time to write actual macros.

Declarative macros in Rust use pattern matching. Each macro_rules! definition contains one or more rules. The compiler tries each rule top-to-bottom and expands the first one that matches. In this step, you'll write three macros that explore these core mechanics.

In this step, get ready to:
  • Write a multi-rule macro with literal token matching.
  • Explore fragment specifiers with stringify!.
  • Generate functions dynamically with a code-generating macro.
Write multi-rule macros with literal token matching

A single macro can have multiple rules, each matching a different token pattern. The compiler tries rules in the order they appear and uses the first one that fully matches.

You'll build a log_level! macro with four rules. Three rules match a specific keyword (error:, warn:, info:) before the message. The fourth is a catch-all fallback.

  • In src/lib.rs, add the following macro definition:
// ============================================================
// STEP 2: MACRO FUNDAMENTALS
// ============================================================

/// Multi-rule macro with literal token matching.
/// Rules are tried top-to-bottom; the first full match wins.
macro_rules! log_level {
    (error: $msg:expr) => {
        format!("[ERROR] {}", $msg)
    };
    (warn: $msg:expr) => {
        format!("[WARN] {}", $msg)
    };
    (info: $msg:expr) => {
        format!("[INFO] {}", $msg)
    };
    ($msg:expr) => {
        format!("[DEBUG] {}", $msg)
    };
}

What does this macro do?

Each rule inside the curly braces is a pattern-to-expansion pair separated by =>. The part before the arrow is the matcher. The part after is the transcriber (what gets generated).

The literal tokens error:, warn:, and info: must appear verbatim in the macro call for their rule to match. $msg:expr captures whatever comes after the keyword as a complete Rust expression.

The last rule has no keyword prefix, so it matches any expression on its own. Order matters here. If you put the fallback rule first, it would match everything and the keyword rules would never trigger.

  • Save src/lib.rs.
  • Confirm the macro compiles by running:
cargo test

You should see the output compile with zero tests. No test functions exist yet, so the compiler just checks that the macro syntax is valid.

Seeing a compile error?

Check that every rule ends with a semicolon (};) and the macro's outer braces are balanced. A missing semicolon after a rule's closing brace is the most common mistake.

help me fix a compile error in my macro_rules! definition

The macro compiles, but you need tests to prove each rule works. Time to set up the test module.

  • Below the log_level! macro in src/lib.rs, add the test module with the first test:
// ============================================================
// TESTS
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // --- Step 2: Fundamentals ---

    #[test]
    fn test_log_level_rules() {
        assert_eq!(log_level!(error: "disk full"), "[ERROR] disk full");
        assert_eq!(log_level!(warn: "low memory"), "[WARN] low memory");
        assert_eq!(log_level!(info: "started"), "[INFO] started");
        assert_eq!(log_level!("fallback"), "[DEBUG] fallback");
    }
}

What does this test do?

The #[cfg(test)] attribute tells Cargo to compile this module only when running tests. use super::* imports everything from the parent module so the test can call log_level!.

Each assert_eq! invokes the macro with a different pattern. The first three use literal keyword prefixes. The last one triggers the fallback rule because it has no keyword.

  • Save src/lib.rs and run the tests:
cargo test

You should see test_log_level_rules ... ok in the output. One test, four assertions, all passing.

Test failing with a wrong value?

Double-check the format strings in each rule. [ERROR], [WARN], [INFO], and [DEBUG] must match the test expectations exactly, including brackets and capitalization.

help me debug my failing log_level macro test

Explore fragment specifiers with a describe! macro

When a macro captures input with $name:specifier, the specifier tells Rust's parser what grammar category to match. An expr capture parses a complete expression. A tt capture takes exactly one token tree (a single token or a delimited group). A ty capture parses a complete type.

What is a fragment specifier?

Fragment specifiers are labels like ident, expr, ty, literal, path, block, and tt that constrain what a macro capture variable can accept. Each one maps to a specific part of Rust's grammar.

Choosing the right specifier matters. If you use expr when you need ident, the macro will accept too many inputs. If you use literal when you need expr, it will reject valid ones.

  • In src/lib.rs, add the describe! macro below the log_level! macro and above the test module:
/// Demonstrates key fragment specifiers using stringify!.
macro_rules! describe {
    (ident: $x:ident) => { stringify!($x) };
    (expr: $x:expr) => { stringify!($x) };
    (ty: $x:ty) => { stringify!($x) };
    (literal: $x:literal) => { stringify!($x) };
    (path: $x:path) => { stringify!($x) };
    (block: $x:block) => { stringify!($x) };
    (tt: $x:tt) => { stringify!($x) };
}

What does this macro do?

Each rule matches a literal label (ident:, expr:, etc.) followed by a capture using that specifier. The built-in stringify! macro converts whatever was captured back into a string literal at compile time. This lets you inspect what the compiler actually captured.

In the 2024 edition, the expr specifier no longer matches UnderscoreExpression or ConstBlockExpression at the top level. The expr_2021 specifier preserves the old behavior if you need it.

  • Save src/lib.rs and confirm it compiles by running:
cargo test

Your existing test_log_level_rules test still passes. The new macro compiles but has no tests yet.

Getting an unexpected token error?

Make sure each rule's literal label matches the format label: $x:specifier with a colon and space. Also verify the specifier names are spelled exactly as shown (ident, expr, ty, literal, path, block, tt).

help me fix a fragment specifier error in my describe! macro

Now add a test that exercises each fragment specifier arm.

  • Inside the mod tests block in src/lib.rs, add this test below test_log_level_rules:
    #[test]
    fn test_describe_fragment_specifiers() {
        assert_eq!(describe!(ident: my_var), "my_var");
        assert_eq!(describe!(expr: 1 + 2), "1 + 2");
        assert_eq!(describe!(ty: Vec<String>), "Vec<String>");
        assert_eq!(describe!(literal: 42), "42");
        assert_eq!(describe!(path: std::collections::HashMap), "std::collections::HashMap");
        assert_eq!(describe!(tt: hello), "hello");
    }

What does this test do?

Each assertion calls describe! with a different specifier arm and checks that stringify! returns the expected string. Notice how expr: 1 + 2 captures the entire 1 + 2 as one expression, and ty: Vec<String> captures a generic type.

The tt: hello arm captures a single token tree. A token tree is either one token or a group of tokens inside matching delimiters (parentheses, brackets, or braces).

  • Save src/lib.rs and run the tests:
cargo test

You should see two tests passing: test_log_level_rules and test_describe_fragment_specifiers.

Assertion mismatch on stringify! output?

The stringify! macro preserves whitespace exactly as written. If your test expects "1 + 2" but the macro receives 1+2 without spaces, the assertion will fail. Match the spacing in the test invocation to the expected string.

help me fix a stringify! assertion mismatch in my describe! macro test

Generate code with a make_fn! macro

Macros do not just transform data. They can generate entirely new Rust code. By combining ident, ty, and block captures, you can build a macro that writes a function definition for you.

  • In src/lib.rs, add the make_fn! macro below describe! and above the test module:
/// Generates a function from captured ident, type, and block.
macro_rules! make_fn {
    ($name:ident() -> $ret:ty $body:block) => {
        fn $name() -> $ret $body
    };
}

What does this macro do?

The matcher $name:ident() -> $ret:ty $body:block captures three pieces: a function name (ident), a return type (ty), and a brace-delimited block of code (block). The transcriber assembles them into a valid fn definition.

The ident specifier is special because the captured identifier can be used as a real name in generated code. This is what lets you create new functions, variables, or types at compile time.

  • Save src/lib.rs and confirm it compiles by running:
cargo test

Your two existing tests still pass. The new macro compiles but has no test yet.

Compile error mentioning a type or block?

Check that the pattern uses $ret:ty (not $ret:expr) and $body:block (not $body:expr). A block must include the curly braces. An expr would try to parse the braces as part of an expression, which changes the meaning.

help me fix a compile error in my make_fn! macro

Now prove the macro works by generating a function and calling it.

  • Inside the mod tests block in src/lib.rs, add this test below test_describe_fragment_specifiers:
    #[test]
    fn test_make_fn() {
        make_fn!(get_answer() -> i32 { 42 });
        assert_eq!(get_answer(), 42);
    }

What does this test do?

The make_fn! invocation generates a real function called get_answer that returns 42. The next line calls that generated function and asserts its return value. The function exists only because the macro expanded it at compile time.

  • Save src/lib.rs and run the full test suite:
cargo test

You should see all three tests passing: test_log_level_rules, test_describe_fragment_specifiers, and test_make_fn.

Test failing or function not found?

If you see an error about get_answer not being found, make sure the make_fn! invocation appears inside the test function. The macro generates the function at the call site. If it is outside the test, the function might not be in scope.

help me debug my make_fn! macro test

✔️ Awesome, I've got everything!

Great. Double check you've saved src/lib.rs and all three tests pass.

ⓧ I'd like to double check the full code

Here is the complete src/lib.rs at the end of this step:

// ============================================================
// STEP 2: MACRO FUNDAMENTALS
// ============================================================

/// Multi-rule macro with literal token matching.
/// Rules are tried top-to-bottom; the first full match wins.
macro_rules! log_level {
    (error: $msg:expr) => {
        format!("[ERROR] {}", $msg)
    };
    (warn: $msg:expr) => {
        format!("[WARN] {}", $msg)
    };
    (info: $msg:expr) => {
        format!("[INFO] {}", $msg)
    };
    ($msg:expr) => {
        format!("[DEBUG] {}", $msg)
    };
}

/// Demonstrates key fragment specifiers using stringify!.
macro_rules! describe {
    (ident: $x:ident) => { stringify!($x) };
    (expr: $x:expr) => { stringify!($x) };
    (ty: $x:ty) => { stringify!($x) };
    (literal: $x:literal) => { stringify!($x) };
    (path: $x:path) => { stringify!($x) };
    (block: $x:block) => { stringify!($x) };
    (tt: $x:tt) => { stringify!($x) };
}

/// Generates a function from captured ident, type, and block.
macro_rules! make_fn {
    ($name:ident() -> $ret:ty $body:block) => {
        fn $name() -> $ret $body
    };
}

// ============================================================
// TESTS
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // --- Step 2: Fundamentals ---

    #[test]
    fn test_log_level_rules() {
        assert_eq!(log_level!(error: "disk full"), "[ERROR] disk full");
        assert_eq!(log_level!(warn: "low memory"), "[WARN] low memory");
        assert_eq!(log_level!(info: "started"), "[INFO] started");
        assert_eq!(log_level!("fallback"), "[DEBUG] fallback");
    }

    #[test]
    fn test_describe_fragment_specifiers() {
        assert_eq!(describe!(ident: my_var), "my_var");
        assert_eq!(describe!(expr: 1 + 2), "1 + 2");
        assert_eq!(describe!(ty: Vec<String>), "Vec<String>");
        assert_eq!(describe!(literal: 42), "42");
        assert_eq!(describe!(path: std::collections::HashMap), "std::collections::HashMap");
        assert_eq!(describe!(tt: hello), "hello");
    }

    #[test]
    fn test_make_fn() {
        make_fn!(get_answer() -> i32 { 42 });
        assert_eq!(get_answer(), 42);
    }
}

You now have three working macros covering rule ordering, fragment specifiers, and code generation. Next up, you'll harness repetition operators to build variadic constructors and a recursive counting macro.

Master Repetition and Counting

Your macros so far handle fixed patterns. Each rule matches a specific shape of input. But real-world Rust code constantly works with collections of unknown size.

In this step, you will build macros that accept a variable number of arguments using repetition operators. You will also solve the surprisingly tricky problem of counting tokens at compile time.

In this step, get ready to:
  • Build variadic constructors for Vec and HashMap using repetition operators.
  • Implement recursive counting as the stable replacement for the unstable ${count()}.
  • Handle nested repetition for matrix-like data structures.
Build variadic constructors using repetition operators

Repetition operators let a macro accept a variable number of inputs. The syntax $( ... ),* means "match the pattern zero or more times, separated by commas." The separator token goes between the ) and the *.

You will start by building avec!, a vec!-like constructor that demonstrates how repetition works in both the matcher and the transcriber.

  • In src/lib.rs, find the tests section. It starts with this comment:
// ============================================================
// TESTS
// ============================================================
  • Add the following code above that tests section comment:
// ============================================================
// STEP 3: REPETITION AND COUNTING
// ============================================================

/// Vec-like constructor with repetition.
macro_rules! avec {
    () => {
        Vec::new()
    };
    ($($element:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($element);)+
            v
        }
    };
}

What does this macro do?

  • The first rule () => { Vec::new() } handles the zero-element case. It is a separate rule because + requires at least one match.
  • $($element:expr),+ matches one or more expressions separated by commas. The + means "one or more" while * means "zero or more."
  • $(,)? optionally matches a trailing comma. The ? operator means "zero or one."
  • In the transcriber, $(v.push($element);)+ repeats the push statement for each captured element. The repetition in the output mirrors the repetition in the input.
  • Save src/lib.rs.
  • You won't see a change yet. The next chunk adds tests for this macro.

Now add the tests that prove avec! handles empty input, multiple elements, and trailing commas.

  • Inside the mod tests block, find the closing } of the test_make_fn test function.
  • Add the following tests below it:
    // --- Step 3: Repetition ---

    #[test]
    fn test_avec_empty() {
        let v: Vec<i32> = avec!();
        assert!(v.is_empty());
    }

    #[test]
    fn test_avec_elements() {
        let v = avec![1, 2, 3];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_avec_trailing_comma() {
        let v = avec![1, 2, 3,];
        assert_eq!(v, vec![1, 2, 3]);
    }

What do these tests cover?

  • test_avec_empty verifies the zero-element rule triggers and returns an empty Vec.
  • test_avec_elements uses avec![1, 2, 3] with square brackets. Macros invoked with ![], !(), or !{} are all equivalent. The delimiter choice is purely stylistic.
  • test_avec_trailing_comma confirms the $(,)? optional trailing comma works correctly.
  • Save src/lib.rs.
  • Verify the tests pass by running:
cargo test
  • You should see all tests passing, including the three new avec tests.

Tests not compiling?

  • Check that the avec! macro is defined outside of mod tests, above the tests section. Macros must be defined before they are used due to textual scoping.
  • Make sure the tests are inside the mod tests block, not below its closing }.
  • Verify that use super::*; is at the top of the test module. This import brings macros into scope.

Help me debug a macro compilation error in my avec! tests.

Now build a hash_map! constructor that uses a custom => token as the separator between keys and values.

  • In src/lib.rs, add the following macro below the avec! macro you just wrote:
/// HashMap constructor with custom => separator.
macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = ::std::collections::HashMap::new();
            $(map.insert($key, $value);)*
            map
        }
    };
}

What's different about this macro?

  • The => between $key:expr and $value:expr is a custom separator. You can use any token as a separator inside repetition patterns.
  • This uses * instead of + because an empty hash map is a valid single-rule case. No separate empty rule needed.
  • ::std::collections::HashMap uses the absolute path so callers don't need a use import.
  • Save src/lib.rs.
  • You won't see a change yet. The next chunk adds the test for this macro.
  • Inside mod tests, find the test_avec_trailing_comma test you just added.
  • Add the following test below it:
    #[test]
    fn test_hash_map() {
        let m = hash_map! {
            "name" => "Rust",
            "type" => "language",
        };
        assert_eq!(m.get("name"), Some(&"Rust"));
        assert_eq!(m.get("type"), Some(&"language"));
    }

What does this test do?

The test invokes hash_map! with curly braces (!{}) to visually match standard HashMap initialization style. It also includes a trailing comma to confirm the $(,)? pattern works.

  • Save src/lib.rs and verify by running:
cargo test
  • You should see all tests passing, now including test_hash_map.

Seeing a type error on HashMap?

  • Make sure the macro uses the full absolute path ::std::collections::HashMap with the leading ::. Without the leading colons, the compiler may not find the type.
  • Check that the => separator appears in both the matcher pattern and in the test invocation. If you used : or = instead, the tokens won't match.

Help me debug my hash_map! macro.

Implement recursive counting with a count_tts! macro

A common need is counting how many items were passed to a macro. There is no stable built-in way to do this. The ${count()} metavariable expression exists in the language RFC but remains unstable.

The standard workaround used in production code is a recursive pattern that peels off one token tree at a time.

  • In src/lib.rs, add the following macro below the hash_map! macro:
/// Recursive token-tree counter (stable replacement for unstable ${count()}).
macro_rules! count_tts {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count_tts!($($tail)*) };
}

How does recursive counting work?

  • The base case () => { 0usize } fires when no tokens remain.
  • The recursive case captures one token tree as $head:tt and the rest as $($tail:tt)*. Each step adds 1 and recurses with the tail.
  • A token tree (tt) is either a single token or a delimited group (anything inside (), [], or {}). This distinction matters for counting.
  • Save src/lib.rs.
  • You won't see a change yet. The next chunk adds a test with a surprising result.
  • Inside mod tests, find the test_hash_map test.
  • Add the following test below it:
    #[test]
    fn test_count_tts() {
        assert_eq!(count_tts!(), 0);
        assert_eq!(count_tts!(a), 1);
        assert_eq!(count_tts!(a b c), 3);
        assert_eq!(count_tts!(a, b, c), 5);
    }

Why does count_tts!(a, b, c) return 5?

Each comma is also a token tree. So a, b, c contains five token trees: a, ,, b, ,, and c. The macro counts raw tokens, not "arguments" separated by commas.

Compare this with count_tts!(a b c) which returns 3. Without commas, there are exactly three token trees.

  • Save src/lib.rs and verify by running:
cargo test
  • You should see all tests passing, including test_count_tts.

Seeing a recursion error?

  • Make sure the base case () => { 0usize } comes before the recursive case. Without it, the macro recurses forever and hits the recursion limit.
  • Check that the recursive case uses $($tail:tt)* (with *), not +. The * allows the tail to be empty so the base case can eventually match.

Help me fix my recursive count_tts! macro.

Handle nested repetition with a matrix! macro

Repetitions can nest. An outer repetition can iterate over rows while an inner repetition iterates over values within each row. The key rule is that the nesting in the transcriber must mirror the matcher's nesting structure.

  • In src/lib.rs, add the following macro below the count_tts! macro:
/// Nested repetition for matrix-like data.
macro_rules! matrix {
    ( $( [ $($val:expr),* ] );* ) => {
        vec![ $( vec![$($val),*] ),* ]
    };
}

How does nested repetition work here?

  • The outer $( ... );* iterates over rows separated by ; tokens.
  • The inner $($val:expr),* iterates over comma-separated values within each bracket-delimited row.
  • In the transcriber, vec![ $( vec![$($val),*] ),* ] mirrors this nesting exactly. The outer repetition produces a comma-separated list of inner vec! calls.
  • Save src/lib.rs.
  • You won't see a change yet. The next chunk adds a test to verify it.
  • Inside mod tests, find the test_count_tts test.
  • Add the following test below it:
    #[test]
    fn test_matrix() {
        let m = matrix!([1, 2, 3]; [4, 5, 6]);
        assert_eq!(m, vec![vec![1, 2, 3], vec![4, 5, 6]]);
    }

What does this test do?

The test creates a 2x3 matrix. [1, 2, 3] and [4, 5, 6] are the two rows, separated by ;. The macro expands this into a Vec<Vec<i32>> with two inner vectors.

  • Save src/lib.rs and run the full test suite:
cargo test
  • You should see all tests passing, including test_matrix. The total test count should now be 10.

Matrix test not compiling?

  • Check that the semicolons (;) in the test match the separator in the macro matcher. The macro expects rows separated by ;, not commas.
  • Make sure the bracket delimiters in the macro matcher are literal [ and ] tokens, not part of a repetition group delimiter.

Help me debug my matrix! macro.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/lib.rs and that cargo test shows 10 tests passing.

ⓧ I'd like to double check the full code

// ============================================================
// STEP 2: MACRO FUNDAMENTALS
// ============================================================

/// Multi-rule macro with literal token matching.
/// Rules are tried top-to-bottom; the first full match wins.
macro_rules! log_level {
    (error: $msg:expr) => {
        format!("[ERROR] {}", $msg)
    };
    (warn: $msg:expr) => {
        format!("[WARN] {}", $msg)
    };
    (info: $msg:expr) => {
        format!("[INFO] {}", $msg)
    };
    ($msg:expr) => {
        format!("[DEBUG] {}", $msg)
    };
}

/// Demonstrates key fragment specifiers using stringify!.
macro_rules! describe {
    (ident: $x:ident) => { stringify!($x) };
    (expr: $x:expr) => { stringify!($x) };
    (ty: $x:ty) => { stringify!($x) };
    (literal: $x:literal) => { stringify!($x) };
    (path: $x:path) => { stringify!($x) };
    (block: $x:block) => { stringify!($x) };
    (tt: $x:tt) => { stringify!($x) };
}

/// Generates a function from captured ident, type, and block.
macro_rules! make_fn {
    ($name:ident() -> $ret:ty $body:block) => {
        fn $name() -> $ret $body
    };
}

// ============================================================
// STEP 3: REPETITION AND COUNTING
// ============================================================

/// Vec-like constructor with repetition.
macro_rules! avec {
    () => {
        Vec::new()
    };
    ($($element:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($element);)+
            v
        }
    };
}

/// HashMap constructor with custom => separator.
macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = ::std::collections::HashMap::new();
            $(map.insert($key, $value);)*
            map
        }
    };
}

/// Recursive token-tree counter (stable replacement for unstable ${count()}).
macro_rules! count_tts {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count_tts!($($tail)*) };
}

/// Nested repetition for matrix-like data.
macro_rules! matrix {
    ( $( [ $($val:expr),* ] );* ) => {
        vec![ $( vec![$($val),*] ),* ]
    };
}

// ============================================================
// TESTS
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // --- Step 2: Fundamentals ---

    #[test]
    fn test_log_level_rules() {
        assert_eq!(log_level!(error: "disk full"), "[ERROR] disk full");
        assert_eq!(log_level!(warn: "low memory"), "[WARN] low memory");
        assert_eq!(log_level!(info: "started"), "[INFO] started");
        assert_eq!(log_level!("fallback"), "[DEBUG] fallback");
    }

    #[test]
    fn test_describe_fragment_specifiers() {
        assert_eq!(describe!(ident: my_var), "my_var");
        assert_eq!(describe!(expr: 1 + 2), "1 + 2");
        assert_eq!(describe!(ty: Vec<String>), "Vec<String>");
        assert_eq!(describe!(literal: 42), "42");
        assert_eq!(describe!(path: std::collections::HashMap), "std::collections::HashMap");
        assert_eq!(describe!(tt: hello), "hello");
    }

    #[test]
    fn test_make_fn() {
        make_fn!(get_answer() -> i32 { 42 });
        assert_eq!(get_answer(), 42);
    }

    // --- Step 3: Repetition ---

    #[test]
    fn test_avec_empty() {
        let v: Vec<i32> = avec!();
        assert!(v.is_empty());
    }

    #[test]
    fn test_avec_elements() {
        let v = avec![1, 2, 3];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_avec_trailing_comma() {
        let v = avec![1, 2, 3,];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_hash_map() {
        let m = hash_map! {
            "name" => "Rust",
            "type" => "language",
        };
        assert_eq!(m.get("name"), Some(&"Rust"));
        assert_eq!(m.get("type"), Some(&"language"));
    }

    #[test]
    fn test_count_tts() {
        assert_eq!(count_tts!(), 0);
        assert_eq!(count_tts!(a), 1);
        assert_eq!(count_tts!(a b c), 3);
        assert_eq!(count_tts!(a, b, c), 5);
    }

    #[test]
    fn test_matrix() {
        let m = matrix!([1, 2, 3]; [4, 5, 6]);
        assert_eq!(m, vec![vec![1, 2, 3], vec![4, 5, 6]]);
    }
}

You now have macros that handle variable-length input with three different repetition operators, recursive counting, and nested patterns. Next up, you will explore how Rust's macro hygiene system protects your code from surprising variable collisions.

Explore Hygiene, Scoping, and Debugging

Your macro library now handles multi-rule matching, fragment specifiers, repetition, and recursive counting. In this step, you'll explore the rules that govern how macros interact with the code around them.

Rust's macro system has built-in safeguards that prevent macro-generated code from accidentally interfering with the caller's variables. Understanding these safeguards is essential for writing macros that compose safely with any surrounding code.

In this step, get ready to:
  • Observe mixed-site hygiene and fragment opacity.
  • Understand follow-set restrictions and $crate.
Observe mixed-site hygiene and fragment opacity

In most languages, if a macro creates a local variable called result, and the caller also has a variable called result, one would overwrite the other. Rust prevents this entirely through a system called mixed-site hygiene.

What is mixed-site hygiene?

In macro_rules!, local variables and labels are resolved at the macro definition site. Other symbols (types, functions) are resolved at the invocation site.

This means a macro's internal let result does NOT shadow the caller's result. Both variables coexist safely because the compiler treats them as belonging to different scopes.

  • In src/lib.rs, find the closing } of the matrix! macro. Add the following section header and macro below it:
// ============================================================
// STEP 4: HYGIENE, SCOPING, AND DEBUGGING
// ============================================================

/// Demonstrates mixed-site hygiene: internal `result` variable
/// does not collide with caller's variables of the same name.
macro_rules! hygienic_add {
    ($a:expr, $b:expr) => {
        {
            let result = $a + $b;
            result
        }
    };
}

What does this macro do?

  • hygienic_add! captures two expressions, adds them, and stores the sum in an internal let result binding.
  • The key test will be whether this internal result variable collides with a caller's own result variable. Hygiene guarantees it won't.

Now write a test that proves the macro's internal variable stays isolated from the caller's code.

  • Scroll down to the mod tests block. Find the last test (test_matrix), and add the following test below it:
    // --- Step 4: Hygiene ---

    #[test]
    fn test_hygiene_no_collision() {
        let result = "not a number";
        let sum = hygienic_add!(3, 4);
        assert_eq!(sum, 7);
        assert_eq!(result, "not a number");
    }

What does this test prove?

The test declares its own result variable (a string) before calling hygienic_add! (which internally creates its own let result as an integer).

If hygiene were broken, the macro's result would overwrite the caller's string. Instead, both assertions pass: the macro returns 7, and the original result remains "not a number".

  • Save src/lib.rs.
  • Confirm the hygiene test passes by running:
cargo test test_hygiene_no_collision

You should see test result: ok. 1 passed in the output.

Test failing or not compiling?

  • Make sure the hygienic_add! macro is defined above the #[cfg(test)] line, not inside mod tests. Macros must be defined in the outer module scope to be visible to tests.
  • Check that the test function is inside the mod tests { ... } block, not after its closing brace.
  • help me debug my hygienic_add test

Now explore a subtler aspect of the macro system: how different fragment specifiers affect the identity of captured tokens.

What is fragment opacity?

Once a token sequence is captured by a typed fragment specifier (like expr), it becomes an opaque AST node. If you forward it to another macro, that macro sees a single opaque expression, not the original tokens.

Capturing as tt preserves the raw token structure. This distinction matters when composing macros that pass captured fragments to other macros.

  • In src/lib.rs, add these two macros directly below hygienic_add! (still above the #[cfg(test)] line):
/// Captures input as expr (opaque AST node).
macro_rules! capture_then_stringify {
    ($e:expr) => { stringify!($e) };
}

/// Captures input as raw token trees (preserves token structure).
macro_rules! capture_as_tt_then_stringify {
    ($($t:tt)*) => { stringify!($($t)*) };
}

What's the difference between these two macros?

  • capture_then_stringify! captures with $e:expr. The compiler parses the input into a complete expression AST node before passing it to stringify!.
  • capture_as_tt_then_stringify! captures with $($t:tt)*. The raw token trees flow through without being parsed into any grammar category. stringify! sees the original tokens.
  • Add the following test inside mod tests, below test_hygiene_no_collision:
    #[test]
    fn test_fragment_opacity() {
        let s = capture_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");

        let s = capture_as_tt_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");
    }

What does this test reveal?

Both macros produce the same stringified output for 1 + 2. The difference becomes visible when you forward the capture to a second macro that tries to re-parse it.

The expr capture passes an opaque node that cannot be re-matched by a pattern. The tt capture passes raw tokens that other macros can match freely. This matters when composing macros together.

  • Save src/lib.rs and confirm all hygiene and opacity tests pass by running:
cargo test

You should see all previous tests plus the two new ones (test_hygiene_no_collision and test_fragment_opacity) passing.

Seeing a compilation error?

  • Check that both capture_then_stringify! and capture_as_tt_then_stringify! are defined outside mod tests, in the main module scope.
  • Verify the macro names match exactly between the definition and the test invocations. They are long names, so double-check for typos.
  • help me fix my fragment opacity macros
Understand follow-set restrictions and $crate

Fragment specifiers don't just control what a macro can capture. They also restrict what token can follow a capture. These rules, called follow-set restrictions, exist so the compiler can parse macro invocations unambiguously.

What are follow-set restrictions?

The compiler enforces which tokens can appear after each fragment specifier. For example, expr can only be followed by =>, ,, or ;.

A pattern like $e:expr + $f:expr would fail to compile because + is not in the follow set of expr. The specifiers tt, ident, and lifetime have no follow-set restrictions at all.

  • In src/lib.rs, add these two macros below capture_as_tt_then_stringify! (still above #[cfg(test)]):
/// Demonstrates $crate and module_path!.
macro_rules! crate_name {
    () => { module_path!() };
}

/// Follow-set demo: expr must be followed by a valid follower token.
macro_rules! follow_set_demo {
    ($e:expr, $f:expr) => { $e + $f };
}

What do these macros demonstrate?

  • crate_name! uses module_path!() to return the current crate's path. In real exported macros, you would use $crate at the start of paths (like $crate::SomeType) to ensure they resolve correctly no matter where the macro is invoked.
  • follow_set_demo! uses a comma between its two expr captures. This works because , is in the follow set of expr. Using + as the separator instead would cause a compiler error.
  • Add the following tests inside mod tests, below test_fragment_opacity:
    #[test]
    fn test_follow_set() {
        assert_eq!(follow_set_demo!(3, 4), 7);
    }

    #[test]
    fn test_crate_name() {
        let name = crate_name!();
        assert!(name.contains("macro_patterns"));
    }

What are these tests checking?

  • test_follow_set confirms that follow_set_demo!(3, 4) parses correctly with the comma separator and returns 7.
  • test_crate_name asserts the returned module path contains "macro_patterns", proving that module_path! resolves to the defining crate's path.
  • Save src/lib.rs and run the full test suite:
cargo test

You should see all tests passing, including the four new ones: test_hygiene_no_collision, test_fragment_opacity, test_follow_set, and test_crate_name.

Tests not compiling?

  • If you see an error about crate_name or follow_set_demo not being found, verify both macros are defined in the outer module scope (above #[cfg(test)]).
  • Make sure you have use super::*; at the top of your mod tests block. This import was part of the original scaffold and brings all macros into scope.
  • help me debug my follow-set and crate_name tests

✔️ Awesome, I've got everything!

Great. Double-check you've saved src/lib.rs and all tests pass.

ⓧ I'd like to double check the full code

// ============================================================
// STEP 2: MACRO FUNDAMENTALS
// ============================================================

/// Multi-rule macro with literal token matching.
/// Rules are tried top-to-bottom; the first full match wins.
macro_rules! log_level {
    (error: $msg:expr) => {
        format!("[ERROR] {}", $msg)
    };
    (warn: $msg:expr) => {
        format!("[WARN] {}", $msg)
    };
    (info: $msg:expr) => {
        format!("[INFO] {}", $msg)
    };
    ($msg:expr) => {
        format!("[DEBUG] {}", $msg)
    };
}

/// Demonstrates key fragment specifiers using stringify!.
macro_rules! describe {
    (ident: $x:ident) => { stringify!($x) };
    (expr: $x:expr) => { stringify!($x) };
    (ty: $x:ty) => { stringify!($x) };
    (literal: $x:literal) => { stringify!($x) };
    (path: $x:path) => { stringify!($x) };
    (block: $x:block) => { stringify!($x) };
    (tt: $x:tt) => { stringify!($x) };
}

/// Generates a function from captured ident, type, and block.
macro_rules! make_fn {
    ($name:ident() -> $ret:ty $body:block) => {
        fn $name() -> $ret $body
    };
}

// ============================================================
// STEP 3: REPETITION AND COUNTING
// ============================================================

/// Vec-like constructor with repetition.
macro_rules! avec {
    () => {
        Vec::new()
    };
    ($($element:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($element);)+
            v
        }
    };
}

/// HashMap constructor with custom => separator.
macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = ::std::collections::HashMap::new();
            $(map.insert($key, $value);)*
            map
        }
    };
}

/// Recursive token-tree counter (stable replacement for unstable ${count()}).
macro_rules! count_tts {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count_tts!($($tail)*) };
}

/// Nested repetition for matrix-like data.
macro_rules! matrix {
    ( $( [ $($val:expr),* ] );* ) => {
        vec![ $( vec![$($val),*] ),* ]
    };
}

// ============================================================
// STEP 4: HYGIENE, SCOPING, AND DEBUGGING
// ============================================================

/// Demonstrates mixed-site hygiene: internal `result` variable
/// does not collide with caller's variables of the same name.
macro_rules! hygienic_add {
    ($a:expr, $b:expr) => {
        {
            let result = $a + $b;
            result
        }
    };
}

/// Captures input as expr (opaque AST node).
macro_rules! capture_then_stringify {
    ($e:expr) => { stringify!($e) };
}

/// Captures input as raw token trees (preserves token structure).
macro_rules! capture_as_tt_then_stringify {
    ($($t:tt)*) => { stringify!($($t)*) };
}

/// Demonstrates $crate and module_path!.
macro_rules! crate_name {
    () => { module_path!() };
}

/// Follow-set demo: expr must be followed by a valid follower token.
macro_rules! follow_set_demo {
    ($e:expr, $f:expr) => { $e + $f };
}

// ============================================================
// TESTS
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // --- Step 2: Fundamentals ---

    #[test]
    fn test_log_level_rules() {
        assert_eq!(log_level!(error: "disk full"), "[ERROR] disk full");
        assert_eq!(log_level!(warn: "low memory"), "[WARN] low memory");
        assert_eq!(log_level!(info: "started"), "[INFO] started");
        assert_eq!(log_level!("fallback"), "[DEBUG] fallback");
    }

    #[test]
    fn test_describe_fragment_specifiers() {
        assert_eq!(describe!(ident: my_var), "my_var");
        assert_eq!(describe!(expr: 1 + 2), "1 + 2");
        assert_eq!(describe!(ty: Vec<String>), "Vec<String>");
        assert_eq!(describe!(literal: 42), "42");
        assert_eq!(describe!(path: std::collections::HashMap), "std::collections::HashMap");
        assert_eq!(describe!(tt: hello), "hello");
    }

    #[test]
    fn test_make_fn() {
        make_fn!(get_answer() -> i32 { 42 });
        assert_eq!(get_answer(), 42);
    }

    // --- Step 3: Repetition ---

    #[test]
    fn test_avec_empty() {
        let v: Vec<i32> = avec!();
        assert!(v.is_empty());
    }

    #[test]
    fn test_avec_elements() {
        let v = avec![1, 2, 3];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_avec_trailing_comma() {
        let v = avec![1, 2, 3,];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_hash_map() {
        let m = hash_map! {
            "name" => "Rust",
            "type" => "language",
        };
        assert_eq!(m.get("name"), Some(&"Rust"));
        assert_eq!(m.get("type"), Some(&"language"));
    }

    #[test]
    fn test_count_tts() {
        assert_eq!(count_tts!(), 0);
        assert_eq!(count_tts!(a), 1);
        assert_eq!(count_tts!(a b c), 3);
        assert_eq!(count_tts!(a, b, c), 5);
    }

    #[test]
    fn test_matrix() {
        let m = matrix!([1, 2, 3]; [4, 5, 6]);
        assert_eq!(m, vec![vec![1, 2, 3], vec![4, 5, 6]]);
    }

    // --- Step 4: Hygiene ---

    #[test]
    fn test_hygiene_no_collision() {
        let result = "not a number";
        let sum = hygienic_add!(3, 4);
        assert_eq!(sum, 7);
        assert_eq!(result, "not a number");
    }

    #[test]
    fn test_fragment_opacity() {
        let s = capture_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");

        let s = capture_as_tt_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");
    }

    #[test]
    fn test_follow_set() {
        assert_eq!(follow_set_demo!(3, 4), 7);
    }

    #[test]
    fn test_crate_name() {
        let name = crate_name!();
        assert!(name.contains("macro_patterns"));
    }
}

Your macros now respect Rust's scoping rules and you've seen firsthand how hygiene and fragment opacity protect your code. Next up, you'll build advanced patterns: internal rules, TT munchers, and the callback pattern.

Build Advanced Macro Patterns

You now understand how macros expand, how hygiene protects your variables, and how fragment opacity affects composition. These mechanics are the foundation for three powerful patterns that production Rust code relies on.

In this step, you will build macros that organize themselves with internal rules, process input token-by-token with a TT muncher, and compose with other macros through callbacks. These patterns turn macro_rules! from a convenience feature into a serious metaprogramming tool.

In this step, get ready to:
  • Organize complex macros with internal rules using the @ convention.
  • Build a TT muncher that evaluates arithmetic expressions token by token.
  • Compose macros together using the callback pattern.
Implement internal rules with the @ convention

As macros grow in complexity, they often need helper logic that callers should never invoke directly. Internal rules solve this by using a distinctive prefix token (conventionally @) so the helper rules only match when the macro calls itself.

  • In src/lib.rs, find the // TESTS comment block near the bottom of the file.
  • Add the following section comment and sorted! macro directly above that // TESTS block:
// ============================================================
// STEP 5: ADVANCED PATTERNS
// ============================================================

/// Internal rules with @ convention.
macro_rules! sorted {
    ($($val:expr),+ $(,)?) => {
        sorted!(@make_vec $($val),+)
    };
    (@make_vec $($val:expr),+) => {
        {
            let mut v = vec![$($val),+];
            v.sort();
            v
        }
    };
}

What does this macro do?

The sorted! macro has two rules that work together.

  • The public rule ($($val:expr),+ $(,)?) accepts comma-separated values (with an optional trailing comma) and immediately delegates to the internal rule by calling sorted!(@make_vec ...).
  • The internal rule (@make_vec $($val:expr),+) does the real work: it builds a Vec, sorts it, and returns it.
  • Because @ is not valid at the start of a normal Rust expression, external callers can never accidentally match the internal rule.

Why place internal rules after public rules?

Macro rules are tried top-to-bottom. The public rule appears first so normal invocations like sorted!(3, 1, 5) match it immediately. The @make_vec rule only fires when the macro explicitly calls itself with the @ prefix.

You won't see a change yet. The next code chunk adds a test that verifies the macro works.

  • Inside the mod tests block, find the test_crate_name test function (the last test in the file).
  • Add the following test below it, before the closing } of mod tests:
    // --- Step 5: Advanced Patterns ---

    #[test]
    fn test_sorted_internal_rules() {
        let v = sorted!(3, 1, 4, 1, 5);
        assert_eq!(v, vec![1, 1, 3, 4, 5]);
    }

What does this test prove?

The test passes unsorted values (3, 1, 4, 1, 5) through the public rule. If the internal @make_vec delegation works correctly, the result is a sorted Vec: [1, 1, 3, 4, 5].

  • Save src/lib.rs.
  • Verify the macro works by running:
cargo test test_sorted_internal_rules

You should see test test_sorted_internal_rules ... ok in the output.

Test not compiling?

  • Make sure the sorted! macro definition is above the // TESTS section, not inside mod tests. Macros defined with macro_rules! use textual scoping, so they must appear before any code that calls them.
  • Check that the @make_vec rule uses $($val),+ with a + (one or more), not *. The public rule already requires at least one element.

Help me debug my sorted! macro with internal rules.

Build a TT muncher and implement the callback pattern

A TT muncher is a macro that processes input one token tree at a time, consuming from the front and recursively calling itself with the remainder. This lets you handle arbitrary-length input with custom logic at each step.

  • In src/lib.rs, add the eval! macro directly below the sorted! macro you just added:
/// TT Muncher: evaluates left-to-right arithmetic (+ and - only).
macro_rules! eval {
    // Base case: no more tokens to consume, emit the accumulated expression
    (@accum $acc:expr,) => { $acc };
    // Recursive case: munch a + literal from the front
    (@accum $acc:expr, + $next:literal $($rest:tt)*) => {
        eval!(@accum ($acc + $next), $($rest)*)
    };
    // Recursive case: munch a - literal from the front
    (@accum $acc:expr, - $next:literal $($rest:tt)*) => {
        eval!(@accum ($acc - $next), $($rest)*)
    };
    // Entry point: capture the first literal, start accumulation
    ($first:literal $($rest:tt)*) => {
        eval!(@accum $first, $($rest)*)
    };
}

How does this TT muncher work?

The entry rule ($first:literal $($rest:tt)*) captures the first number and forwards everything else to the @accum internal rule.

Each recursive step munches an operator and literal from the front (like + 5), wraps the accumulator (e.g. ($acc + $next)), and recurses with the remaining tokens. When nothing is left, the base case (@accum $acc:expr,) emits the final expression.

Why are TT munchers quadratic?

Each recursive invocation matches against all remaining token trees. For N tokens, the total work is proportional to N + (N-1) + (N-2) + ... + 1. Prefer standard repetition operators when possible, and keep TT muncher inputs short.

You won't see a change yet. The next code chunk adds a test.

  • Inside mod tests, add the following test below test_sorted_internal_rules:
    #[test]
    fn test_tt_muncher_eval() {
        assert_eq!(eval!(10 + 5 - 3 + 1), 13);
        assert_eq!(eval!(100 - 50 - 25), 25);
        assert_eq!(eval!(42), 42);
    }

What do these test cases cover?

  • eval!(10 + 5 - 3 + 1) tests mixed operators with four values.
  • eval!(100 - 50 - 25) tests multiple subtractions in sequence.
  • eval!(42) tests the base case where a single literal hits the entry rule and immediately resolves through @accum.
  • Save src/lib.rs.
  • Run the eval test by running:
cargo test test_tt_muncher_eval

You should see test test_tt_muncher_eval ... ok in the output.

Seeing a recursion limit error?

  • Make sure the base case rule (@accum $acc:expr,) has a trailing comma after $acc:expr. Without it, the base case never matches and the macro recurses forever.
  • Check that each recursive rule ends with , $($rest)*) so the comma separator is present between the accumulator and the remaining tokens.

Help me debug my eval! TT muncher macro.

The TT muncher works for processing arbitrary token streams. But what if you want one macro to pass its result into another macro? Rust's macro expansion order prevents this. One macro cannot directly receive the output of another macro as input.

The callback pattern works around this. Instead of trying to nest macro outputs, you pass the name of the destination macro as an ident and forward the arguments as raw token trees.

  • Add the following two macros below the eval! macro in src/lib.rs:
/// Callback pattern: forwards arguments to a named macro.
macro_rules! call_with_idents {
    ($callback:ident, $($args:tt)*) => {
        $callback!($($args)*)
    };
}

/// Target macro for the callback pattern.
macro_rules! make_sum_fn {
    ($name:ident, $($val:expr),+) => {
        fn $name() -> i64 {
            0 $(+ $val)+
        }
    };
}

How does the callback pattern work?

  • call_with_idents! captures a macro name as $callback:ident and the remaining input as $($args:tt)*. It expands to $callback!($($args)*), invoking the named macro with the forwarded tokens.
  • make_sum_fn! is the target. It takes a function name and values, then generates a function that returns their sum. The expression 0 $(+ $val)+ starts from zero and adds each captured value using repetition.

You won't see a change yet. The next code chunk adds the test.

  • Inside mod tests, add the following test below test_tt_muncher_eval:
    #[test]
    fn test_callback_pattern() {
        call_with_idents!(make_sum_fn, total, 10, 20, 30);
        assert_eq!(total(), 60);
    }

What is this test doing?

The invocation call_with_idents!(make_sum_fn, total, 10, 20, 30) expands to make_sum_fn!(total, 10, 20, 30), which generates a function called total() that returns 0 + 10 + 20 + 30. The assert confirms the result is 60.

  • Save src/lib.rs.
  • Run the full test suite to confirm everything passes by running:
cargo test

You should see all tests passing, including test_sorted_internal_rules, test_tt_muncher_eval, and test_callback_pattern.

Callback test not compiling?

  • Check that call_with_idents! is defined above the mod tests block, not inside it. Textual scoping requires the macro to appear before its invocation.
  • Make sure make_sum_fn! uses i64 as the return type, not i32. The macro generates a function returning i64.

Help me debug my callback macros.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/lib.rs and all tests pass.

ⓧ I'd like to double check the full code

Here is the complete src/lib.rs as it should look at the end of this step.

// ============================================================
// STEP 2: MACRO FUNDAMENTALS
// ============================================================

/// Multi-rule macro with literal token matching.
/// Rules are tried top-to-bottom; the first full match wins.
macro_rules! log_level {
    (error: $msg:expr) => {
        format!("[ERROR] {}", $msg)
    };
    (warn: $msg:expr) => {
        format!("[WARN] {}", $msg)
    };
    (info: $msg:expr) => {
        format!("[INFO] {}", $msg)
    };
    ($msg:expr) => {
        format!("[DEBUG] {}", $msg)
    };
}

/// Demonstrates key fragment specifiers using stringify!.
macro_rules! describe {
    (ident: $x:ident) => { stringify!($x) };
    (expr: $x:expr) => { stringify!($x) };
    (ty: $x:ty) => { stringify!($x) };
    (literal: $x:literal) => { stringify!($x) };
    (path: $x:path) => { stringify!($x) };
    (block: $x:block) => { stringify!($x) };
    (tt: $x:tt) => { stringify!($x) };
}

/// Generates a function from captured ident, type, and block.
macro_rules! make_fn {
    ($name:ident() -> $ret:ty $body:block) => {
        fn $name() -> $ret $body
    };
}

// ============================================================
// STEP 3: REPETITION AND COUNTING
// ============================================================

/// Vec-like constructor with repetition.
macro_rules! avec {
    () => {
        Vec::new()
    };
    ($($element:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($element);)+
            v
        }
    };
}

/// HashMap constructor with custom => separator.
macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = ::std::collections::HashMap::new();
            $(map.insert($key, $value);)*
            map
        }
    };
}

/// Recursive token-tree counter (stable replacement for unstable ${count()}).
macro_rules! count_tts {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count_tts!($($tail)*) };
}

/// Nested repetition for matrix-like data.
macro_rules! matrix {
    ( $( [ $($val:expr),* ] );* ) => {
        vec![ $( vec![$($val),*] ),* ]
    };
}

// ============================================================
// STEP 4: HYGIENE, SCOPING, AND DEBUGGING
// ============================================================

/// Demonstrates mixed-site hygiene: internal `result` variable
/// does not collide with caller's variables of the same name.
macro_rules! hygienic_add {
    ($a:expr, $b:expr) => {
        {
            let result = $a + $b;
            result
        }
    };
}

/// Captures input as expr (opaque AST node).
macro_rules! capture_then_stringify {
    ($e:expr) => { stringify!($e) };
}

/// Captures input as raw token trees (preserves token structure).
macro_rules! capture_as_tt_then_stringify {
    ($($t:tt)*) => { stringify!($($t)*) };
}

/// Demonstrates $crate and module_path!.
macro_rules! crate_name {
    () => { module_path!() };
}

/// Follow-set demo: expr must be followed by a valid follower token.
macro_rules! follow_set_demo {
    ($e:expr, $f:expr) => { $e + $f };
}

// ============================================================
// STEP 5: ADVANCED PATTERNS
// ============================================================

/// Internal rules with @ convention.
macro_rules! sorted {
    ($($val:expr),+ $(,)?) => {
        sorted!(@make_vec $($val),+)
    };
    (@make_vec $($val:expr),+) => {
        {
            let mut v = vec![$($val),+];
            v.sort();
            v
        }
    };
}

/// TT Muncher: evaluates left-to-right arithmetic (+ and - only).
macro_rules! eval {
    (@accum $acc:expr,) => { $acc };
    (@accum $acc:expr, + $next:literal $($rest:tt)*) => {
        eval!(@accum ($acc + $next), $($rest)*)
    };
    (@accum $acc:expr, - $next:literal $($rest:tt)*) => {
        eval!(@accum ($acc - $next), $($rest)*)
    };
    ($first:literal $($rest:tt)*) => {
        eval!(@accum $first, $($rest)*)
    };
}

/// Callback pattern: forwards arguments to a named macro.
macro_rules! call_with_idents {
    ($callback:ident, $($args:tt)*) => {
        $callback!($($args)*)
    };
}

/// Target macro for the callback pattern.
macro_rules! make_sum_fn {
    ($name:ident, $($val:expr),+) => {
        fn $name() -> i64 {
            0 $(+ $val)+
        }
    };
}

// ============================================================
// TESTS
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // --- Step 2: Fundamentals ---

    #[test]
    fn test_log_level_rules() {
        assert_eq!(log_level!(error: "disk full"), "[ERROR] disk full");
        assert_eq!(log_level!(warn: "low memory"), "[WARN] low memory");
        assert_eq!(log_level!(info: "started"), "[INFO] started");
        assert_eq!(log_level!("fallback"), "[DEBUG] fallback");
    }

    #[test]
    fn test_describe_fragment_specifiers() {
        assert_eq!(describe!(ident: my_var), "my_var");
        assert_eq!(describe!(expr: 1 + 2), "1 + 2");
        assert_eq!(describe!(ty: Vec<String>), "Vec<String>");
        assert_eq!(describe!(literal: 42), "42");
        assert_eq!(describe!(path: std::collections::HashMap), "std::collections::HashMap");
        assert_eq!(describe!(tt: hello), "hello");
    }

    #[test]
    fn test_make_fn() {
        make_fn!(get_answer() -> i32 { 42 });
        assert_eq!(get_answer(), 42);
    }

    // --- Step 3: Repetition ---

    #[test]
    fn test_avec_empty() {
        let v: Vec<i32> = avec!();
        assert!(v.is_empty());
    }

    #[test]
    fn test_avec_elements() {
        let v = avec![1, 2, 3];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_avec_trailing_comma() {
        let v = avec![1, 2, 3,];
        assert_eq!(v, vec![1, 2, 3]);
    }

    #[test]
    fn test_hash_map() {
        let m = hash_map! {
            "name" => "Rust",
            "type" => "language",
        };
        assert_eq!(m.get("name"), Some(&"Rust"));
        assert_eq!(m.get("type"), Some(&"language"));
    }

    #[test]
    fn test_count_tts() {
        assert_eq!(count_tts!(), 0);
        assert_eq!(count_tts!(a), 1);
        assert_eq!(count_tts!(a b c), 3);
        assert_eq!(count_tts!(a, b, c), 5);
    }

    #[test]
    fn test_matrix() {
        let m = matrix!([1, 2, 3]; [4, 5, 6]);
        assert_eq!(m, vec![vec![1, 2, 3], vec![4, 5, 6]]);
    }

    // --- Step 4: Hygiene ---

    #[test]
    fn test_hygiene_no_collision() {
        let result = "not a number";
        let sum = hygienic_add!(3, 4);
        assert_eq!(sum, 7);
        assert_eq!(result, "not a number");
    }

    #[test]
    fn test_fragment_opacity() {
        let s = capture_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");

        let s = capture_as_tt_then_stringify!(1 + 2);
        assert_eq!(s, "1 + 2");
    }

    #[test]
    fn test_follow_set() {
        assert_eq!(follow_set_demo!(3, 4), 7);
    }

    #[test]
    fn test_crate_name() {
        let name = crate_name!();
        assert!(name.contains("macro_patterns"));
    }

    // --- Step 5: Advanced Patterns ---

    #[test]
    fn test_sorted_internal_rules() {
        let v = sorted!(3, 1, 4, 1, 5);
        assert_eq!(v, vec![1, 1, 3, 4, 5]);
    }

    #[test]
    fn test_tt_muncher_eval() {
        assert_eq!(eval!(10 + 5 - 3 + 1), 13);
        assert_eq!(eval!(100 - 50 - 25), 25);
        assert_eq!(eval!(42), 42);
    }

    #[test]
    fn test_callback_pattern() {
        call_with_idents!(make_sum_fn, total, 10, 20, 30);
        assert_eq!(total(), 60);
    }
}

Your macro pattern library now covers every major macro_rules! technique: internal rules for organization, TT munchers for token-by-token processing, and callbacks for cross-macro composition. Ready for a challenge? The Secret Mission takes these patterns further with push-down accumulation.

Secret mission

Push-Down Accumulation Pipeline

You have built TT munchers, internal rules, and callbacks. Now combine them all into one macro. Build a pipeline! macro that uses push-down accumulation to chain function calls left-to-right, transforming pipeline!(value => fn1 => fn2 => fn3) into fn3(fn2(fn1(value))).

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with zero external dependencies, so there are no ongoing costs.

Decide whether to keep your project files on your machine or delete them entirely.

Resources you used:

  • The macro_patterns Cargo project folder on your Desktop.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your macro library.

  • Your macro_patterns folder stays on your Desktop. You can open it anytime and run cargo test to verify everything still works.
  • There are no background processes, no running servers, and no cloud resources. Nothing is consuming memory or incurring costs.

Pause - I'll come back to this later

Since this is a local-only project with no running processes, pausing and keeping are the same thing.

  • Your macro_patterns folder is just files on disk. Leave it where it is and come back whenever you like.
  • When you return, open a terminal, navigate to the macro_patterns folder, and run cargo test to pick up where you left off.

Delete - I don't want to use this again

Remove the project folder entirely. This deletes all your macro code and tests.

  • Delete the macro_patterns folder by running this command in your terminal.

macOS

cd ~/Desktop
rm -rf macro_patterns

Windows

cd $HOME\Desktop
rmdir /s /q macro_patterns
  • Confirm the folder is gone by running:
ls

You should no longer see macro_patterns in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete reference library of Rust declarative macros, covering every major macro_rules! pattern from basic rule matching all the way to recursive token munching.

What you learned:

  • Built macros using multi-rule matching, fragment specifiers, and repetition operators to generate functions, variadic constructors, recursive counters, and nested data structures at compile time.
  • Observed Rust's mixed-site hygiene, fragment opacity, and follow-set restrictions firsthand, understanding how the compiler protects macro expansion from subtle bugs.
  • Implemented advanced patterns including internal rules with the @ convention, a TT muncher that evaluates arithmetic token by token, and the callback pattern for cross-macro composition.
  • Secret Mission: built a pipeline! macro using push-down accumulation to chain function calls left-to-right.

Ready to quiz yourself?