Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ follows <https://www.conventionalcommits.org/en/v1.0.0/> 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

Expand Down
86 changes: 62 additions & 24 deletions macros/src/dynify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,32 @@ use crate::lifetime::TraitContext;
use crate::utils::*;

pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
let rename = syn::parse2::<Option<Ident>>(attr)?;
let input_item = syn::parse2::<syn::Item>(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::<Option<Ident>>(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<Ident>, mut dyn_trait: syn::ItemTrait) -> Result<TokenStream> {
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() {
Expand Down Expand Up @@ -51,11 +66,17 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
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,
Expand All @@ -65,27 +86,36 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {

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<Ident>, mut dyn_fn: syn::ItemFn) -> Result<TokenStream> {
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() {
Expand All @@ -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)*))
},
}
}
Expand Down Expand Up @@ -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<TransformResult> {
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| {
Expand All @@ -174,7 +212,7 @@ fn transform_fn(
});
let output_type = match &sig.output {
ReturnType::Default => ReturnType::Type(
<Token![->]>::default(),
NewToken![->],
parse_quote_spanned!(fn_span => ::dynify::r#priv::Fn<
(#input_types),
dyn #output_lifetime + ::core::future::Future<Output = ()>
Expand Down
13 changes: 13 additions & 0 deletions macros/src/dynify_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions macros/src/dynify_tests/fn_renamed.rs
Original file line number Diff line number Diff line change
@@ -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<Output = String>,
>
where
'_arg1: 'dynify,
{
::dynify::__from_fn!([] test, _arg1,)
}
fn main() {}
17 changes: 17 additions & 0 deletions macros/src/dynify_tests/fn_returning_async.rs
Original file line number Diff line number Diff line change
@@ -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<Output = String>,
>
where
'_arg1: 'dynify,
{
::dynify::__from_fn!([] test, _arg1,)
}
fn main() {}
9 changes: 9 additions & 0 deletions macros/src/dynify_tests/fn_returning_impl.rs
Original file line number Diff line number Diff line change
@@ -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() {}
4 changes: 2 additions & 2 deletions macros/src/lifetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -210,7 +210,7 @@ impl visit_mut::VisitMut for LifetimeCollector<'_> {

fn default_where_clause(where_clause: &mut Option<syn::WhereClause>) -> &mut syn::WhereClause {
where_clause.get_or_insert_with(|| syn::WhereClause {
where_token: <Token![where]>::default(),
where_token: NewToken![where],
predicates: Punctuated::new(),
})
}
Expand Down
4 changes: 4 additions & 0 deletions macros/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ macro_rules! as_variant {
};
}

macro_rules! NewToken {
($($tt:tt)*) => (<::syn::Token![$($tt)*]>::default());
}

pub(crate) fn quote_with<F: Fn(&mut TokenStream)>(f: F) -> QuoteWith<F> {
QuoteWith(f)
}
Expand Down
2 changes: 1 addition & 1 deletion tests/compile_fail/dynify_with_unknown_receiver.stderr
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
5 changes: 4 additions & 1 deletion tests/compile_fail/dynify_with_unsupported_item.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#[dynify::dynify]
fn test() {}
fn test1() {}

#[dynify::dynify]
fn test2() -> FakeImpl {}

#[dynify::dynify]
opaque_trait!();
Expand Down
20 changes: 13 additions & 7 deletions tests/compile_fail/dynify_with_unsupported_item.stderr
Original file line number Diff line number Diff line change
@@ -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!();
| ^^^^^^^^^^^^^^^^
5 changes: 5 additions & 0 deletions tests/compile_pass/macro_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
todo!()
}

fn main() {}