Rust - Syn and Quote

I am trying to learn how the syn and quote libraries work and basically how parsing and code gen are done. So I wrote a simple macro to convert a Rust function to a .json schema. The macro is given below. It works well except for a case where we have the Where clause.
Here is the macro below:




use proc_macro::{self, TokenStream};
use quote::{quote, format_ident};
use syn::{
    parse_macro_input, ItemFn, ReturnType, Attribute, parse::Parse, parse::ParseStream, FnArg, Generics, Visibility,
    LitStr, Lit,Expr, Meta, Result, WherePredicate
};


// Custom attribute parser
struct MacroAttributes {
    description: Option<String>,
}

impl Parse for MacroAttributes {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let description = if input.peek(LitStr) {
            let desc_lit = input.parse::<LitStr>()?;
            Some(desc_lit.value())
        } else {
            None
        };

        Ok(MacroAttributes { description })
    }
}

// Function to extract documentation comments from function attributes.
fn extract_doc_comments(attrs: &[Attribute]) -> Result<Option<String>> {
    let mut docs = Vec::new();
    for attr in attrs {
        match &attr.meta {
            Meta::NameValue(nv) if nv.path.is_ident("doc") => {
                if let Expr::Lit(expr_lit) = &nv.value {
                    if let Lit::Str(lit) = &expr_lit.lit {
                        docs.push(lit.value());
                    }
                }
            },
            _ => {}
        }
    }

    if docs.is_empty() {
        Ok(None)
    } else {
        Ok(Some(docs.join("\n")))
    }
}


