427: `syn` and `quote!` Basics
Tutorial Video
Text description (accessibility)
This video demonstrates the "427: `syn` and `quote!` Basics" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Writing proc macros without `syn` and `quote` is like writing a compiler without an AST — you'd be manipulating raw token streams manually. Key difference from OCaml: 1. **Ergonomics**: `quote!`'s `#var` interpolation is concise; OCaml's `Ast_builder` requires explicit node construction (`Ast_builder.Default.eapply`).
Tutorial
The Problem
Writing proc macros without syn and quote is like writing a compiler without an AST — you'd be manipulating raw token streams manually. syn parses Rust token streams into a rich AST: DeriveInput, ItemFn, Type, Expr, Ident. quote! generates Rust code from these AST nodes with clean #variable interpolation. Together, they are the standard toolkit for every serious Rust proc macro, used by serde, tokio, clap, and virtually every derive macro in the ecosystem.
Understanding syn and quote is the gateway to implementing production-quality proc macros that generate correct, well-formatted Rust code.
🎯 Learning Outcomes
syn::parse_macro_input! parses a TokenStream into a typed ASTDeriveInput provides ident, generics, and data (struct/enum/union)quote! uses #ident interpolation to generate code from parsed valuesproc_macro2::TokenStream vs. proc_macro::TokenStream (the bridge between proc macro boundary and quote)syn::Fields to generate per-field codeCode Example
#![allow(clippy::all)]
//! syn and quote Basics
//!
//! Understanding the crates used in proc macros.
/// syn parses Rust tokens into AST.
/// quote generates Rust tokens from templates.
/// This example shows the concepts.
/// A field descriptor (what syn might parse).
pub struct FieldInfo {
pub name: String,
pub ty: String,
}
/// Generate code string (what quote does).
pub fn generate_getter(field: &FieldInfo) -> String {
format!(
"pub fn {}(&self) -> &{} {{ &self.{} }}",
field.name, field.ty, field.name
)
}
/// Generate setter.
pub fn generate_setter(field: &FieldInfo) -> String {
format!(
"pub fn set_{}(&mut self, value: {}) {{ self.{} = value; }}",
field.name, field.ty, field.name
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_getter() {
let f = FieldInfo {
name: "x".into(),
ty: "i32".into(),
};
let code = generate_getter(&f);
assert!(code.contains("fn x(&self)"));
assert!(code.contains("&i32"));
}
#[test]
fn test_generate_setter() {
let f = FieldInfo {
name: "y".into(),
ty: "String".into(),
};
let code = generate_setter(&f);
assert!(code.contains("set_y"));
assert!(code.contains("value: String"));
}
#[test]
fn test_field_info() {
let f = FieldInfo {
name: "age".into(),
ty: "u32".into(),
};
assert_eq!(f.name, "age");
assert_eq!(f.ty, "u32");
}
#[test]
fn test_getter_contains_self() {
let f = FieldInfo {
name: "data".into(),
ty: "Vec<u8>".into(),
};
let code = generate_getter(&f);
assert!(code.contains("&self"));
}
#[test]
fn test_setter_contains_mut() {
let f = FieldInfo {
name: "count".into(),
ty: "usize".into(),
};
let code = generate_setter(&f);
assert!(code.contains("&mut self"));
}
}Key Differences
quote!'s #var interpolation is concise; OCaml's Ast_builder requires explicit node construction (Ast_builder.Default.eapply).syn's typed AST ensures you're working with valid Rust constructs; OCaml's Parsetree is also typed but more verbose.quote! generates hygienic identifiers using proc_macro2::Span::call_site(); OCaml PPX inherits OCaml's lack of macro hygiene.syn's generics.split_for_impl() handles the complex case of generic impl blocks; OCaml requires manual handling of type parameters.OCaml Approach
OCaml's ppxlib provides Ast_pattern for parsing and Ast_builder for generation — the direct equivalents of syn and quote. Ast_pattern.(pstr (pstr_type __ __)) matches type declarations; Ast_builder.Default.str builds string AST nodes. The functional style of OCaml makes AST traversal through pattern matching more natural, but the verbosity of explicit AST construction matches quote!'s more concise interpolation.
Full Source
#![allow(clippy::all)]
//! syn and quote Basics
//!
//! Understanding the crates used in proc macros.
/// syn parses Rust tokens into AST.
/// quote generates Rust tokens from templates.
/// This example shows the concepts.
/// A field descriptor (what syn might parse).
pub struct FieldInfo {
pub name: String,
pub ty: String,
}
/// Generate code string (what quote does).
pub fn generate_getter(field: &FieldInfo) -> String {
format!(
"pub fn {}(&self) -> &{} {{ &self.{} }}",
field.name, field.ty, field.name
)
}
/// Generate setter.
pub fn generate_setter(field: &FieldInfo) -> String {
format!(
"pub fn set_{}(&mut self, value: {}) {{ self.{} = value; }}",
field.name, field.ty, field.name
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_getter() {
let f = FieldInfo {
name: "x".into(),
ty: "i32".into(),
};
let code = generate_getter(&f);
assert!(code.contains("fn x(&self)"));
assert!(code.contains("&i32"));
}
#[test]
fn test_generate_setter() {
let f = FieldInfo {
name: "y".into(),
ty: "String".into(),
};
let code = generate_setter(&f);
assert!(code.contains("set_y"));
assert!(code.contains("value: String"));
}
#[test]
fn test_field_info() {
let f = FieldInfo {
name: "age".into(),
ty: "u32".into(),
};
assert_eq!(f.name, "age");
assert_eq!(f.ty, "u32");
}
#[test]
fn test_getter_contains_self() {
let f = FieldInfo {
name: "data".into(),
ty: "Vec<u8>".into(),
};
let code = generate_getter(&f);
assert!(code.contains("&self"));
}
#[test]
fn test_setter_contains_mut() {
let f = FieldInfo {
name: "count".into(),
ty: "usize".into(),
};
let code = generate_setter(&f);
assert!(code.contains("&mut self"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_getter() {
let f = FieldInfo {
name: "x".into(),
ty: "i32".into(),
};
let code = generate_getter(&f);
assert!(code.contains("fn x(&self)"));
assert!(code.contains("&i32"));
}
#[test]
fn test_generate_setter() {
let f = FieldInfo {
name: "y".into(),
ty: "String".into(),
};
let code = generate_setter(&f);
assert!(code.contains("set_y"));
assert!(code.contains("value: String"));
}
#[test]
fn test_field_info() {
let f = FieldInfo {
name: "age".into(),
ty: "u32".into(),
};
assert_eq!(f.name, "age");
assert_eq!(f.ty, "u32");
}
#[test]
fn test_getter_contains_self() {
let f = FieldInfo {
name: "data".into(),
ty: "Vec<u8>".into(),
};
let code = generate_getter(&f);
assert!(code.contains("&self"));
}
#[test]
fn test_setter_contains_mut() {
let f = FieldInfo {
name: "count".into(),
ty: "usize".into(),
};
let code = generate_setter(&f);
assert!(code.contains("&mut self"));
}
}
Deep Comparison
OCaml vs Rust: syn quote basics
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
impl FieldCount for T { fn field_count() -> usize { N } } where N is the number of fields in the struct.syn patterns, generate impl TypeName for T { fn type_name() -> &'static str { "T" } } and also fn field_types() -> Vec<&'static str> listing each field's type name as a string.serde_derive's source for its Serialize derive. Identify which syn types it uses to extract field names and types, and how quote! generates the serialize_struct call. Write a simplified version that generates JSON-string serialization (without serde's runtime).