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}