divan_macros/
lib.rs

1//! Macros for [Divan](https://github.com/nvzqz/divan), a statistically-comfy
2//! benchmarking library brought to you by [Nikolai Vazquez](https://hachyderm.io/@nikolai).
3//!
4//! See [`divan`](https://docs.rs/divan) crate for documentation.
5
6use proc_macro::TokenStream;
7use quote::{quote, ToTokens};
8
9mod attr_options;
10mod tokens;
11
12use attr_options::*;
13use syn::{Expr, FnArg};
14
15#[derive(Clone, Copy)]
16enum Macro<'a> {
17    Bench { fn_sig: &'a syn::Signature },
18    BenchGroup,
19}
20
21impl Macro<'_> {
22    fn name(&self) -> &'static str {
23        match self {
24            Self::Bench { .. } => "bench",
25            Self::BenchGroup => "bench_group",
26        }
27    }
28}
29
30/// Lists of comma-separated `#[cfg]` parameters.
31mod systems {
32    use super::*;
33
34    pub fn elf() -> proc_macro2::TokenStream {
35        quote! {
36            target_os = "android",
37            target_os = "dragonfly",
38            target_os = "freebsd",
39            target_os = "fuchsia",
40            target_os = "haiku",
41            target_os = "illumos",
42            target_os = "linux",
43            target_os = "netbsd",
44            target_os = "openbsd"
45        }
46    }
47
48    pub fn mach_o() -> proc_macro2::TokenStream {
49        quote! {
50            target_os = "ios",
51            target_os = "macos",
52            target_os = "tvos",
53            target_os = "watchos"
54        }
55    }
56}
57
58/// Attributes applied to a `static` containing a pointer to a function to run
59/// before `main`.
60fn pre_main_attrs() -> proc_macro2::TokenStream {
61    let elf = systems::elf();
62    let mach_o = systems::mach_o();
63
64    quote! {
65        #[used]
66        #[cfg_attr(windows, link_section = ".CRT$XCU")]
67        #[cfg_attr(any(#elf), link_section = ".init_array")]
68        #[cfg_attr(any(#mach_o), link_section = "__DATA,__mod_init_func,mod_init_funcs")]
69    }
70}
71
72fn unsupported_error(attr_name: &str) -> proc_macro2::TokenStream {
73    let elf = systems::elf();
74    let mach_o = systems::mach_o();
75
76    let error = format!("Unsupported target OS for `#[divan::{attr_name}]`");
77
78    quote! {
79        #[cfg(not(any(windows, #elf, #mach_o)))]
80        ::std::compile_error!(#error);
81    }
82}
83
84#[proc_macro_attribute]
85pub fn bench(options: TokenStream, item: TokenStream) -> TokenStream {
86    let option_none = tokens::option_none();
87    let option_some = tokens::option_some();
88
89    let fn_item = item.clone();
90    let fn_item = syn::parse_macro_input!(fn_item as syn::ItemFn);
91    let fn_sig = &fn_item.sig;
92
93    let attr = Macro::Bench { fn_sig };
94    let attr_name = attr.name();
95
96    let options = match AttrOptions::parse(options, attr) {
97        Ok(options) => options,
98        Err(compile_error) => return compile_error,
99    };
100
101    // Items needed by generated code.
102    let AttrOptions { private_mod, .. } = &options;
103
104    let fn_ident = &fn_sig.ident;
105    let fn_name = fn_ident.to_string();
106    let fn_name_pretty = fn_name.strip_prefix("r#").unwrap_or(&fn_name);
107
108    // Find any `#[ignore]` attribute so that we can use its span to help
109    // compiler diagnostics.
110    let ignore_attr_ident =
111        fn_item.attrs.iter().map(|attr| attr.meta.path()).find(|path| path.is_ident("ignore"));
112
113    // If the function is `extern "ABI"`, it is wrapped in a Rust-ABI function.
114    let is_extern_abi = fn_sig.abi.is_some();
115
116    let fn_args = &fn_sig.inputs;
117
118    let type_param: Option<(usize, &syn::TypeParam)> = fn_sig
119        .generics
120        .params
121        .iter()
122        .enumerate()
123        .filter_map(|(i, param)| match param {
124            syn::GenericParam::Type(param) => Some((i, param)),
125            _ => None,
126        })
127        .next();
128
129    let const_param: Option<(usize, &syn::ConstParam)> = fn_sig
130        .generics
131        .params
132        .iter()
133        .enumerate()
134        .filter_map(|(i, param)| match param {
135            syn::GenericParam::Const(param) => Some((i, param)),
136            _ => None,
137        })
138        .next();
139
140    let is_type_before_const = match (type_param, const_param) {
141        (Some((t, _)), Some((c, _))) => t < c,
142        _ => false,
143    };
144
145    // Prefixed with "__" to prevent IDEs from recommending using this symbol.
146    //
147    // The static is local to intentionally cause a compile error if this
148    // attribute is used multiple times on the same function.
149    let static_ident = syn::Ident::new(
150        &format!("__DIVAN_BENCH_{}", fn_name_pretty.to_uppercase()),
151        fn_ident.span(),
152    );
153
154    let meta = entry_meta_expr(&fn_name, &options, ignore_attr_ident);
155
156    let bench_entry_runner = quote! { #private_mod::BenchEntryRunner };
157
158    // Creates a `__DIVAN_ARGS` global variable to be used in the entry.
159    let bench_args_global = if options.args_expr.is_some() {
160        quote! {
161            static __DIVAN_ARGS: #private_mod::BenchArgs = #private_mod::BenchArgs::new();
162        }
163    } else {
164        Default::default()
165    };
166
167    // The last argument type is used as the only `args` item type because we
168    // currently only support one runtime argument.
169    let last_arg_type = if options.args_expr.is_some() {
170        fn_args.last().map(|arg| match arg {
171            FnArg::Receiver(arg) => &*arg.ty,
172            FnArg::Typed(arg) => &*arg.ty,
173        })
174    } else {
175        None
176    };
177
178    let last_arg_type_tokens = last_arg_type
179        .map(|ty| match ty {
180            // Remove lifetime from references to not use the lifetime outside
181            // of its declaration. This allows benchmarks to take arguments with
182            // lifetimes.
183            syn::Type::Reference(ty) if ty.lifetime.is_some() => {
184                let mut ty = ty.clone();
185                ty.lifetime = None;
186                ty.to_token_stream()
187            }
188
189            _ => ty.to_token_stream(),
190        })
191        .unwrap_or_default();
192
193    // Some argument literals need an explicit type.
194    let arg_return_tokens = options
195        .args_expr
196        .as_ref()
197        .map(|args| match args {
198            // Empty array.
199            Expr::Array(args) if args.elems.is_empty() => quote! {
200                -> [#last_arg_type_tokens; 0]
201            },
202
203            _ => Default::default(),
204        })
205        .unwrap_or_default();
206
207    // Creates a function expr for the benchmarking function, optionally
208    // monomorphized with generic parameters.
209    let make_bench_fn = |generics: &[&dyn ToTokens]| {
210        let mut fn_expr = if generics.is_empty() {
211            // Use identifier as-is.
212            fn_ident.to_token_stream()
213        } else {
214            // Apply generic arguments.
215            quote! { #fn_ident::< #(#generics),* > }
216        };
217
218        // Handle function arguments.
219        match (fn_args.len(), &options.args_expr) {
220            // Simple benchmark with no arguments provided.
221            (0, None) => {
222                // Wrap in Rust ABI.
223                if is_extern_abi {
224                    fn_expr = quote! { || #fn_expr() };
225                }
226
227                quote! {
228                    #bench_entry_runner::Plain(|divan /* Bencher */| divan.bench(#fn_expr))
229                }
230            }
231
232            // `args` option used without function arguments; handled earlier in
233            // `AttrOptions::parse`.
234            (0, Some(_)) => unreachable!(),
235
236            // `Bencher` function argument.
237            (1, None) => {
238                // Wrap in Rust ABI.
239                if is_extern_abi {
240                    fn_expr = quote! { |divan /* Bencher */| #fn_expr(divan) };
241                }
242
243                quote! { #bench_entry_runner::Plain(#fn_expr) }
244            }
245
246            // Function argument comes from `args` option.
247            (1, Some(args)) => quote! {
248                #bench_entry_runner::Args(|| __DIVAN_ARGS.runner(
249                    || #arg_return_tokens { #args },
250
251                    |arg| #private_mod::ToStringHelper(arg).to_string(),
252
253                    |divan, __divan_arg| divan.bench(|| #fn_expr(
254                        #private_mod::Arg::<#last_arg_type_tokens>::get(__divan_arg)
255                    )),
256                ))
257            },
258
259            // `Bencher` and `args` option function arguments.
260            (2, Some(args)) => quote! {
261                #bench_entry_runner::Args(|| __DIVAN_ARGS.runner(
262                    || #arg_return_tokens { #args },
263
264                    |arg| #private_mod::ToStringHelper(arg).to_string(),
265
266                    |divan, __divan_arg| #fn_expr(
267                        divan,
268                        #private_mod::Arg::<#last_arg_type_tokens>::get(__divan_arg),
269                    ),
270                ))
271            },
272
273            // Ensure `args` is set if arguments are provided after `Bencher`.
274            (_, None) => quote! {
275                ::std::compile_error!(::std::concat!(
276                    "expected 'args' option containing '",
277                    ::std::stringify!(#last_arg_type_tokens),
278                    "'",
279                ))
280            },
281
282            // `args` option used with unsupported number of arguments; handled
283            // earlier in `AttrOptions::parse`.
284            (_, Some(_)) => unreachable!(),
285        }
286    };
287
288    let pre_main_attrs = pre_main_attrs();
289    let unsupported_error = unsupported_error(attr_name);
290
291    // Creates a `GroupEntry` static for generic benchmarks.
292    let make_generic_group = |generic_benches: proc_macro2::TokenStream| {
293        let entry = quote! {
294            #private_mod::GroupEntry {
295                meta: #meta,
296                generic_benches: #option_some({ #generic_benches }),
297            }
298        };
299
300        quote! {
301            #unsupported_error
302
303            // Push this static into `GROUP_ENTRIES` before `main` is called.
304            static #static_ident: #private_mod::GroupEntry = {
305                {
306                    // Add `push` to the initializer section.
307                    #pre_main_attrs
308                    static PUSH: extern "C" fn() = push;
309
310                    extern "C" fn push() {
311                        static NODE: #private_mod::EntryList<#private_mod::GroupEntry>
312                            = #private_mod::EntryList::new(&#static_ident);
313
314                        #private_mod::GROUP_ENTRIES.push(&NODE);
315                    }
316                }
317
318                // All generic entries share the same `BenchArgs` instance for
319                // efficiency and to ensure all entries use the same values, or
320                // at least the same names in the case of interior mutability.
321                #bench_args_global
322
323                #entry
324            };
325        }
326    };
327
328    // Creates a `GenericBenchEntry` expr for a generic benchmark instance.
329    let make_generic_bench_entry =
330        |ty: Option<&dyn ToTokens>, const_value: Option<&dyn ToTokens>| {
331            let generic_const_value = const_value.map(|const_value| quote!({ #const_value }));
332
333            let generics: Vec<&dyn ToTokens> = {
334                let mut generics = Vec::new();
335
336                generics.extend(generic_const_value.as_ref().map(|t| t as &dyn ToTokens));
337                generics.extend(ty);
338
339                if is_type_before_const {
340                    generics.reverse();
341                }
342
343                generics
344            };
345
346            let bench_fn = make_bench_fn(&generics);
347
348            let type_value = match ty {
349                Some(ty) => quote! {
350                    #option_some(#private_mod::EntryType::new::<#ty>())
351                },
352                None => option_none.clone(),
353            };
354
355            let const_value = match const_value {
356                Some(const_value) => quote! {
357                    #option_some(#private_mod::EntryConst::new(&#const_value))
358                },
359                None => option_none.clone(),
360            };
361
362            quote! {
363                #private_mod::GenericBenchEntry {
364                    group: &#static_ident,
365                    bench: #bench_fn,
366                    ty: #type_value,
367                    const_value: #const_value,
368                }
369            }
370        };
371
372    let generated_items: proc_macro2::TokenStream = match &options.generic.consts {
373        // Only specified `types = []` or `consts = []`; generate nothing.
374        _ if options.generic.is_empty() => Default::default(),
375
376        None => match &options.generic.types {
377            // No generics; generate a simple benchmark entry.
378            None => {
379                let bench_fn = make_bench_fn(&[]);
380
381                let entry = quote! {
382                    #private_mod::BenchEntry {
383                        meta: #meta,
384                        bench: #bench_fn,
385                    }
386                };
387
388                quote! {
389                    // Push this static into `BENCH_ENTRIES` before `main` is
390                    // called.
391                    static #static_ident: #private_mod::BenchEntry = {
392                        {
393                            // Add `push` to the initializer section.
394                            #pre_main_attrs
395                            static PUSH: extern "C" fn() = push;
396
397                            extern "C" fn push() {
398                                static NODE: #private_mod::EntryList<#private_mod::BenchEntry>
399                                    = #private_mod::EntryList::new(&#static_ident);
400
401                                #private_mod::BENCH_ENTRIES.push(&NODE);
402                            }
403                        }
404
405                        #bench_args_global
406
407                        #entry
408                    };
409                }
410            }
411
412            // Generate a benchmark group entry with generic benchmark entries.
413            Some(GenericTypes::List(generic_types)) => {
414                let generic_benches =
415                    generic_types.iter().map(|ty| make_generic_bench_entry(Some(&ty), None));
416
417                make_generic_group(quote! {
418                    &[&[#(#generic_benches),*]]
419                })
420            }
421        },
422
423        // Generate a benchmark group entry with generic benchmark entries.
424        Some(Expr::Array(generic_consts)) => {
425            let consts_count = generic_consts.elems.len();
426            let const_type = &const_param.unwrap().1.ty;
427
428            let generic_benches = options.generic.types_iter().map(|ty| {
429                let generic_benches = (0..consts_count).map(move |i| {
430                    let const_value = quote! { __DIVAN_CONSTS[#i] };
431                    make_generic_bench_entry(ty, Some(&const_value))
432                });
433
434                // `static` is necessary because `EntryConst` uses interior
435                // mutability to cache the `ToString` result.
436                quote! {
437                    static __DIVAN_GENERIC_BENCHES: [#private_mod::GenericBenchEntry; #consts_count] = [#(#generic_benches),*];
438                    &__DIVAN_GENERIC_BENCHES
439                }
440            });
441
442            make_generic_group(quote! {
443                // We refer to our own slice because it:
444                // - Type-checks values, even if `generic_benches` is empty
445                //   because the user set `types = []`
446                // - Prevents re-computing constants, which can slightly improve
447                //   compile time given that Miri is slow
448                const __DIVAN_CONSTS: &[#const_type] = &#generic_consts;
449
450                &[#({ #generic_benches }),*]
451            })
452        }
453
454        // Generate a benchmark group entry with generic benchmark entries over
455        // an expression of constants.
456        //
457        // This is limited to a maximum of 20 because we need some constant to
458        // instantiate each function instance.
459        Some(generic_consts) => {
460            // The maximum number of elements for non-array expressions.
461            const MAX_EXTERN_COUNT: usize = 20;
462
463            let const_type = &const_param.unwrap().1.ty;
464
465            let generic_benches = options.generic.types_iter().map(|ty| {
466                let generic_benches = (0..MAX_EXTERN_COUNT).map(move |i| {
467                    let const_value = quote! {
468                        // Fallback to the first constant if out of bounds.
469                        __DIVAN_CONSTS[if #i < __DIVAN_CONST_COUNT { #i } else { 0 }]
470                    };
471                    make_generic_bench_entry(ty, Some(&const_value))
472                });
473
474                // `static` is necessary because `EntryConst` uses interior
475                // mutability to cache the `ToString` result.
476                quote! {
477                    static __DIVAN_GENERIC_BENCHES: [#private_mod::GenericBenchEntry; __DIVAN_CONST_COUNT]
478                        = match #private_mod::shrink_array([#(#generic_benches),*]) {
479                            Some(array) => array,
480                            _ => panic!("external 'consts' cannot contain more than 20 values"),
481                        };
482
483                    &__DIVAN_GENERIC_BENCHES
484                }
485            });
486
487            make_generic_group(quote! {
488                const __DIVAN_CONST_COUNT: usize = __DIVAN_CONSTS.len();
489                const __DIVAN_CONSTS: &[#const_type] = &#generic_consts;
490
491                &[#({ #generic_benches }),*]
492            })
493        }
494    };
495
496    // Append our generated code to the existing token stream.
497    let mut result = item;
498    result.extend(TokenStream::from(generated_items));
499    result
500}
501
502#[proc_macro_attribute]
503pub fn bench_group(options: TokenStream, item: TokenStream) -> TokenStream {
504    let attr = Macro::BenchGroup;
505    let attr_name = attr.name();
506
507    let options = match AttrOptions::parse(options, attr) {
508        Ok(options) => options,
509        Err(compile_error) => return compile_error,
510    };
511
512    // Items needed by generated code.
513    let AttrOptions { private_mod, .. } = &options;
514
515    let option_none = tokens::option_none();
516
517    // TODO: Make module parsing cheaper by parsing only the necessary parts.
518    let mod_item = item.clone();
519    let mod_item = syn::parse_macro_input!(mod_item as syn::ItemMod);
520
521    let mod_ident = &mod_item.ident;
522    let mod_name = mod_ident.to_string();
523    let mod_name_pretty = mod_name.strip_prefix("r#").unwrap_or(&mod_name);
524
525    // Find any `#[ignore]` attribute so that we can use its span to help
526    // compiler diagnostics.
527    //
528    // TODO: Fix `unused_attributes` warning when using `#[ignore]` on a module.
529    let ignore_attr_ident =
530        mod_item.attrs.iter().map(|attr| attr.meta.path()).find(|path| path.is_ident("ignore"));
531
532    // Prefixed with "__" to prevent IDEs from recommending using this symbol.
533    //
534    // By having the static be local, we cause a compile error if this attribute
535    // is used multiple times on the same function.
536    let static_ident = syn::Ident::new(
537        &format!("__DIVAN_GROUP_{}", mod_name_pretty.to_uppercase()),
538        mod_ident.span(),
539    );
540
541    let meta = entry_meta_expr(&mod_name, &options, ignore_attr_ident);
542
543    let pre_main_attrs = pre_main_attrs();
544    let unsupported_error = unsupported_error(attr_name);
545
546    let generated_items = quote! {
547        #unsupported_error
548
549        // Push this static into `GROUP_ENTRIES` before `main` is called.
550        static #static_ident: #private_mod::EntryList<#private_mod::GroupEntry> = {
551            {
552                // Add `push` to the initializer section.
553                #pre_main_attrs
554                static PUSH: extern "C" fn() = push;
555
556                extern "C" fn push() {
557                    #private_mod::GROUP_ENTRIES.push(&#static_ident);
558                }
559            }
560
561            #private_mod::EntryList::new({
562                static #static_ident: #private_mod::GroupEntry = #private_mod::GroupEntry {
563                    meta: #meta,
564                    generic_benches: #option_none,
565                };
566
567                &#static_ident
568            })
569        };
570    };
571
572    // Append our generated code to the existing token stream.
573    let mut result = item;
574    result.extend(TokenStream::from(generated_items));
575    result
576}
577
578/// Constructs an `EntryMeta` expression.
579fn entry_meta_expr(
580    raw_name: &str,
581    options: &AttrOptions,
582    ignore_attr_ident: Option<&syn::Path>,
583) -> proc_macro2::TokenStream {
584    let AttrOptions { private_mod, .. } = &options;
585
586    let raw_name_pretty = raw_name.strip_prefix("r#").unwrap_or(raw_name);
587
588    let display_name: &dyn ToTokens = match &options.name_expr {
589        Some(name) => name,
590        None => &raw_name_pretty,
591    };
592
593    let bench_options = options.bench_options_fn(ignore_attr_ident);
594
595    quote! {
596        #private_mod::EntryMeta {
597            raw_name: #raw_name,
598            display_name: #display_name,
599            bench_options: #bench_options,
600            module_path: ::std::module_path!(),
601
602            // `Span` location info is nightly-only, so use macros.
603            location: #private_mod::EntryLocation {
604                file: ::std::file!(),
605                line: ::std::line!(),
606                col: ::std::column!(),
607            },
608        }
609    }
610}