diff --git a/lib/proc-macros/Cargo.toml b/lib/proc-macros/Cargo.toml index 3b526c3396..15c74e5047 100644 --- a/lib/proc-macros/Cargo.toml +++ b/lib/proc-macros/Cargo.toml @@ -13,3 +13,4 @@ proc-macro = true proc-macro2 = { workspace=true } quote = { workspace=true } syn = { workspace=true } + diff --git a/lib/proc-macros/src/content_hash.rs b/lib/proc-macros/src/content_hash.rs index f859b5fee6..d0875a1ddf 100644 --- a/lib/proc-macros/src/content_hash.rs +++ b/lib/proc-macros/src/content_hash.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; -use quote::{quote, quote_spanned}; +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, Variant}; pub fn add_trait_bounds(mut generics: Generics) -> Generics { for param in &mut generics.params { @@ -44,6 +44,53 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { quote! {} } }, + Data::Enum(ref data) => { + let match_hash_statements = + data.variants + .iter() + .enumerate() + .map(|(i, v)| match &v.fields { + Fields::Named(fields) => match_arm_with_fields(i, v, fields.named.iter()), + Fields::Unnamed(fields) => { + match_arm_with_fields(i, v, fields.unnamed.iter()) + } + Fields::Unit => { + let ix: u8 = 0; + quote_spanned! {v.span() => + Self::#v => {state.update(&#ix.to_le_bytes());} + } + } + }); + quote! { + match self { + #(#match_hash_statements)* + } + } + } _ => unimplemented!("ContentHash can only be derived for structs."), } } + +fn match_arm_with_fields<'a>( + index: usize, + variant: &Variant, + fields: impl IntoIterator, +) -> TokenStream { + let bindings = 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::>(); + let variant_id = &variant.ident; + let ix = u8::try_from(index).unwrap(); + + quote_spanned! {variant.span() => + Self::#variant_id( #(#bindings),* ) => { + state.update(&#ix.to_le_bytes()); + #( crate::content_hash::ContentHash::hash(#bindings, state); )* + } + } +} diff --git a/lib/src/content_hash.rs b/lib/src/content_hash.rs index dc2040d7ab..aefb660623 100644 --- a/lib/src/content_hash.rs +++ b/lib/src/content_hash.rs @@ -9,7 +9,7 @@ pub(crate) use jj_lib_proc_macros::ContentHash; /// Variable-length sequences should hash a 64-bit little-endian representation /// of their length, then their elements in order. Unordered containers should /// order their elements according to their `Ord` implementation. Enums should -/// hash a 32-bit little-endian encoding of the ordinal number of the enum +/// hash an 8-bit little-endian encoding of the ordinal number of the enum /// variant, then the variant's fields in lexical order. /// /// Structs can implement `ContentHash` by using `#[derive(ContentHash)]`. @@ -234,6 +234,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 { + None, + Some(T), + } + assert_eq!(hash(&Option::::None), hash(&MyOption::::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]