// Function to generate JSON-like schema information for generics in a function.
fn process_generics(generics: &Generics) -> Option<proc_macro2::TokenStream> {
    let type_params: Vec<_> = generics.type_params().map(|param| {
        let ident = &param.ident; // Identifier of the type parameter.
        let bounds = param.bounds.iter().map(|b| quote! {#b}).collect::<Vec<_>>();
        quote! {
            {"name": stringify!(#ident), "bounds": [#(#bounds),*]}
        }
    }).collect();

    // Debug print to see the processed generics
    eprintln!("Processed Generics: {:?}", type_params);

    if type_params.is_empty() {
        None // No type parameters to process.
    } else {
        Some(quote! { [#(#type_params),*] })
    }
}


fn process_where_clause(generics: &Generics) -> Option<proc_macro2::TokenStream> {
    generics.where_clause.as_ref().map(|where_clause| {
        let where_preds: Vec<_> = where_clause.predicates.iter().map(|predicate| {
            if let WherePredicate::Type(predicate) = predicate {
                let bounded_ty = &predicate.bounded_ty;
                let bounds = predicate.bounds.iter().map(|b| quote! { #b }).collect::<Vec<_>>();
                quote! { #bounded_ty: #(#bounds)+* }
            } else {
                quote! { compile_error!("Unsupported where predicate type"); }
            }
        }).collect();

        // Correctly format the complete where clause
        quote! { where #(#where_preds),* }
    })
}


// Similar function for lifetimes.
fn process_lifetimes(generics: &Generics) -> Option<proc_macro2::TokenStream> {
    let lifetimes: Vec<_> = generics.lifetimes().map(|lifetime| {
        let ident = &lifetime.lifetime.ident;
        quote! {stringify!(#ident)}
    }).collect();

    if lifetimes.is_empty() {
        None
    } else {
        Some(quote! { [#(#lifetimes),*] })
    }
}



// The `myschema` macro function that processes attributes and the function to which it is applied.
#[proc_macro_attribute]
pub fn myschema(attr: TokenStream, item: TokenStream) -> TokenStream {
    eprintln!("Attributes: {:?}", attr);
    eprintln!("Item: {:?}", item);

    let attrs = parse_macro_input!(attr as MacroAttributes);

    let input_fn = parse_macro_input!(item as ItemFn);
    let fn_name = &input_fn.sig.ident;
    let visibility_str = match &input_fn.vis {
        Visibility::Public(_) => "public",
        Visibility::Restricted(res) => if res.path.is_ident("crate") { "crate" } else { "restricted" },
        Visibility::Inherited => "private",
    };
    let generics = &input_fn.sig.generics;
    let generics_info = process_generics(&input_fn.sig.generics);
    let lifetimes_info = process_lifetimes(&input_fn.sig.generics);
    let where_info = process_where_clause(&input_fn.sig.generics);
    let return_type = match &input_fn.sig.output {
        ReturnType::Default => None,
        ReturnType::Type(_, t) => Some(quote! {#t}),
    };
    let is_async = input_fn.sig.asyncness.is_some();
    let doc_comments = match extract_doc_comments(&input_fn.attrs) {
        Ok(Some(docs)) => docs,
        Ok(None) => String::new(),
        Err(_) => return TokenStream::from(quote! { compile_error!("Error parsing doc comments"); })
    };

    // Generate the JSON-like schema for the function parameters.
    let inputs: Vec<_> = input_fn.sig.inputs.iter().map(|arg| {
        if let FnArg::Typed(pat_type) = arg {
            let pat = &pat_type.pat;
            let ty = &pat_type.ty;
            quote! {
                {
                    "name": stringify!(#pat),
                    "type": stringify!(#ty)
                }
            }
        } else {
            quote! {}
        }
    }).collect();

    let mut schema_fields = vec![
        quote! { "name": stringify!(#fn_name) },
        quote! { "visibility": #visibility_str },
        quote! { "documentation": #doc_comments },
    ];

    if let Some(desc) = attrs.description {
        schema_fields.push(quote! { "description": #desc });
    }

    if !inputs.is_empty() {
        schema_fields.push(quote! { "parameters": [#(#inputs),*] });
    }

    if let Some(rt) = return_type {
        schema_fields.push(quote! { "return_type": stringify!(#rt) });
    }

    if is_async {
        schema_fields.push(quote! { "is_async": true });
    }

    if let Some(generics) = generics_info {
        schema_fields.push(quote! { "generics": #generics });
    }


    if let Some(where_clause) = where_info {
        schema_fields.push(quote! { "where_clause": #where_clause });
    }

    if let Some(lifetimes) = lifetimes_info {
        schema_fields.push(quote! { "lifetimes": #lifetimes });
    }

    let schema_function_name = format_ident!("schema_{}", fn_name);

    let schema_fn = quote! {
        pub fn #schema_function_name #generics() -> ::std::result::Result<String, serde_json::Error> {
            let schema = serde_json::json!({ #(#schema_fields),* });
            serde_json::to_string_pretty(&schema)
        }
    };

    let output = quote! {
        #input_fn
        #schema_fn
    };

    output.into()
}

The macro compiles and works for all the test cases except the first one (multiply) below:

use std::ops::Mul;

use rust_macro::myschema; // Import the schema macro

// This fails
#[myschema("Generic multiplication")]
pub fn multiply<T>(x: T, y: T) -> T
where
    T: Mul<Output = T>
{
    x * y
}

/// My async function
#[myschema("Asynchronous fetch")]
pub async fn fetch_data(url: &str) -> Result<String, &'static str> {
    Ok("data".to_string())
}

#[myschema("Multiplies two numbers")]
pub fn multiply_doc(x: i32, y: i32) -> i32 {
    x * y
}


#[myschema("Determines the longest of two string slices")]
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

#[myschema("Private function")]
fn private_function() -> bool {
    true
}

fn main() {
    if let Ok(schema_output) = schema_multiply::<i32>() {
        println!("Generated schema: {}", schema_output);
    }

    if let Ok(schema_output) = schema_fetch_data() {
        println!("Generated schema: {}", schema_output);
    }

    if let Ok(schema_output) = schema_multiply_doc() {
        println!("Generated schema: {}", schema_output);
    }

    if let Ok(schema_output) = schema_longest() {
        println!("Generated schema: {}", schema_output);
    }

    if let Ok(schema_output) = schema_private_function() {
        println!("Generated schema: {}", schema_output);
    }
}

Instead I get the error:

   Compiling rust-macro v0.1.0 (/home/richard/Documents/rust_projects/practical-rust/rust-macro)
   Compiling rust-macro-test v0.1.0 (/home/richard/Documents/rust_projects/practical-rust/rust-macro-test)
Attributes: TokenStream [Literal { kind: Str, symbol: "Generic multiplication", suffix: None, span: #0 bytes(99..123) }]
Item: TokenStream [Ident { ident: "pub", span: #0 bytes(126..129) }, Ident { ident: "fn", span: #0 bytes(130..132) }, Ident { ident: "multiply", span: #0 bytes(133..141) }, Punct { ch: '<', spacing: Alone, span: #0 bytes(141..142) }, Ident { ident: "T", span: #0 bytes(142..143) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(143..144) }, Group { delimiter: Parenthesis, stream: TokenStream [Ident { ident: "x", span: #0 bytes(145..146) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(146..147) }, Ident { ident: "T", span: #0 bytes(148..149) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(149..150) }, Ident { ident: "y", span: #0 bytes(151..152) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(152..153) }, Ident { ident: "T", span: #0 bytes(154..155) }], span: #0 bytes(144..156) }, Punct { ch: '-', spacing: Joint, span: #0 bytes(157..158) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(158..159) }, Ident { ident: "T", span: #0 bytes(160..161) }, Ident { ident: "where", span: #0 bytes(162..167) }, Ident { ident: "T", span: #0 bytes(172..173) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(173..174) }, Ident { ident: "Mul", span: #0 bytes(175..178) }, Punct { ch: '<', spacing: Alone, span: #0 bytes(178..179) }, Ident { ident: "Output", span: #0 bytes(179..185) }, Punct { ch: '=', spacing: Alone, span: #0 bytes(186..187) }, Ident { ident: "T", span: #0 bytes(188..189) }, Punct { ch: '>', spacing: Alone, span: #0 bytes(189..190) }, Group { delimiter: Brace, stream: TokenStream [Ident { ident: "x", span: #0 bytes(197..198) }, Punct { ch: '*', spacing: Alone, span: #0 bytes(199..200) }, Ident { ident: "y", span: #0 bytes(201..202) }], span: #0 bytes(191..204) }]
Processed Generics: [TokenStream [Group { delimiter: Brace, stream: TokenStream [Literal { kind: Str, symbol: "name", suffix: None, span: #5 bytes(88..125) }, Punct { ch: ':', spacing: Alone, span: #5 bytes(88..125) }, Ident { ident: "stringify", span: #5 bytes(88..125) }, Punct { ch: '!', spacing: Alone, span: #5 bytes(88..125) }, Group { delimiter: Parenthesis, stream: TokenStream [Ident { ident: "T", span: #0 bytes(142..143) }], span: #5 bytes(88..125) }, Punct { ch: ',', spacing: Alone, span: #5 bytes(88..125) }, Literal { kind: Str, symbol: "bounds", suffix: None, span: #5 bytes(88..125) }, Punct { ch: ':', spacing: Alone, span: #5 bytes(88..125) }, Group { delimiter: Bracket, stream: TokenStream [], span: #5 bytes(88..125) }], span: #5 bytes(88..125) }]]
error: no rules expected the token `where`
   --> src/main.rs:6:1
    |
6   | #[myschema("Generic multiplication")]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no rules expected this token in macro call
    |
note: while trying to match meta-variable `$e:expr`
   --> /home/richard/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde_json-1.0.116/src/macros.rs:303:6
    |
303 |     ($e:expr , $($tt:tt)*) => {};
    |      ^^^^^^^
    = note: this error originates in the attribute macro `myschema` (in Nightly builds, run with -Z macro-backtrace for more info)

Kindly assist, as I don't know what else to do.

You can use cargo expand (I think it needs to be installed separately, cargo install cargo-expand) to view the expansion of your macros, up to the point where further macro expansion fails. If you're using an IDE, it can handle the process for you. Call the intention "recursively expand macro", or "single-step macro expansion", or whatever it's named in your IDE.

It's clear from the error that your proc macro passes wrong tokens to some macro in the expansion, but finding the exact bug just by reading source is a bit too much.

Thanks. I will try the macro expansion.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.