Declarative and Procedural Macros in Depth
Parsing Token Streams
While the compiler passes a raw proc_macro::TokenStream (a flat sequence of Group, Ident, Punct, and Literal token trees) into a procedural macro, real-world procedural macros use the standard proc-macro2, syn, and quote triad to parse token streams into typed syntax trees and turn Rust AST nodes back into output token streams.
The proc-macro2, syn, and quote Triad
Input `proc_macro::TokenStream` (from `rustc`)
|
| `syn::parse_macro_input!(input as DeriveInput)`
v
Typed AST (`syn::DeriveInput`, `syn::ItemFn`, or custom `syn::parse::Parse`)
|
| Inspect fields, generics, and attributes; interpolate into `quote! { ... }`
v
`proc_macro2::TokenStream`
|
| `.into()`
v
Output `proc_macro::TokenStream` (returned to `rustc`)
| Crate | Primary Role | Why It Exists |
|---|---|---|
proc-macro2 |
Wrapper around proc_macro::TokenStream and Span |
proc_macro types panic if used outside the compiler's proc-macro execution context; proc-macro2 allows unit-testing macro logic in normal cargo test binaries! |
syn |
Recursive-descent parser for Rust syntax and custom DSLs | Parses TokenStream into rich AST structs (DeriveInput, ItemFn, Expr, Type, Generics) |
quote |
Quasi-quoting macro quote! { ... } with #var and #(#items)* interpolation |
Converts Rust syntax with interpolated AST variables back into a proc_macro2::TokenStream |
Implementing a Custom Token-Stream Parser in Standard Rust
To understand how syn::parse::Parse works under the hood, here is a complete, runnable recursive-descent token parser over token trees that parses key: Type = default_expr declarations and generates a struct definition:
#[derive(Debug, PartialEq)]
enum SimpleToken<'a> {
Ident(&'a str),
Colon,
Equals,
Literal(&'a str),
}
#[derive(Debug)]
struct FieldSpec<'a> {
name: &'a str,
ty: &'a str,
default_lit: &'a str,
}
fn parse_field_spec<'a>(tokens: &[SimpleToken<'a>]) -> Result<FieldSpec<'a>, &'static str> {
match tokens {
[
SimpleToken::Ident(name),
SimpleToken::Colon,
SimpleToken::Ident(ty),
SimpleToken::Equals,
SimpleToken::Literal(default_lit),
] => Ok(FieldSpec {
name,
ty,
default_lit,
}),
_ => Err("expected `<name>: <Type> = <literal>`"),
}
}
fn main() {
let stream = [
SimpleToken::Ident("port"),
SimpleToken::Colon,
SimpleToken::Ident("u16"),
SimpleToken::Equals,
SimpleToken::Literal("8080"),
];
let spec = parse_field_spec(&stream).unwrap();
println!(
"parsed FieldSpec -> pub {}: {} (default = {})",
spec.name, spec.ty, spec.default_lit
);
}
parsed FieldSpec -> pub port: u16 (default = 8080)
Parsing Custom Syntax with syn::parse::Parse
In a procedural macro crate using syn, any custom DSL struct can implement syn::parse::Parse:
// Inside a `proc-macro = true` crate with `syn`:
/*
use syn::parse::{Parse, ParseStream};
use syn::{Expr, Ident, Result, Token};
struct KeyValueInput {
key: Ident,
_arrow: Token![=>],
value: Expr,
}
impl Parse for KeyValueInput {
fn parse(input: ParseStream) -> Result<Self> {
Ok(KeyValueInput {
key: input.parse()?,
_arrow: input.parse()?,
value: input.parse()?,
})
}
}
*/
fn main() {
println!("syn::parse::Parse advances a cursor over ParseStream token by token.");
}
syn::parse::Parse advances a cursor over ParseStream token by token.