use proc_macro2::{Delimiter, Group, Span, TokenStream as TokenStream2, TokenTree};
use quote::{quote, quote_spanned, ToTokens};
use syn::{Attribute, Error, FnArg, Ident, Pat, PatIdent, Path, Result, Signature, Token};
use crate::options::TraitSpec;
use crate::shape::{
argument_kind, classify_receiver, classify_return, last_segment, turbofish, Access,
ArgumentKind, Conversion, ReceiverKind, ReturnModel, ReturnSite, Value, Wrapper,
};
use crate::subject::{Target, Unforwardable};
fn hygienic(name: &str) -> Ident {
Ident::new(name, Span::mixed_site())
}
#[derive(Clone, Copy)]
pub(crate) enum ArgumentNames {
WrittenOrGenerated,
Written,
}
fn plain(ident: Ident) -> Pat {
Pat::Ident(PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident,
subpat: None,
})
}
#[derive(Clone, Copy)]
pub(crate) enum SelfArguments {
Dispatch,
PassThrough,
}
pub(crate) struct MethodPlan {
receiver: ReceiverKind,
self_token: Token![self],
arguments: Vec<Ident>,
same_variant: Vec<(usize, Access)>,
turbofish: Option<TokenStream2>,
return_model: ReturnModel,
pub(crate) future: bool,
awaited: bool,
unsafety: bool,
}
impl MethodPlan {
pub(crate) fn new(
signature: &mut Signature,
names: ArgumentNames,
self_arguments: SelfArguments,
spec: Option<&TraitSpec>,
site: &ReturnSite,
) -> Result<Self> {
let (receiver, self_token) = match signature.receiver() {
Some(receiver) => (classify_receiver(receiver)?, receiver.self_token),
None => {
return Err(Error::new_spanned(
&*signature,
"a forwarded method needs a receiver",
))
}
};
let mut arguments = vec![];
let mut same_variant = vec![];
for (index, input) in signature.inputs.iter_mut().skip(1).enumerate() {
let FnArg::Typed(pat_type) = input else {
unreachable!("the receiver is the first input")
};
let binding = match (&*pat_type.pat, names) {
(Pat::Ident(pattern), ArgumentNames::Written) => pattern.ident.clone(),
(Pat::Ident(pattern), ArgumentNames::WrittenOrGenerated)
if pattern.by_ref.is_none() && pattern.subpat.is_none() =>
{
let ident = pattern.ident.clone();
*pat_type.pat = plain(ident.clone());
ident
}
(_, ArgumentNames::WrittenOrGenerated) => {
let ident = hygienic(&format!("__argument{index}"));
*pat_type.pat = plain(ident.clone());
ident
}
(other, ArgumentNames::Written) => {
return Err(Error::new_spanned(
other,
"a forwarded parameter must be a plain identifier",
))
}
};
if let SelfArguments::Dispatch = self_arguments {
if let ArgumentKind::SameVariant(access) = argument_kind(&pat_type.ty)? {
same_variant.push((index, access));
}
}
arguments.push(binding);
}
let (return_model, future) = classify_return(&signature.output, spec, site)?;
Ok(Self {
receiver,
self_token,
arguments,
same_variant,
turbofish: turbofish(signature),
return_model,
future,
awaited: signature.asyncness.is_some() || future,
unsafety: signature.unsafety.is_some(),
})
}
pub(crate) fn has_same_variant(&self) -> bool {
!self.same_variant.is_empty()
}
pub(crate) fn scrutinee(&self, site: Span) -> TokenStream2 {
let receiver = receiver_scrutinee(self.receiver, &self.self_token, site);
if self.same_variant.is_empty() {
return receiver;
}
let arguments = self
.same_variant
.iter()
.map(|(index, _)| &self.arguments[*index]);
quote_spanned! { site => (#receiver, #(#arguments),*) }
}
fn same_binding(index: usize) -> Ident {
hygienic(&format!("__same{index}"))
}
fn pattern(&self, target: &Target, prefix: &Path, binding: &Ident) -> TokenStream2 {
let own = target.pattern(prefix, binding);
if self.same_variant.is_empty() {
return own;
}
let same = self
.same_variant
.iter()
.map(|(index, _)| target.pattern(prefix, &Self::same_binding(*index)));
quote! { (#own, #(#same),*) }
}
fn call_arguments(&self, target: &Target, site: Span) -> Vec<TokenStream2> {
self.arguments
.iter()
.enumerate()
.map(
|(index, ident)| match self.same_variant.iter().find(|(same, _)| *same == index) {
Some((_, access)) => {
payload_expression(target.deref, *access, &Self::same_binding(index), site)
}
None => quote!(#ident),
},
)
.collect()
}
pub(crate) fn arm(&self, target: &Target, context: &ArmContext) -> Result<Arm> {
let ArmContext {
callee,
method,
subject,
prefix,
lift,
site,
} = context;
let site = *site;
let Target {
ident: target_ident,
payload,
cfgs,
deref,
..
} = target;
let ReturnModel {
wrapper,
value: needs,
} = self.return_model;
let wrapper = if lift.is_some() {
Wrapper::None
} else {
wrapper
};
if *deref && matches!(needs, Value::Rewrap) {
return Err(Error::new_spanned(
payload,
format!("`{method}` returns `Self`, which cannot be wrapped back into the dereferenced payload of `{target_ident}`; drop `deref` on it or {}", callee.by_hand()),
));
}
let inner = hygienic("__inner");
let receiver = match (callee, self.receiver) {
(Callee::Method { .. }, ReceiverKind::Value(_)) => quote!(#inner),
_ => receiver_expression(self.receiver, target, &inner, method, site)?,
};
let arguments = self.call_arguments(target, site);
let turbofish = &self.turbofish;
let mut value = match callee {
Callee::Qualified { trait_path, method } => {
let target_type = target.target_type();
quote_spanned! { site =>
<#target_type as #trait_path>::#method #turbofish (#receiver #(, #arguments)*)
}
}
Callee::Method { name } => {
quote_spanned! { site => #receiver.#name #turbofish (#(#arguments),*) }
}
};
if self.unsafety {
value = quote_spanned! { site => unsafe { #value } };
}
if self.awaited {
value = quote_spanned! { site => #value.await };
}
let rebuild_error = || {
Error::new_spanned(
payload,
format!("`{method}` returns `Self`, which cannot be built around the other fields of `{subject}`; {}", callee.rebuild_hint()),
)
};
value = match (wrapper, needs) {
(_, Value::Plain) => value,
(Wrapper::None, Value::Rewrap) => {
target.rebuild(prefix, value).ok_or_else(rebuild_error)?
}
(Wrapper::None, Value::Into) => {
quote_spanned! { site => ::core::convert::Into::into(#value) }
}
(_, Value::Rewrap) => {
let constructor = target.constructor(prefix).ok_or_else(rebuild_error)?;
quote_spanned! { site => #value.map(#constructor) }
}
(_, Value::Into) => {
quote_spanned! { site => #value.map(::core::convert::Into::into) }
}
};
if let Wrapper::Result {
error: Conversion::Into,
} = wrapper
{
value = quote_spanned! { site => #value.map_err(::core::convert::Into::into) };
}
if let Some(lift) = lift {
value = quote_spanned! { site => #lift(#value) };
}
Ok(Arm {
cfgs: cfgs.clone(),
pattern: self.pattern(target, prefix, &inner),
value,
})
}
pub(crate) fn fallback(&self, value: TokenStream2) -> Arm {
let moved = |access: &Access| !matches!(access, Access::Shared);
if !self.same_variant.iter().any(|(_, access)| moved(access)) {
return Arm {
cfgs: vec![],
pattern: quote!(_),
value,
};
}
let positions = self.same_variant.iter().map(|(index, access)| {
if moved(access) {
Self::same_binding(*index).into_token_stream()
} else {
quote!(_)
}
});
let rebound = self
.same_variant
.iter()
.filter(|(_, access)| moved(access))
.map(|(index, _)| {
let name = &self.arguments[*index];
let binding = Self::same_binding(*index);
quote!(let #name = #binding;)
});
Arm {
cfgs: vec![],
pattern: quote!((_, #(#positions),*)),
value: quote!({ #(#rebound)* #value }),
}
}
}
pub(crate) struct ArmContext<'a> {
pub(crate) callee: Callee<'a>,
pub(crate) method: &'a Ident,
pub(crate) subject: &'a Ident,
pub(crate) prefix: Path,
pub(crate) lift: Option<&'a Path>,
pub(crate) site: Span,
}
pub(crate) enum Callee<'a> {
Qualified {
trait_path: &'a Path,
method: &'a Ident,
},
Method { name: &'a Ident },
}
impl Callee<'_> {
pub(crate) fn by_hand(&self) -> &'static str {
match self {
Callee::Qualified { .. } => "implement the method by hand",
Callee::Method { .. } => "give it a body",
}
}
fn rebuild_hint(&self) -> &'static str {
match self {
Callee::Qualified { .. } => "write the method in a `#[whichever::dispatch] impl` block",
Callee::Method { .. } => "give it a body instead",
}
}
}
pub(crate) struct Arm {
pub(crate) cfgs: Vec<Attribute>,
pub(crate) pattern: TokenStream2,
pub(crate) value: TokenStream2,
}
impl Arm {
pub(crate) fn unforwardable(
prefix: &Path,
unforwardable: &Unforwardable,
value: TokenStream2,
) -> Self {
let ident = &unforwardable.ident;
Arm {
cfgs: unforwardable.cfgs.clone(),
pattern: quote!(#prefix::#ident { .. }),
value,
}
}
pub(crate) fn rest(value: TokenStream2) -> Self {
Arm {
cfgs: vec![],
pattern: quote!(_),
value,
}
}
}
impl ToTokens for Arm {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Arm {
cfgs,
pattern,
value,
} = self;
tokens.extend(quote! { #(#cfgs)* #pattern => #value, });
}
}
pub(crate) struct Forwarding {
pub(crate) scrutinee: TokenStream2,
pub(crate) arms: Vec<Arm>,
pub(crate) fallback: Option<Arm>,
pub(crate) future: bool,
}
impl Forwarding {
pub(crate) fn body(&self, span: Span) -> TokenStream2 {
let Forwarding {
scrutinee,
arms,
fallback,
future,
} = self;
let mut body = quote_spanned! { span => match #scrutinee { #(#arms)* #fallback } };
if *future {
body = quote_spanned! { span => async move { #body } };
}
let mut group = Group::new(Delimiter::Brace, body);
group.set_span(span);
TokenTree::Group(group).into()
}
}
fn receiver_scrutinee(kind: ReceiverKind, self_token: &Token![self], site: Span) -> TokenStream2 {
match kind {
ReceiverKind::Value(_) => quote!(#self_token),
ReceiverKind::Boxed => quote_spanned!(site => *#self_token),
ReceiverKind::Pinned(Access::Exclusive) => {
quote_spanned!(site => ::core::pin::Pin::get_mut(#self_token))
}
ReceiverKind::Pinned(_) => quote_spanned!(site => ::core::pin::Pin::get_ref(#self_token)),
}
}
fn receiver_expression(
kind: ReceiverKind,
target: &Target,
binding: &Ident,
method: &Ident,
site: Span,
) -> Result<TokenStream2> {
match kind {
ReceiverKind::Value(access) => Ok(payload_expression(target.deref, access, binding, site)),
ReceiverKind::Boxed => {
if !target.holds_box() {
let ident = &target.ident;
let payload = &target.payload;
let spelled = payload.to_token_stream().to_string().replace(' ', "");
let advice = if last_segment(payload).is_some_and(|segment| segment.ident == "Box")
{
format!("add `#[dispatch(deref)]` to `{ident}`")
} else {
format!("hold `Box<{spelled}>` in a `#[dispatch(deref)]` variant")
};
return Err(Error::new_spanned(
payload,
format!("`{method}` takes `self: Box<Self>`, and `{ident}` holds a plain `{spelled}`; {advice}, or implement the method by hand"),
));
}
Ok(quote!(#binding))
}
ReceiverKind::Pinned(access) => {
let payload = payload_expression(target.deref, access, binding, site);
Ok(quote_spanned!(site => ::core::pin::Pin::new(#payload)))
}
}
}
fn payload_expression(deref: bool, access: Access, binding: &Ident, site: Span) -> TokenStream2 {
if !deref {
return quote!(#binding);
}
match access {
Access::Shared => quote_spanned!(site => &**#binding),
Access::Exclusive => quote_spanned!(site => &mut **#binding),
Access::Owned => quote_spanned!(site => *#binding),
}
}