Skip to content

Commit 84883ca

Browse files
authored
feat(macros): support transformation of a function for #[dynify] (#13)
1 parent 3f2b478 commit 84883ca

File tree

12 files changed

+149
-35
lines changed

12 files changed

+149
-35
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ follows <https://www.conventionalcommits.org/en/v1.0.0/> to track changes.
4242
- Support downcasting a `Buffered` pointer ([#10]).
4343
- Support unwrapping a `Buffered` pointer ([#11]).
4444
- Add a helper macro `#[dynify]` for trait transformations ([#12]).
45+
- Support transformation of a function for `#[dynify]` ([#13])
4546

4647
[#10]: https://github.com/loichyan/dynify/pull/10
4748
[#11]: https://github.com/loichyan/dynify/pull/11
4849
[#12]: https://github.com/loichyan/dynify/pull/12
50+
[#13]: https://github.com/loichyan/dynify/pull/13
4951

5052
## [0.1.0] - 2025-07-06
5153

macros/src/dynify.rs

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,32 @@ use crate::lifetime::TraitContext;
66
use crate::utils::*;
77

88
pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
9+
let rename = syn::parse2::<Option<Ident>>(attr)?;
910
let input_item = syn::parse2::<syn::Item>(input.clone())?;
10-
// TODO: support non-trait items
11-
let mut dyn_trait = as_variant!(input_item, syn::Item::Trait)
12-
.ok_or_else(|| syn::Error::new_spanned(&input, "non-trait item is not supported yet"))?;
13-
let mut trait_impl_items = TokenStream::new();
11+
let output = match input_item {
12+
syn::Item::Trait(t) => expand_trait(rename, t)?,
13+
syn::Item::Fn(f) => expand_fn(rename, f)?,
14+
item => {
15+
return Err(syn::Error::new_spanned(
16+
&item,
17+
"expected a `fn` or `trait` item",
18+
))
19+
},
20+
};
21+
Ok(quote!(
22+
#[allow(async_fn_in_trait)]
23+
#input
24+
#output
25+
))
26+
}
1427

15-
let dyn_trait_name = syn::parse2::<Option<Ident>>(attr)?
16-
.unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident));
17-
let trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name);
28+
fn expand_trait(rename: Option<Ident>, mut dyn_trait: syn::ItemTrait) -> Result<TokenStream> {
29+
let dyn_trait_name = rename.unwrap_or_else(|| format_ident!("Dyn{}", dyn_trait.ident));
30+
let input_trait_name = std::mem::replace(&mut dyn_trait.ident, dyn_trait_name);
1831
let dyn_trait_name = &dyn_trait.ident;
19-
let impl_target = format_ident!("{}Implementor", trait_name);
32+
33+
let impl_target = format_ident!("{}Implementor", input_trait_name);
34+
let mut trait_impl_items = TokenStream::new();
2035

2136
let (_, ty_generics, where_clause) = dyn_trait.generics.split_for_impl();
2237
for item in dyn_trait.items.iter_mut() {
@@ -51,11 +66,17 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
5166
let context = TraitContext {
5267
generics: &dyn_trait.generics,
5368
};
54-
let transformed = transform_fn(&context, sig, false)?;
69+
let transformed = transform_fn(Some(&context), sig, false)?;
5570
// TODO: support `#[dynify(skip)]`
71+
// TODO: support nested `#[dynify]`
5672
let attrs_outer = attrs.outer();
5773
let attrs_inner = attrs.inner();
58-
let impl_body = quote_transformed_body(transformed, &impl_target, sig);
74+
let target = quote_with(|tokens| {
75+
impl_target.to_tokens(tokens);
76+
NewToken![::].to_tokens(tokens);
77+
sig.ident.to_tokens(tokens);
78+
});
79+
let impl_body = quote_transformed_body(transformed, &target, sig);
5980
quote!(#(#attrs_outer)* #sig { #(#attrs_inner)* #impl_body })
6081
},
6182
_ => continue,
@@ -65,27 +86,36 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
6586

6687
let impl_generics = quote_impl_generics(&dyn_trait.generics);
6788
Ok(quote!(
68-
#[allow(async_fn_in_trait)]
69-
#input
70-
7189
#[allow(async_fn_in_trait)]
7290
#[allow(clippy::type_complexity)]
7391
#dyn_trait
7492

7593
#[allow(clippy::type_complexity)]
76-
impl<#impl_generics #impl_target: #trait_name #ty_generics>
94+
impl<#impl_generics #impl_target: #input_trait_name #ty_generics>
7795
#dyn_trait_name #ty_generics for #impl_target
7896
#where_clause { #trait_impl_items }
7997
))
8098
}
8199

100+
fn expand_fn(rename: Option<Ident>, mut dyn_fn: syn::ItemFn) -> Result<TokenStream> {
101+
let syn::ItemFn { sig, attrs, .. } = &mut dyn_fn;
102+
103+
let dyn_fn_name = rename.unwrap_or_else(|| format_ident!("dyn_{}", sig.ident));
104+
let input_fn_name = std::mem::replace(&mut sig.ident, dyn_fn_name);
105+
106+
let transformed = transform_fn(None, sig, true)?;
107+
let attrs_outer = attrs.outer();
108+
let attrs_inner = attrs.inner();
109+
let impl_body = quote_transformed_body(transformed, &input_fn_name, sig);
110+
Ok(quote!(#(#attrs_outer)* #sig { #(#attrs_inner)* #impl_body }))
111+
}
112+
82113
/// Generates implementation body for a transformed function.
83114
fn quote_transformed_body(
84115
transformed: TransformResult,
85-
target: &Ident,
116+
target: &dyn ToTokens,
86117
sig: &syn::Signature,
87118
) -> impl ToTokens {
88-
let ident = &sig.ident;
89119
let arg_idents = sig.inputs.pairs().map(|p| {
90120
quote_with(move |tokens| {
91121
match p.value() {
@@ -95,16 +125,17 @@ fn quote_transformed_body(
95125
p.punct_or_default().to_tokens(tokens);
96126
})
97127
});
128+
98129
match transformed {
99130
TransformResult::Noop if sig.asyncness.is_some() => {
100-
quote!(#target::#ident(#(#arg_idents)*).await)
131+
quote!(#target (#(#arg_idents)*).await)
101132
},
102133
TransformResult::Noop => {
103-
quote!(#target::#ident(#(#arg_idents)*))
134+
quote!(#target (#(#arg_idents)*))
104135
},
105136
TransformResult::Function | TransformResult::Method => {
106137
let recv = sig.receiver().map(|r| &r.self_token);
107-
quote!(::dynify::__from_fn!([#recv] #target::#ident, #(#arg_idents)*))
138+
quote!(::dynify::__from_fn!([#recv] #target, #(#arg_idents)*))
108139
},
109140
}
110141
}
@@ -135,25 +166,32 @@ enum TransformResult {
135166
/// Transforms the supplied function into a dynified one, returning `true` only
136167
/// if the transformation is successful.
137168
fn transform_fn(
138-
context: &TraitContext,
169+
context: Option<&TraitContext>,
139170
sig: &mut syn::Signature,
140171
force: bool,
141172
) -> Result<TransformResult> {
142173
let fn_span = sig.ident.span();
143174
if sig.asyncness.is_none() && get_impl_type(&sig.output).is_none() {
144-
return Ok(TransformResult::Noop);
175+
if force {
176+
return Err(syn::Error::new(
177+
fn_span,
178+
"input function must return an `impl` type",
179+
));
180+
} else {
181+
return Ok(TransformResult::Noop);
182+
}
145183
}
146184

147185
let sealed_recv = match sig.receiver() {
148186
Some(r) => crate::receiver::infer_receiver(r)
149-
.ok_or_else(|| syn::Error::new(r.self_token.span, "cannot determine receiver type"))
187+
.ok_or_else(|| syn::Error::new(r.self_token.span, "unsupported receiver type"))
150188
.map(Some)?,
151189
None if force => None,
152190
None => return Ok(TransformResult::Noop),
153191
};
154192

155193
let output_lifetime = Lifetime::new("'dynify", fn_span);
156-
crate::lifetime::inject_output_lifetime(Some(context), sig, &output_lifetime)?;
194+
crate::lifetime::inject_output_lifetime(context, sig, &output_lifetime)?;
157195

158196
// Infer the appropriate output type
159197
let input_types = quote_with(|tokens| {
@@ -174,7 +212,7 @@ fn transform_fn(
174212
});
175213
let output_type = match &sig.output {
176214
ReturnType::Default => ReturnType::Type(
177-
<Token![->]>::default(),
215+
NewToken![->],
178216
parse_quote_spanned!(fn_span => ::dynify::r#priv::Fn<
179217
(#input_types),
180218
dyn #output_lifetime + ::core::future::Future<Output = ()>

macros/src/dynify_tests.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ define_macro_tests!(
7979
quote!(MyDynTrait),
8080
quote!(trait Trait { async fn test(&self); }),
8181
)]
82+
// == Functions == //
83+
#[case::fn_returning_impl(
84+
quote!(),
85+
quote!(fn test() -> impl core::any::Any { todo!() }),
86+
)]
87+
#[case::fn_returning_async(
88+
quote!(),
89+
quote!(async fn test(_arg1: &str) -> String { todo!() }),
90+
)]
91+
#[case::fn_renamed(
92+
quote!(my_dyn_test),
93+
quote!(async fn test(_arg1: &str) -> String { todo!() }),
94+
)]
8295
fn ui(#[case] test_name: &str, #[case] attr: TokenStream, #[case] input: TokenStream) {
8396
let output = expand(attr, input).unwrap();
8497
// Append `fn main() {}` so that they can pass compile tests
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* This file is @generated for testing purpose */
2+
#[allow(async_fn_in_trait)]
3+
async fn test(_arg1: &str) -> String {
4+
todo!()
5+
}
6+
fn my_dyn_test<'_arg1, 'dynify>(
7+
_arg1: &'_arg1 str,
8+
) -> ::dynify::r#priv::Fn<
9+
(&'_arg1 str,),
10+
dyn 'dynify + ::core::future::Future<Output = String>,
11+
>
12+
where
13+
'_arg1: 'dynify,
14+
{
15+
::dynify::__from_fn!([] test, _arg1,)
16+
}
17+
fn main() {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* This file is @generated for testing purpose */
2+
#[allow(async_fn_in_trait)]
3+
async fn test(_arg1: &str) -> String {
4+
todo!()
5+
}
6+
fn dyn_test<'_arg1, 'dynify>(
7+
_arg1: &'_arg1 str,
8+
) -> ::dynify::r#priv::Fn<
9+
(&'_arg1 str,),
10+
dyn 'dynify + ::core::future::Future<Output = String>,
11+
>
12+
where
13+
'_arg1: 'dynify,
14+
{
15+
::dynify::__from_fn!([] test, _arg1,)
16+
}
17+
fn main() {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* This file is @generated for testing purpose */
2+
#[allow(async_fn_in_trait)]
3+
fn test() -> impl core::any::Any {
4+
todo!()
5+
}
6+
fn dyn_test<'dynify>() -> ::dynify::r#priv::Fn<(), dyn 'dynify + core::any::Any> {
7+
::dynify::__from_fn!([] test,)
8+
}
9+
fn main() {}

macros/src/lifetime.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use quote::format_ident;
55
use syn::punctuated::Punctuated;
66
use syn::spanned::Spanned;
77
use syn::visit_mut::VisitMut;
8-
use syn::{parse_quote, parse_quote_spanned, visit_mut, FnArg, Ident, Lifetime, Result, Token};
8+
use syn::{parse_quote, parse_quote_spanned, visit_mut, FnArg, Ident, Lifetime, Result};
99

1010
pub(crate) struct TraitContext<'a> {
1111
pub generics: &'a syn::Generics,
@@ -210,7 +210,7 @@ impl visit_mut::VisitMut for LifetimeCollector<'_> {
210210

211211
fn default_where_clause(where_clause: &mut Option<syn::WhereClause>) -> &mut syn::WhereClause {
212212
where_clause.get_or_insert_with(|| syn::WhereClause {
213-
where_token: <Token![where]>::default(),
213+
where_token: NewToken![where],
214214
predicates: Punctuated::new(),
215215
})
216216
}

macros/src/utils.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ macro_rules! as_variant {
1717
};
1818
}
1919

20+
macro_rules! NewToken {
21+
($($tt:tt)*) => (<::syn::Token![$($tt)*]>::default());
22+
}
23+
2024
pub(crate) fn quote_with<F: Fn(&mut TokenStream)>(f: F) -> QuoteWith<F> {
2125
QuoteWith(f)
2226
}

tests/compile_fail/dynify_with_unknown_receiver.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: cannot determine receiver type
1+
error: unsupported receiver type
22
--> tests/compile_fail/dynify_with_unknown_receiver.rs:3:19
33
|
44
3 | async fn test(self: MySelf);

tests/compile_fail/dynify_with_unsupported_item.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#[dynify::dynify]
2-
fn test() {}
2+
fn test1() {}
3+
4+
#[dynify::dynify]
5+
fn test2() -> FakeImpl {}
36

47
#[dynify::dynify]
58
opaque_trait!();

0 commit comments

Comments
 (0)