use indexmap::map::IndexMap;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::{ItemFn, Token};
mod arg_reconciler;
mod attr_parser;
#[proc_macro_attribute]
pub fn named(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut f: ItemFn = syn::parse_macro_input!(item);
let name = f.sig.ident.clone();
let arg_reconciler::ArgDetails { args, defaults } = match arg_reconciler::reconcile(&f, attr) {
Ok(v) => v,
Err(err) => {
let mut m = quote! { macro_rules! #name { ($($idents:ident = $exprs:expr),*) => { unimplemented!() } } };
m.extend(err.to_compile_error());
return m.into();
}
};
let dunder_name = syn::Ident::new(&format!("__{}", name), name.span());
let inner_name = syn::Ident::new(&format!("{}_inner", dunder_name), name.span());
f.sig.ident = dunder_name.clone();
let mut ts = f.into_token_stream();
{
let mut branches = Vec::with_capacity(5 * args.len() + 3);
for completed in 0..=args.len() {
let (already_parsed_exprs, still_being_parsed) = args.split_at(completed);
let match_exprs: Punctuated<_, Token![,]> = already_parsed_exprs
.iter()
.map(|expr| quote! { $#expr:expr }.into_iter().collect::<TokenStream>())
.collect();
let already_parsed_exprs: Punctuated<_, Token![,]> = already_parsed_exprs
.iter()
.map(|expr| quote! { $#expr })
.collect();
let remaining_defaults: IndexMap<_, _> = defaults
.iter()
.skip(already_parsed_exprs.len())
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
branches.push({
let missing_required: Vec<String> = remaining_defaults
.iter()
.filter(|(_k, v)| v.is_none())
.map(|(k, _v)| k.clone())
.collect();
let rhs = if !missing_required.is_empty() {
report_missing(&missing_required)
} else {
let mut values = already_parsed_exprs.clone();
values.extend(
remaining_defaults
.values()
.cloned()
.map(|v| v.unwrap())
.collect::<Punctuated<_, Token![,]>>(),
);
quote! { #dunder_name(#values) }
};
quote! { (#match_exprs) => { #rhs }; }
});
if let Some(next_missing_ident) = still_being_parsed.iter().next() {
branches.push({
let mut match_exprs = match_exprs.clone();
match_exprs.push(quote! { #next_missing_ident = $#next_missing_ident:expr});
let mut values = already_parsed_exprs.clone();
values.push(quote! { $#next_missing_ident });
quote! { (#match_exprs) => { #inner_name!(#values) }; }
});
branches.push({
let mut match_exprs = match_exprs.clone();
match_exprs.push(quote! { #next_missing_ident = $#next_missing_ident:expr});
match_exprs.push(quote! { $($keys:ident = $values:expr),+ }.to_token_stream());
let mut exprs = already_parsed_exprs.clone();
exprs.push(quote! { $#next_missing_ident });
exprs.push(quote! { $($keys = $values),+ }.to_token_stream());
quote! { (#match_exprs) => { #inner_name!(#exprs) }; }
});
branches.push({
let mut match_exprs = match_exprs.clone();
match_exprs.push(quote! { $key:ident = $value:expr });
let mut values = already_parsed_exprs.clone();
let rhs = match remaining_defaults.iter().next().unwrap() {
(_name, Some(next_default_value)) => {
values.push(quote! { #next_default_value });
values.push(quote! { $key = $value });
quote! { #inner_name!(#values) }
}
(missing_name, None) => {
report_missing(&[missing_name.clone()])
}
};
quote! { (#match_exprs) => { #rhs }; }
});
branches.push({
let mut match_exprs = match_exprs.clone();
match_exprs.push(quote! { $($keys:ident = $values:expr),+ });
let mut already_parsed_exprs = already_parsed_exprs.clone();
let rhs = match remaining_defaults.iter().next().clone().unwrap() {
(_name, Some(next_default_value)) => {
already_parsed_exprs.push(quote! { #next_default_value });
already_parsed_exprs.push(quote! { $($keys = $values),+ });
quote! { #inner_name!(#already_parsed_exprs) }
}
(missing_name, None) => {
report_missing(&[missing_name.clone()])
}
};
quote! { (#match_exprs) => { #rhs }; }
});
}
}
branches.push({
let match_exprs: Punctuated<_, Token![,]> = args
.iter()
.map(|expr| quote! { $#expr:expr }.into_iter().collect::<TokenStream>())
.collect();
let expected_names = format_names(&args.iter().map(|v| v.to_string()).collect::<Vec<_>>());
let expected_names = quote! { #expected_names };
quote! { (#match_exprs, $ident:ident = $expr:expr) => { compile_error!(concat!("Unrecognized named argument - got value for argument `", stringify!($ident), "` but only expected ", #expected_names)) }; }
});
branches.push({
let match_exprs: Punctuated<_, Token![,]> = args
.iter()
.map(|expr| quote! { $#expr:expr }.into_iter().collect::<TokenStream>())
.collect();
let expected_names = format_names(&args.iter().map(|v| v.to_string()).collect::<Vec<_>>());
let expected_names = quote! { #expected_names };
quote! { (#match_exprs, $ident:ident = $expr:expr, $($idents:ident = $exprs:expr),+) => { compile_error!(concat!("Unrecognized named argument - got value for argument `", stringify!($ident), "` but only expected ", #expected_names)) }; }
});
ts.extend(quote! { macro_rules! #inner_name { #(#branches)* } });
}
{
let mut branches = Vec::with_capacity(5);
if args.is_empty() {
branches.push(quote! { () => { #dunder_name() }; });
} else {
let first_name = args[0].clone();
let first_expr = quote! { $#first_name:expr };
let first_default = defaults.iter().next().map(|(_k, v)| v.clone()).unwrap();
let first_default = first_default.map(|v| quote! { #v });
branches
.push(quote! { (#first_name = #first_expr) => { #inner_name!($#first_name) }; });
branches.push(
quote! { (#first_name = #first_expr, $($keys:ident = $values:expr),+) => { #inner_name!($#first_name, $($keys = $values),+) }; }
);
branches.push({
let rhs = if first_default.is_some() {
quote! { #inner_name!(#first_default, $other = $other_value, $($keys = $values),+) }
} else {
report_missing(&[first_name.to_string()])
};
quote! { ($other:ident = $other_value:expr, $($keys:ident = $values:expr),+) => { #rhs }; }
});
branches.push({
let rhs = if first_default.is_some() {
quote! { #inner_name!(#first_default, $other = $other_value) }
} else {
report_missing(&[first_name.to_string()])
};
quote! { ($other:ident = $other_value:expr) => { #rhs }; }
});
branches.push(quote! { () => { #inner_name!() }; });
}
ts.extend(quote! {
macro_rules! #name {
#(#branches)*
}
});
}
ts.into()
}
fn report_missing(missing: &[String]) -> TokenStream {
let maybe_s = if missing.len() == 1 { "" } else { "s" };
let missing_str = format!(
"Must specify value{} for non-defaulted argument{}: {}",
maybe_s,
maybe_s,
format_names(missing),
);
quote! { compile_error!(#missing_str) }
}
fn format_names(names: &[String]) -> String {
if names.len() == 1 {
format!("`{}`", names[0])
} else {
format!("[{}]", names.join(", "))
}
}