Skip to Content
Course content

243: cargo-expand for Macro Debugging

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been staring at this compiler error for ten minutes, and I'm starting to lose my mind. You know that feeling? When the Rust compiler tells you there's a type mismatch on line 12, but line 12 is just a single macro call like generate_api_handlers!(User, Post)? The compiler is technically correct—the error is happening *inside* the expansion—but it's showing me the source code, not the generated code.

When you're writing macro_rules! or procedural macros, you're essentially writing a program that writes another program. If that second program is buggy, you're flying blind. I'm tired of guessing what the macro is actually spitting out, so I'm going to use cargo-expand.

The Invisible Wall

Here is what I'm working with. I wanted a quick way to implement a Summary trait for a few different structs without typing the same boilerplate over and over. I wrote this macro:

trait Summary {
    fn summarize(&self) -> String;
}

macro_rules! impl_summary {
    ($struct_name:ident, $field:ident) => {
        impl Summary for $struct_name {
            fn summarize(&self) -> String {
                format!("The {} is {}", stringify!($struct_name), self.$field)
            }
        }
    };
}

struct User { username: String }
struct Product { sku: String }

impl_summary!(User, username);
impl_summary!(Product, sku);

This works fine. But then I tried to get fancy. I wanted to handle cases where the field might not be a string, so I tried to add a ToString bound in a more complex version. Suddenly, I'm getting an error saying the trait bound `_ : ToString` is not satisfied, but the error pointer is just hovering over the macro call. I can't see which specific expansion is failing or why the compiler thinks the type is wrong.

Pulling Back the Curtain

I can't debug what I can't see. First, I need to make sure I have the tool installed, because cargo-expand isn't part of the standard toolchain installation.

cargo install cargo-expand

Now, instead of cargo check or cargo run, I'll run:

cargo expand

The first thing I notice is that it takes a bit longer than a normal build because it actually runs the compiler's expansion phase and then prints the result to the terminal. When the output hits my screen, I scroll down to where my impl_summary! calls were. Here is what I see:

impl Summary for User {
    fn summarize(&self) -> String {
        format!("The User is {}", self.username)
    }
}

impl Summary for Product {
    fn summarize(&self) -> String {
        format!("The Product is {}", self.sku)
    }
}

In this simple case, it looks perfect. But as I move to my "fancy" version where I tried to introduce a generic wrapper, cargo expand reveals the disaster. I see that my macro was accidentally expanding into a nested loop of trait implementations that I didn't intend to create. I had a typo in my token tree—a misplaced semicolon—that caused the macro to repeat the impl block inside itself.

The "Aha!" Moment

If I had relied solely on the compiler errors, I would have spent an hour questioning my understanding of trait bounds. Instead, seeing the literal code cargo-expand produced made the mistake obvious. The code it printed was syntactically valid Rust, but logically insane. It looked like this:

impl Summary for User {
    impl Summary for User { // Wait, what? Why is this here?
        fn summarize(&self) -> String { ... }
    }
}

I immediately see the problem: my macro pattern was capturing more than it should have, and I was emitting an impl block inside another impl block. I fix the macro pattern, run cargo expand again, and verify that the output now looks like clean, standard Rust code. Only then do I go back to cargo run.

My rule of thumb now? If a macro produces a compiler error that doesn't make immediate sense, stop guessing. Expand the code, find the line that looks "weird," and fix the generator, not the generated code.




📋 Practical Task

Debugging the "Auto-Logger" Macro

You have been given a piece of code that uses a macro to implement a Loggable trait for various structs. However, the code is failing to compile with a confusing error regarding "expected struct, found tuple".

Your task is to use cargo-expand to identify why the macro is generating invalid Rust code and then fix the macro definition.

trait Loggable {
    fn log(&self);
}

macro_rules! make_loggable {
    ($t:ident) => {
        impl Loggable for $t {
            fn log(&self) {
                println!("Logging instance of {}", stringify!($t));
            }
        }
    };
}

struct Database { connection_string: String }
struct Cache { capacity: usize }

// One of these is causing a hidden expansion error
make_loggable!(Database);
make_loggable!(Cache);
make_loggable!( (u32, u32) ); // This is the problematic line

Requirements:

  • Install cargo-expand.
  • Run cargo expand to see how the macro handles the tuple (u32, u32).
  • Observe that $t:ident cannot actually match a tuple, leading to a failure that cargo-expand makes visible.
  • Modify the macro to use $t:ty (type) instead of $t:ident (identifier) so that it can correctly expand for both structs and tuples.
  • Verify the fix by running cargo expand one last time to ensure the output is valid Rust.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.