Skip to content

Commit

Permalink
Add support for deriving ContentHash for Enums
Browse files Browse the repository at this point in the history
Here's an example of what the derived output looks like for an enum:

```rust
pub enum TreeValue {
    File { id: FileId, executable: bool },
    Symlink(SymlinkId),
    Tree(TreeId),
    GitSubmodule(CommitId),
    Conflict(ConflictId),
}
#[automatically_derived]
impl ::jj_lib::content_hash::ContentHash for TreeValue {
    fn hash(&self, state: &mut impl digest::Update) {
        match self {
            Self::File { id, executable } => {
                state.update(&0u32.to_le_bytes());
                ::jj_lib::content_hash::ContentHash::hash(id, state);
                ::jj_lib::content_hash::ContentHash::hash(executable, state);
            }
            Self::Symlink(field_0) => {
                state.update(&1u32.to_le_bytes());
                ::jj_lib::content_hash::ContentHash::hash(field_0, state);
            }
            Self::Tree(field_0) => {
                state.update(&2u32.to_le_bytes());
                ::jj_lib::content_hash::ContentHash::hash(field_0, state);
            }
            Self::GitSubmodule(field_0) => {
                state.update(&3u32.to_le_bytes());
                ::jj_lib::content_hash::ContentHash::hash(field_0, state);
            }
            Self::Conflict(field_0) => {
                state.update(&4u32.to_le_bytes());
                ::jj_lib::content_hash::ContentHash::hash(field_0, state);
            }
        }
    }
}
```


#3054
  • Loading branch information
emesterhazy committed Feb 20, 2024
1 parent 8e1a6c7 commit 966a550
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 4 deletions.
68 changes: 64 additions & 4 deletions lib/proc-macros/src/content_hash.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{parse_quote, Data, Fields, GenericParam, Generics, Index};
use syn::{parse_quote, Data, Field, Fields, GenericParam, Generics, Index};

pub fn add_trait_bounds(mut generics: Generics) -> Generics {
for param in &mut generics.params {
Expand Down Expand Up @@ -46,6 +46,66 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream {
quote! {}
}
},
_ => unimplemented!("ContentHash can only be derived for structs."),
// Generates a match statement with a match arm and hash implementation
// for each of the variants in the enum.
Data::Enum(ref data) => {
let match_hash_statements = data.variants.iter().enumerate().map(|(i, v)| {
let variant_id = &v.ident;
match &v.fields {
Fields::Named(fields) => {
let bindings = enum_bindings(fields.named.iter());
let ix = index_to_ordinal(i);
quote_spanned! {v.span() =>
Self::#variant_id{ #(#bindings),* } => {
::jj_lib::content_hash::ContentHash::hash(&#ix, state);
#( ::jj_lib::content_hash::ContentHash::hash(#bindings, state); )*
}
}
}
Fields::Unnamed(fields) => {
let bindings = enum_bindings(fields.unnamed.iter());
let ix = index_to_ordinal(i);
quote_spanned! {v.span() =>
Self::#variant_id( #(#bindings),* ) => {
::jj_lib::content_hash::ContentHash::hash(&#ix, state);
#( ::jj_lib::content_hash::ContentHash::hash(#bindings, state); )*
}
}
}
Fields::Unit => {
let ix = index_to_ordinal(i);
quote_spanned! {v.span() =>
Self::#variant_id => {
::jj_lib::content_hash::ContentHash::hash(&#ix, state);
}
}
}
}
});
quote! {
match self {
#(#match_hash_statements)*
}
}
}
Data::Union(_) => unimplemented!("ContentHash cannot be derived for unions."),
}
}

// The documentation for `ContentHash` specifies that the hash impl for each
// enum variant should hash the ordinal number of the enum variant as a little
// endian u32 before hashing the variant's fields, if any.
fn index_to_ordinal(ix: usize) -> u32 {
u32::try_from(ix).expect("The number of enum variants overflows a u32.")
}

fn enum_bindings<'a>(fields: impl IntoIterator<Item = &'a Field>) -> Vec<Ident> {
fields
.into_iter()
.enumerate()
.map(|(i, f)| {
// If the field is named, use the name, otherwise generate a placeholder name.
f.ident.clone().unwrap_or(format_ident!("field_{}", i))
})
.collect::<Vec<_>>()
}
12 changes: 12 additions & 0 deletions lib/src/content_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ mod tests {
);
}

// Test that the derived version of `ContentHash` matches the that's
// manually implemented for `std::Option`.
#[test]
fn derive_for_enum() {
#[derive(ContentHash)]
enum MyOption<T> {
None,
Some(T),
}
assert_eq!(hash(&Option::<i32>::None), hash(&MyOption::<i32>::None));
assert_eq!(hash(&Some(1)), hash(&MyOption::Some(1)));
}
// This will be removed once all uses of content_hash! are replaced by the
// derive version.
#[test]
Expand Down

0 comments on commit 966a550

Please sign in to comment.