Macro System: macro_rules! AST Matching vs Procedural Derive Macros (syn/quote)
- Last updated
- Prerequisites:
- Rust syntax, traits, and derives
- Understanding of ASTs and metaprogramming
- rust
- macros
- macro-rules
- procedural-macros
- syn
- quote
Read at your depth
The practical view
Rust has two macro systems. macro_rules! is declarative: you define patterns with matchers ($x:expr, $y:ty) that expand to token templates. Procedural macros are functions that process TokenStreams: #[derive(...)] (derive macros), attribute macros (#[test]), and function-like macros (println!). The syn crate parses a TokenStream into an AST, quote generates code with #var interpolation. Common derives (#[derive(Debug, Clone, PartialEq)]) are procedural macros in the std/derive tooling. Write a derive: create a proc-macro crate, #[proc_macro_derive(MyTrait)] pub fn derive(input: TokenStream) -> TokenStream.
The same idea in other frameworks
N/A — Rust language feature.N/A — Rust feature.N/A — Rust feature.N/A — Rust feature.Legacy vs modern
macro_rules! template vs procedural derive
Declarative macros substitute patterns; procedural macros compute on the AST — e.g. implementing a trait by inspecting struct fields.
impl fmt::Debug for Point { ... } // ❌ OLD: boilerplate per struct#[derive(Debug, Clone, PartialEq, Eq)] // ✅ NEW: generated implsInterview gotchas
Context
This tests the macro taxonomy — interviewers want the pattern-matching vs AST-computing distinction and the practical guidance for when procedural power is needed.
The mechanical answer
macro_rules! is declarative and shape-based: you write match patterns ($x:expr) and expansion templates; the compiler substitutes token fragments. It's great for small syntax extensions, repeated patterns (vec!, println!), and readability — but it cannot inspect the semantic structure (fields, generics) or do computation on the AST; it's pure token-tree substitution with limited recursion. Procedural macros are full functions over TokenStream: they parse the input with syn into a rich AST (struct fields, generics, attributes), compute arbitrarily, and emit new code with quote. Choose macro_rules! for simple, syntactic sugar; choose procedural macros when you need to read the item's structure (derive implementations, attribute transformations like serde's #[serde(rename)], or building complex DSLs). The cost of procedural macros is compile time and complexity (syn/quote, proc-macro crates).
Trap
The naive answer is 'procedural macros are just more powerful macro_rules'. They operate on a different level (AST computation vs token substitution) and live in separate proc-macro crates. Another trap: thinking macro_rules! can inspect fields — it cannot; only procedural macros parse structure. Candidates who contrast the two expansion models show real understanding.
Context
This probes the identifier-collision problem — interviewers want the syntax-context mechanism and how procedural macros handle (or don't handle) hygiene.
The mechanical answer
Macro hygiene prevents identifiers introduced by a macro from colliding with identifiers in the caller's code. For macro_rules!, Rust applies hygiene via syntax contexts: a local variable declared inside the macro gets a context marker, so it cannot accidentally capture or be captured by a same-named variable at the call site. This is why a macro can use a temp variable named 'x' without clobbering the caller's 'x'. Procedural macros, by default, produce tokens with the CALLER's hygiene context — they are NOT automatically hygienic (the quote! spans come from the invocation). This is why procedural macro authors use Span::call_site() / Span::mixed_site() deliberately and often generate helper identifiers via generate_ident or unique prefixes (e.g. __impl_MyTrait_for_#name) to avoid collisions with user code. Hygiene is a correctness concern for any macro that introduces names.
Trap
The naive answer is 'all Rust macros are hygienic'. macro_rules! is hygienic for locals; procedural macros are not automatically (they use call-site spans). Another trap: assuming generated helper names can't collide — a derive that generates a function without a unique prefix can clash with a user-defined function of the same name. Candidates who describe the syntax-context model and Span::mixed_site show real depth.