Skip to content

Commit

Permalink
Merge pull request #109 from ralexstokes/transparent-attr
Browse files Browse the repository at this point in the history
add support for `transparent` mode of derive operation
  • Loading branch information
ralexstokes authored Oct 13, 2023
2 parents cf07d15 + 741f487 commit c00a465
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 29 deletions.
3 changes: 3 additions & 0 deletions ssz-rs-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ proc-macro = true
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"

[dev-dependencies]
ssz_rs = { path = "../ssz-rs" }
23 changes: 18 additions & 5 deletions ssz-rs-derive/README.md
Original file line number Diff line number Diff line change
@@ -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.
160 changes: 136 additions & 24 deletions ssz-rs-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
}
}
}
Expand Down Expand Up @@ -333,21 +350,55 @@ fn is_valid_none_identifier(ident: &Ident) -> bool {
*ident == format_ident!("None")
}

fn filter_ssz_attrs<'a>(
attrs: impl Iterator<Item = &'a Attribute>,
) -> impl Iterator<Item = &'a Attribute> {
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<Item = &'a Field>) {
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() {
Expand All @@ -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`"
Expand Down Expand Up @@ -430,20 +494,64 @@ 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! {
impl #impl_generics ssz_rs::SimpleSerialize for #name #ty_generics {}
}
}

#[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<HelperAttr> {
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::<Vec<_>>(),
_ => 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;
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions ssz-rs-derive/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit c00a465

Please sign in to comment.