Skip to content

Commit

Permalink
Implement a procedural macro to derive the ContentHash trait for structs
Browse files Browse the repository at this point in the history
This is a no-op in terms of function, but provides a nicer way to derive the
ContentHash trait for structs using the `#[derive(ContentHash)]` syntax used
for other traits such as `Debug`.

This commit only adds the macro. A subsequent commit will replace uses of
`content_hash!{}` with `#[derive(ContentHash)]`.

The new macro generates nice error messages, just like the old macro, although
they are slightly different.

Before:
```
error[E0277]: the trait bound `NotImplemented: content_hash::ContentHash` is not satisfied
   --> lib/src/content_hash.rs:242:30
    |
242 |             struct Broken{t: NotImplemented}
    |                              ^^^^^^^^^^^^^^ the trait `content_hash::ContentHash` is not implemented for `NotImplemented`
    |
    = help: the following other types implement trait `content_hash::ContentHash`:
              bool
              i32
              i64
              u8
              std::collections::HashMap<K, V>
              BTreeMap<K, V>
              std::collections::HashSet<K>
              content_hash::tests::test_struct_sanity::Foo
            and 38 others
```

After:
```
error[E0277]: the trait bound `NotImplemented: content_hash::ContentHash` is not satisfied
   --> lib/src/content_hash.rs:247:13
    |
247 |             t: NotImplemented,
    |             ^ the trait `content_hash::ContentHash` is not implemented for `NotImplemented`
    |
    = help: the following other types implement trait `content_hash::ContentHash`:
              bool
              i32
              i64
              u8
              std::collections::HashMap<K, V>
              BTreeMap<K, V>
              std::collections::HashSet<K>
              content_hash::tests::test_struct_sanity::Foo
            and 38 others

For more information about this error, try `rustc --explain E0277`.
error: could not compile `jj-lib` (lib test) due to 2 previous errors
```
  • Loading branch information
emesterhazy committed Feb 14, 2024
1 parent 2652809 commit 4821670
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 3 deletions.
14 changes: 12 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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]
Expand Down
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
15 changes: 15 additions & 0 deletions lib/proc-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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}
38 changes: 38 additions & 0 deletions lib/proc-macros/src/content_hash.rs
Original file line number Diff line number Diff line change
@@ -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."),
}
}
29 changes: 29 additions & 0 deletions lib/proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
}
26 changes: 26 additions & 0 deletions lib/src/content_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use blake2::Blake2b512;
use itertools::Itertools as _;
pub use proc_macros::ContentHash;

/// Portable, stable hashing suitable for identifying values
///
Expand Down Expand Up @@ -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<Option<i32>>, y: i64}
}

#[derive(ContentHash)]
struct FooDerive {
x: Vec<Option<i32>>,
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<Blake2b512> {
blake2b_hash(x)
}
Expand Down

0 comments on commit 4821670

Please sign in to comment.