use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote, quote_spanned};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
visit_mut::VisitMut,
};
use synstructure::{AddBounds, decl_derive};
fn find_collect_meta(attrs: &[syn::Attribute]) -> syn::Result<Option<&syn::Attribute>> {
let mut found = None;
for attr in attrs {
if attr.path().is_ident("collect") && found.replace(attr).is_some() {
return Err(syn::parse::Error::new_spanned(
attr.path(),
"Cannot specify multiple `#[collect]` attributes! Consider merging them.",
));
}
}
Ok(found)
}
fn usage_error(meta: &syn::meta::ParseNestedMeta, msg: &str) -> syn::parse::Error {
meta.error(format_args!("{msg}. `#[collect(...)]` requires one mode (`require_static`, `no_drop`, or `unsafe_drop`) and optionally `bound = \"...\"`."))
}
#[derive(PartialEq)]
enum Mode {
RequireStatic,
NoDrop,
UnsafeDrop,
}
fn collect_derive(mut s: synstructure::Structure) -> TokenStream {
let mut mode = None;
let mut override_bound = None;
let result = match find_collect_meta(&s.ast().attrs) {
Ok(Some(attr)) => attr.parse_nested_meta(|meta| {
if meta.path.is_ident("bound") {
if override_bound.is_some() {
return Err(usage_error(&meta, "multiple bounds specified"));
}
let lit: syn::LitStr = meta.value()?.parse()?;
override_bound = Some(lit);
return Ok(());
}
meta.input.parse::<syn::parse::Nothing>()?;
if mode.is_some() {
return Err(usage_error(&meta, "multiple modes specified"));
} else if meta.path.is_ident("require_static") {
mode = Some(Mode::RequireStatic);
} else if meta.path.is_ident("no_drop") {
mode = Some(Mode::NoDrop);
} else if meta.path.is_ident("unsafe_drop") {
mode = Some(Mode::UnsafeDrop);
} else {
return Err(usage_error(&meta, "unknown option"));
}
Ok(())
}),
Ok(None) => Ok(()),
Err(err) => Err(err),
};
if let Err(err) = result {
return err.to_compile_error();
}
let Some(mode) = mode else {
panic!(
"{}",
"deriving `Collect` requires a `#[collect(...)]` attribute"
);
};
let where_clause = if mode == Mode::RequireStatic {
quote!(where Self: 'static)
} else {
override_bound
.as_ref()
.map(|x| {
x.parse()
.expect("`#[collect]` failed to parse explicit trait bound expression")
})
.unwrap_or_else(|| quote!())
};
let mut errors = vec![];
let collect_impl = if mode == Mode::RequireStatic {
s.clone().add_bounds(AddBounds::None).gen_impl(quote! {
gen unsafe impl ::aiscript_arena::Collect for @Self #where_clause {
#[inline]
fn needs_trace() -> bool {
false
}
}
})
} else {
let mut needs_trace_body = TokenStream::new();
quote!(false).to_tokens(&mut needs_trace_body);
let mut static_bindings = vec![];
s.filter(|b| match find_collect_meta(&b.ast().attrs) {
Ok(Some(attr)) => {
let mut static_binding = false;
let result = attr.parse_nested_meta(|meta| {
if meta.input.is_empty() && meta.path.is_ident("require_static") {
static_binding = true;
static_bindings.push(b.ast().ty.clone());
Ok(())
} else {
Err(meta.error("Only `#[collect(require_static)]` is supported on a field"))
}
});
errors.extend(result.err());
!static_binding
}
Ok(None) => true,
Err(err) => {
errors.push(err);
true
}
});
for static_binding in static_bindings {
s.add_where_predicate(syn::parse_quote! { #static_binding: 'static });
}
if let syn::Data::Enum(..) = s.ast().data {
for v in s.variants() {
for attr in v.ast().attrs {
if attr.path().is_ident("collect") {
errors.push(syn::parse::Error::new_spanned(
attr.path(),
"`#[collect]` is not suppported on enum variants",
));
}
}
}
}
for v in s.variants() {
for b in v.bindings() {
let ty = &b.ast().ty;
let call_span = b.ast().span().resolved_at(Span::call_site());
quote_spanned!(call_span=>
|| <#ty as ::aiscript_arena::Collect>::needs_trace()
)
.to_tokens(&mut needs_trace_body);
}
}
let trace_body = s.each(|bi| {
let call_span = bi.ast().span().resolved_at(Span::call_site());
quote_spanned!(call_span=>
{
let bi = #bi;
::aiscript_arena::Collect::trace(bi, cc)
}
)
});
let bounds_type = if override_bound.is_some() {
AddBounds::None
} else {
AddBounds::Generics
};
s.clone().add_bounds(bounds_type).gen_impl(quote! {
gen unsafe impl ::aiscript_arena::Collect for @Self #where_clause {
#[inline]
fn needs_trace() -> bool {
#needs_trace_body
}
#[inline]
fn trace(&self, cc: &::aiscript_arena::Collection) {
match *self { #trace_body }
}
}
})
};
let drop_impl = if mode == Mode::NoDrop {
let mut s = s;
s.add_bounds(AddBounds::None).gen_impl(quote! {
gen impl ::aiscript_arena::__MustNotImplDrop for @Self {}
})
} else {
quote!()
};
let errors = errors.into_iter().map(|e| e.to_compile_error());
quote! {
#collect_impl
#drop_impl
#(#errors)*
}
}
decl_derive! {
[Collect, attributes(collect)] =>
collect_derive
}
#[doc(hidden)]
#[proc_macro]
pub fn __unelide_lifetimes(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
struct Input {
lt: syn::Lifetime,
ty: syn::Type,
}
impl Parse for Input {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lt: syn::Lifetime = input.parse()?;
let _: syn::Token!(;) = input.parse()?;
let ty: syn::Type = input.parse()?;
Ok(Self { lt, ty })
}
}
struct UnelideLifetimes(syn::Lifetime);
impl VisitMut for UnelideLifetimes {
fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
if i.ident == "_" {
*i = self.0.clone();
}
}
}
let mut input = syn::parse_macro_input!(input as Input);
UnelideLifetimes(input.lt).visit_type_mut(&mut input.ty);
input.ty.to_token_stream().into()
}