use nu_ansi_term::{Color, ansi::RESET};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, format_ident, quote, quote_spanned};
use std::sync::{Once, OnceLock};
use syn::{
Attribute, Error, FnArg, Ident, ImplItem, ImplItemFn, Item, ItemFn, ItemImpl, ItemMod, LitStr,
Result, Token, parse, parse_macro_input, parse_quote, spanned::Spanned,
};
#[allow(unused)]
#[cfg(doctest)]
mod helper;
static IS_HELPER_MODULE_ADDED: Once = Once::new();
static PRINT_RUN_DEFAULTS: OnceLock<PrintRunArgs> = OnceLock::new();
#[derive(Clone, Debug, Default)]
struct PrintRunArgs {
colored: Option<bool>,
duration: Option<bool>,
indent: Option<bool>,
skip: Option<bool>,
supress_labels: Option<bool>,
timestamps: Option<bool>,
__struct_prefix: Option<String>,
}
impl PrintRunArgs {
fn merge(&mut self, override_args: &PrintRunArgs) {
self.colored = override_args.colored.or(self.colored);
self.duration = override_args.duration.or(self.duration);
self.indent = override_args.indent.or(self.indent);
self.skip = override_args.skip.or(self.skip);
self.supress_labels = override_args.supress_labels.or(self.supress_labels);
self.timestamps = override_args.timestamps.or(self.timestamps);
self.__struct_prefix = override_args
.__struct_prefix
.clone()
.or_else(|| self.__struct_prefix.clone());
}
fn add_globals(&mut self) {
if let Some(glob) = get_print_run_defaults() {
if let Some(v) = glob.colored {
self.colored = Some(v);
}
if let Some(v) = glob.duration {
self.duration = Some(v);
}
if let Some(v) = glob.indent {
self.indent = Some(v);
}
if let Some(v) = glob.skip {
self.skip = Some(v);
}
if let Some(v) = glob.supress_labels {
self.supress_labels = Some(v);
}
if let Some(v) = glob.timestamps {
self.timestamps = Some(v);
}
}
}
fn to_attribute(&self) -> Attribute {
let arg_idents = self.to_idents();
let pre = self.__struct_prefix.as_ref().and_then(|p| Some(p.as_str()));
match (arg_idents.is_empty(), pre) {
(true, None) => parse_quote! {
#[print_run::print_run]
},
(true, Some(pre_val)) => parse_quote! {
#[print_run::print_run(__struct_prefix = #pre_val)]
},
(false, None) => parse_quote! {
#[print_run::print_run( #(#arg_idents),* )]
},
(false, Some(pre_val)) => parse_quote! {
#[print_run::print_run( #(#arg_idents),*, __struct_prefix = #pre_val )]
},
}
}
fn to_idents(&self) -> Vec<Ident> {
let mut result = Vec::new();
if self.colored == Some(true) {
result.push(format_ident!("colored"));
}
if self.duration == Some(true) {
result.push(format_ident!("duration"));
}
if self.indent == Some(true) {
result.push(format_ident!("indent"));
}
if self.skip == Some(true) {
result.push(format_ident!("skip"));
}
if self.supress_labels == Some(true) {
result.push(format_ident!("supress_labels"));
}
if self.timestamps == Some(true) {
result.push(format_ident!("timestamps"));
}
result
}
}
impl parse::Parse for PrintRunArgs {
fn parse(input: parse::ParseStream) -> Result<Self> {
let mut args = PrintRunArgs::default();
while !input.is_empty() {
let ident: Ident = input.parse()?;
let _ = input.parse::<Option<Token![,]>>();
match ident.to_string().as_str() {
"colored" => args.colored = Some(true),
"duration" => args.duration = Some(true),
"indent" => args.indent = Some(true),
"skip" => args.skip = Some(true),
"supress_labels" => args.supress_labels = Some(true),
"timestamps" => args.timestamps = Some(true),
"__struct_prefix" => {
let _ = input.parse::<Option<Token![=]>>()?;
let lit: LitStr = input.parse()?;
args.__struct_prefix = Some(lit.value().to_string());
}
other => {
return Err(Error::new(
ident.span(),
format!("Unknown attribute '{}'", other),
));
}
}
}
Ok(args)
}
}
macro_rules! or_else {
($cond:expr, $true_:expr, $false_:expr) => {
if $cond { $true_ } else { $false_ }
};
}
macro_rules! or_nothing {
($cond:expr, $true_:expr) => {
or_else!($cond, $true_, quote! {})
};
}
macro_rules! or_empty_str {
($cond:expr, $true_:expr) => {
or_else!($cond, $true_, quote! { || "".to_string() })
};
}
macro_rules! colorize {
($txt:expr, $col_name: ident) => {
format!("{}{}{}", Color::$col_name.prefix().to_string(), $txt, RESET)
};
}
macro_rules! colorize_fn {
($color_name: ident) => {{
let color = Color::$color_name.prefix().to_string();
quote! {
|txt: String| format!("{}{}{}", #color, txt, #RESET)
}
}};
($color_name: ident, "bold") => {{
let color = Color::$color_name.bold().prefix().to_string();
quote! {
|txt: String| format!("{}{}{}", #color, txt, #RESET)
}
}};
}
macro_rules! create_timestamp {
($colored:expr) => {{
let colorize = or_else!(
$colored,
colorize_fn!(DarkGray),
quote! { |txt: String| txt }
);
quote! {
|| {
let now = std::time::SystemTime::now();
let epoch = now
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards");
let total_secs = epoch.as_secs();
let millis = epoch.subsec_millis();
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs / 60) % 60;
let seconds = total_secs % 60;
let ts = format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis);
let ts = {#colorize}(ts);
format!("{} ", ts) }
}
}};
}
macro_rules! create_duration {
($colored:expr, $supress_labels:expr) => {{
let colorize = or_else!($colored, colorize_fn!(Green), quote! { |txt: String| txt });
let supress_labels = $supress_labels;
quote! {
|start: std::time::Instant| {
let elapsed = start.elapsed().as_nanos();
let dur =
if elapsed < 1_000 {
format!("{}ns", elapsed)
} else if elapsed < 1_000_000 {
format!("{:.2}µs", elapsed as f64 / 1_000.0)
} else if elapsed < 1_000_000_000 {
format!("{:.2}ms", elapsed as f64 / 1_000_000.0)
} else {
format!("{:.2}s", elapsed as f64 / 1_000_000_000.0)
}
;
let dur = {#colorize}(dur); if #supress_labels {
format!("[{}]", dur)
} else {
format!(" in {}", dur)
}
}
}
}};
}
macro_rules! create_indent {
($val:expr, $ch:expr) => {{
let val = $val;
let ch = $ch;
let depth_path: syn::Expr = if cfg!(doctest) {
syn::parse_str("crate::helper::DEPTH").unwrap()
} else {
syn::parse_str("crate::__print_run_helper::DEPTH").unwrap()
};
quote! {
|| {#depth_path}.with(|depth| {
let depth_val = *depth.borrow();
*depth.borrow_mut() = depth_val.saturating_add_signed(#val);
let depth_val= depth_val.saturating_add_signed((#val-1) / 2);
let spaces = "┆ ".repeat(depth_val);
format!("{}{} ", spaces, #ch)
})
}
}};
}
fn get_print_run_defaults() -> Option<&'static PrintRunArgs> {
PRINT_RUN_DEFAULTS.get()
}
#[proc_macro_attribute]
pub fn print_run_defaults(attr: TokenStream, input: TokenStream) -> TokenStream {
let attr_clone = attr.clone();
let args = parse_macro_input!(attr as PrintRunArgs);
if PRINT_RUN_DEFAULTS.set(args).is_err() {
return Error::new_spanned(
TokenStream2::from(attr_clone),
"print run defaults already set",
)
.to_compile_error()
.into();
}
input
}
#[proc_macro_attribute]
pub fn print_run(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as PrintRunArgs);
if let Ok(mut func) = parse::<ItemFn>(item.clone()) {
let new_args = extract_and_flatten_print_args(&args, &mut func.attrs);
return print_run_fn(new_args, func);
}
if let Ok(mut module) = parse::<ItemMod>(item.clone()) {
let new_args = extract_and_flatten_print_args(&args, &mut module.attrs);
return print_run_mod(new_args, module);
}
if let Ok(mut implementation) = parse::<ItemImpl>(item.clone()) {
let new_args = extract_and_flatten_print_args(&args, &mut implementation.attrs);
return print_run_impl(new_args, implementation);
}
Error::new_spanned(
TokenStream2::from(item),
"#[print_run] can only be used on functions, implementations or inline modules",
)
.to_compile_error()
.into()
}
fn print_run_fn(mut args: PrintRunArgs, mut fn_item: ItemFn) -> TokenStream {
args.add_globals();
let PrintRunArgs {
colored,
duration,
indent,
skip,
supress_labels,
timestamps,
__struct_prefix,
} = args;
let colored = colored == Some(true);
let duration = duration == Some(true);
let indent = indent == Some(true);
let skip = skip == Some(true);
let supress_labels = supress_labels == Some(true);
let timestamps = timestamps == Some(true);
if skip {
let use_msg = parse_quote! { use std::println as msg; };
fn_item.block.stmts.insert(0, use_msg);
return fn_item.to_token_stream().into();
}
let ItemFn {
attrs,
vis,
sig,
block,
} = fn_item;
let fn_name = sig.ident.to_string();
let prefix = __struct_prefix.unwrap_or("".into());
let fn_name = format!("{prefix}{fn_name}");
let start_label = or_else!(!supress_labels, "starting", "");
let end_label = or_else!(!supress_labels, "ended", "");
let start = or_else!(colored, colorize!(fn_name.clone(), Yellow), fn_name.clone());
let end = or_else!(colored, colorize!(fn_name.clone(), Blue), fn_name.clone());
let create_timestamp_fn = or_empty_str!(timestamps, create_timestamp!(colored));
let duration_fn = or_else!(
duration,
create_duration!(colored, supress_labels),
quote! { |_| "".to_string() }
);
let indent_top = or_empty_str!(indent, create_indent!(1isize, "┌"));
let indent_bottom = or_empty_str!(indent, create_indent!(-1isize, "└"));
let indent_body = or_empty_str!(indent, create_indent!(0isize, ""));
let colorize_msg = if colored {
colorize_fn!(White, "bold")
} else {
colorize_fn!(Default, "bold")
};
let msg_macro = quote! {
#[allow(unused)]
macro_rules! msg {
($($arg:tt)*) => {{
let ts = {#create_timestamp_fn}();
let indent = {#indent_body}();
let msg = format!($($arg)*);
let msg = {#colorize_msg}(msg);
println!("{}{} {}", ts, indent, msg);
}};
}
};
let new_block = quote_spanned! (block.to_token_stream().span() =>
{
let ts = {#create_timestamp_fn}();
let start = std::time::Instant::now();
let indent = {#indent_top}();
println!("{}{}{} {}", ts, indent, #start, #start_label);
#msg_macro
let result = {
#block
};
let dur = {#duration_fn}(start);
let ts = {#create_timestamp_fn}();
let indent = {#indent_bottom}();
println!("{}{}{} {}{}", ts, indent, #end, #end_label, dur);
result
}
);
let helper_module = define_helper_module();
quote! {
#(#attrs)*
#vis #sig #new_block
#helper_module
}
.into()
}
fn print_run_mod(args: PrintRunArgs, mut module_item: ItemMod) -> TokenStream {
let content = match module_item.content {
Some((_, ref mut items)) => items,
_ => {
return Error::new_spanned(
module_item.mod_token,
"`#[print_run]` only supports inline modules",
)
.to_compile_error()
.into();
}
};
for item in content {
match item {
Item::Fn(func) => {
let new_args = extract_and_flatten_print_args(&args, &mut func.attrs);
func.attrs.push(new_args.to_attribute());
}
Item::Impl(item_impl) => {
let new_args = extract_and_flatten_print_args(&args, &mut item_impl.attrs);
item_impl.attrs.push(new_args.to_attribute());
}
_ => {}
}
}
let helper_module = define_helper_module();
let module_tokens = module_item.into_token_stream();
quote! { #module_tokens #helper_module }.into()
}
fn print_run_impl(args: PrintRunArgs, mut impl_item: ItemImpl) -> TokenStream {
let ty_str = (&impl_item.self_ty).into_token_stream().to_string();
for impl_item in &mut impl_item.items {
if let ImplItem::Fn(method) = impl_item {
let mut new_args = extract_and_flatten_print_args(&args, &mut method.attrs);
let is_static = is_static_method(&method);
let ty_str = ty_str.clone() + if is_static { "::" } else { "." };
new_args.__struct_prefix = Some(ty_str);
method.attrs.push(new_args.to_attribute());
}
}
let helper_module = define_helper_module();
let impl_tokens = impl_item.into_token_stream();
quote! { #impl_tokens #helper_module }.into()
}
fn extract_and_flatten_print_args(
parent: &PrintRunArgs,
attrs: &mut Vec<Attribute>,
) -> PrintRunArgs {
let mut merged_args = get_print_run_defaults()
.as_deref()
.and_then(|a| Some(a.clone()))
.unwrap_or(PrintRunArgs::default());
attrs.retain(|attr| {
if attr.path().is_ident("print_run") {
if let Ok(parsed) = attr.parse_args::<PrintRunArgs>() {
merged_args.merge(&parsed);
}
false } else {
true }
});
merged_args.merge(parent);
merged_args.add_globals();
merged_args
}
fn define_helper_module() -> TokenStream2 {
let mut define = false;
IS_HELPER_MODULE_ADDED.call_once(|| define = true);
or_nothing!(
define,
quote! {
#[doc(hidden)]
#[allow(unused)]
pub(crate) mod __print_run_helper {
use std::cell::RefCell;
thread_local! {
pub static DEPTH: RefCell<usize> = RefCell::new(0);
}
}
}
)
}
fn is_static_method(method: &ImplItemFn) -> bool {
match method.sig.inputs.first() {
Some(FnArg::Receiver(_)) => false, Some(FnArg::Typed(_)) => true, None => true, }
}