use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
GenericParam, Ident, ImplItem, ItemImpl, Path, Type, WherePredicate, parse_quote,
visit::Visit,
visit_mut::{self, VisitMut},
};
#[proc_macro_attribute]
pub fn future_form(attr: TokenStream, item: TokenStream) -> TokenStream {
let Ok(input) = syn::parse::<ItemImpl>(item.clone()) else {
let item2: TokenStream2 = item.into();
let msg = detect_non_impl_item(&item2);
return make_error(proc_macro2::Span::call_site(), &msg);
};
let kinds = match parse_kinds(&attr) {
Ok(k) => k,
Err(err) => return err,
};
match generate_impls(&input, &kinds) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn detect_non_impl_item(tokens: &TokenStream2) -> String {
use proc_macro2::TokenTree;
let first_ident = tokens.clone().into_iter().find_map(|tt| {
if let TokenTree::Ident(ident) = tt {
Some(ident.to_string())
} else {
None
}
});
match first_ident.as_deref() {
Some("trait") => {
"#[future_form] can only be applied to impl blocks, not trait definitions; \
define your trait normally with a `K: FutureForm` parameter, then use \
#[future_form] on the impl blocks"
.to_string()
}
Some("fn" | "async") => {
"#[future_form] can only be applied to impl blocks, not free functions; \
write a trait with `K: FutureForm` and apply #[future_form] to its impl"
.to_string()
}
Some("struct") => {
"#[future_form] can only be applied to impl blocks, not struct definitions".to_string()
}
Some("enum") => {
"#[future_form] can only be applied to impl blocks, not enum definitions".to_string()
}
Some("mod") => "#[future_form] can only be applied to impl blocks, not modules".to_string(),
_ => "#[future_form] can only be applied to impl blocks".to_string(),
}
}
fn make_error(span: proc_macro2::Span, msg: &str) -> TokenStream {
syn::Error::new(span, msg).to_compile_error().into()
}
fn builtin_future_path(ident: &Ident) -> Option<Path> {
if ident == "Sendable" {
Some(parse_quote!(::futures::future::BoxFuture))
} else if ident == "Local" {
Some(parse_quote!(::futures::future::LocalBoxFuture))
} else {
None
}
}
fn is_likely_variant(ident: &Ident) -> bool {
let s = ident.to_string();
if !s.chars().next().is_some_and(char::is_uppercase) {
return false;
}
if s == "where" || s == "Self" {
return false;
}
if s.len() == 1 {
return false;
}
let common_traits = [
"Send",
"Sync",
"Clone",
"Copy",
"Debug",
"Display",
"Default",
"Fn",
"FnMut",
"FnOnce",
"Future",
"Iterator",
"IntoIterator",
"From",
"Into",
"TryFrom",
"TryInto",
"AsRef",
"AsMut",
"Eq",
"PartialEq",
"Ord",
"PartialOrd",
"Hash",
"Sized",
"Unpin",
"Drop",
];
if common_traits.contains(&s.as_str()) {
return false;
}
true
}
fn suggest_variant(ident: &str) -> String {
let lower = ident.to_lowercase();
if lower == "sendable" {
return "; did you mean `Sendable`?".to_string();
}
if lower == "local" {
return "; did you mean `Local`?".to_string();
}
match ident {
"Send" | "BoxFuture" => "; did you mean `Sendable`?".to_string(),
"LocalBoxFuture" => "; did you mean `Local`?".to_string(),
"HostSendable" | "HostLocal" | "HostDriven" | "HostFuture" => {
"; host-driven variants were removed — use `Sendable` or `Local` with `future_form_ffi::poll_once::PollOnce` instead".to_string()
}
_ => String::new(),
}
}
#[allow(clippy::expect_used)] fn parse_kinds(attr: &TokenStream) -> Result<Vec<FutureFormVariant>, TokenStream> {
use proc_macro2::TokenTree;
let attr2: TokenStream2 = attr.clone().into();
let tokens: Vec<TokenTree> = attr2.into_iter().collect();
if tokens.is_empty() {
return Err(make_error(
proc_macro2::Span::call_site(),
"missing FutureForm variants: expected #[future_form(Sendable)], #[future_form(Local)], or #[future_form(Sendable, Local)]",
));
}
let mut kinds = Vec::new();
let mut i = 0;
while i < tokens.len() {
while i < tokens.len() {
if let TokenTree::Punct(p) = tokens.get(i).expect("bounds checked")
&& p.as_char() == ','
{
i += 1;
continue;
}
break;
}
if i >= tokens.len() {
break;
}
let (kind_path, future_path, variant_span) = match tokens.get(i).expect("bounds checked") {
TokenTree::Ident(ident) => {
if is_likely_variant(ident) {
let future = builtin_future_path(ident);
let path: Path = parse_quote!(#ident);
(path, future, ident.span())
} else {
let hint = suggest_variant(&ident.to_string());
return Err(make_error(
ident.span(),
&format!("expected FutureForm variant, found `{ident}`{hint}"),
));
}
}
other @ (TokenTree::Group(_) | TokenTree::Punct(_) | TokenTree::Literal(_)) => {
return Err(make_error(
other.span(),
"expected FutureForm variant (e.g., `Sendable`, `Local`, or custom type)",
));
}
};
i += 1;
let extra_bounds = if i < tokens.len() {
if let TokenTree::Ident(ident) = tokens.get(i).expect("bounds checked") {
if ident == "where" {
i += 1;
let (predicates, new_i) = collect_where_clause(&tokens, i, variant_span)?;
i = new_i;
predicates
} else {
vec![]
}
} else {
vec![]
}
} else {
vec![]
};
kinds.push(FutureFormVariant {
kind_path,
future_path,
extra_bounds,
});
}
if kinds.is_empty() {
return Err(make_error(
proc_macro2::Span::call_site(),
"missing FutureForm variants: expected #[future_form(Sendable)], #[future_form(Local)], or #[future_form(Sendable, Local)]",
));
}
let mut seen: Vec<&Path> = Vec::new();
for kind in &kinds {
if let Some(dup) = seen.iter().find(|p| paths_ident_equal(p, &kind.kind_path)) {
let dup_name = dup
.segments
.last()
.map_or_else(|| "unknown".to_string(), |s| s.ident.to_string());
return Err(make_error(
kind.kind_path
.segments
.last()
.map_or_else(proc_macro2::Span::call_site, |s| s.ident.span()),
&format!(
"duplicate FutureForm variant `{dup_name}`; each variant may only appear once"
),
));
}
seen.push(&kind.kind_path);
}
Ok(kinds)
}
#[allow(clippy::expect_used)] fn collect_where_clause(
tokens: &[proc_macro2::TokenTree],
start: usize,
span: proc_macro2::Span,
) -> Result<(Vec<WherePredicate>, usize), TokenStream> {
use proc_macro2::TokenTree;
let mut i = start;
let mut current_predicate_tokens: Vec<TokenTree> = Vec::new();
let mut predicates = Vec::new();
let mut in_bound = false;
let mut angle_depth: u32 = 0;
while i < tokens.len() {
let token = tokens.get(i).expect("bounds checked");
if let TokenTree::Punct(p) = token {
match p.as_char() {
'<' => angle_depth = angle_depth.saturating_add(1),
'>' => angle_depth = angle_depth.saturating_sub(1),
':' if angle_depth == 0 => in_bound = true,
',' if angle_depth == 0 => in_bound = false,
_ => {}
}
}
let at_top_level = angle_depth == 0;
if at_top_level
&& !in_bound
&& current_predicate_tokens.is_empty()
&& let TokenTree::Ident(ident) = token
&& is_likely_variant(ident)
&& !tokens
.get(i + 1)
.is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':'))
{
return Ok((predicates, i));
}
if at_top_level
&& let TokenTree::Punct(p) = token
&& p.as_char() == ','
{
let next_is_variant = tokens.get(i + 1).is_some_and(|t| {
if let TokenTree::Ident(id) = t {
is_likely_variant(id)
&& !tokens.get(i + 2).is_some_and(
|t2| matches!(t2, TokenTree::Punct(p2) if p2.as_char() == ':'),
)
} else {
false
}
});
if next_is_variant {
if !current_predicate_tokens.is_empty() {
predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?);
}
i += 1; return Ok((predicates, i));
}
if !current_predicate_tokens.is_empty() {
predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?);
current_predicate_tokens.clear();
}
i += 1;
continue;
}
current_predicate_tokens.push(token.clone());
i += 1;
}
if !current_predicate_tokens.is_empty() {
predicates.push(parse_predicate_tokens(¤t_predicate_tokens, span)?);
}
Ok((predicates, i))
}
fn parse_predicate_tokens(
tokens: &[proc_macro2::TokenTree],
fallback_span: proc_macro2::Span,
) -> Result<WherePredicate, TokenStream> {
use proc_macro2::TokenTree;
let token_stream: TokenStream2 = tokens.iter().cloned().collect();
let token_str = token_stream.to_string();
let span = tokens.first().map_or(fallback_span, TokenTree::span);
syn::parse2::<WherePredicate>(token_stream).map_err(|e| {
let comma_hint = tokens.last().and_then(|last| {
if let TokenTree::Ident(ident) = last
&& is_likely_variant(ident)
{
Some(format!("; did you forget a comma before `{ident}`?"))
} else {
None
}
});
let hint = comma_hint.unwrap_or_default();
make_error(
span,
&format!("malformed where clause `{token_str}`: {e}{hint}"),
)
})
}
fn generate_impls(input: &ItemImpl, kinds: &[FutureFormVariant]) -> syn::Result<TokenStream2> {
if input.trait_.is_none() {
return Err(syn::Error::new_spanned(
&input.self_ty,
"#[future_form] can only be applied to trait impls, not inherent impls; \
move these methods into a trait and apply #[future_form] to the trait impl",
));
}
let k_param = find_k_param(input)?;
check_for_async_fn(input, &k_param)?;
check_k_usage(input, &k_param)?;
let impls: Vec<TokenStream2> = kinds
.iter()
.map(|kind| generate_impl_for_kind(input, &k_param, kind))
.collect();
Ok(quote! {
#(#impls)*
})
}
fn check_for_async_fn(input: &ItemImpl, k_param: &Ident) -> syn::Result<()> {
for item in &input.items {
if let ImplItem::Fn(method) = item
&& method.sig.asyncness.is_some()
{
return Err(syn::Error::new_spanned(
method.sig.asyncness,
format!(
"`async fn` is not supported in #[future_form] impl blocks; \
return `{k_param}::Future<'_, T>` and use \
`{k_param}::from_future(async {{ ... }})` instead"
),
));
}
}
Ok(())
}
fn check_k_usage(input: &ItemImpl, k_param: &Ident) -> syn::Result<()> {
if let Some((_, ref trait_path, _)) = input.trait_ {
let mut finder = IdentFinder {
target: k_param.clone(),
found: false,
};
finder.visit_path(trait_path);
if finder.found {
return Ok(());
}
}
{
let mut finder = IdentFinder {
target: k_param.clone(),
found: false,
};
finder.visit_type(&input.self_ty);
if finder.found {
return Ok(());
}
}
for item in &input.items {
let mut finder = IdentFinder {
target: k_param.clone(),
found: false,
};
finder.visit_impl_item(item);
if finder.found {
return Ok(());
}
}
Err(syn::Error::new(
k_param.span(),
format!(
"the FutureForm parameter `{k_param}` is not referenced in this impl block; \
#[future_form] may not be needed here"
),
))
}
#[derive(Clone)]
struct FutureFormVariant {
kind_path: Path,
future_path: Option<Path>,
extra_bounds: Vec<WherePredicate>,
}
struct KindReplacer {
from_ident: Ident,
to_path: Path,
future_path: Option<Path>,
}
impl VisitMut for KindReplacer {
fn visit_path_mut(&mut self, path: &mut Path) {
visit_mut::visit_path_mut(self, path);
if let Some(first) = path.segments.first()
&& first.ident == self.from_ident
&& first.arguments.is_empty()
{
if path.segments.len() == 1 {
*path = self.to_path.clone();
} else if let Some(second) = path.segments.get(1) {
if second.ident == "Future" {
if let Some(ref concrete_future) = self.future_path {
let args = second.arguments.clone();
let mut new_path = concrete_future.clone();
if let Some(last) = new_path.segments.last_mut() {
last.arguments = args;
}
*path = new_path;
} else {
let remaining: Vec<_> = path.segments.iter().skip(1).cloned().collect();
let mut new_path = self.to_path.clone();
new_path.segments.extend(remaining);
*path = new_path;
}
} else {
let remaining: Vec<_> = path.segments.iter().skip(1).cloned().collect();
let mut new_path = self.to_path.clone();
new_path.segments.extend(remaining);
*path = new_path;
}
}
}
}
}
struct IdentFinder {
target: Ident,
found: bool,
}
impl<'ast> Visit<'ast> for IdentFinder {
fn visit_path(&mut self, path: &'ast Path) {
if let Some(first) = path.segments.first()
&& path.segments.len() == 1
&& first.ident == self.target
&& first.arguments.is_empty()
{
self.found = true;
}
syn::visit::visit_path(self, path);
}
}
fn find_k_param(input: &ItemImpl) -> syn::Result<Ident> {
let mut candidates: Vec<Ident> = Vec::new();
for param in &input.generics.params {
if let GenericParam::Type(type_param) = param {
for bound in &type_param.bounds {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
let path = &trait_bound.path;
if path
.segments
.last()
.is_some_and(|s| s.ident == "FutureForm")
{
candidates.push(type_param.ident.clone());
}
}
}
}
}
if let Some(ref where_clause) = input.generics.where_clause {
for pred in &where_clause.predicates {
if let WherePredicate::Type(type_pred) = pred {
let has_future_form = type_pred.bounds.iter().any(|bound| {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
trait_bound
.path
.segments
.last()
.is_some_and(|s| s.ident == "FutureForm")
} else {
false
}
});
if has_future_form
&& let Type::Path(type_path) = &type_pred.bounded_ty
&& type_path.qself.is_none()
&& type_path.path.segments.len() == 1
&& let Some(seg) = type_path.path.segments.first()
&& seg.arguments.is_empty()
&& !candidates.contains(&seg.ident)
{
candidates.push(seg.ident.clone());
}
}
}
}
match candidates.len() {
0 => Err(syn::Error::new_spanned(
&input.generics,
"expected a type parameter with FutureForm bound (e.g., `K: FutureForm`)",
)),
1 => Ok(candidates.swap_remove(0)),
_ => {
let names: Vec<String> = candidates.iter().map(ToString::to_string).collect();
Err(syn::Error::new_spanned(
&input.generics,
format!(
"found multiple type parameters with FutureForm bound ({}); \
only one FutureForm parameter is supported",
names.join(", ")
),
))
}
}
}
fn generate_impl_for_kind(
input: &ItemImpl,
k_param: &Ident,
variant: &FutureFormVariant,
) -> TokenStream2 {
let kind_path: Path = variant.kind_path.clone();
let future_path: Option<Path> = variant.future_path.clone();
let mut new_generics = input.generics.clone();
new_generics.params = new_generics
.params
.into_iter()
.filter(|p| {
if let GenericParam::Type(tp) = p {
tp.ident != *k_param
} else {
true
}
})
.collect();
let mut param_replacer = KindReplacer {
from_ident: k_param.clone(),
to_path: kind_path.clone(),
future_path: future_path.clone(),
};
for param in &mut new_generics.params {
if let GenericParam::Type(tp) = param {
param_replacer.visit_type_param_mut(tp);
}
}
if let Some(ref mut where_clause) = new_generics.where_clause {
where_clause.predicates = where_clause
.predicates
.clone()
.into_iter()
.filter_map(|pred| {
if predicate_references_ident(&pred, k_param) {
let rewritten = rewrite_predicate(&pred, k_param, &kind_path, &future_path);
if is_tautological_predicate(&rewritten, &kind_path) {
None
} else {
Some(rewritten)
}
} else {
Some(pred)
}
})
.collect();
for bound in &variant.extra_bounds {
where_clause.predicates.push(bound.clone());
}
} else if !variant.extra_bounds.is_empty() {
let mut predicates = syn::punctuated::Punctuated::new();
for bound in &variant.extra_bounds {
predicates.push(bound.clone());
}
new_generics.where_clause = Some(syn::WhereClause {
where_token: syn::token::Where::default(),
predicates,
});
}
let new_self_ty = replace_ident_in_type(&input.self_ty, k_param, &kind_path, &future_path);
let new_trait = input.trait_.as_ref().map(|(bang, path, for_token)| {
let new_path = replace_ident_in_path(path, k_param, &kind_path, &future_path);
(*bang, new_path, *for_token)
});
let new_items: Vec<ImplItem> = input
.items
.iter()
.map(|item| transform_impl_item(item, k_param, &kind_path, &future_path))
.collect();
let (impl_generics, _, where_clause) = new_generics.split_for_impl();
let trait_tokens = new_trait.map(|(bang, path, for_token)| {
quote! { #bang #path #for_token }
});
quote! {
impl #impl_generics #trait_tokens #new_self_ty #where_clause {
#(#new_items)*
}
}
}
fn predicate_references_ident(pred: &syn::WherePredicate, ident: &Ident) -> bool {
let mut finder = IdentFinder {
target: ident.clone(),
found: false,
};
finder.visit_where_predicate(pred);
finder.found
}
#[allow(clippy::ref_option)]
fn rewrite_predicate(
pred: &WherePredicate,
from: &Ident,
kind_path: &Path,
future_path: &Option<Path>,
) -> WherePredicate {
let mut pred = pred.clone();
let mut replacer = KindReplacer {
from_ident: from.clone(),
to_path: kind_path.clone(),
future_path: future_path.clone(),
};
replacer.visit_where_predicate_mut(&mut pred);
pred
}
fn paths_ident_equal(a: &Path, b: &Path) -> bool {
a.leading_colon.is_some() == b.leading_colon.is_some()
&& a.segments.len() == b.segments.len()
&& a.segments
.iter()
.zip(b.segments.iter())
.all(|(sa, sb)| sa.ident == sb.ident)
}
fn is_tautological_predicate(pred: &WherePredicate, kind_path: &Path) -> bool {
let WherePredicate::Type(type_pred) = pred else {
return false;
};
let Type::Path(type_path) = &type_pred.bounded_ty else {
return false;
};
if type_path.qself.is_some() || !paths_ident_equal(&type_path.path, kind_path) {
return false;
}
type_pred.bounds.iter().all(|bound| match bound {
syn::TypeParamBound::Trait(trait_bound) => {
let is_maybe_sized = matches!(trait_bound.modifier, syn::TraitBoundModifier::Maybe(_))
&& trait_bound
.path
.segments
.last()
.is_some_and(|s| s.ident == "Sized");
let is_future_form = trait_bound
.path
.segments
.last()
.is_some_and(|s| s.ident == "FutureForm");
is_maybe_sized || is_future_form
}
syn::TypeParamBound::Lifetime(_)
| syn::TypeParamBound::PreciseCapture(_)
| syn::TypeParamBound::Verbatim(_)
| _ => false,
})
}
#[allow(clippy::ref_option)] fn replace_ident_in_type(
ty: &Type,
from: &Ident,
kind_path: &Path,
future_path: &Option<Path>,
) -> Type {
let mut ty = ty.clone();
let mut replacer = KindReplacer {
from_ident: from.clone(),
to_path: kind_path.clone(),
future_path: future_path.clone(),
};
replacer.visit_type_mut(&mut ty);
ty
}
#[allow(clippy::ref_option)]
fn replace_ident_in_path(
path: &Path,
from: &Ident,
kind_path: &Path,
future_path: &Option<Path>,
) -> Path {
let mut path = path.clone();
let mut replacer = KindReplacer {
from_ident: from.clone(),
to_path: kind_path.clone(),
future_path: future_path.clone(),
};
replacer.visit_path_mut(&mut path);
path
}
#[allow(clippy::ref_option)]
fn transform_impl_item(
item: &ImplItem,
k_param: &Ident,
kind_path: &Path,
future_path: &Option<Path>,
) -> ImplItem {
let mut item = item.clone();
let mut replacer = KindReplacer {
from_ident: k_param.clone(),
to_path: kind_path.clone(),
future_path: future_path.clone(),
};
replacer.visit_impl_item_mut(&mut item);
item
}