benchy_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{LitStr, Token};
4
5/// Parses the inner part of main!("name", fn1, fn2) or main!(fn1, fn2)
6struct MainArgs {
7    name: Option<String>,
8    benchmarks: Vec<syn::Path>,
9}
10
11impl syn::parse::Parse for MainArgs {
12    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
13        let name = if input.peek(LitStr) {
14            Some(input.parse::<LitStr>()?.value())
15        } else {
16            None
17        };
18
19        if name.is_some() && !input.is_empty() {
20            input.parse::<Token![,]>()?;
21        }
22
23        let mut benchmarks = Vec::new();
24        while !input.is_empty() {
25            let benchmark = input.parse::<syn::Path>()?;
26            benchmarks.push(benchmark);
27            if input.is_empty() {
28                break;
29            }
30            input.parse::<syn::Token![,]>()?;
31        }
32
33        Ok(MainArgs { name, benchmarks })
34    }
35}
36
37#[proc_macro]
38pub fn main(item: TokenStream) -> TokenStream {
39    let main_args = match syn::parse::<MainArgs>(item) {
40        Ok(args) => args,
41        Err(err) => return err.to_compile_error().into(),
42    };
43
44    expand_main(main_args)
45        .unwrap_or_else(|err| err.to_compile_error())
46        .into()
47}
48
49fn expand_main(MainArgs { name, benchmarks }: MainArgs) -> syn::Result<proc_macro2::TokenStream> {
50    let mut bench_runs = Vec::new();
51    for mut bench in benchmarks {
52        let bench_struct_path = {
53            // Turn `add` to `Benchmark_add` and `adder::add` to `adder::Benchmark_add`
54            let last_segment = bench.segments.last().unwrap();
55            bench.segments.last_mut().unwrap().ident = syn::Ident::new(
56                &format!("Benchmark_{}", last_segment.ident),
57                last_segment.ident.span(),
58            );
59            bench
60        };
61
62        bench_runs.push(quote! {
63            {
64                let name = #bench_struct_path::name();
65                if let Some(params) = #bench_struct_path::params() {
66                    let (param_names, params): (Vec<String>, Vec<_>) = params.into_iter().unzip();
67                    let params = param_names.iter().map(|n| n.as_str()).zip(params.into_iter()).collect::<Vec<_>>();
68                    bench.benchmark_with(&name, params, |b, p| {
69                        #bench_struct_path::run(b, p.clone());
70                    });
71                } else {
72                    bench.benchmark(&name, None, |b| {
73                        #bench_struct_path::run_without_param(b);
74                    });
75                }
76            }
77        });
78    }
79
80    let name = match name {
81        Some(name) => syn::LitStr::new(&name, proc_macro2::Span::call_site()).into_token_stream(),
82        None => quote! {
83            core::module_path!()
84        },
85    };
86
87    Ok(quote! {
88        fn main() {
89            extern crate benchy as _benchy;
90            use _benchy::BenchmarkFn;
91
92            let mut bench = _benchy::Benchmark::from_env(#name);
93
94            #(#bench_runs)*
95
96            bench.output();
97        }
98    })
99}
100
101/// Parses the inner part of:
102/// - `#[benchmark!("name", [("param1", param1), ("param2", param2)])`
103/// - or `#benchmark([("param1", param1), ("param2", param2)])`
104/// - or `#[benchmark("name")]`
105/// - or `#[benchmark]`
106struct BenchMarkArgs {
107    name: Option<String>,
108    params: Option<syn::Expr>,
109}
110
111impl syn::parse::Parse for BenchMarkArgs {
112    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
113        let name = match input.parse::<syn::LitStr>() {
114            Ok(lit) => Some(lit.value()),
115            Err(_) => None,
116        };
117
118        let params = if !input.is_empty() {
119            if name.is_some() {
120                input.parse::<syn::Token![,]>()?;
121            }
122
123            Some(input.parse::<syn::Expr>()?)
124        } else {
125            None
126        };
127
128        if !input.is_empty() {
129            return Err(input.error("Unexpected input"));
130        }
131
132        Ok(BenchMarkArgs { name, params })
133    }
134}
135
136#[proc_macro_attribute]
137pub fn benchmark(attr: TokenStream, item: TokenStream) -> TokenStream {
138    expand_benchmark(attr, item)
139        .unwrap_or_else(|err| err.to_compile_error())
140        .into()
141}
142
143fn expand_benchmark(attr: TokenStream, item: TokenStream) -> syn::Result<proc_macro2::TokenStream> {
144    let attr = proc_macro2::TokenStream::from(attr);
145    let BenchMarkArgs { name, params } = syn::parse::<BenchMarkArgs>(attr.into())?;
146    let item_fn = syn::parse::<syn::ItemFn>(item)?;
147    let fn_vis = &item_fn.vis;
148    let fn_name = &item_fn.sig.ident;
149
150    let (param_type, takes_param) = match item_fn.sig.inputs.iter().nth(1) {
151        None => (quote! { () }, false),
152        Some(syn::FnArg::Typed(pat_type)) => (pat_type.ty.to_token_stream(), true),
153        Some(_) => {
154            return Err(syn::Error::new_spanned(
155                item_fn.sig.inputs,
156                "Expected second parameter to be a typed pattern",
157            ));
158        }
159    };
160
161    let bench_struct_name = syn::Ident::new(&format!("Benchmark_{}", fn_name), fn_name.span());
162
163    let params = match takes_param {
164        true => {
165            quote! { Some(#params.into_iter().map(|(name, value)| (name.into(), value)).collect()) }
166        }
167        false => quote! { None },
168    };
169
170    let name_impl = match name {
171        Some(name) => quote! { #name.to_owned() },
172        None => quote! { stringify!(#fn_name).to_owned() },
173    };
174
175    let run_impl = match takes_param {
176        true => quote! {
177            fn run(b: &mut _benchy::BenchmarkRun, p: Self::ParamType) {
178                #fn_name(b, p);
179            }
180        },
181        false => quote! {
182            fn run_without_param(b: &mut _benchy::BenchmarkRun) {
183                #fn_name(b);
184            }
185        },
186    };
187
188    Ok(quote! {
189        #[allow(non_camel_case_types)]
190        #fn_vis struct #bench_struct_name;
191
192        #[allow(non_upper_case_globals)]
193        const _: () = {
194            extern crate benchy as _benchy;
195
196            #[automatically_derived]
197            impl _benchy::BenchmarkFn for #bench_struct_name {
198                type ParamType = #param_type;
199
200                fn name() -> String {
201                    #name_impl
202                }
203
204                fn params() -> Option<Vec<(String, Self::ParamType)>> {
205                    #params
206                }
207
208                #run_impl
209            }
210        };
211
212        #item_fn
213    })
214}