diff --git a/Cargo.lock b/Cargo.lock index e501321056..e781ee1141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,6 +1672,7 @@ dependencies = [ "pest_derive", "pollster", "pretty_assertions", + "proc-macros", "prost", "rand", "rand_chacha", @@ -2213,13 +2214,22 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macros" +version = "0.14.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "prodash" version = "28.0.0" diff --git a/Cargo.toml b/Cargo.toml index 3f7f41e483..3aabcb7141 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = [] [workspace] resolver = "2" -members = ["cli", "lib", "lib/testutils", "lib/gen-protos"] +members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] [workspace.package] version = "0.14.0" @@ -66,8 +66,10 @@ pest = "2.7.7" pest_derive = "2.7.7" pollster = "0.3.0" pretty_assertions = "1.4.0" +proc-macro2 = "1.0.78" prost = "0.12.3" prost-build = "0.12.3" +quote = "1.0.35" rand = "0.8.5" rand_chacha = "0.3.1" rayon = "1.8.1" @@ -85,6 +87,7 @@ smallvec = { version = "1.13.0", features = [ "union", ] } strsim = "0.11.0" +syn = "2.0.48" tempfile = "3.10.0" test-case = "3.3.1" textwrap = "0.16.0" @@ -110,6 +113,7 @@ zstd = "0.12.4" # their own (alphabetically sorted) block jj-lib = { path = "lib", version = "0.14.0" } +proc-macros = { path = "lib/proc-macros" } testutils = { path = "lib/testutils" } [profile.release] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7a480cd05e..da26335b3f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -42,6 +42,7 @@ once_cell = { workspace = true } pest = { workspace = true } pest_derive = { workspace = true } pollster = { workspace = true } +proc-macros = { workspace = true } prost = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } diff --git a/lib/proc-macros/Cargo.toml b/lib/proc-macros/Cargo.toml new file mode 100644 index 0000000000..42e04fdc87 --- /dev/null +++ b/lib/proc-macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "proc-macros" +publish = false + +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } + +[lib] +proc-macro = true + +[dependencies] +syn = {workspace=true} +quote ={workspace=true} +proc-macro2 = {workspace=true} diff --git a/lib/proc-macros/src/content_hash.rs b/lib/proc-macros/src/content_hash.rs new file mode 100644 index 0000000000..a487bb64ee --- /dev/null +++ b/lib/proc-macros/src/content_hash.rs @@ -0,0 +1,38 @@ +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; +use syn::{Data, Fields, Index}; + +pub fn generate_hash_impl(data: &Data) -> TokenStream { + match *data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => { + let hash_statements = fields.named.iter().map(|f| { + let field_name = &f.ident; + quote_spanned! {f.span()=> + crate::content_hash::ContentHash::hash(&self.#field_name, state); + // self.#field_name.hash(state); + } + }); + quote! { + #(#hash_statements)* + } + } + Fields::Unnamed(ref fields) => { + let hash_statements = fields.unnamed.iter().enumerate().map(|(i, f)| { + let index = Index::from(i); + quote_spanned! {f.span() => + crate::content_hash::ContentHash::hash(&self.#index, state); + } + }); + quote! { + #(#hash_statements)* + } + } + Fields::Unit => { + quote! {} + } + }, + _ => unimplemented!("ContentHash can only be derived for structs."), + } +} diff --git a/lib/proc-macros/src/lib.rs b/lib/proc-macros/src/lib.rs new file mode 100644 index 0000000000..cdbeae4829 --- /dev/null +++ b/lib/proc-macros/src/lib.rs @@ -0,0 +1,29 @@ +mod content_hash; + +extern crate proc_macro; + +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +/// Derives the `ContentHash` trait for a struct by calling `ContentHash::hash` +/// on each of the struct members in the order that they're declared. All +/// members of the struct must implement the `ContentHash` trait. +#[proc_macro_derive(ContentHash)] +pub fn derive_content_hash(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + // The name of the struct. + let name = &input.ident; + + // Generate an expression to hash each of the fields in the struct. + let hash_impl = content_hash::generate_hash_impl(&input.data); + + let expanded = quote! { + impl crate::content_hash::ContentHash for #name { + fn hash(&self, state: &mut impl digest::Update) { + #hash_impl + } + } + }; + expanded.into() +} diff --git a/lib/src/content_hash.rs b/lib/src/content_hash.rs index 8f5d67a3a7..2ec21ff4aa 100644 --- a/lib/src/content_hash.rs +++ b/lib/src/content_hash.rs @@ -2,6 +2,7 @@ use blake2::Blake2b512; use itertools::Itertools as _; +pub use proc_macros::ContentHash; /// Portable, stable hashing suitable for identifying values /// @@ -231,6 +232,31 @@ mod tests { ); } + // This will be removed once all uses of content_hash! are replaced by the + // derive version. + #[test] + fn derive_is_equivalent_to_macro() { + content_hash! { + struct FooMacro { x: Vec>, y: i64} + } + + #[derive(ContentHash)] + struct FooDerive { + x: Vec>, + y: i64, + } + + let foo_macro = FooMacro { + x: vec![None, Some(42)], + y: 17, + }; + let foo_derive = FooDerive { + x: vec![None, Some(42)], + y: 17, + }; + assert_eq!(hash(&foo_macro), hash(&foo_derive)); + } + fn hash(x: &(impl ContentHash + ?Sized)) -> digest::Output { blake2b_hash(x) }