#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::arithmetic_side_effects)]
extern crate alloc;
use {
alloc::{format, string::ToString, vec::Vec},
proc_macro::TokenStream,
quote::quote,
regex::Regex,
syn::{
parse::{Parse, ParseStream},
parse_macro_input, parse_str,
punctuated::Punctuated,
Error, Expr, ItemFn, LitInt, LitStr, Token,
},
};
const DEFAULT_BUFFER_SIZE: &str = "200";
struct LogArgs {
buffer_len: LitInt,
format_string: LitStr,
args: Punctuated<Expr, Token![,]>,
}
impl Parse for LogArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let buffer_len = if input.peek(LitInt) {
let literal = input.parse()?;
input.parse::<Token![,]>()?;
literal
} else {
parse_str::<LitInt>(DEFAULT_BUFFER_SIZE)?
};
let format_string = input.parse()?;
let args = if input.is_empty() {
Punctuated::new()
} else {
input.parse::<Token![,]>()?;
Punctuated::parse_terminated(input)?
};
Ok(LogArgs {
buffer_len,
format_string,
args,
})
}
}
#[proc_macro]
pub fn log(input: TokenStream) -> TokenStream {
let LogArgs {
buffer_len,
format_string,
args,
} = parse_macro_input!(input as LogArgs);
let parsed_string = format_string.value();
let placeholder_regex = Regex::new(r"\{.*?\}").unwrap();
let placeholders: Vec<_> = placeholder_regex
.find_iter(&parsed_string)
.map(|m| m.as_str())
.collect();
if placeholders.len() != args.len() {
let arg_message = if args.is_empty() {
"but no arguments were given".to_string()
} else {
format!(
"but there is {} {}",
args.len(),
if args.len() == 1 {
"argument"
} else {
"arguments"
}
)
};
return Error::new_spanned(
format_string,
format!(
"{} positional arguments in format string, {}",
placeholders.len(),
arg_message
),
)
.to_compile_error()
.into();
}
if !placeholders.is_empty() {
let mut replaced_parts = Vec::new();
let parts: Vec<&str> = placeholder_regex.split(&parsed_string).collect();
let part_iter = parts.iter();
let mut arg_iter = args.iter();
let mut ph_iter = placeholders.iter();
for part in part_iter {
if !part.is_empty() {
replaced_parts.push(quote! { logger.append(#part) });
}
if let Some(arg) = arg_iter.next() {
let placeholder = ph_iter.next().unwrap();
match *placeholder {
"{}" => {
replaced_parts.push(quote! { logger.append(#arg) });
}
value if value.starts_with("{:.") => {
let Ok(precision) = value[3..value.len() - 1].parse::<u8>() else {
return Error::new_spanned(
format_string,
format!("invalid precision format: {value}"),
)
.to_compile_error()
.into();
};
replaced_parts.push(quote! {
logger.append_with_args(
#arg,
&[atlas_program_log::logger::Argument::Precision(#precision)]
)
});
}
value if value.starts_with("{:<.") || value.starts_with("{:>.") => {
let Ok(size) = value[4..value.len() - 1].parse::<usize>() else {
return Error::new_spanned(
format_string,
format!("invalid truncate size format: {value}"),
)
.to_compile_error()
.into();
};
match value.chars().nth(2) {
Some('<') => {
replaced_parts.push(quote! {
logger.append_with_args(
#arg,
&[atlas_program_log::logger::Argument::TruncateStart(#size)]
)
});
}
Some('>') => {
replaced_parts.push(quote! {
logger.append_with_args(
#arg,
&[atlas_program_log::logger::Argument::TruncateEnd(#size)]
)
});
}
_ => {
return Error::new_spanned(
format_string,
format!("invalid truncate format: {value}"),
)
.to_compile_error()
.into();
}
}
}
_ => {
return Error::new_spanned(
format_string,
format!("invalid placeholder: {placeholder}"),
)
.to_compile_error()
.into();
}
}
}
}
TokenStream::from(quote! {
{
let mut logger = ::atlas_program_log::logger::Logger::<#buffer_len>::default();
#(#replaced_parts;)*
logger.log();
}
})
} else {
TokenStream::from(
quote! {::atlas_program_log::logger::log_message(#format_string.as_bytes());},
)
}
}
#[proc_macro_attribute]
pub fn log_cu_usage(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let block = &input.block;
input.block = syn::parse_quote!({
let cu_before = unsafe { ::atlas_program_log::logger::remaining_compute_units() };
let __result = (|| #block)();
let cu_after = unsafe { ::atlas_program_log::logger::remaining_compute_units() };
let introspection_cost = 102;
let consumed = cu_before - cu_after - introspection_cost;
::atlas_program_log::log!("Function {} consumed {} compute units", stringify!(#fn_name), consumed);
__result
});
quote!(#input).into()
}