use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Data,
DeriveInput,
Fields,
Path,
Token,
Type,
Visibility,
parse::{
Parse,
ParseStream,
},
parse_macro_input,
punctuated::Punctuated,
spanned::Spanned as _,
token,
};
#[proc_macro_derive(Adapter, attributes(field))]
pub fn derive_adapter(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
struct FieldEntry {
is_new: bool,
is_reflexive: bool,
path: Path,
}
fn eat_keyword(input: ParseStream, keyword: &str) -> syn::Result<bool> {
let is_kw = input.peek(syn::Ident) && !input.peek2(Token![::]) && {
let fork = input.fork();
let ident: syn::Ident = fork.parse()?;
ident == keyword && !fork.is_empty() && !fork.peek(Token![,])
};
if is_kw {
let _: syn::Ident = input.parse()?;
}
Ok(is_kw)
}
impl Parse for FieldEntry {
fn parse(input: ParseStream) -> syn::Result<Self> {
let is_new = eat_keyword(input, "new")?;
let is_reflexive = eat_keyword(input, "reflexive")?;
let path: Path = input.parse()?;
Ok(Self {
is_new,
is_reflexive,
path,
})
}
}
fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new(
input.span(),
"`Adapter` can only be derived for structs",
));
};
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new(
data.fields.span(),
"`Adapter` can only be derived for structs with named fields",
));
};
for attr in &input.attrs {
if !attr.path().is_ident("repr") {
continue;
}
let mut packed = false;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("packed") {
packed = true;
}
if meta.input.peek(token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: TokenStream2 = content.parse()?;
}
Ok(())
})?;
if packed {
return Err(syn::Error::new(
attr.span(),
"`Adapter` cannot be derived for `#[repr(packed)]` structs: \
packed fields may be under-aligned, which would make the \
generated `Adapter` unsound",
));
}
}
let struct_name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut out = TokenStream2::new();
for field in &fields.named {
let field_name = field.ident.as_ref().expect("named field has an ident");
let field_ty = &field.ty;
for attr in &field.attrs {
if !attr.path().is_ident("field") {
continue;
}
let entries =
attr.parse_args_with(Punctuated::<FieldEntry, Token![,]>::parse_terminated)?;
for entry in entries {
out.extend(emit_entry(
&entry,
&input.vis,
struct_name,
&impl_generics,
&ty_generics,
where_clause,
field_name,
field_ty,
)?);
}
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments, reason = "splicing several syn fragments")]
fn emit_entry(
entry: &FieldEntry,
vis: &Visibility,
struct_name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
field_name: &syn::Ident,
field_ty: &Type,
) -> syn::Result<TokenStream2> {
let marker = &entry.path;
let new_marker = if entry.is_new {
let Some(ident) = marker.get_ident() else {
return Err(syn::Error::new(
marker.span(),
"`new` markers must be a single identifier, not a path",
));
};
quote! {
#vis enum #ident {}
#[automatically_derived]
impl ::flense::Field for #ident {
type Type = #field_ty;
}
}
} else {
TokenStream2::new()
};
let reflexive_impl = if entry.is_reflexive {
quote! {
#[automatically_derived]
unsafe impl ::flense::Adapter<#marker> for #field_ty {
const OFFSET: usize = 0;
}
}
} else {
TokenStream2::new()
};
let type_guard = if entry.is_new {
TokenStream2::new()
} else {
quote! {
trait SameType<T> {}
impl<T> SameType<T> for T {}
fn assert_field_type<FLENSE_DERIVE_T: SameType<<#marker as ::flense::Field>::Type>>() {}
let _ = assert_field_type::<#field_ty>;
}
};
Ok(quote! {
#new_marker
#reflexive_impl
#[automatically_derived]
unsafe impl #impl_generics ::flense::Adapter<#marker>
for #struct_name #ty_generics #where_clause
{
const OFFSET: usize = {
#type_guard
let offset = ::core::mem::offset_of!(Self, #field_name);
::core::assert!(
offset % ::core::mem::align_of::<#field_ty>() == 0,
"flense: field is not aligned for its type; the derived `Adapter` would be unsound",
);
offset
};
}
})
}