diff --git a/ssz-rs-derive/Cargo.toml b/ssz-rs-derive/Cargo.toml index 774e9bdb..e03f73ce 100644 --- a/ssz-rs-derive/Cargo.toml +++ b/ssz-rs-derive/Cargo.toml @@ -17,3 +17,6 @@ proc-macro = true syn = "1.0" quote = "1.0" proc-macro2 = "1.0" + +[dev-dependencies] +ssz_rs = { path = "../ssz-rs" } diff --git a/ssz-rs-derive/README.md b/ssz-rs-derive/README.md index b0565080..29f763dd 100644 --- a/ssz-rs-derive/README.md +++ b/ssz-rs-derive/README.md @@ -1,15 +1,28 @@ # ssz_rs_derive -This package provides two proc derive macros: `SimpleSerialize` and `Serializable`. +This package provides the following proc derive macros: +* `Serializable` +* `Merkleized` +* `SimpleSerialize` `SimpleSerialize` derives the functionality required to implement the main package's `SimpleSerialize` trait. `Serializable` only derives the encoding and decoding routines, in the event a user does not want to pull in the merkleization machinery. +`Merkleized` only provides an implementation of that trait, if your type only needs to provide the hashing functionality. + Supports: -- struct (as SSZ container) where each field is also `SimpleSerialize` or `Serializable` -- tuple struct with one field where the field is `SimpleSerialize` or `Serializable` +- struct where each field is also `SimpleSerialize` or `Serializable` - enums with "unnamed" and unit members while respecting the rules of SSZ unions +- tuple struct with one field where the field is `SimpleSerialize` or `Serializable` +- enums in "wrapper" mode, requiring the `transparent` attribute. + +Derivations on structs provide implementations of the relevant traits for a custom struct definition to represent a SSZ container type. + +Derivations on enums (without `transparent`) provide implementations of the relevant traits for SSZ union types. + +Derivations on tuple structs facilitates the "newtype" pattern and delegates to the inner type for its implementation of the relevant traits. + +Derivations on enums *with* `transparent` supports delegation to the inner variants for the implementation of the relevant traits. -Note: example usage can be found in the tests of the `container` and `union` -modules of the `ssz_rs` crate, along with the `examples` in that crate. +Example usage can be found in the tests of the `container` and `union` modules of the `ssz_rs` crate, along with the `examples` in that crate. diff --git a/ssz-rs-derive/src/lib.rs b/ssz-rs-derive/src/lib.rs index ae5f7201..119ad86a 100644 --- a/ssz-rs-derive/src/lib.rs +++ b/ssz-rs-derive/src/lib.rs @@ -3,12 +3,17 @@ //! Refer to the `examples` in the `ssz_rs` crate for a better idea on how to use this derive macro. use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; -use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, Generics, Ident}; +use syn::{ + parse_macro_input, spanned::Spanned, Attribute, Data, DeriveInput, Field, Fields, Generics, + Ident, Meta, NestedMeta, PathArguments, +}; // NOTE: copied here from `ssz_rs` crate as it is unlikely to change // and can keep it out of the crate's public interface. const BYTES_PER_CHUNK: usize = 32; +const SSZ_HELPER_ATTRIBUTE: &str = "ssz"; + fn derive_serialize_impl(data: &Data) -> TokenStream { match data { Data::Struct(ref data) => { @@ -256,7 +261,12 @@ fn derive_size_hint_impl(data: &Data) -> TokenStream { } } -fn derive_merkleization_impl(data: &Data, name: &Ident, generics: &Generics) -> TokenStream { +fn derive_merkleization_impl( + data: &Data, + name: &Ident, + generics: &Generics, + helper_attr: Option<&HelperAttr>, +) -> TokenStream { let method = match data { Data::Struct(ref data) => { let fields = match data.fields { @@ -292,11 +302,18 @@ fn derive_merkleization_impl(data: &Data, name: &Ident, generics: &Generics) -> let variant_name = &variant.ident; match &variant.fields { Fields::Unnamed(..) => { - quote_spanned! { variant.span() => - Self::#variant_name(value) => { - let selector = #i; - let data_root = value.hash_tree_root()?; - Ok(ssz_rs::__internal::mix_in_selector(&data_root, selector)) + // NOTE: validated to only be `transparent` operation at this point... + if helper_attr.is_some() { + quote_spanned! { variant.span() => + Self::#variant_name(value) => value.hash_tree_root(), + } + } else { + quote_spanned! { variant.span() => + Self::#variant_name(value) => { + let selector = #i; + let data_root = value.hash_tree_root()?; + Ok(ssz_rs::__internal::mix_in_selector(&data_root, selector)) + } } } } @@ -333,21 +350,55 @@ fn is_valid_none_identifier(ident: &Ident) -> bool { *ident == format_ident!("None") } +fn filter_ssz_attrs<'a>( + attrs: impl Iterator, +) -> impl Iterator { + attrs.filter(|&f| { + if let Some(path) = f.path.segments.first() { + path.ident == format_ident!("{SSZ_HELPER_ATTRIBUTE}") + } else { + false + } + }) +} + +fn validate_no_attrs<'a>(fields: impl Iterator) { + let mut ssz_attrs = fields.flat_map(|field| filter_ssz_attrs(field.attrs.iter())); + if ssz_attrs.next().is_some() { + panic!("macro attribute `{SSZ_HELPER_ATTRIBUTE}` is only allowed at struct or enum level") + } +} + // Validates the incoming data follows the rules // for mapping the Rust term to something that can // implement the `SimpleSerialize` trait. // // Panics if validation fails which aborts the macro derivation. -fn validate_derive_data(data: &Data) { +fn validate_derive_input(data: &Data, helper_attrs: &[HelperAttr]) { + if helper_attrs.len() > 1 { + panic!("only one argument to the helper attribute is allowed") + } + let mut is_transparent = false; + if let Some(attr) = helper_attrs.first() { + match attr { + HelperAttr::Transparent => is_transparent = true, + } + } + if is_transparent && !matches!(data, Data::Enum(..)) { + panic!("`transparent` option is only compatible with enums") + } match data { Data::Struct(ref data) => match data.fields { Fields::Named(ref fields) => { if fields.named.is_empty() { panic!("ssz_rs containers with no fields are illegal") } + validate_no_attrs(fields.named.iter()) } - Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => {} - _ => panic!("Structs with unit or multiple unnnamed fields are not supported"), + Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => { + validate_no_attrs(fields.unnamed.iter()) + } + _ => panic!("structs with unit or multiple unnnamed fields are not supported"), }, Data::Enum(ref data) => { if data.variants.is_empty() { @@ -361,26 +412,39 @@ fn validate_derive_data(data: &Data) { let mut none_forbidden = false; let mut already_has_none = false; for (i, variant) in data.variants.iter().enumerate() { + let mut variant_ssz_attrs = filter_ssz_attrs(variant.attrs.iter()); + if variant_ssz_attrs.next().is_some() { + panic!("macro attribute `{SSZ_HELPER_ATTRIBUTE}` is only allowed at struct or enum level") + } + validate_no_attrs(variant.fields.iter()); match &variant.fields { Fields::Unnamed(inner) => { if i == 0 { none_forbidden = true; } if inner.unnamed.len() != 1 { - panic!("Enums can only have 1 type per variant"); + panic!("enums can only have 1 type per variant"); } + validate_no_attrs(inner.unnamed.iter()); } Fields::Unit => { + if is_transparent { + panic!("`transparent` option is only compatible with unnamed variants") + } if none_forbidden { - panic!("only the first variant can be `None`"); + panic!( + "found unit variant that conflicts with previous unnamed variants" + ); } if already_has_none { panic!("cannot duplicate a unit variant (as only `None` is allowed)"); } if !is_valid_none_identifier(&variant.ident) { - panic!("Variant identifier is invalid: must be `None`"); + panic!("variant identifier is invalid: must be `None`"); + } + if i != 0 { + panic!("only the first variant can be unit type (and must be `None`)") } - assert!(i == 0); if data.variants.len() < 2 { panic!( "SSZ unions must have more than 1 selector if the first is `None`" @@ -430,7 +494,7 @@ fn derive_serializable_impl( } } -fn derive_simple_serialilze_impl(name: &Ident, generics: &Generics) -> proc_macro2::TokenStream { +fn derive_simple_serialize_impl(name: &Ident, generics: &Generics) -> proc_macro2::TokenStream { let (impl_generics, ty_generics, _) = generics.split_for_impl(); quote! { @@ -438,12 +502,56 @@ fn derive_simple_serialilze_impl(name: &Ident, generics: &Generics) -> proc_macr } } -#[proc_macro_derive(Serializable)] +#[derive(Debug)] +enum HelperAttr { + Transparent, +} + +fn parse_helper_attr(ident: &Ident) -> HelperAttr { + match ident.to_string().as_str() { + "transparent" => HelperAttr::Transparent, + ident => panic!("unsupported helper attribute:{ident}"), + } +} + +fn extract_helper_attrs(input: &DeriveInput) -> Vec { + filter_ssz_attrs(input.attrs.iter()) + .flat_map(|attr| { + let meta = attr.parse_meta().unwrap(); + match meta { + Meta::List(args) => args + .nested + .iter() + .map(|arg| match arg { + NestedMeta::Meta(meta) => { + if let Meta::Path(path) = meta { + assert!(path.leading_colon.is_none()); + assert_eq!(path.segments.len(), 1); + let path = &path.segments[0]; + match path.arguments { + PathArguments::None => parse_helper_attr(&path.ident), + _ => panic!("no arguments are supported to attribute symbols"), + } + } else { + panic!("unsupported argument to helper attribute"); + } + } + _ => panic!("literals unsupported in attributes"), + }) + .collect::>(), + _ => panic!("only list-like attributes are supported"), + } + }) + .collect() +} + +#[proc_macro_derive(Serializable, attributes(ssz))] pub fn derive_serializable(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let data = &input.data; - validate_derive_data(data); + let helper_attrs = extract_helper_attrs(&input); + validate_derive_input(data, &helper_attrs); let name = &input.ident; let generics = &input.generics; @@ -452,34 +560,38 @@ pub fn derive_serializable(input: proc_macro::TokenStream) -> proc_macro::TokenS proc_macro::TokenStream::from(expansion) } -#[proc_macro_derive(Merkleized)] +#[proc_macro_derive(Merkleized, attributes(ssz))] pub fn derive_merkleized(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let data = &input.data; - validate_derive_data(data); + let helper_attrs = extract_helper_attrs(&input); + validate_derive_input(data, &helper_attrs); + let helper_attr = helper_attrs.first(); let name = &input.ident; let generics = &input.generics; - let expansion = derive_merkleization_impl(data, name, generics); + let expansion = derive_merkleization_impl(data, name, generics, helper_attr); proc_macro::TokenStream::from(expansion) } -#[proc_macro_derive(SimpleSerialize)] +#[proc_macro_derive(SimpleSerialize, attributes(ssz))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let data = &input.data; - validate_derive_data(data); + let helper_attrs = extract_helper_attrs(&input); + validate_derive_input(data, &helper_attrs); + let helper_attr = helper_attrs.first(); let name = &input.ident; let generics = &input.generics; - let merkleization_impl = derive_merkleization_impl(data, name, generics); + let merkleization_impl = derive_merkleization_impl(data, name, generics, helper_attr); let serializable_impl = derive_serializable_impl(data, name, generics); - let simple_serialize_impl = derive_simple_serialilze_impl(name, generics); + let simple_serialize_impl = derive_simple_serialize_impl(name, generics); let expansion = quote! { #serializable_impl diff --git a/ssz-rs-derive/tests/mod.rs b/ssz-rs-derive/tests/mod.rs new file mode 100644 index 00000000..94f5c86f --- /dev/null +++ b/ssz-rs-derive/tests/mod.rs @@ -0,0 +1,27 @@ +use ssz_rs::prelude::*; +use ssz_rs_derive::SimpleSerialize; + +#[derive(Debug, SimpleSerialize)] +struct Foo { + a: u8, + b: u32, +} + +#[derive(Debug, SimpleSerialize)] +#[ssz(transparent)] +enum Bar { + A(u8), + B(Foo), +} + +#[derive(Debug, SimpleSerialize)] +struct Wrapper(Foo); + +#[test] +fn test_transparent_helper() { + let mut f = Foo { a: 23, b: 445 }; + let f_root = f.hash_tree_root().unwrap(); + let mut bar = Bar::B(f); + let bar_root = bar.hash_tree_root().unwrap(); + assert_eq!(f_root, bar_root); +}