ExamplesBy LevelBy TopicLearning Paths
427 Fundamental

427: `syn` and `quote!` Basics

Functional Programming

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

  • • Understand how syn::parse_macro_input! parses a TokenStream into a typed AST
  • • Learn how DeriveInput provides ident, generics, and data (struct/enum/union)
  • • See how quote! uses #ident interpolation to generate code from parsed values
  • • Understand proc_macro2::TokenStream vs. proc_macro::TokenStream (the bridge between proc macro boundary and quote)
  • • Learn how to iterate struct fields using syn::Fields to generate per-field code
  • Code 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

  • Ergonomics: quote!'s #var interpolation is concise; OCaml's Ast_builder requires explicit node construction (Ast_builder.Default.eapply).
  • Type safety: syn's typed AST ensures you're working with valid Rust constructs; OCaml's Parsetree is also typed but more verbose.
  • Hygiene: quote! generates hygienic identifiers using proc_macro2::Span::call_site(); OCaml PPX inherits OCaml's lack of macro hygiene.
  • Generics: 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"));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Field counter: Write the body of a derive macro (using the patterns from this example) that generates impl FieldCount for T { fn field_count() -> usize { N } } where N is the number of fields in the struct.
  • Type stringifier: Using 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 skeleton: Study 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).
  • Open Source Repos