use std::cell::OnceCell;
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{
Data, DataEnum, DeriveInput, Field, Fields, GenericParam, Generics, Index, Member, Type,
parse_macro_input,
};
thread_local! {
static SIZE_BYTES_PATH: OnceCell<Option<String>> = const { OnceCell::new() };
}
fn size_bytes_path(span: Span) -> syn::Result<TokenStream2> {
let path = SIZE_BYTES_PATH.with(|cell| cell.get_or_init(resolve_size_bytes_path).clone());
match path {
Some(path) => Ok(path
.parse()
.expect("resolved trait path should be valid tokens")),
None => Err(syn::Error::new(
span,
"`SizeBytes` trait not found in default locations, either manually set location with `#[size_bytes(crate_root = some::path)]`, or depend on `rerun` or `re_byte_size`",
)),
}
}
fn resolve_size_bytes_path() -> Option<String> {
let found = crate_name("re_byte_size")
.ok()
.or_else(|| crate_name("rerun").ok())?;
let krate = match found {
FoundCrate::Itself => "crate".to_owned(),
FoundCrate::Name(name) => format!("::{name}"),
};
Some(format!("{krate}::SizeBytes"))
}
#[proc_macro_derive(SizeBytes, attributes(size_bytes))]
pub fn derive_size_bytes(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let options = parse_type_options(&input.attrs)?;
let trait_path = match &options.crate_root {
Some(crate_root) => quote! { #crate_root::SizeBytes },
None => size_bytes_path(name.span())?,
};
let generics = add_trait_bounds(input.generics.clone(), &trait_path);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let (is_pod, heap_size_body) = match &input.data {
Data::Struct(data) => struct_body(&data.fields, &trait_path)?,
Data::Enum(data) => enum_body(data, &trait_path)?,
Data::Union(_) => {
return Err(syn::Error::new(
input.ident.span(),
"`SizeBytes` cannot be derived for unions",
));
}
};
let profile_stmt = if options.profile {
quote! { ::re_tracing::profile_function!(); }
} else {
quote!()
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics #trait_path for #name #ty_generics #where_clause {
const IS_POD: bool = #is_pod;
fn heap_size_bytes(&self) -> u64 {
#profile_stmt
#heap_size_body
}
}
})
}
#[derive(Default)]
struct TypeOptions {
profile: bool,
crate_root: Option<syn::Path>,
}
fn parse_type_options(attrs: &[syn::Attribute]) -> syn::Result<TypeOptions> {
let mut options = TypeOptions::default();
for attr in attrs {
if !attr.path().is_ident("size_bytes") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("profile") {
options.profile = true;
Ok(())
} else if meta.path.is_ident("crate_root") {
options.crate_root = Some(meta.value()?.parse()?);
Ok(())
} else {
Err(meta.error("unknown `size_bytes` option, expected `profile` or `crate_root`"))
}
})?;
}
Ok(options)
}
fn struct_body(
fields: &Fields,
trait_path: &TokenStream2,
) -> syn::Result<(TokenStream2, TokenStream2)> {
let mut members = Vec::new();
let mut types = Vec::new();
let mut ignored = Vec::new();
for (index, field) in fields.iter().enumerate() {
let member = member_for(index, field);
if is_ignored(field)? {
ignored.push(member);
continue;
}
members.push(member);
types.push(&field.ty);
}
let is_pod = all_pod_expr(&types, trait_path);
let terms: Vec<TokenStream2> = std::iter::zip(&members, &types)
.map(|(member, ty)| {
quote! {
(if <#ty as #trait_path>::IS_POD {
0
} else {
#trait_path::heap_size_bytes(&self.#member)
})
}
})
.collect();
let sum = sum_expr(&terms);
let body = quote! {
#( let _ = &self.#ignored; )*
#sum
};
Ok((is_pod, body))
}
fn enum_body(
data: &DataEnum,
trait_path: &TokenStream2,
) -> syn::Result<(TokenStream2, TokenStream2)> {
let mut all_types: Vec<&Type> = Vec::new();
let mut arms: Vec<TokenStream2> = Vec::new();
for variant in &data.variants {
let variant_ident = &variant.ident;
let mut bindings: Vec<Ident> = Vec::new();
let mut binding_types: Vec<&Type> = Vec::new();
let pattern = match &variant.fields {
Fields::Named(named) => {
let mut patterns = Vec::new();
for field in &named.named {
let ident = field.ident.clone().expect("named field has an identifier");
if is_ignored(field)? {
let binding = format_ident!("_{}", ident);
patterns.push(quote! { #ident: #binding });
continue;
}
all_types.push(&field.ty);
binding_types.push(&field.ty);
patterns.push(quote! { #ident });
bindings.push(ident);
}
quote! { Self::#variant_ident { #(#patterns),* } }
}
Fields::Unnamed(unnamed) => {
let mut patterns = Vec::new();
for (index, field) in unnamed.unnamed.iter().enumerate() {
if is_ignored(field)? {
let binding = format_ident!("_field_{}", index);
patterns.push(quote! { #binding });
continue;
}
all_types.push(&field.ty);
binding_types.push(&field.ty);
let binding = format_ident!("field_{}", index);
patterns.push(quote! { #binding });
bindings.push(binding);
}
quote! { Self::#variant_ident( #(#patterns),* ) }
}
Fields::Unit => quote! { Self::#variant_ident },
};
let terms: Vec<TokenStream2> = std::iter::zip(&bindings, &binding_types)
.map(|(binding, ty)| {
quote! {
(if <#ty as #trait_path>::IS_POD {
0
} else {
#trait_path::heap_size_bytes(#binding)
})
}
})
.collect();
let sum = sum_expr(&terms);
arms.push(quote! { #pattern => #sum, });
}
let is_pod = all_pod_expr(&all_types, trait_path);
let body = if data.variants.is_empty() {
quote! { 0 }
} else {
quote! {
match self {
#(#arms)*
}
}
};
Ok((is_pod, body))
}
fn member_for(index: usize, field: &Field) -> Member {
match &field.ident {
Some(ident) => Member::Named(ident.clone()),
None => Member::Unnamed(Index::from(index)),
}
}
fn all_pod_expr(types: &[&Type], trait_path: &TokenStream2) -> TokenStream2 {
match types.split_first() {
None => quote! { true },
Some((first, rest)) => quote! {
<#first as #trait_path>::IS_POD
#( && <#rest as #trait_path>::IS_POD )*
},
}
}
fn sum_expr(terms: &[TokenStream2]) -> TokenStream2 {
match terms.split_first() {
None => quote! { 0 },
Some((first, rest)) => quote! { #first #( + #rest )* },
}
}
fn add_trait_bounds(mut generics: Generics, trait_path: &TokenStream2) -> Generics {
for param in &mut generics.params {
if let GenericParam::Type(type_param) = param {
type_param.bounds.push(syn::parse_quote!(#trait_path));
}
}
generics
}
fn is_ignored(field: &Field) -> syn::Result<bool> {
let mut ignored = false;
for attr in &field.attrs {
if !attr.path().is_ident("size_bytes") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ignore") {
ignored = true;
Ok(())
} else {
Err(meta.error("unknown `size_bytes` option, expected `ignore`"))
}
})?;
}
Ok(ignored)
}