Parse Macro Input into an AST with syn

Mục tiêu: Use the syn::parse function to transform the raw proc\_macro::TokenStream input of a derive macro into a structured syn::DeriveInput, which represents the code as an Abstract Syntax Tree (AST).


Fantastic! You have successfully scaffolded your procedural macro. The skeleton is in place, and the compiler now knows that the derive_builder function is responsible for implementing #[derive(Builder)]. It’s time to put on our surgical gloves and begin the real work: processing the user’s code. Our first task is to take the raw input from the compiler and transform it into a format we can actually work with.

From a Stream of Tokens to a Structured Tree

When the compiler calls our derive_builder function, it passes the user’s struct definition as a proc_macro::TokenStream. Think of this TokenStream as a one-dimensional sequence of lexical tokens. For a struct like struct Command { ... }, the stream would contain tokens like struct, Command, {, ..., }. While this is the fundamental “currency” of macros, trying to deduce the struct’s name or its fields by manually iterating through these tokens would be incredibly complex and fragile.

What we need is a structured representation of this code, much like how a biologist would prefer a labeled anatomical model over a pile of bones. This structured representation is called an Abstract Syntax Tree (AST). An AST is a tree-like data structure that represents the grammatical structure of source code.

This is where the syn crate comes into play. Its primary purpose is to act as a parser, a tool that consumes a linear sequence of tokens and produces a hierarchical AST.

The Power of syn::parse

The main entry point for syn’s parsing capabilities is the syn::parse function. This function is brilliantly simple: it takes a TokenStream as input and attempts to parse it into a specific Rust data type that represents a piece of Rust syntax.

For a custom derive macro, the input will always be a struct, enum, or union definition. syn provides a specific AST node for this exact purpose: syn::DeriveInput. This struct contains everything we need to know about the item the #[derive] macro is attached to, including:

  • Its attributes (like #[allow(dead_code)])
  • Its visibility (pub or private)
  • Its name (the identifier, e.g., Command)
  • Its generic parameters (e.g., <T>)
  • Its data (the fields of a struct or variants of an enum)

Our task is to call syn::parse on the input TokenStream and tell it we expect to get a syn::DeriveInput back. We’ll then replace the unimplemented!() placeholder with this parsing logic.

Here is the updated derive_builder function. We are replacing the first unimplemented!() with our parsing logic and adding another one below it as a placeholder for the next steps.

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // We replace the previous `unimplemented!()` with our parsing logic.
    // syn::parse takes the TokenStream from the compiler and attempts to parse
    // it into a syn::DeriveInput struct. This struct is a structured representation
    // of the Rust item (like a struct or enum) that the derive macro is on.
    // The `?` operator or `.unwrap()` is used because parsing can fail if the
    // token stream does not represent a valid derive input.
    let ast = syn::parse(input).unwrap();

    // The code generation logic will go here. For now, we use `unimplemented!()`
    // as a placeholder. Our function must still return a TokenStream.
    unimplemented!()
}

A Quick Word on .unwrap()

You’ll notice we are using .unwrap() on the result of syn::parse(input). This is because the parse function can fail. For instance, a user might mistakenly try to #[derive(Builder)] on a function, which is not a valid input for a derive macro. In such cases, syn::parse returns an Err value containing a helpful compile-time error message.

Using .unwrap() tells our program, “I expect this parsing to succeed, and if it doesn’t, it’s okay to panic and stop the compilation.” For now, this is a convenient simplification that lets us focus on the “happy path.” Later in the project, we will replace this with more robust error handling to provide graceful and informative error messages to the user.

You have now taken the first and most critical step in implementing the macro’s logic: you’ve transformed the raw, unstructured stream of code from the compiler into a clean, structured ast variable that you can easily inspect.

In the next step, we will start exploring this ast variable to extract the information we need to begin generating our builder, starting with the name of the struct itself.

Further Reading

This parsing will result in a syn::DeriveInput struct, which represents the item the #[derive] is attached to (e.g., a struct or enum).

Mục tiêu:


In the previous task, you successfully executed the crucial first step of our macro’s logic: parsing the raw input. By calling let ast = syn::parse(input).unwrap();, you transformed the compiler’s TokenStream into a new variable named ast. But what exactly is this ast variable? This task is all about understanding the powerful data structure you now have at your fingertips.

The Blueprint of Code: syn::DeriveInput

The variable ast is not just a generic object; it has a very specific and important type: syn::DeriveInput. This struct, provided by the syn crate, is the top-level representation of a Rust item that can have a #[derive] macro applied to it. This includes structs, enums, and unions.

Think of it this way: the TokenStream was like a raw, unformatted text file of the user’s code. The syn::parse function acted like a sophisticated program that read this text file and organized all of its information into a structured, searchable database. That database is the syn::DeriveInput struct. It’s an Abstract Syntax Tree (AST) node that serves as a detailed blueprint of the user’s code, making it easy for us to inspect and analyze.

