use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, spanned::Spanned};
pub fn expand_derive_soable(input: DeriveInput) -> syn::Result<TokenStream> {
let struct_name = &input.ident;
let generics = &input.generics;
let (fields, is_tuple_struct) = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => Ok((&fields_named.named, false)),
Fields::Unnamed(fields_unnamed) => Ok((&fields_unnamed.unnamed, true)),
Fields::Unit => Err(syn::Error::new(
input.span(),
"Soable cannot be derived for unit structs",
)),
},
_ => Err(syn::Error::new(
input.span(),
"Soable can only be derived for structs",
)),
}?;
if fields.len() == 1 {
return Err(syn::Error::new(
fields.span(),
"Soable cannot be derived for single-field structs; use a normal Vec instead",
));
}
let field_names: Vec<proc_macro2::Ident> = if is_tuple_struct {
(0..fields.len())
.map(|i| quote::format_ident!("_{}", i))
.collect()
} else {
fields
.iter()
.map(|f| f.ident.as_ref().unwrap().clone())
.collect()
};
let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();
let field_vis: Vec<_> = fields.iter().map(|f| &f.vis).collect();
let ref_struct_name = quote::format_ident!("{}Ref", struct_name);
let mut_struct_name = quote::format_ident!("{}Mut", struct_name);
let slice_struct_name = quote::format_ident!("{}Slice", struct_name);
let slice_mut_struct_name = quote::format_ident!("{}SliceMut", struct_name);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let type_params = generics.type_params().collect::<Vec<_>>();
let lifetime_params = generics.lifetimes().collect::<Vec<_>>();
let has_type_generics = !type_params.is_empty();
let has_lifetime_generics = !lifetime_params.is_empty();
let helper_generics = if has_lifetime_generics || has_type_generics {
quote! { <'soa, #(#lifetime_params,)* #(#type_params),*> }
} else {
quote! { <'soa> }
};
let helper_ty_generics = if has_lifetime_generics || has_type_generics {
let lifetime_idents = lifetime_params.iter().map(|lp| &lp.lifetime);
let type_idents = type_params.iter().map(|tp| &tp.ident);
quote! { <'soa, #(#lifetime_idents,)* #(#type_idents),*> }
} else {
quote! { <'soa> }
};
let lifetime_bounds = if has_lifetime_generics {
let bounds = lifetime_params.iter().map(|lp| {
let lifetime = &lp.lifetime;
quote! { #lifetime: 'soa }
});
quote! { #(#bounds),* }
} else {
quote! {}
};
let combined_where_clause = match (where_clause, has_lifetime_generics) {
(Some(wc), true) => quote! { #wc, #lifetime_bounds },
(Some(wc), false) => quote! { #wc },
(None, true) => quote! { where #lifetime_bounds },
(None, false) => quote! {},
};
let struct_vis = &input.vis;
let struct_pattern = if is_tuple_struct {
quote! { Self(#(#field_names),*) }
} else {
quote! { Self { #(#field_names),* } }
};
let expanded = quote! {
#[allow(dead_code)]
#struct_vis struct #ref_struct_name #helper_generics #combined_where_clause {
#(#field_vis #field_names: &'soa #field_types),*
}
impl #helper_generics Copy for #ref_struct_name #helper_ty_generics #combined_where_clause {}
impl #helper_generics Clone for #ref_struct_name #helper_ty_generics #combined_where_clause {
fn clone(&self) -> Self {
*self
}
}
#[allow(dead_code)]
#struct_vis struct #mut_struct_name #helper_generics #combined_where_clause {
#(#field_vis #field_names: &'soa mut #field_types),*
}
#[allow(dead_code)]
#struct_vis struct #slice_struct_name #helper_generics #combined_where_clause {
#(#field_vis #field_names: &'soa [#field_types]),*
}
impl #helper_generics Copy for #slice_struct_name #helper_ty_generics #combined_where_clause {}
impl #helper_generics Clone for #slice_struct_name #helper_ty_generics #combined_where_clause {
fn clone(&self) -> Self {
*self
}
}
#[allow(dead_code)]
#struct_vis struct #slice_mut_struct_name #helper_generics #combined_where_clause {
#(#field_vis #field_names: &'soa mut [#field_types]),*
}
unsafe impl #impl_generics soavec::SoAble for #struct_name #ty_generics #where_clause {
type TupleRepr = (#(#field_types),*);
type Ref<'soa> = #ref_struct_name #helper_ty_generics where Self: 'soa;
type Mut<'soa> = #mut_struct_name #helper_ty_generics where Self: 'soa;
type Slice<'soa> = #slice_struct_name #helper_ty_generics where Self: 'soa;
type SliceMut<'soa> = #slice_mut_struct_name #helper_ty_generics where Self: 'soa;
fn into_tuple(value: Self) -> Self::TupleRepr {
let #struct_pattern = value;
(#(#field_names),*)
}
fn from_tuple(value: Self::TupleRepr) -> Self {
let (#(#field_names),*) = value;
#struct_pattern
}
fn as_ref<'soa>(
_: std::marker::PhantomData<&'soa Self>,
value: <Self::TupleRepr as soavec::SoATuple>::Pointers,
) -> Self::Ref<'soa> {
let (#(#field_names),*) = value;
unsafe {
#ref_struct_name {
#(#field_names: #field_names.as_ref()),*
}
}
}
fn as_mut<'soa>(
_: std::marker::PhantomData<&'soa mut Self>,
value: <Self::TupleRepr as soavec::SoATuple>::Pointers,
) -> Self::Mut<'soa> {
let (#(mut #field_names),*) = value;
unsafe {
#mut_struct_name {
#(#field_names: #field_names.as_mut()),*
}
}
}
fn as_slice<'soa>(
_: std::marker::PhantomData<&'soa Self>,
value: <Self::TupleRepr as soavec::SoATuple>::Pointers,
len: u32,
) -> Self::Slice<'soa> {
let __soa_len = len as usize;
let (#(#field_names),*) = value;
unsafe {
#slice_struct_name {
#(#field_names: core::slice::from_raw_parts(#field_names.as_ptr(), __soa_len)),*
}
}
}
fn as_mut_slice<'soa>(
_: std::marker::PhantomData<&'soa mut Self>,
value: <Self::TupleRepr as soavec::SoATuple>::Pointers,
len: u32,
) -> Self::SliceMut<'soa> {
let __soa_len = len as usize;
let (#(#field_names),*) = value;
unsafe {
#slice_mut_struct_name {
#(#field_names: core::slice::from_raw_parts_mut(#field_names.as_ptr(), __soa_len)),*
}
}
}
}
};
Ok(expanded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_derive_soable() {
let input: DeriveInput = syn::parse_quote! {
struct TestStruct {
a: u32,
b: u64,
}
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("impl soavec :: SoAble for TestStruct"));
assert!(result.contains("type TupleRepr = (u32 , u64)"));
assert!(result.contains("fn into_tuple"));
assert!(result.contains("struct TestStructRef"));
assert!(result.contains("struct TestStructMut"));
assert!(result.contains("struct TestStructSlice"));
assert!(result.contains("struct TestStructSliceMut"));
}
#[test]
fn test_derives_visibility() {
let input: DeriveInput = syn::parse_quote! {
pub struct TestStruct {
a: u32,
b: u64,
}
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("pub struct TestStructMut"));
assert!(result.contains("pub struct TestStructRef"));
assert!(result.contains("pub struct TestStructSlice"));
assert!(result.contains("pub struct TestStructSliceMut"));
}
#[test]
fn test_expand_derive_soable_lifetime() {
let input: DeriveInput = syn::parse_quote! {
struct TestStruct<'a> {
a: &'a u32,
b: &'a u64,
}
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("impl < 'a > soavec :: SoAble for TestStruct < 'a >"),);
assert!(result.contains("type TupleRepr = (& 'a u32 , & 'a u64)"));
assert!(result.contains("fn into_tuple"));
assert!(result.contains("TestStructRef < 'soa , 'a , >"));
assert!(result.contains("'a : 'soa"));
assert!(result.contains("& 'soa & 'a u32"));
}
#[test]
fn test_simple_lifetime() {
let input: DeriveInput = syn::parse_quote! {
struct WithLifetime<'a> {
data: &'a str,
count: u32,
}
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("'a : 'soa"));
assert!(result.contains("& 'soa & 'a str"));
assert!(result.contains("& 'soa u32"));
}
#[test]
fn test_complex_generics() {
let input: DeriveInput = syn::parse_quote! {
struct ComplexStruct<'a, 'b, T, U>
where
T: Clone,
U: Default
{
first: &'a T,
second: &'b str,
owned: U,
}
};
let result = expand_derive_soable(input).unwrap();
let result_str = result.to_string();
assert!(result_str.contains("< 'soa , 'a , 'b , T , U >"));
assert!(result_str.contains("'a : 'soa"));
assert!(result_str.contains("'b : 'soa"));
assert!(result_str.contains("T : Clone"));
assert!(result_str.contains("U : Default"));
}
#[test]
fn test_tuple_struct() {
let input: DeriveInput = syn::parse_quote! {
struct TupleStruct(u32, f64, String);
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("struct TupleStructRef"));
assert!(result.contains("struct TupleStructMut"));
assert!(result.contains("struct TupleStructSlice"));
assert!(result.contains("struct TupleStructSliceMut"));
assert!(result.contains("_0 : & 'soa u32"));
assert!(result.contains("_1 : & 'soa f64"));
assert!(result.contains("_2 : & 'soa String"));
}
#[test]
fn test_field_visibility_preservation() {
let input: DeriveInput = syn::parse_quote! {
pub struct MixedVisStruct {
pub public_field: u32,
private_field: u64,
}
};
let result = expand_derive_soable(input).unwrap().to_string();
assert!(result.contains("pub public_field : & 'soa u32"));
assert!(result.contains("private_field : & 'soa u64"));
assert!(!result.contains("pub private_field : & 'soa u64"));
}
#[test]
fn test_single_field_error() {
let input: DeriveInput = syn::parse_quote! {
struct SingleField {
a: u32,
}
};
let result = expand_derive_soable(input);
assert!(result.is_err());
}
#[test]
fn test_zero_field_error() {
let input: DeriveInput = syn::parse_quote! {
struct ZeroField;
};
let result = expand_derive_soable(input);
assert!(result.is_err());
}
}