use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::{parse_macro_input, ImplItem, Item, ItemFn, LitInt, LitStr};
#[derive(Clone, Copy)]
pub(crate) enum Format {
Table,
Json,
JsonPretty,
}
impl Format {
pub(crate) fn to_tokens(self) -> proc_macro2::TokenStream {
match self {
Format::Table => quote!(hotpath::Format::Table),
Format::Json => quote!(hotpath::Format::Json),
Format::JsonPretty => quote!(hotpath::Format::JsonPretty),
}
}
}
pub fn main_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let mut percentiles: Vec<u8> = vec![95];
let mut format = Format::Table;
let mut limit: usize = 15;
let mut timeout: Option<u64> = None;
if !attr.is_empty() {
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("percentiles") {
meta.input.parse::<syn::Token![=]>()?;
let content;
syn::bracketed!(content in meta.input);
let mut vals = Vec::new();
while !content.is_empty() {
let li: LitInt = content.parse()?;
let v: u8 = li.base10_parse()?;
if !(0..=100).contains(&v) {
return Err(
meta.error(format!("Invalid percentile {} (must be 0..=100)", v))
);
}
vals.push(v);
if !content.is_empty() {
content.parse::<syn::Token![,]>()?;
}
}
if vals.is_empty() {
return Err(meta.error("At least one percentile must be specified"));
}
percentiles = vals;
return Ok(());
}
if meta.path.is_ident("format") {
meta.input.parse::<syn::Token![=]>()?;
let lit: LitStr = meta.input.parse()?;
format =
match lit.value().as_str() {
"table" => Format::Table,
"json" => Format::Json,
"json-pretty" => Format::JsonPretty,
other => return Err(meta.error(format!(
"Unknown format {:?}. Expected one of: \"table\", \"json\", \"json-pretty\"",
other
))),
};
return Ok(());
}
if meta.path.is_ident("limit") {
meta.input.parse::<syn::Token![=]>()?;
let li: LitInt = meta.input.parse()?;
limit = li.base10_parse()?;
return Ok(());
}
if meta.path.is_ident("timeout") {
meta.input.parse::<syn::Token![=]>()?;
let li: LitInt = meta.input.parse()?;
timeout = Some(li.base10_parse()?);
return Ok(());
}
Err(meta.error(
"Unknown parameter. Supported: percentiles=[..], format=\"..\", limit=N, timeout=N",
))
});
if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
return e.to_compile_error().into();
}
}
let percentiles_array = quote! { &[#(#percentiles),*] };
let format_token = format.to_tokens();
let asyncness = sig.asyncness.is_some();
let fn_name = &sig.ident;
let base_builder = quote! {
let caller_name: &'static str =
concat!(module_path!(), "::", stringify!(#fn_name));
hotpath::FunctionsGuardBuilder::new(caller_name)
.percentiles(#percentiles_array)
.limit(#limit)
.format(#format_token)
};
let guard_init = if let Some(timeout_ms) = timeout {
quote! {
let _hotpath = {
#base_builder
.build_with_timeout(std::time::Duration::from_millis(#timeout_ms))
};
}
} else {
quote! {
let _hotpath = {
#base_builder.build()
};
}
};
let body = quote! {
#guard_init
#block
};
let wrapped_body = if asyncness {
quote! { async { #body }.await }
} else {
body
};
let output = quote! {
#vis #sig {
#wrapped_body
}
};
output.into()
}
pub fn measure_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let name = sig.ident.to_string();
let asyncness = sig.asyncness.is_some();
let mut log_result = false;
if !attr.is_empty() {
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("log") {
meta.input.parse::<syn::Token![=]>()?;
let lit: syn::LitBool = meta.input.parse()?;
log_result = lit.value();
return Ok(());
}
Err(meta.error("Unknown parameter. Supported: log = true"))
});
if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
return e.to_compile_error().into();
}
}
let wrapped = if log_result {
let loc = quote! { concat!(module_path!(), "::", #name) };
if asyncness {
quote! {
hotpath::functions::measure_with_log_async(#loc, || async #block).await
}
} else {
quote! {
hotpath::functions::measure_with_log(#loc, false, false, || #block)
}
}
} else {
let guard_init = quote! {
let _guard = hotpath::functions::MeasurementGuard::build(
concat!(module_path!(), "::", #name),
false,
#asyncness
);
#block
};
if asyncness {
quote! { async { #guard_init }.await }
} else {
guard_init
}
};
let output = quote! {
#(#attrs)*
#vis #sig {
#wrapped
}
};
output.into()
}
pub fn future_fn_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
if sig.asyncness.is_none() {
return syn::Error::new_spanned(
sig.fn_token,
"The #[future_fn] attribute can only be applied to async functions",
)
.to_compile_error()
.into();
}
let mut log_result = false;
if !attr.is_empty() {
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("log") {
meta.input.parse::<syn::Token![=]>()?;
let lit: syn::LitBool = meta.input.parse()?;
log_result = lit.value();
return Ok(());
}
Err(meta.error("Unknown parameter. Supported: log = true"))
});
if let Err(e) = parser.parse2(proc_macro2::TokenStream::from(attr)) {
return e.to_compile_error().into();
}
}
let fn_name = &sig.ident;
let wrapped_body = if log_result {
quote! {
{
const FUTURE_LOC: &'static str = concat!(module_path!(), "::", stringify!(#fn_name));
hotpath::futures::init_futures_state();
hotpath::InstrumentFutureLog::instrument_future_log(
async #block,
FUTURE_LOC
).await
}
}
} else {
quote! {
{
const FUTURE_LOC: &'static str = concat!(module_path!(), "::", stringify!(#fn_name));
hotpath::futures::init_futures_state();
hotpath::InstrumentFuture::instrument_future(
async #block,
FUTURE_LOC
).await
}
}
};
let output = quote! {
#(#attrs)*
#vis #sig {
#wrapped_body
}
};
output.into()
}
pub fn skip_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
pub fn measure_all_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
let parsed_item = parse_macro_input!(item as Item);
match parsed_item {
Item::Mod(mut module) => {
if let Some((_brace, items)) = &mut module.content {
for it in items.iter_mut() {
if let Item::Fn(func) = it {
if !has_hotpath_skip_or_measure(&func.attrs) {
let func_tokens = TokenStream::from(quote!(#func));
let transformed = measure_impl(TokenStream::new(), func_tokens);
*func = syn::parse_macro_input!(transformed as ItemFn);
}
}
}
}
TokenStream::from(quote!(#module))
}
Item::Impl(mut impl_block) => {
for item in impl_block.items.iter_mut() {
if let ImplItem::Fn(method) = item {
if !has_hotpath_skip_or_measure(&method.attrs) {
let func_tokens = TokenStream::from(quote!(#method));
let transformed = measure_impl(TokenStream::new(), func_tokens);
*method = syn::parse_macro_input!(transformed as syn::ImplItemFn);
}
}
}
TokenStream::from(quote!(#impl_block))
}
_ => panic!("measure_all can only be applied to modules or impl blocks"),
}
}
fn has_hotpath_skip_or_measure(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
let path = attr.path();
if path.segments.len() == 2
&& path.segments[0].ident == "hotpath"
&& path.segments[1].ident == "skip"
{
return true;
}
if path.segments.len() == 2
&& path.segments[0].ident == "hotpath"
&& path.segments[1].ident == "measure"
{
return true;
}
if path.is_ident("cfg_attr") {
let attr_str = quote!(#attr).to_string();
if attr_str.contains("hotpath")
&& (attr_str.contains("skip") || attr_str.contains("measure"))
{
return true;
}
}
false
})
}