| 1 | use proc_macro::TokenStream; |
| 2 | use quote::{quote, ToTokens}; |
| 3 | use syn::{ |
| 4 | parse::{Parse, Parser}, |
| 5 | spanned::Spanned, |
| 6 | Expr, ExprArray, Ident, Token, Type, |
| 7 | }; |
| 8 | |
| 9 | use crate::{tokens, Macro}; |
| 10 | |
| 11 | /// Values from parsed options shared between `#[divan::bench]` and |
| 12 | /// `#[divan::bench_group]`. |
| 13 | /// |
| 14 | /// The `crate` option is not included because it is only needed to get proper |
| 15 | /// access to `__private`. |
| 16 | pub(crate) struct AttrOptions { |
| 17 | /// `divan::__private`. |
| 18 | pub private_mod: proc_macro2::TokenStream, |
| 19 | |
| 20 | /// Custom name for the benchmark or group. |
| 21 | pub name_expr: Option<Expr>, |
| 22 | |
| 23 | /// `IntoIterator` from which to provide runtime arguments. |
| 24 | pub args_expr: Option<Expr>, |
| 25 | |
| 26 | /// Options for generic functions. |
| 27 | pub generic: GenericOptions, |
| 28 | |
| 29 | /// The `BenchOptions.counters` field and its value, followed by a comma. |
| 30 | pub counters: proc_macro2::TokenStream, |
| 31 | |
| 32 | /// Options used directly as `BenchOptions` fields. |
| 33 | /// |
| 34 | /// Option reuse is handled by the compiler ensuring `BenchOptions` fields |
| 35 | /// are not repeated. |
| 36 | pub bench_options: Vec<(Ident, Expr)>, |
| 37 | } |
| 38 | |
| 39 | impl AttrOptions { |
| 40 | pub fn parse(tokens: TokenStream, target_macro: Macro) -> Result<Self, TokenStream> { |
| 41 | let macro_name = target_macro.name(); |
| 42 | |
| 43 | let mut divan_crate = None::<syn::Path>; |
| 44 | let mut name_expr = None::<Expr>; |
| 45 | let mut args_expr = None::<Expr>; |
| 46 | let mut bench_options = Vec::new(); |
| 47 | |
| 48 | let mut counters = Vec::<(proc_macro2::TokenStream, Option<&str>)>::new(); |
| 49 | let mut counters_ident = None::<Ident>; |
| 50 | |
| 51 | let mut seen_bytes_count = false; |
| 52 | let mut seen_chars_count = false; |
| 53 | let mut seen_cycles_count = false; |
| 54 | let mut seen_items_count = false; |
| 55 | |
| 56 | let mut generic = GenericOptions::default(); |
| 57 | |
| 58 | let attr_parser = syn::meta::parser(|meta| { |
| 59 | macro_rules! error { |
| 60 | ($($t:tt)+) => { |
| 61 | return Err(meta.error(format_args!($($t)+))) |
| 62 | }; |
| 63 | } |
| 64 | |
| 65 | let Some(ident) = meta.path.get_ident() else { |
| 66 | error!("unsupported ' {macro_name}' option" ); |
| 67 | }; |
| 68 | |
| 69 | let ident_name = ident.to_string(); |
| 70 | let ident_name = ident_name.strip_prefix("r#" ).unwrap_or(&ident_name); |
| 71 | |
| 72 | let repeat_error = || error!("repeated ' {macro_name}' option ' {ident_name}'" ); |
| 73 | let unsupported_error = || error!("unsupported ' {macro_name}' option ' {ident_name}'" ); |
| 74 | |
| 75 | macro_rules! parse { |
| 76 | ($storage:expr) => { |
| 77 | if $storage.is_none() { |
| 78 | $storage = Some(meta.value()?.parse()?); |
| 79 | } else { |
| 80 | return repeat_error(); |
| 81 | } |
| 82 | }; |
| 83 | } |
| 84 | |
| 85 | match ident_name { |
| 86 | "crate" => parse!(divan_crate), |
| 87 | "name" => parse!(name_expr), |
| 88 | "types" => { |
| 89 | match target_macro { |
| 90 | Macro::Bench { fn_sig } => { |
| 91 | if fn_sig.generics.type_params().next().is_none() { |
| 92 | error!("generic type required for ' {macro_name}' option ' {ident_name}'" ); |
| 93 | } |
| 94 | } |
| 95 | _ => return unsupported_error(), |
| 96 | } |
| 97 | |
| 98 | parse!(generic.types); |
| 99 | } |
| 100 | "consts" => { |
| 101 | match target_macro { |
| 102 | Macro::Bench { fn_sig } => { |
| 103 | if fn_sig.generics.const_params().next().is_none() { |
| 104 | error!("generic const required for ' {macro_name}' option ' {ident_name}'" ); |
| 105 | } |
| 106 | } |
| 107 | _ => return unsupported_error(), |
| 108 | } |
| 109 | |
| 110 | parse!(generic.consts); |
| 111 | } |
| 112 | "args" => { |
| 113 | match target_macro { |
| 114 | Macro::Bench { fn_sig } => { |
| 115 | if !matches!(fn_sig.inputs.len(), 1 | 2) { |
| 116 | return Err(meta.error(format_args!("function argument required for ' {macro_name}' option ' {ident_name}'" ))); |
| 117 | } |
| 118 | } |
| 119 | _ => return unsupported_error(), |
| 120 | } |
| 121 | |
| 122 | parse!(args_expr); |
| 123 | } |
| 124 | "counter" => { |
| 125 | if counters_ident.is_some() { |
| 126 | return repeat_error(); |
| 127 | } |
| 128 | let value: Expr = meta.value()?.parse()?; |
| 129 | counters.push((value.into_token_stream(), None)); |
| 130 | counters_ident = Some(Ident::new("counters" , ident.span())); |
| 131 | } |
| 132 | "counters" => { |
| 133 | if counters_ident.is_some() { |
| 134 | return repeat_error(); |
| 135 | } |
| 136 | let values: ExprArray = meta.value()?.parse()?; |
| 137 | counters.extend( |
| 138 | values.elems.into_iter().map(|elem| (elem.into_token_stream(), None)), |
| 139 | ); |
| 140 | counters_ident = Some(ident.clone()); |
| 141 | } |
| 142 | |
| 143 | "bytes_count" if seen_bytes_count => return repeat_error(), |
| 144 | "chars_count" if seen_chars_count => return repeat_error(), |
| 145 | "cycles_count" if seen_cycles_count => return repeat_error(), |
| 146 | "items_count" if seen_items_count => return repeat_error(), |
| 147 | |
| 148 | "bytes_count" | "chars_count" | "cycles_count" | "items_count" => { |
| 149 | let name = match ident_name { |
| 150 | "bytes_count" => { |
| 151 | seen_bytes_count = true; |
| 152 | "BytesCount" |
| 153 | } |
| 154 | "chars_count" => { |
| 155 | seen_chars_count = true; |
| 156 | "CharsCount" |
| 157 | } |
| 158 | "cycles_count" => { |
| 159 | seen_cycles_count = true; |
| 160 | "CyclesCount" |
| 161 | } |
| 162 | "items_count" => { |
| 163 | seen_items_count = true; |
| 164 | "ItemsCount" |
| 165 | } |
| 166 | _ => unreachable!(), |
| 167 | }; |
| 168 | |
| 169 | let value: Expr = meta.value()?.parse()?; |
| 170 | counters.push((value.into_token_stream(), Some(name))); |
| 171 | counters_ident = Some(Ident::new("counters" , proc_macro2::Span::call_site())); |
| 172 | } |
| 173 | |
| 174 | _ => { |
| 175 | let value: Expr = match meta.value() { |
| 176 | Ok(value) => value.parse()?, |
| 177 | |
| 178 | // If the option is missing `=`, use a `true` literal. |
| 179 | Err(_) => Expr::Lit(syn::ExprLit { |
| 180 | lit: syn::LitBool::new(true, meta.path.span()).into(), |
| 181 | attrs: Vec::new(), |
| 182 | }), |
| 183 | }; |
| 184 | |
| 185 | bench_options.push((ident.clone(), value)); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | Ok(()) |
| 190 | }); |
| 191 | |
| 192 | match attr_parser.parse(tokens) { |
| 193 | Ok(()) => {} |
| 194 | Err(error) => return Err(error.into_compile_error().into()), |
| 195 | } |
| 196 | |
| 197 | let divan_crate = divan_crate.unwrap_or_else(|| syn::parse_quote!(::divan)); |
| 198 | let private_mod = quote! { #divan_crate::__private }; |
| 199 | |
| 200 | let counters = counters.iter().map(|(expr, type_name)| match type_name { |
| 201 | Some(type_name) => { |
| 202 | let type_name = Ident::new(type_name, proc_macro2::Span::call_site()); |
| 203 | quote! { |
| 204 | // We do a scoped import for the expression to override any |
| 205 | // local `From` trait. |
| 206 | { |
| 207 | use ::std::convert::From as _; |
| 208 | |
| 209 | #divan_crate::counter::#type_name::from(#expr) |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 | None => expr.to_token_stream(), |
| 214 | }); |
| 215 | |
| 216 | let counters = counters_ident |
| 217 | .map(|ident| { |
| 218 | quote! { |
| 219 | #ident: #private_mod::new_counter_set() #(.with(#counters))* , |
| 220 | } |
| 221 | }) |
| 222 | .unwrap_or_default(); |
| 223 | |
| 224 | Ok(Self { private_mod, name_expr, args_expr, generic, counters, bench_options }) |
| 225 | } |
| 226 | |
| 227 | /// Produces a function expression for creating `LazyLock<BenchOptions>`. |
| 228 | /// |
| 229 | /// If the `#[ignore]` attribute is specified, this be provided its |
| 230 | /// identifier to set `BenchOptions` using its span. Doing this instead of |
| 231 | /// creating the `ignore` identifier ourselves improves compiler error |
| 232 | /// diagnostics. |
| 233 | pub fn bench_options_fn( |
| 234 | &self, |
| 235 | ignore_attr_ident: Option<&syn::Path>, |
| 236 | ) -> proc_macro2::TokenStream { |
| 237 | fn is_lit_array(expr: &Expr) -> bool { |
| 238 | let Expr::Array(expr) = expr else { |
| 239 | return false; |
| 240 | }; |
| 241 | expr.elems.iter().all(|elem| matches!(elem, Expr::Lit { .. })) |
| 242 | } |
| 243 | |
| 244 | let private_mod = &self.private_mod; |
| 245 | let option_some = tokens::option_some(); |
| 246 | |
| 247 | // Directly set fields on `BenchOptions`. This simplifies things by: |
| 248 | // - Having a single source of truth |
| 249 | // - Making unknown options a compile error |
| 250 | // |
| 251 | // We use `..` (struct update syntax) to ensure that no option is set |
| 252 | // twice, even if raw identifiers are used. This also has the accidental |
| 253 | // benefit of Rust Analyzer recognizing fields and emitting suggestions |
| 254 | // with docs and type info. |
| 255 | if self.bench_options.is_empty() && self.counters.is_empty() && ignore_attr_ident.is_none() |
| 256 | { |
| 257 | tokens::option_none() |
| 258 | } else { |
| 259 | let options_iter = self.bench_options.iter().map(|(option, value)| { |
| 260 | let option_name = option.to_string(); |
| 261 | let option_name = option_name.strip_prefix("r#" ).unwrap_or(&option_name); |
| 262 | |
| 263 | let wrapped_value: proc_macro2::TokenStream; |
| 264 | let value: &dyn ToTokens = match option_name { |
| 265 | "threads" => { |
| 266 | wrapped_value = if is_lit_array(value) { |
| 267 | // If array of literals, just use `&[...]`. |
| 268 | quote! { ::std::borrow::Cow::Borrowed(&#value) } |
| 269 | } else { |
| 270 | quote! { #private_mod::IntoThreads::into_threads(#value) } |
| 271 | }; |
| 272 | |
| 273 | &wrapped_value |
| 274 | } |
| 275 | |
| 276 | // If the option is a `Duration`, use `IntoDuration` to be |
| 277 | // polymorphic over `Duration` or `u64`/`f64` seconds. |
| 278 | "min_time" | "max_time" => { |
| 279 | wrapped_value = |
| 280 | quote! { #private_mod::IntoDuration::into_duration(#value) }; |
| 281 | &wrapped_value |
| 282 | } |
| 283 | |
| 284 | _ => value, |
| 285 | }; |
| 286 | |
| 287 | quote! { #option: #option_some(#value), } |
| 288 | }); |
| 289 | |
| 290 | let ignore = match ignore_attr_ident { |
| 291 | Some(ignore_attr_ident) => quote! { #ignore_attr_ident: #option_some(true), }, |
| 292 | None => Default::default(), |
| 293 | }; |
| 294 | |
| 295 | let counters = &self.counters; |
| 296 | |
| 297 | quote! { |
| 298 | #option_some(::std::sync::LazyLock::new(|| { |
| 299 | #[allow(clippy::needless_update)] |
| 300 | #private_mod::BenchOptions { |
| 301 | #(#options_iter)* |
| 302 | |
| 303 | // Ignore comes after options so that options take |
| 304 | // priority in compiler error diagnostics. |
| 305 | #ignore |
| 306 | |
| 307 | #counters |
| 308 | |
| 309 | ..::std::default::Default::default() |
| 310 | } |
| 311 | })) |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | /// Options for generic functions. |
| 318 | #[derive (Default)] |
| 319 | pub struct GenericOptions { |
| 320 | /// Generic types over which to instantiate benchmark functions. |
| 321 | pub types: Option<GenericTypes>, |
| 322 | |
| 323 | /// `const` array/slice over which to instantiate benchmark functions. |
| 324 | pub consts: Option<Expr>, |
| 325 | } |
| 326 | |
| 327 | impl GenericOptions { |
| 328 | /// Returns `true` if set exclusively to either: |
| 329 | /// - `types = []` |
| 330 | /// - `consts = []` |
| 331 | pub fn is_empty(&self) -> bool { |
| 332 | match (&self.types, &self.consts) { |
| 333 | (Some(types: &GenericTypes), None) => types.is_empty(), |
| 334 | (None, Some(Expr::Array(consts: &ExprArray))) => consts.elems.is_empty(), |
| 335 | _ => false, |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | /// Returns an iterator of multiple `Some` for types, or a single `None` if |
| 340 | /// there are no types. |
| 341 | pub fn types_iter(&self) -> Box<dyn Iterator<Item = Option<&dyn ToTokens>> + '_> { |
| 342 | match &self.types { |
| 343 | None => Box::new(std::iter::once(None)), |
| 344 | Some(GenericTypes::List(types: &Vec)) => { |
| 345 | Box::new(types.iter().map(|t: &TokenStream| Some(t as &dyn ToTokens))) |
| 346 | } |
| 347 | } |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | /// Generic types over which to instantiate benchmark functions. |
| 352 | pub enum GenericTypes { |
| 353 | /// List of types, e.g. `[i32, String, ()]`. |
| 354 | List(Vec<proc_macro2::TokenStream>), |
| 355 | } |
| 356 | |
| 357 | impl Parse for GenericTypes { |
| 358 | fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { |
| 359 | let content: ParseBuffer<'_>; |
| 360 | syn::bracketed!(content in input); |
| 361 | |
| 362 | Ok(Self::List( |
| 363 | contentimpl Iterator |
| 364 | .parse_terminated(parser:Type::parse, separator:Token![,])? |
| 365 | .into_iter() |
| 366 | .map(|ty: Type| ty.into_token_stream()) |
| 367 | .collect(), |
| 368 | )) |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | impl GenericTypes { |
| 373 | pub fn is_empty(&self) -> bool { |
| 374 | match self { |
| 375 | Self::List(list: &Vec) => list.is_empty(), |
| 376 | } |
| 377 | } |
| 378 | } |
| 379 | |