Anatomy of a DeriveInput

The syn::DeriveInput struct has several fields that give us a complete picture of the item our macro is attached to. Let’s explore the most important ones for our Builder project.

Imagine a user has written this code:

#[derive(Builder)]
pub struct Command {
    executable: String,
    args: Vec<String>,
}

Our ast variable, which is an instance of DeriveInput, would conceptually hold the following information:

  • ident: This field, of type syn::Ident, holds the name of the item. For the example above, ast.ident would give us the identifier Command. This is absolutely essential, as we will use it to name our generated builder struct (e.g., CommandBuilder).
  • vis: This field, of type syn::Visibility, stores the visibility of the item. In our example, it would represent pub. We will use this to ensure our generated CommandBuilder has the same visibility as the original Command struct, which is a crucial aspect of good macro hygiene.
  • data: This is perhaps the most critical field. Its type is syn::Data, which is an enum with three variants: Data::Struct, Data::Enum, and Data::Union. This is how we determine what kind of item we are working with. For our Builder macro, which only works on structs, we will need to check that ast.data is the Data::Struct variant. Inside this variant, we will find all the information about the struct’s fields—their names, types, and attributes.
  • generics: This field holds any generic parameters and where clauses defined on the item. For instance, if the struct were struct MyStruct<T> where T: Clone { ... }, the generics field would give us access to <T> where T: Clone. This is vital for writing macros that work correctly with generic types.
  • attrs: This is a Vec<syn::Attribute> containing any attributes attached to the item, like #[allow(dead_code)].

Here is a conceptual diagram of what the ast variable holds for our Command struct example:

// Our `ast` variable (a `syn::DeriveInput` instance)
ast: DeriveInput {
    // Field: `vis`
    // Holds the visibility of the struct
    vis: Visibility::Public(pub),

    // Field: `ident`
    // Holds the name of the struct
    ident: "Command",

    // Field: `generics`
    // Holds generic parameters, empty in this case
    generics: { ... },

    // Field: `data`
    // This is the most important part for us! It tells us this is a struct
    // and contains the fields inside.
    data: Data::Struct {
        struct_token: struct,
        fields: Fields::Named {
            // This is our next target! This collection will contain
            // `executable: String` and `args: Vec<String>`.
            named: [ ... ],
        },
        semi_token: None,
    },
    ...
}

By successfully parsing the input into a DeriveInput, you’ve turned a complex metaprogramming problem into a standard data manipulation task. You no longer need to worry about the raw syntax of Rust; you can now simply access the fields of this ast struct to get all the information you need.

In our next step, we will do exactly that. We will dive into the ast variable to extract the two most important pieces of information for our builder: the name of the struct (ast.ident) and the list of its fields (found within ast.data).

Further Reading

To familiarize yourself more with the structure you’ll be working with, it’s highly recommended to look at the official syn documentation.

Parse TokenStream into a Syntax Tree

Mục tiêu: Learn to parse the raw TokenStream from a procedural macro into a syn::DeriveInput abstract syntax tree (AST) using syn::parse and handle the Result with unwrap for initial error handling.


In our previous tasks, we methodically parsed the raw TokenStream into a syn::DeriveInput struct and explored its structure conceptually. You now have a clear mental model of the ast variable as a well-organized blueprint of the user’s code. It’s time to write the code that performs this transformation and assigns the resulting blueprint to a variable, making it available for the rest of our macro’s logic.

From Potential Failure to Concrete Success: Parsing and Unwrapping

The process of parsing is not guaranteed to succeed. A user might accidentally try to #[derive(Builder)] on a function or a module, which our macro is not designed to handle. The syn::parse function is built with this reality in mind. Instead of directly returning a syn::DeriveInput, it returns a Result<syn::DeriveInput, syn::Error>.

Let’s quickly demystify the Result type, as it is one of the cornerstones of error handling in Rust.

The Result Enum

Result<T, E> is a standard library enum with two possible variants:

  1. Ok(T): Represents a successful outcome, containing the value of type T. In our case, this would be Ok(syn::DeriveInput).
  2. Err(E): Represents a failure, containing an error value of type E. For syn, this would be Err(syn::Error), which holds rich information about what went wrong during parsing.

To get the syn::DeriveInput out of the Result, we need to handle both possibilities. For now, we will use a simple and direct method: .unwrap().

The .unwrap() method is defined on Result and Option types. Its behavior is straightforward: * If the Result is Ok(value), .unwrap() extracts and returns the value. * If the Result is Err(error), .unwrap() will panic.

A panic in a procedural macro will halt the compilation process and display a panic message to the user. While not the most user-friendly way to report errors (we will improve this later), it’s a perfectly acceptable and common simplification during the initial development of a macro. It allows us to focus on the “happy path” where the input is valid.

Implementing the Parsing Logic

