quantum_pulse_macros/
lib.rs

1//! Procedural macros for the quantum-pulse profiling library.
2//!
3//! This crate provides derive macros to automatically implement profiling traits,
4//! reducing boilerplate code and making it easier to integrate profiling into your applications.
5
6use proc_macro::TokenStream;
7use quote::{format_ident, quote};
8use std::collections::HashMap;
9use syn::{parse_macro_input, Data, DeriveInput};
10
11/// Derives the `Operation` trait for enums, automatically generating category implementations.
12///
13/// This macro generates unique category structs for each distinct category name found in the
14/// enum variants, and implements the `Operation` trait to return the appropriate category
15/// for each variant.
16///
17/// # Attributes
18///
19/// The macro supports the `#[category(...)]` attribute on enum variants with the following parameters:
20/// - `name`: The name of the category (optional, defaults to variant name)
21/// - `description`: A description of the category (optional, defaults to category name)
22///
23/// # Important Behavior
24///
25/// When multiple variants use the same category name:
26/// - Only one category struct is generated per unique category name
27/// - The first `description` encountered for a category name is used
28/// - Subsequent descriptions for the same category name are ignored
29///
30/// # Example
31///
32/// ```rust,ignore
33/// use quantum_pulse::{ProfileOp, Operation};
34///
35/// #[derive(Debug, ProfileOp)]
36/// enum MyOperation {
37///     // Category with both name and description
38///     #[category(name = "IO", description = "Input/Output operations")]
39///     ReadFile,
40///
41///     // Same category, description is ignored (first one wins)
42///     #[category(name = "IO", description = "This description is ignored")]
43///     WriteFile,
44///
45///     // Category with only name (description defaults to name)
46///     #[category(name = "Network")]
47///     HttpRequest,
48///
49///     // No category attribute (uses variant name as category)
50///     Compute,
51///
52///     // Supports enum variants with data
53///     #[category(name = "Database")]
54///     Query(String),
55///
56///     // Supports enum variants with named fields
57///     #[category(name = "Cache")]
58///     CacheOp { key: String, ttl: u64 },
59/// }
60/// ```
61///
62/// # Generated Code
63///
64/// For each unique category, the macro generates:
65/// - A hidden struct implementing the `Category` trait
66/// - An implementation of `Operation::get_category()` that returns the appropriate category
67///
68/// # Panics
69///
70/// - If applied to anything other than an enum
71/// - If the category attribute parsing fails
72#[proc_macro_derive(Operation, attributes(category))]
73pub fn derive_operation(input: TokenStream) -> TokenStream {
74    let input = parse_macro_input!(input as DeriveInput);
75    let enum_name = &input.ident;
76
77    let data_enum = match &input.data {
78        Data::Enum(data) => data,
79        _ => panic!("Operation can only be derived for enums"),
80    };
81
82    // Track unique categories by name
83    let mut categories: HashMap<String, CategoryInfo> = HashMap::new();
84    let mut variant_categories: Vec<String> = Vec::new();
85
86    // First pass: collect all categories and their info
87    for variant in &data_enum.variants {
88        let variant_ident = &variant.ident;
89        let mut category_name = None;
90        let mut category_description = None;
91
92        // Parse the category attribute
93        for attr in &variant.attrs {
94            if attr.path().is_ident("category") {
95                let nested = attr.parse_nested_meta(|meta| {
96                    if meta.path.is_ident("name") {
97                        let value = meta.value()?;
98                        let s: syn::LitStr = value.parse()?;
99                        category_name = Some(s.value());
100                    } else if meta.path.is_ident("description") {
101                        let value = meta.value()?;
102                        let s: syn::LitStr = value.parse()?;
103                        category_description = Some(s.value());
104                    } else {
105                        return Err(meta.error("unrecognized category attribute"));
106                    }
107                    Ok(())
108                });
109
110                if let Err(err) = nested {
111                    panic!("Failed to parse category attribute: {}", err);
112                }
113            }
114        }
115
116        // Determine the category name (default to empty string if not specified)
117        let final_category_name = category_name.unwrap_or_else(|| String::new());
118
119        // Only update the category info if it hasn't been defined yet or if this one has a description
120        if !categories.contains_key(&final_category_name) {
121            categories.insert(
122                final_category_name.clone(),
123                CategoryInfo {
124                    name: final_category_name.clone(),
125                    description: category_description.unwrap_or_else(|| {
126                        if final_category_name.is_empty() {
127                            format!("{}", variant_ident)
128                        } else {
129                            final_category_name.clone()
130                        }
131                    }),
132                },
133            );
134        } else if category_description.is_some() {
135            // If this category already exists but this variant provides a description,
136            // only update if the existing one doesn't have a custom description
137            let existing = categories.get(&final_category_name).unwrap();
138            if existing.description == final_category_name
139                || (final_category_name.is_empty()
140                    && existing.description == format!("{}", variant_ident))
141            {
142                categories.insert(
143                    final_category_name.clone(),
144                    CategoryInfo {
145                        name: final_category_name.clone(),
146                        description: category_description.unwrap(),
147                    },
148                );
149            }
150        }
151
152        variant_categories.push(final_category_name);
153    }
154
155    // Generate category structs for unique categories
156    let category_defs: Vec<_> = categories
157        .values()
158        .map(|cat_info| {
159            let struct_name = format_ident!(
160                "__Category_{}_{}",
161                enum_name,
162                sanitize_ident(&cat_info.name)
163            );
164            let cat_name = &cat_info.name;
165            let cat_description = &cat_info.description;
166
167            quote! {
168                #[doc(hidden)]
169                #[allow(non_camel_case_types)]
170                #[derive(Debug)]
171                struct #struct_name;
172
173                impl quantum_pulse::Category for #struct_name {
174                    fn get_name(&self) -> &str {
175                        #cat_name
176                    }
177
178                    fn get_description(&self) -> &str {
179                        #cat_description
180                    }
181                }
182            }
183        })
184        .collect();
185
186    // Generate match arms for the Operation implementation
187    let match_arms: Vec<_> = data_enum
188        .variants
189        .iter()
190        .zip(variant_categories.iter())
191        .map(|(variant, category_name)| {
192            let variant_ident = &variant.ident;
193            let struct_name =
194                format_ident!("__Category_{}_{}", enum_name, sanitize_ident(category_name));
195
196            // Handle enum variants with fields
197            let pattern = match &variant.fields {
198                syn::Fields::Unit => quote! { #enum_name::#variant_ident },
199                syn::Fields::Unnamed(_) => quote! { #enum_name::#variant_ident(..) },
200                syn::Fields::Named(_) => quote! { #enum_name::#variant_ident{..} },
201            };
202
203            quote! {
204                #pattern => &#struct_name as &dyn quantum_pulse::Category,
205            }
206        })
207        .collect();
208
209    // Handle empty enums specially
210    let operation_impl = if data_enum.variants.is_empty() {
211        quote! {
212            impl quantum_pulse::Operation for #enum_name {
213                fn get_category(&self) -> &dyn quantum_pulse::Category {
214                    match *self {}
215                }
216            }
217        }
218    } else {
219        quote! {
220            impl quantum_pulse::Operation for #enum_name {
221                fn get_category(&self) -> &dyn quantum_pulse::Category {
222                    match self {
223                        #(#match_arms)*
224                    }
225                }
226            }
227        }
228    };
229
230    let expanded = quote! {
231        #(#category_defs)*
232
233        #operation_impl
234    };
235
236    TokenStream::from(expanded)
237}
238
239/// Information about a category collected from enum variant attributes.
240///
241/// This struct holds the parsed category information that will be used
242/// to generate the category implementation.
243struct CategoryInfo {
244    /// The name of the category as specified in the attribute or derived from variant name
245    name: String,
246    /// The description of the category, defaults to the name if not specified
247    description: String,
248}
249
250/// Sanitizes a string to be a valid Rust identifier.
251///
252/// Replaces any non-alphanumeric characters (except underscore) with underscores
253/// to ensure the resulting string can be used as part of a struct name.
254///
255/// # Arguments
256///
257/// * `s` - The string to sanitize
258///
259/// # Returns
260///
261/// A string safe to use as a Rust identifier component
262///
263/// # Example
264///
265/// ```ignore
266/// assert_eq!(sanitize_ident("My-Category"), "My_Category");
267/// assert_eq!(sanitize_ident("IO/Network"), "IO_Network");
268/// ```
269fn sanitize_ident(s: &str) -> String {
270    s.chars()
271        .map(|c| {
272            if c.is_alphanumeric() || c == '_' {
273                c
274            } else {
275                '_'
276            }
277        })
278        .collect()
279}