use proc_macro2::{Delimiter, Group, Span, TokenStream as TokenStream2, TokenTree};
use quote::{quote_spanned, ToTokens};
use syn::{Attribute, Error, FnArg, Ident, Member, Pat, PatIdent, Path, Result, Signature, Token};
use crate::options::TraitSpec;
use crate::route::Route;
use crate::shape::{
argument_kind, classify_receiver, classify_return, is_self, last_segment, turbofish, Access,
ArgumentKind, Conversion, FutureShape, ReceiverKind, ReturnSite,
};
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 requires_sized(signature: &Signature) -> bool {
let Some(where_clause) = &signature.generics.where_clause else {
return false;
};
where_clause.predicates.iter().any(|predicate| {
let syn::WherePredicate::Type(predicate) = predicate else {
return false;
};
is_self(&predicate.bounded_ty)
&& predicate.bounds.iter().any(|bound| match bound {
syn::TypeParamBound::Trait(bound) => {
matches!(bound.modifier, syn::TraitBoundModifier::None)
&& bound
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Sized")
}
_ => false,
})
})
}
fn holds_a_box(payload: &syn::Type) -> bool {
last_segment(payload).is_some_and(|segment| segment.ident == "Box")
}
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: Conversion,
pub(crate) future: Option<FutureShape>,
awaited: bool,
unsafety: bool,
requires_sized: bool,
}
impl MethodPlan {
pub(crate) fn new(
signature: &mut Signature,
names: ArgumentNames,
self_arguments: SelfArguments,
spec: Option<&TraitSpec>,
site: &ReturnSite,
resolution: Option<Span>,
) -> Result<Self> {
let (receiver, mut 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 resolve = |mut ident: Ident| {
if let Some(resolution) = resolution {
ident.set_span(ident.span().resolved_at(resolution));
}
ident
};
if let Some(resolution) = resolution {
self_token.span = self_token.span.resolved_at(resolution);
if let Some(FnArg::Receiver(receiver)) = signature.inputs.first_mut() {
receiver.self_token = self_token;
}
}
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 = resolve(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)?;
let awaited = signature.asyncness.is_some() || future.is_some();
Ok(Self {
receiver,
self_token,
arguments,
same_variant,
turbofish: turbofish(signature),
return_model,
future,
awaited,
unsafety: signature.unsafety.is_some(),
requires_sized: requires_sized(signature),
})
}
pub(crate) fn has_same_variant(&self) -> bool {
!self.same_variant.is_empty()
}
pub(crate) fn requires_sized(&self) -> bool {
self.requires_sized
}
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, site: Span) -> TokenStream2 {
let own = target.pattern(prefix, binding, site);
if self.same_variant.is_empty() {
return own;
}
let same = self
.same_variant
.iter()
.map(|(index, _)| target.pattern(prefix, &Self::same_binding(*index), site));
quote_spanned! { site => (#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.route, *access, &Self::same_binding(index), site)
}
None => quote_spanned!(site => #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,
route,
..
} = target;
let conversion = if lift.is_some() {
self.return_model.clone().unwrapped()
} else {
self.return_model.clone()
};
if !route.is_direct() && conversion.rewraps() {
let message = if route.is_deref() {
format!("`{method}` returns `Self`, which cannot be wrapped back into the dereferenced payload of `{target_ident}`; drop `deref` on it or {}", callee.by_hand())
} else {
format!("`{method}` returns `Self`, which cannot be wrapped back through the route `{}` of `{target_ident}`, whose other fields are gone; drop `via` on it or {}", route.spelled(), callee.by_hand())
};
return Err(Error::new_spanned(payload, message));
}
let exclusive = matches!(self.receiver, ReceiverKind::Value(Access::Exclusive))
|| matches!(self.receiver, ReceiverKind::Pinned(Access::Exclusive))
|| self
.same_variant
.iter()
.any(|(_, access)| matches!(access, Access::Exclusive));
if route.ends_in_a_method() && exclusive {
return Err(Error::new_spanned(
payload,
format!("`{method}` takes `&mut self`, and the route `{}` of `{target_ident}` ends in a method call, whose result cannot be borrowed mutably; route to a field, or {}", route.spelled(), callee.by_hand()),
));
}
let owned = matches!(self.receiver, ReceiverKind::Value(Access::Owned))
|| self
.same_variant
.iter()
.any(|(_, access)| matches!(access, Access::Owned));
if owned && route.starts_with_deref() && !holds_a_box(payload) {
let spelled = payload.to_token_stream().to_string().replace(' ', "");
return Err(Error::new_spanned(
payload,
format!("`{method}` takes `self` by value, and `{target_ident}` dereferences a `{spelled}`, which a value cannot be moved out of; hold a `Box`, or {}", callee.by_hand()),
));
}
let inner = hygienic("__inner");
let receiver = match (callee, self.receiver) {
(Callee::Method { .. }, ReceiverKind::Value(access)) => {
route.reach_for_postfix(&inner, !matches!(access, Access::Owned), site)
}
(Callee::Place { .. }, _) => quote_spanned!(site => #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 qualified = match target.target_type() {
Some(target_type) => quote_spanned! { site => <#target_type as #trait_path> },
None => quote_spanned! { site => <_ as #trait_path> },
};
quote_spanned! { site =>
#qualified::#method #turbofish (#receiver #(, #arguments)*)
}
}
Callee::Method { name } => {
quote_spanned! { site => #receiver.#name #turbofish (#(#arguments),*) }
}
Callee::Place { field, borrow } => {
let through_reference =
!matches!(self.receiver, ReceiverKind::Value(Access::Owned));
let place = match field {
Some(field) => {
let reached = route.reach_for_postfix(&inner, through_reference, site);
quote_spanned! { site => #reached.#field }
}
None => route.reach(&inner, through_reference, site),
};
if field.is_none() && route.is_direct() {
place
} else {
match borrow {
None => place,
Some(None) => quote_spanned! { site => &#place },
Some(Some(_)) => quote_spanned! { site => &mut #place },
}
}
}
};
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 = convert(value, &conversion, target, prefix, site, &rebuild_error)?;
if let Some(lift) = lift {
value = quote_spanned! { site => #lift(#value) };
}
Ok(Arm {
cfgs: target.arm_attributes(site),
pattern: self.pattern(target, prefix, &inner, site),
value,
site,
})
}
pub(crate) fn fallback(&self, value: TokenStream2, site: Span) -> Arm {
let moved = |access: &Access| !matches!(access, Access::Shared);
if !self.same_variant.iter().any(|(_, access)| moved(access)) {
return Arm::rest(value, site);
}
let positions = self.same_variant.iter().map(|(index, access)| {
if moved(access) {
Self::same_binding(*index).into_token_stream()
} else {
quote_spanned!(site => _)
}
});
let rebound = self
.same_variant
.iter()
.filter(|(_, access)| moved(access))
.map(|(index, _)| {
let name = &self.arguments[*index];
let binding = Self::same_binding(*index);
quote_spanned!(site => let #name = #binding;)
});
Arm {
cfgs: vec![],
pattern: quote_spanned!(site => (_, #(#positions),*)),
value: quote_spanned!(site => { #(#rebound)* #value }),
site,
}
}
}
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 },
Place {
field: Option<&'a Member>,
borrow: Option<Option<Token![mut]>>,
},
}
impl Callee<'_> {
pub(crate) fn by_hand(&self) -> &'static str {
match self {
Callee::Qualified { .. } => "implement the method by hand",
Callee::Method { .. } | Callee::Place { .. } => "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 { .. } | Callee::Place { .. } => "give it a body instead",
}
}
}
pub(crate) struct Arm {
pub(crate) cfgs: Vec<Attribute>,
pub(crate) pattern: TokenStream2,
pub(crate) value: TokenStream2,
pub(crate) site: Span,
}
impl Arm {
pub(crate) fn unforwardable(
prefix: &Path,
unforwardable: &Unforwardable,
value: TokenStream2,
site: Span,
) -> Self {
let ident = &unforwardable.ident;
Arm {
cfgs: unforwardable.arm_attributes(site),
pattern: quote_spanned!(site => #prefix::#ident { .. }),
value,
site,
}
}
pub(crate) fn rest(value: TokenStream2, site: Span) -> Self {
Arm {
cfgs: vec![],
pattern: quote_spanned!(site => _),
value,
site,
}
}
}
impl ToTokens for Arm {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Arm {
cfgs,
pattern,
value,
site,
} = self;
tokens.extend(quote_spanned! { *site => #(#cfgs)* #pattern => #value, });
}
}
pub(crate) struct Forwarding {
pub(crate) scrutinee: TokenStream2,
pub(crate) arms: Vec<Arm>,
pub(crate) fallback: Option<Arm>,
pub(crate) future: Option<FutureShape>,
}
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 } };
match future {
None => {}
Some(FutureShape::Opaque) => {
body = quote_spanned! { span => async move { #body } };
}
Some(FutureShape::Boxed(path)) => {
body = quote_spanned! { span => #path::pin(async move { #body }) };
}
}
let mut group = Group::new(Delimiter::Brace, body);
group.set_span(span);
TokenTree::Group(group).into()
}
}
fn convert(
value: TokenStream2,
conversion: &Conversion,
target: &Target,
prefix: &Path,
site: Span,
rebuild_error: &dyn Fn() -> Error,
) -> Result<TokenStream2> {
let path = |conversion: &Conversion| -> Result<Option<TokenStream2>> {
Ok(match conversion {
Conversion::Keep => None,
Conversion::Rewrap => Some(target.constructor(prefix, site).ok_or_else(rebuild_error)?),
Conversion::Into => Some(quote_spanned! { site => ::core::convert::Into::into }),
_ => unreachable!("a conversion under a wrapper is a path"),
})
};
Ok(match conversion {
Conversion::Keep => value,
Conversion::Rewrap => target
.rebuild(prefix, value, site)
.ok_or_else(rebuild_error)?,
Conversion::Into => quote_spanned! { site => ::core::convert::Into::into(#value) },
Conversion::Map(inner) => match path(inner)? {
Some(path) => quote_spanned! { site => #value.map(#path) },
None => value,
},
Conversion::MapErr(inner, error) => {
let mut value = match path(inner)? {
Some(path) => quote_spanned! { site => #value.map(#path) },
None => value,
};
if let Some(path) = path(error)? {
value = quote_spanned! { site => #value.map_err(#path) };
}
value
}
Conversion::Tuple(items) => {
let bindings: Vec<Ident> = (0..items.len())
.map(|index| hygienic(&format!("__element{index}")))
.collect();
let converted = items
.iter()
.zip(&bindings)
.map(|(item, binding)| {
let binding = quote_spanned!(site => #binding);
convert(binding, item, target, prefix, site, rebuild_error)
})
.collect::<Result<Vec<_>>>()?;
quote_spanned! { site => {
let (#(#bindings,)*) = #value;
(#(#converted,)*)
} }
}
})
}
fn receiver_scrutinee(kind: ReceiverKind, self_token: &Token![self], site: Span) -> TokenStream2 {
match kind {
ReceiverKind::Value(_) => quote_spanned!(site => #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.route, 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_spanned!(site => #binding))
}
ReceiverKind::Pinned(access) => {
let payload = payload_expression(&target.route, access, binding, site);
Ok(quote_spanned!(site => ::core::pin::Pin::new(#payload)))
}
}
}
fn payload_expression(route: &Route, access: Access, binding: &Ident, site: Span) -> TokenStream2 {
if route.is_direct() {
return quote_spanned!(site => #binding);
}
let reached = route.reach(binding, !matches!(access, Access::Owned), site);
if route.ends_in_a_method() {
return reached;
}
match access {
Access::Shared => quote_spanned!(site => &#reached),
Access::Exclusive => quote_spanned!(site => &mut #reached),
Access::Owned => reached,
}
}