We will now replace the unimplemented!() placeholder in our derive_builder function with the single line of code that performs the parsing. This line calls syn::parse, handles the Result with .unwrap(), and assigns the successful result to a new variable called ast.

Here is the updated code for src/lib.rs. Note how we’ve replaced the first placeholder and added a new one for the logic that will follow.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input TokenStream into a syntax tree.
    // `syn::parse` returns a `Result`, so we use `.unwrap()` to panic if parsing fails.
    // This is a temporary simplification; we'll add proper error handling later.
    // The `ast` variable now holds a structured representation of the user's struct.
    let ast = syn::parse(input).unwrap();

    // The code generation logic will go here. For now, we use `unimplemented!()`
    // as a placeholder. Our function must still return a TokenStream.
    unimplemented!()
}

With this change, you have written the first piece of active logic in your macro! You have successfully:

  1. Called the syn parser on the compiler-provided input.
  2. Handled the possibility of parsing failure (for now, by panicking).
  3. Stored the resulting Abstract Syntax Tree in the ast variable.

This ast variable is now the key that unlocks all the information we need. The raw, opaque TokenStream has been transformed into a structured, queryable syn::DeriveInput struct.

What’s Next?

You are now perfectly positioned for the next step: “Extracting Structural Information”. We will begin to inspect the ast variable to pull out the essential pieces of data required to generate our builder: the name of the original struct and the names and types of its fields.

Further Reading

To deepen your understanding of Rust’s fundamental error handling mechanisms, please explore the official documentation:

Note: Using .unwrap() is a temporary simplification. We will implement proper error handling later.

Mục tiêu:


You’ve now written the first line of executable logic in your macro: let ast = syn::parse(input).unwrap();. This is a significant milestone! However, the .unwrap() part of that line deserves special attention. Your current task is to understand why this is a temporary simplification and what a more robust solution will look like down the road.

The Double-Edged Sword: The Simplicity and Danger of .unwrap()

In the previous task, you added the line that parses the input TokenStream. As we discussed, syn::parse returns a Result, which can be either Ok(DeriveInput) on success or Err(syn::Error) on failure. The .unwrap() method is a direct, no-frills way to handle this Result: if it’s Ok, it gives you the inner value; if it’s Err, it panics.

A panic in a procedural macro has a very specific effect: it immediately crashes the compiler thread that was processing your macro. For the end-user—the developer using your #[derive(Builder)] macro—this results in a compilation failure. The error message they see will typically be a long stack trace pointing to the .unwrap() line inside your macro’s source code.

Why is this a “simplification”?

  • It’s Simple: It allows us to get the DeriveInput we need in one line and move on to the core logic of our builder, assuming the input is correct. We don’t have to write any error-handling code yet.
  • It Works (for now): For development, especially when you are the only user, a panic is a clear signal that something went wrong with the parsing.

Why is it not a long-term solution?

  • Poor User Experience: A panic is not a user-friendly error message. Imagine a developer uses your macro incorrectly, for instance, on an enum. The error they get will be cryptic and will point into the internals of your library. They won’t see a clear message like, “the Builder macro can only be used on structs.” This is a frustrating experience and makes your macro feel unpolished and difficult to use.
  • Lack of Precision: A panic is an all-or-nothing failure. The syn::Error type that syn::parse produces on failure is much more powerful. It contains information about what went wrong and, crucially, where it went wrong in the user’s source code. A good macro should leverage this information to produce a beautiful, targeted compile-time error that highlights the exact problematic code, just like the official Rust compiler does.

The Professional Approach: Graceful Error Handling

The idiomatic and professional way to handle errors in a procedural macro is to catch the Err variant and transform it into a TokenStream that represents a compiler error.

Here’s a preview of the concept (you do not need to implement this now):

  1. Instead of using .unwrap(), you would use a match statement or ? operator.
  2. If parsing fails, you get the syn::Error value.
  3. The syn::Error type has a method called to_compile_error(). This method converts the detailed error information into a TokenStream.
  4. When the compiler receives this special TokenStream as the return value from your macro, it renders it as a clean, helpful compile-time error message, pointing directly at the user’s code.

This approach transforms a scary panic message into a helpful diagnostic, elevating your macro from a simple tool to a professional-grade library component.

Our Path Forward

For now, using .unwrap() is the right choice. It allows us to focus on the core “happy path” logic: parsing the struct, extracting its fields, and generating the builder code. We have intentionally deferred the complexity of error handling to a later enhancement step. Once our builder is fully functional, we will circle back and replace every .unwrap() with this more robust error-handling strategy.

You have now officially completed the “Parsing the Input Struct” step! You have successfully transformed the raw input from the compiler into a structured syn::DeriveInput stored in the ast variable. This variable is your key to the user’s code, and you are now perfectly prepared for the next step: “Extracting Structural Information”, where you will begin to pull the struct’s name and fields from your newly created ast.

Further Reading

To learn more about the concepts of error handling in procedural macros, which we will implement later, these resources are invaluable.