diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9b01f..a71ec6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,10 +42,12 @@ follows to track changes. - Support downcasting a `Buffered` pointer ([#10]). - Support unwrapping a `Buffered` pointer ([#11]). - Add a helper macro `#[dynify]` for trait transformations ([#12]). +- Support transformation of a function for `#[dynify]` ([#13]) [#10]: https://github.com/loichyan/dynify/pull/10 [#11]: https://github.com/loichyan/dynify/pull/11 [#12]: https://github.com/loichyan/dynify/pull/12 +[#13]: https://github.com/loichyan/dynify/pull/13 ## [0.1.0] - 2025-07-06 diff --git a/macros/src/dynify.rs b/macros/src/dynify.rs index 5811ba8..2c09d32 100644 --- a/macros/src/dynify.rs +++ b/macros/src/dynify.rs @@ -6,17 +6,32 @@ use crate::lifetime::TraitContext; use crate::utils::*; pub fn expand(attr: TokenStream, input: TokenStream) -> Result { + let rename = syn::parse2::>(attr)?; let input_item = syn::parse2::(input.clone())?; - // TODO: support non-trait items - let mut dyn_trait = as_variant!(input_item, syn::Item::Trait) - .ok_or_else(|| syn::Error::new_spanned(&input, "non-trait item is not supported yet"))?; - let mut trait_impl_items = TokenStream::new(); + let output = match input_item { + syn::Item::Trait(t) => expand_trait(rename, t)?, + syn::Item::Fn(f) => expand_fn(rename, f)?, + item => { + return Err(syn::Error::new_spanned( + &item, + "expected a `fn` or `trait` item", + )) + }, + }; + Ok(quote!( + #[allow(async_fn_in_trait)] + #input + #output + )) +} - let dyn_trait_name = syn::parse2::>(attr)? - .unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident)); - let trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name); +fn expand_trait(rename: Option, mut dyn_trait: syn::ItemTrait) -> Result { + let dyn_trait_name = rename.unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident)); + let input_trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name); let dyn_trait_name = &dyn_trait.ident; - let impl_target = format_ident!("{}Implementor", trait_name); + + let impl_target = format_ident!("{}Implementor", input_trait_name); + let mut trait_impl_items = TokenStream::new(); let (_, ty_generics, where_clause) = dyn_trait.generics.split_for_impl(); for item in dyn_trait.items.iter_mut() { @@ -51,11 +66,17 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result { let context = TraitContext { generics: &dyn_trait.generics, }; - let transformed = transform_fn(&context, sig, false)?; + let transformed = transform_fn(Some(&context), sig, false)?; // TODO: support `#[dynify(skip)]` + // TODO: support nested `#[dynify]` let attrs_outer = attrs.outer(); let attrs_inner = attrs.inner(); - let impl_body = quote_transformed_body(transformed, &impl_target, sig); + let target = quote_with(|tokens| { + impl_target.to_tokens(tokens); + NewToken![::].to_tokens(tokens); + sig.ident.to_tokens(tokens); + }); + let impl_body = quote_transformed_body(transformed, &target, sig); quote!(#(#attrs_outer)* #sig { #(#attrs_inner)* #impl_body }) }, _ => continue, @@ -65,27 +86,36 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result { let impl_generics = quote_impl_generics(&dyn_trait.generics); Ok(quote!( - #[allow(async_fn_in_trait)] - #input - #[allow(async_fn_in_trait)] #[allow(clippy::type_complexity)] #dyn_trait #[allow(clippy::type_complexity)] - impl<#impl_generics #impl_target: #trait_name #ty_generics> + impl<#impl_generics #impl_target: #input_trait_name #ty_generics> #dyn_trait_name #ty_generics for #impl_target #where_clause { #trait_impl_items } )) } +fn expand_fn(rename: Option, mut dyn_fn: syn::ItemFn) -> Result { + let syn::ItemFn { sig, attrs, .. } = &mut dyn_fn; + + let dyn_fn_name = rename.unwrap_or_else(|| format_ident!("dyn_{}", sig.ident)); + let input_fn_name = std::mem::replace(&mut sig.ident, dyn_fn_name); + + let transformed = transform_fn(None, sig, true)?; + let attrs_outer = attrs.outer(); + let attrs_inner = attrs.inner(); + let impl_body = quote_transformed_body(transformed, &input_fn_name, sig); + Ok(quote!(#(#attrs_outer)* #sig { #(#attrs_inner)* #impl_body })) +} + /// Generates implementation body for a transformed function. fn quote_transformed_body( transformed: TransformResult, - target: &Ident, + target: &dyn ToTokens, sig: &syn::Signature, ) -> impl ToTokens { - let ident = &sig.ident; let arg_idents = sig.inputs.pairs().map(|p| { quote_with(move |tokens| { match p.value() { @@ -95,16 +125,17 @@ fn quote_transformed_body( p.punct_or_default().to_tokens(tokens); }) }); + match transformed { TransformResult::Noop if sig.asyncness.is_some() => { - quote!(#target::#ident(#(#arg_idents)*).await) + quote!(#target (#(#arg_idents)*).await) }, TransformResult::Noop => { - quote!(#target::#ident(#(#arg_idents)*)) + quote!(#target (#(#arg_idents)*)) }, TransformResult::Function | TransformResult::Method => { let recv = sig.receiver().map(|r| &r.self_token); - quote!(::dynify::__from_fn!([#recv] #target::#ident, #(#arg_idents)*)) + quote!(::dynify::__from_fn!([#recv] #target, #(#arg_idents)*)) }, } } @@ -135,25 +166,32 @@ enum TransformResult { /// Transforms the supplied function into a dynified one, returning `true` only /// if the transformation is successful. fn transform_fn( - context: &TraitContext, + context: Option<&TraitContext>, sig: &mut syn::Signature, force: bool, ) -> Result { let fn_span = sig.ident.span(); if sig.asyncness.is_none() && get_impl_type(&sig.output).is_none() { - return Ok(TransformResult::Noop); + if force { + return Err(syn::Error::new( + fn_span, + "input function must return an `impl` type", + )); + } else { + return Ok(TransformResult::Noop); + } } let sealed_recv = match sig.receiver() { Some(r) => crate::receiver::infer_receiver(r) - .ok_or_else(|| syn::Error::new(r.self_token.span, "cannot determine receiver type")) + .ok_or_else(|| syn::Error::new(r.self_token.span, "unsupported receiver type")) .map(Some)?, None if force => None, None => return Ok(TransformResult::Noop), }; let output_lifetime = Lifetime::new("'dynify", fn_span); - crate::lifetime::inject_output_lifetime(Some(context), sig, &output_lifetime)?; + crate::lifetime::inject_output_lifetime(context, sig, &output_lifetime)?; // Infer the appropriate output type let input_types = quote_with(|tokens| { @@ -174,7 +212,7 @@ fn transform_fn( }); let output_type = match &sig.output { ReturnType::Default => ReturnType::Type( - ]>::default(), + NewToken![->], parse_quote_spanned!(fn_span => ::dynify::r#priv::Fn< (#input_types), dyn #output_lifetime + ::core::future::Future diff --git a/macros/src/dynify_tests.rs b/macros/src/dynify_tests.rs index 671b747..07357fb 100644 --- a/macros/src/dynify_tests.rs +++ b/macros/src/dynify_tests.rs @@ -79,6 +79,19 @@ define_macro_tests!( quote!(MyDynTrait), quote!(trait Trait { async fn test(&self); }), )] + // == Functions == // + #[case::fn_returning_impl( + quote!(), + quote!(fn test() -> impl core::any::Any { todo!() }), + )] + #[case::fn_returning_async( + quote!(), + quote!(async fn test(_arg1: &str) -> String { todo!() }), + )] + #[case::fn_renamed( + quote!(my_dyn_test), + quote!(async fn test(_arg1: &str) -> String { todo!() }), + )] fn ui(#[case] test_name: &str, #[case] attr: TokenStream, #[case] input: TokenStream) { let output = expand(attr, input).unwrap(); // Append `fn main() {}` so that they can pass compile tests diff --git a/macros/src/dynify_tests/fn_renamed.rs b/macros/src/dynify_tests/fn_renamed.rs new file mode 100644 index 0000000..835943b --- /dev/null +++ b/macros/src/dynify_tests/fn_renamed.rs @@ -0,0 +1,17 @@ +/* This file is @generated for testing purpose */ +#[allow(async_fn_in_trait)] +async fn test(_arg1: &str) -> String { + todo!() +} +fn my_dyn_test<'_arg1, 'dynify>( + _arg1: &'_arg1 str, +) -> ::dynify::r#priv::Fn< + (&'_arg1 str,), + dyn 'dynify + ::core::future::Future, +> +where + '_arg1: 'dynify, +{ + ::dynify::__from_fn!([] test, _arg1,) +} +fn main() {} diff --git a/macros/src/dynify_tests/fn_returning_async.rs b/macros/src/dynify_tests/fn_returning_async.rs new file mode 100644 index 0000000..afb4152 --- /dev/null +++ b/macros/src/dynify_tests/fn_returning_async.rs @@ -0,0 +1,17 @@ +/* This file is @generated for testing purpose */ +#[allow(async_fn_in_trait)] +async fn test(_arg1: &str) -> String { + todo!() +} +fn dyn_test<'_arg1, 'dynify>( + _arg1: &'_arg1 str, +) -> ::dynify::r#priv::Fn< + (&'_arg1 str,), + dyn 'dynify + ::core::future::Future, +> +where + '_arg1: 'dynify, +{ + ::dynify::__from_fn!([] test, _arg1,) +} +fn main() {} diff --git a/macros/src/dynify_tests/fn_returning_impl.rs b/macros/src/dynify_tests/fn_returning_impl.rs new file mode 100644 index 0000000..057d8e8 --- /dev/null +++ b/macros/src/dynify_tests/fn_returning_impl.rs @@ -0,0 +1,9 @@ +/* This file is @generated for testing purpose */ +#[allow(async_fn_in_trait)] +fn test() -> impl core::any::Any { + todo!() +} +fn dyn_test<'dynify>() -> ::dynify::r#priv::Fn<(), dyn 'dynify + core::any::Any> { + ::dynify::__from_fn!([] test,) +} +fn main() {} diff --git a/macros/src/lifetime.rs b/macros/src/lifetime.rs index 106ed74..7214f6a 100644 --- a/macros/src/lifetime.rs +++ b/macros/src/lifetime.rs @@ -5,7 +5,7 @@ use quote::format_ident; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; -use syn::{parse_quote, parse_quote_spanned, visit_mut, FnArg, Ident, Lifetime, Result, Token}; +use syn::{parse_quote, parse_quote_spanned, visit_mut, FnArg, Ident, Lifetime, Result}; pub(crate) struct TraitContext<'a> { pub generics: &'a syn::Generics, @@ -210,7 +210,7 @@ impl visit_mut::VisitMut for LifetimeCollector<'_> { fn default_where_clause(where_clause: &mut Option) -> &mut syn::WhereClause { where_clause.get_or_insert_with(|| syn::WhereClause { - where_token: ::default(), + where_token: NewToken![where], predicates: Punctuated::new(), }) } diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 815c7e8..17aca99 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -17,6 +17,10 @@ macro_rules! as_variant { }; } +macro_rules! NewToken { + ($($tt:tt)*) => (<::syn::Token![$($tt)*]>::default()); +} + pub(crate) fn quote_with(f: F) -> QuoteWith { QuoteWith(f) } diff --git a/tests/compile_fail/dynify_with_unknown_receiver.stderr b/tests/compile_fail/dynify_with_unknown_receiver.stderr index e594b3a..b93dda7 100644 --- a/tests/compile_fail/dynify_with_unknown_receiver.stderr +++ b/tests/compile_fail/dynify_with_unknown_receiver.stderr @@ -1,4 +1,4 @@ -error: cannot determine receiver type +error: unsupported receiver type --> tests/compile_fail/dynify_with_unknown_receiver.rs:3:19 | 3 | async fn test(self: MySelf); diff --git a/tests/compile_fail/dynify_with_unsupported_item.rs b/tests/compile_fail/dynify_with_unsupported_item.rs index ac76c06..f8f4e62 100644 --- a/tests/compile_fail/dynify_with_unsupported_item.rs +++ b/tests/compile_fail/dynify_with_unsupported_item.rs @@ -1,5 +1,8 @@ #[dynify::dynify] -fn test() {} +fn test1() {} + +#[dynify::dynify] +fn test2() -> FakeImpl {} #[dynify::dynify] opaque_trait!(); diff --git a/tests/compile_fail/dynify_with_unsupported_item.stderr b/tests/compile_fail/dynify_with_unsupported_item.stderr index 0a3b3de..4269fd8 100644 --- a/tests/compile_fail/dynify_with_unsupported_item.stderr +++ b/tests/compile_fail/dynify_with_unsupported_item.stderr @@ -1,11 +1,17 @@ -error: non-trait item is not supported yet - --> tests/compile_fail/dynify_with_unsupported_item.rs:2:1 +error: input function must return an `impl` type + --> tests/compile_fail/dynify_with_unsupported_item.rs:2:4 | -2 | fn test() {} - | ^^^^^^^^^^^^ +2 | fn test1() {} + | ^^^^^ -error: non-trait item is not supported yet - --> tests/compile_fail/dynify_with_unsupported_item.rs:5:1 +error: input function must return an `impl` type + --> tests/compile_fail/dynify_with_unsupported_item.rs:5:4 | -5 | opaque_trait!(); +5 | fn test2() -> FakeImpl {} + | ^^^^^ + +error: expected a `fn` or `trait` item + --> tests/compile_fail/dynify_with_unsupported_item.rs:8:1 + | +8 | opaque_trait!(); | ^^^^^^^^^^^^^^^^ diff --git a/tests/compile_pass/macro_example.rs b/tests/compile_pass/macro_example.rs index 4e1b3e8..c624178 100644 --- a/tests/compile_pass/macro_example.rs +++ b/tests/compile_pass/macro_example.rs @@ -14,4 +14,9 @@ pub trait MyAsync<'x, T> { fn foo6<'a>(&self, s: &str) -> impl 'a + Send + std::any::Any; } +#[dynify::dynify] +pub async fn my_func(_id: &str) -> Vec { + todo!() +} + fn main() {}