refuse_macros/
lib.rs

1//! Macros for the [Refuse](https://github.com/khonsulabs/refuse) garbage
2//! collector.
3
4use std::collections::HashSet;
5
6use attribute_derive::FromAttr;
7use manyhow::manyhow;
8use proc_macro2::{Span, TokenStream};
9use quote::quote;
10use syn::{GenericParam, Generics, Lifetime, TraitBound};
11
12/// Derives the `refuse::MapAs` trait for a given struct or enum.
13///
14/// This macro expects the identifier `refuse` to refer to the crate.
15#[manyhow]
16#[proc_macro_derive(MapAs, attributes(map_as))]
17pub fn derive_map_as(input: syn::Item) -> manyhow::Result {
18    match input {
19        syn::Item::Struct(item) => derive_map_as_inner(&item.ident, &item.generics, &item.attrs),
20        syn::Item::Enum(item) => derive_map_as_inner(&item.ident, &item.generics, &item.attrs),
21        _ => manyhow::bail!("Collectable can only derived on structs and enums"),
22    }
23}
24
25fn derive_map_as_inner(
26    ident: &syn::Ident,
27    generics: &syn::Generics,
28    attrs: &[syn::Attribute],
29) -> manyhow::Result {
30    let (impl_gen, type_gen, where_clause) = generics.split_for_impl();
31    let attr = MapAsAttr::from_attributes(attrs)?;
32
33    match (&attr.target, &attr.map) {
34        (Some(target), map_as) => {
35            let map_as = if let Some(map_as) = map_as {
36                quote!(#map_as)
37            } else {
38                quote!(self)
39            };
40            Ok(quote! {
41                impl<#impl_gen> refuse::MapAs for #ident<#type_gen> #where_clause {
42                    type Target = #target;
43
44                    fn map_as(&self) -> &Self::Target {
45                        #map_as
46                    }
47                }
48            })
49        }
50        (None, None) => Ok(quote! {
51            impl<#impl_gen> refuse::NoMapping for #ident<#type_gen> #where_clause {}
52        }),
53        (_, Some(map_as)) => {
54            manyhow::bail!(map_as, "target must be specified when map is provided")
55        }
56    }
57}
58
59/// Derives the `refuse::Trace` trait for a given struct or enum.
60///
61/// This macro expects the identifier `refuse` to refer to the crate.
62#[manyhow]
63#[proc_macro_derive(Trace, attributes(trace))]
64pub fn derive_trace(input: syn::Item) -> manyhow::Result {
65    match input {
66        syn::Item::Struct(item) => derive_struct_trace(item),
67        syn::Item::Enum(item) => derive_enum_trace(item),
68        _ => manyhow::bail!("Collectable can only derived on structs and enums"),
69    }
70}
71
72fn field_accessor(field: &syn::Field, index: usize) -> TokenStream {
73    if let Some(ident) = field.ident.clone() {
74        quote!(#ident)
75    } else {
76        let index = proc_macro2::Literal::usize_unsuffixed(index);
77        quote!(#index)
78    }
79}
80
81fn derive_struct_trace(
82    syn::ItemStruct {
83        ident,
84        mut generics,
85        fields,
86        ..
87    }: syn::ItemStruct,
88) -> manyhow::Result {
89    require_trace_for_generics(&mut generics);
90    let (impl_gen, type_gen, where_clause) = generics.split_for_impl();
91
92    let mut fields_and_types = Vec::new();
93    for (index, field) in fields.iter().enumerate() {
94        let field_attr = TraceFieldAttr::from_attributes(&field.attrs)?;
95        if !field_attr.ignore {
96            let field_accessor = field_accessor(field, index);
97
98            fields_and_types.push((field_accessor, field.ty.clone()));
99        }
100    }
101
102    let types = fields_and_types
103        .iter()
104        .map(|(_, ty)| ty.clone())
105        .collect::<HashSet<_>>();
106    let type_mays = types
107        .into_iter()
108        .map(|ty| quote! {<#ty as refuse::Trace>::MAY_CONTAIN_REFERENCES});
109    let may_contain_refs = quote! {
110        #(#type_mays |)* false
111    };
112
113    let traces = fields_and_types.iter().map(|(field, _)| {
114        quote! {refuse::Trace::trace(&self.#field, tracer)}
115    });
116
117    Ok(quote! {
118        impl #impl_gen refuse::Trace for #ident #type_gen #where_clause {
119            const MAY_CONTAIN_REFERENCES: bool = #may_contain_refs;
120
121            fn trace(&self, tracer: &mut refuse::Tracer) {
122                #(#traces;)*
123            }
124        }
125    })
126}
127
128fn require_trace_for_generics(generics: &mut Generics) {
129    for mut pair in generics.params.pairs_mut() {
130        if let GenericParam::Type(t) = pair.value_mut() {
131            t.bounds.push(syn::TypeParamBound::Trait(
132                TraitBound::from_input(quote!(refuse::Trace)).unwrap(),
133            ));
134            t.bounds.push(syn::TypeParamBound::Trait(
135                TraitBound::from_input(quote!(::std::marker::Send)).unwrap(),
136            ));
137            t.bounds.push(syn::TypeParamBound::Trait(
138                TraitBound::from_input(quote!(::std::marker::Sync)).unwrap(),
139            ));
140            t.bounds.push(syn::TypeParamBound::Lifetime(
141                Lifetime::from_input(quote!('static)).unwrap(),
142            ));
143        }
144    }
145}
146
147fn derive_enum_trace(
148    syn::ItemEnum {
149        ident: enum_name,
150        mut generics,
151        variants,
152        ..
153    }: syn::ItemEnum,
154) -> manyhow::Result {
155    require_trace_for_generics(&mut generics);
156    let (impl_gen, type_gen, where_clause) = generics.split_for_impl();
157
158    let mut all_types = HashSet::new();
159    let mut traces = Vec::new();
160
161    for syn::Variant {
162        ident,
163        fields,
164        attrs,
165        ..
166    } in &variants
167    {
168        let field_attr = TraceFieldAttr::from_attributes(attrs)?;
169        let trace = match fields {
170            syn::Fields::Named(fields) => {
171                if field_attr.ignore {
172                    quote!(Self::#ident { .. } => {})
173                } else {
174                    let mut field_names = Vec::new();
175                    for field in &fields.named {
176                        let field_attr = TraceFieldAttr::from_attributes(&field.attrs)?;
177                        if !field_attr.ignore {
178                            all_types.insert(field.ty.clone());
179
180                            let name = field.ident.clone().expect("name missing");
181
182                            field_names.push(name);
183                        }
184                    }
185                    quote! {Self::#ident { #(#field_names,)* } => {
186                        #(refuse::Trace::trace(#field_names, tracer);)*
187                    }}
188                }
189            }
190            syn::Fields::Unnamed(fields) => {
191                if field_attr.ignore {
192                    quote!(Self::#ident(..) => {})
193                } else {
194                    let mut field_names = Vec::new();
195                    for (index, field) in fields.unnamed.iter().enumerate() {
196                        let field_attr = TraceFieldAttr::from_attributes(&field.attrs)?;
197                        if !field_attr.ignore {
198                            all_types.insert(field.ty.clone());
199
200                            field_names
201                                .push(syn::Ident::new(&format!("f{index}"), Span::call_site()));
202                        }
203                    }
204                    quote! {Self::#ident ( #(#field_names,)* ) => {
205                        #(refuse::Trace::trace(#field_names, tracer);)*
206                    }}
207                }
208            }
209            syn::Fields::Unit => {
210                quote! {Self::#ident => {}}
211            }
212        };
213        traces.push(trace);
214    }
215
216    let type_mays = all_types
217        .into_iter()
218        .map(|ty| quote! {<#ty as refuse::Trace>::MAY_CONTAIN_REFERENCES});
219    let may_contain_refs = quote! {
220        #(#type_mays |)* false
221    };
222    let traces = if traces.is_empty() {
223        TokenStream::default()
224    } else {
225        quote!(
226            match self {
227                #(#traces)*
228            }
229        )
230    };
231    Ok(quote! {
232        impl #impl_gen refuse::Trace for #enum_name #type_gen #where_clause {
233            const MAY_CONTAIN_REFERENCES: bool = #may_contain_refs;
234
235            fn trace(&self, tracer: &mut refuse::Tracer) {
236                #traces
237            }
238        }
239    })
240}
241
242#[derive(FromAttr)]
243#[attribute(ident = map_as)]
244struct MapAsAttr {
245    target: Option<syn::Type>,
246    map: Option<syn::Expr>,
247}
248
249#[derive(FromAttr)]
250#[attribute(ident = trace)]
251struct TraceFieldAttr {
252    ignore: bool,
253}