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