diff --git a/.gitignore b/.gitignore index d66f6bc..809f795 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.lock # Ignore target directory. -/target +**/target # Ignore crappy mac files. .DS_Store diff --git a/Cargo.toml b/Cargo.toml index 739885b..3bc2daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,7 @@ -[package] -name = "hashconsing" -version = "1.6.0" -authors = ["Adrien Champion "] -description = "A hash consing library." -documentation = "https://docs.rs/hashconsing" -homepage = "https://github.com/AdrienChampion/hashconsing" -repository = "https://github.com/AdrienChampion/hashconsing" -readme = "README.md" -categories = [ - "caching", - "compression", - "concurrency", - "data-structures", - "memory-management", +[workspace] +members = [ + "hashconsing", + "hashconsing_derive", ] -keywords = ["hashconsing", "hash", "consing", "sharing", "caching"] -license = "MIT/Apache-2.0" -edition = "2021" -[package.metadata.docs.rs] -features = ["unstable_docrs"] - -[features] -with_ahash = ["ahash"] -unstable_docrs = ["with_ahash"] - -[dependencies] -lazy_static = "1.*" - -[dependencies.ahash] -version = "^0.8.3" -optional = true - -[dev-dependencies] -crossbeam-utils = "^0.8" -trybuild = "^1.0" -rayon = "^1.5" -rand = "0.8" +resolver = "2" \ No newline at end of file diff --git a/hashconsing/Cargo.toml b/hashconsing/Cargo.toml new file mode 100644 index 0000000..394ad64 --- /dev/null +++ b/hashconsing/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "hashconsing" +version = "1.6.0" +authors = ["Adrien Champion "] +description = "A hash consing library." +documentation = "https://docs.rs/hashconsing" +homepage = "https://github.com/AdrienChampion/hashconsing" +repository = "https://github.com/AdrienChampion/hashconsing" +readme = "README.md" +categories = [ + "caching", + "compression", + "concurrency", + "data-structures", + "memory-management", +] +keywords = ["hashconsing", "hash", "consing", "sharing", "caching"] +license = "MIT/Apache-2.0" +edition = "2021" + +[package.metadata.docs.rs] +features = ["unstable_docrs"] + +[features] +with_ahash = ["ahash"] +unstable_docrs = ["with_ahash"] +derive = ["hashconsing-derive"] + +[dependencies] +lazy_static = "1.*" +hashconsing-derive = { version = "0.1.0", optional = true, path = "../hashconsing_derive" } + +[dependencies.ahash] +version = "^0.8.3" +optional = true + +[dev-dependencies] +crossbeam-utils = "=0.8.15" +trybuild = "^1.0" +rayon = "^1.5" +rand = "0.8" \ No newline at end of file diff --git a/src/coll.rs b/hashconsing/src/coll.rs similarity index 97% rename from src/coll.rs rename to hashconsing/src/coll.rs index 85692ef..d656a6d 100644 --- a/src/coll.rs +++ b/hashconsing/src/coll.rs @@ -178,7 +178,7 @@ where } /// An iterator visiting all elements. #[inline] - pub fn iter(&self) -> ::std::collections::hash_set::Iter> { + pub fn iter(&self) -> ::std::collections::hash_set::Iter<'_, HConsed> { self.set.iter() } } @@ -322,12 +322,12 @@ where } /// An iterator visiting all elements. #[inline] - pub fn iter(&self) -> ::std::collections::hash_map::Iter, V> { + pub fn iter(&self) -> ::std::collections::hash_map::Iter<'_, HConsed, V> { self.map.iter() } /// An iterator visiting all elements. #[inline] - pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut, V> { + pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut<'_, HConsed, V> { self.map.iter_mut() } } @@ -437,7 +437,7 @@ mod hash { impl HashU64 { /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(debug)] + #[cfg(debug_assertions)] #[inline(always)] fn test_bytes(bytes: &[u8]) { if bytes.len() != 8 { @@ -451,13 +451,13 @@ mod hash { } /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(not(debug))] + #[cfg(not(debug_assertions))] #[inline(always)] fn test_bytes(_: &[u8]) {} } impl Hasher for HashU64 { fn finish(&self) -> u64 { - unsafe { ::std::mem::transmute(self.buf) } + u64::from_ne_bytes(self.buf) } fn write(&mut self, bytes: &[u8]) { Self::test_bytes(bytes); diff --git a/src/hash_coll.rs b/hashconsing/src/hash_coll.rs similarity index 97% rename from src/hash_coll.rs rename to hashconsing/src/hash_coll.rs index 37d2623..a9b5fad 100644 --- a/src/hash_coll.rs +++ b/hashconsing/src/hash_coll.rs @@ -42,9 +42,9 @@ //! be the natural one, *e.g.* `HConSet`. //! //! However, since `Term` is really an alias for `HConsed`, then if we wanted to declare -//! `HConSet` as an alias for `HashSet` we would get `type HConSet = HashSet< HConsed -//! >` (omitting the custom hasher). That is, our sets would have type `HConSet`, which is -//! not very pretty. We could just define an alias though: `type TermSet = HConSet`, but it +//! `HConSet` as an alias for `HashSet` we would get `type ``HConSet`` = HashSet< ``HConsed`` +//! >` (omitting the custom hasher). That is, our sets would have type ``HConSet``, which is +//! not very pretty. We could just define an alias though: `type TermSet = `HConSet``, but it //! turns out it's better to wrap the actual set in a `struct` anyway. Mostly to be able to define //! `new` and `with_capacity` without relying on a trait (users would need to import) to do that. //! @@ -322,7 +322,7 @@ where { /// An iterator visiting all elements. #[inline] - pub fn iter(&self) -> ::std::collections::hash_set::Iter> { + pub fn iter(&self) -> ::std::collections::hash_set::Iter<'_, HConsed> { self.set.iter() } } @@ -520,12 +520,12 @@ where { /// An iterator visiting all elements. #[inline] - pub fn iter(&self) -> ::std::collections::hash_map::Iter, V> { + pub fn iter(&self) -> ::std::collections::hash_map::Iter<'_, HConsed, V> { self.map.iter() } /// An iterator visiting all elements. #[inline] - pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut, V> { + pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut<'_, HConsed, V> { self.map.iter_mut() } } @@ -643,7 +643,7 @@ mod hash { impl HashU64 { /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(debug)] + #[cfg(debug_assertions)] #[inline(always)] fn test_bytes(bytes: &[u8]) { if bytes.len() != 8 { @@ -657,13 +657,13 @@ mod hash { } /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(not(debug))] + #[cfg(not(debug_assertions))] #[inline(always)] fn test_bytes(_: &[u8]) {} } impl Hasher for HashU64 { fn finish(&self) -> u64 { - let block: u64 = unsafe { ::std::mem::transmute(self.buf) }; + let block: u64 = u64::from_ne_bytes(self.buf); // Multiply by random 64-bit prime to distribute block.wrapping_mul(0xDA5DF7A7BD02F2C7u64) } diff --git a/src/hash_coll/hashers.rs b/hashconsing/src/hash_coll/hashers.rs similarity index 94% rename from src/hash_coll/hashers.rs rename to hashconsing/src/hash_coll/hashers.rs index c7f5e3d..dc77af3 100644 --- a/src/hash_coll/hashers.rs +++ b/hashconsing/src/hash_coll/hashers.rs @@ -50,7 +50,7 @@ pub mod p_hash { impl Hasher { /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(debug)] + #[cfg(debug_assertions)] #[inline(always)] fn test_bytes(bytes: &[u8]) { if bytes.len() != 8 { @@ -64,13 +64,13 @@ pub mod p_hash { } /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(not(debug))] + #[cfg(not(debug_assertions))] #[inline(always)] fn test_bytes(_: &[u8]) {} } impl StdHasherExt for Hasher { fn finish(&self) -> u64 { - let block: u64 = unsafe { ::std::mem::transmute(self.buf) }; + let block: u64 = u64::from_ne_bytes(self.buf); // Multiply by random 64-bit prime to distribute block.wrapping_mul(0xDA5DF7A7BD02F2C7u64) } @@ -112,7 +112,7 @@ pub mod id_hash { impl Hasher { /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(debug)] + #[cfg(debug_assertions)] #[inline(always)] fn test_bytes(bytes: &[u8]) { if bytes.len() != 8 { @@ -126,13 +126,13 @@ pub mod id_hash { } /// Checks that a slice of bytes has the length of a `usize`. Only active /// in debug. - #[cfg(not(debug))] + #[cfg(not(debug_assertions))] #[inline(always)] fn test_bytes(_: &[u8]) {} } impl StdHasherExt for Hasher { fn finish(&self) -> u64 { - unsafe { ::std::mem::transmute(self.buf) } + u64::from_ne_bytes(self.buf) } fn write(&mut self, bytes: &[u8]) { Self::test_bytes(bytes); diff --git a/src/lib.rs b/hashconsing/src/lib.rs similarity index 98% rename from src/lib.rs rename to hashconsing/src/lib.rs index c00789c..96f8211 100644 --- a/src/lib.rs +++ b/hashconsing/src/lib.rs @@ -226,6 +226,14 @@ use std::{ pub extern crate lazy_static; +#[cfg(feature = "derive")] +#[allow(unused_imports)] +#[macro_use] +extern crate hashconsing_derive; +#[cfg(feature = "derive")] +#[doc(hidden)] +pub use hashconsing_derive::*; + #[cfg(test)] mod test; @@ -239,8 +247,7 @@ mod test; /// - `$capa:expr` initial capacity when creating the consign ; /// - `$hash_builder:expr` optional hash builder, an /// implementation of [`std::hash::BuildHasher`] ; -/// - `$typ:typ,` type being hashconsed (the underlying type, not the -/// hashconsed one) ; +/// - `$typ:typ,` type being hashconsed (the underlying type, not the hashconsed one) ; #[macro_export] macro_rules! consign { ( @@ -354,7 +361,7 @@ impl Eq for HConsed {} impl PartialOrd for HConsed { #[inline] fn partial_cmp(&self, other: &Self) -> Option { - self.uid.partial_cmp(&other.uid) + Some(self.cmp(other)) } } impl Ord for HConsed { @@ -441,7 +448,7 @@ impl Eq for WHConsed {} impl PartialOrd for WHConsed { #[inline] fn partial_cmp(&self, other: &Self) -> Option { - self.uid.partial_cmp(&other.uid) + Some(self.cmp(other)) } } impl Ord for WHConsed { @@ -620,7 +627,7 @@ pub trait HashConsign: Sized { /// Reserves capacity for at least `additional` more elements. fn reserve(self, additional: usize); } -impl<'a, T: Hash + Eq + Clone, S: BuildHasher> HashConsign for &'a mut HConsign { +impl HashConsign for &mut HConsign { fn mk_is_new(self, elm: T) -> (HConsed, bool) { // If the element is known and upgradable return it. if let Some(hconsed) = self.get(&elm) { @@ -693,7 +700,7 @@ macro_rules! get { }; } -impl<'a, T: Hash + Eq + Clone> HashConsign for &'a RwLock> { +impl HashConsign for &RwLock> { /// If the element is already in the consign, only read access will be /// requested. fn mk_is_new(self, elm: T) -> (HConsed, bool) { diff --git a/src/test.rs b/hashconsing/src/test.rs similarity index 100% rename from src/test.rs rename to hashconsing/src/test.rs diff --git a/src/test/basic.rs b/hashconsing/src/test/basic.rs similarity index 100% rename from src/test/basic.rs rename to hashconsing/src/test/basic.rs diff --git a/src/test/collect.rs b/hashconsing/src/test/collect.rs similarity index 100% rename from src/test/collect.rs rename to hashconsing/src/test/collect.rs diff --git a/tests/send_sync.rs b/hashconsing/tests/send_sync.rs similarity index 100% rename from tests/send_sync.rs rename to hashconsing/tests/send_sync.rs diff --git a/tests/try_build.rs b/hashconsing/tests/try_build.rs similarity index 100% rename from tests/try_build.rs rename to hashconsing/tests/try_build.rs diff --git a/tests/try_build/issue_1.rs b/hashconsing/tests/try_build/issue_1.rs similarity index 100% rename from tests/try_build/issue_1.rs rename to hashconsing/tests/try_build/issue_1.rs diff --git a/tests/try_build/issue_1.stderr b/hashconsing/tests/try_build/issue_1.stderr similarity index 96% rename from tests/try_build/issue_1.stderr rename to hashconsing/tests/try_build/issue_1.stderr index 379a339..30b1f93 100644 --- a/tests/try_build/issue_1.stderr +++ b/hashconsing/tests/try_build/issue_1.stderr @@ -9,7 +9,6 @@ error[E0277]: `Cell>` cannot be shared between threads safely 39 | | 40 | | loop { ... | -44 | | } 45 | | }); | |_________^ `Cell>` cannot be shared between threads safely | @@ -33,7 +32,7 @@ note: required because it's used within this closure 37 | s.spawn(move |_| { | ^^^^^^^^ note: required by a bound in `crossbeam_utils::thread::Scope::<'env>::spawn` - --> $CARGO/crossbeam-utils-0.8.16/src/thread.rs + --> $CARGO/crossbeam-utils-$VERSION/src/thread.rs | | pub fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> | ----- required by a bound in this associated function diff --git a/hashconsing_derive/Cargo.toml b/hashconsing_derive/Cargo.toml new file mode 100644 index 0000000..2ca579b --- /dev/null +++ b/hashconsing_derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hashconsing-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +darling = "0.20" +proc-macro-error = "1.0" +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } + +# For doc tests +[dev-dependencies] +hashconsing = {path = "../hashconsing", features = ["derive"]} diff --git a/hashconsing_derive/src/lib.rs b/hashconsing_derive/src/lib.rs new file mode 100644 index 0000000..8d4ca61 --- /dev/null +++ b/hashconsing_derive/src/lib.rs @@ -0,0 +1,328 @@ +//! The derive attribute implementation for the hashconsing crate +//! +//! This automatically generates some of the boilerplate that is needed in the standard use case of hashconsing. +//! +//! There are two parts to this: +//! - the static factory which can be referenced as `_FACTORY` +//! - A series of constructor functions for creating each of the variants +//! +//! +//! +//! Example: +//! ```rust +//! use hashconsing::hcons; +//! use std::ops::Deref; +//! +//! // Can optionally turn off the factory or the constructor generation +//! // #[hcons(name = "Type", no_factory, no_constructors)] +//! #[hcons(name = "Type")] +//! #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +//! pub enum ActualType { +//! // Tuple-style variants +//! Named(String), +//! Arrow(Type, Type), +//! Tuple(Vec), +//! Mu(String, Type), +//! Variant(Vec<(String, Type)>), +//! // Struct-style variant +//! Record { fields: Vec<(String, Type)>, is_open: bool }, +//! // Unit variant +//! Unit, +//! } +//! +//! impl ActualType { +//! pub fn is_named(&self) -> bool { +//! matches!(self, Self::Named(_)) +//! } +//! pub fn is_record(&self) -> bool { +//! matches!(self, Self::Record { .. }) +//! } +//! } +//! +//! let named_type = Type::Named("int".to_string()); +//! // Dereferences to the underlying type with access to methods +//! assert!(named_type.is_named()); +//! let tuple = Type::Tuple(vec![named_type.clone()]); +//! assert!(!tuple.is_named()); +//! +//! // Struct-style variant with named fields +//! let record = Type::Record(vec![("x".to_string(), named_type)], false); +//! assert!(record.is_record()); +//! +//! // Unit variant +//! let unit = Type::Unit(); +//! assert!(!unit.is_named()); +//! ``` + +use darling::{ast::NestedMeta, util::Flag, Error, FromMeta, Result}; +use proc_macro::{self, TokenStream}; +use proc_macro2::Span; +use proc_macro_error::{abort_call_site, proc_macro_error}; +use quote::{format_ident, quote}; +use syn::{ + parse_macro_input, punctuated::Punctuated, token::Brace, token::Paren, Data, DataEnum, + DeriveInput, Expr, ExprCall, ExprPath, ExprStruct, FieldValue, Fields, FnArg, Member, Pat, + PatIdent, PatType, Path, PathArguments, PathSegment, Token, +}; + +#[derive(Debug, Default, FromMeta)] +#[darling(and_then = "Self::not_constructors_without_factory")] +struct MacroArgs { + name: String, + no_factory: Flag, + no_constructors: Flag, +} + +impl MacroArgs { + fn not_constructors_without_factory(self) -> Result { + if self.no_factory.is_present() && !self.no_constructors.is_present() { + abort_call_site!( + "unsupported flag usage: Can't implement constructors without a static factory" + ) + }; + Ok(self) + } +} + +#[proc_macro_error] +#[proc_macro_attribute] +pub fn hcons(args: TokenStream, mut input: TokenStream) -> TokenStream { + let parsed_input = input.clone(); + + let attr_args = match NestedMeta::parse_meta_list(args.into()) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(Error::from(e).write_errors()); + } + }; + + let DeriveInput { + ident, + vis, + attrs, + generics: _, + data, + } = parse_macro_input!(parsed_input); + + let args = match MacroArgs::from_list(&attr_args) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(e.write_errors()); + } + }; + + let struct_name = format_ident!("{}", args.name); + let factory_name = format_ident!("{}_FACTORY", args.name); + + /* let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); */ + + let hash_struct = quote! { + #(#attrs)* + #[automatically_derived] + #vis struct #struct_name(hashconsing::HConsed<#ident>); + + #[automatically_derived] + impl std::ops::Deref for #struct_name { + type Target = hashconsing::HConsed<#ident>; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + }; + + let hash_factory = quote! { + hashconsing::consign! { + let #factory_name = consign(50) for #ident ; + } + }; + + let hash_impl = match data { + Data::Enum(DataEnum { variants, .. }) => { + let variant_names = variants.iter().map(|v| &v.ident); + let (variant_field_function_args, variant_field_calling_args): ( + Vec>, + Vec, + ) = variants + .iter() + .map(|v| { + let variant_path = Path { + leading_colon: None, + segments: Punctuated::from_iter(vec![ + PathSegment { + ident: ident.clone(), + arguments: PathArguments::None, + }, + PathSegment { + ident: v.ident.clone(), + arguments: PathArguments::None, + }, + ]), + }; + + match &v.fields { + Fields::Named(named_fields) => { + // Struct-like variant: MyVariant { field_1: Type, field_2: Type } + let (field_values, fn_args): (Vec, Vec) = + named_fields + .named + .iter() + .map(|f| { + let field_ident = f.ident.clone().unwrap(); + ( + FieldValue { + attrs: Vec::new(), + member: Member::Named(field_ident.clone()), + colon_token: None, // shorthand syntax + expr: Expr::Path(ExprPath { + attrs: Vec::new(), + qself: None, + path: Path { + leading_colon: None, + segments: Punctuated::from_iter(vec![ + PathSegment { + ident: field_ident.clone(), + arguments: PathArguments::None, + }, + ]), + }, + }), + }, + FnArg::Typed(PatType { + attrs: Vec::new(), + pat: Box::new(Pat::Ident(PatIdent { + attrs: Vec::new(), + by_ref: None, + mutability: None, + ident: field_ident, + subpat: None, + })), + colon_token: Token![:](Span::call_site()), + ty: Box::new(f.ty.clone()), + }), + ) + }) + .unzip(); + + let variant_expr = ExprStruct { + attrs: Vec::new(), + qself: None, + path: variant_path, + brace_token: Brace(Span::call_site()), + fields: Punctuated::from_iter(field_values), + dot2_token: None, + rest: None, + }; + + (Punctuated::from_iter(fn_args), variant_expr.into()) + } + Fields::Unnamed(_) => { + // Tuple-like variant: MyVariant(Type1, Type2) + let (arg_names, arg_types): (Vec, Vec) = v + .fields + .iter() + .enumerate() + .map(|(i, f)| { + let id = format_ident!("args{}", i); + ( + { + ExprPath { + attrs: Vec::new(), + qself: None, + path: Path { + leading_colon: None, + segments: Punctuated::from_iter(vec![ + PathSegment { + ident: id.clone(), + arguments: PathArguments::None, + }, + ]), + }, + } + .into() + }, + FnArg::Typed(PatType { + attrs: Vec::new(), + pat: Box::new(Pat::Ident(PatIdent { + attrs: Vec::new(), + by_ref: None, + mutability: None, + ident: id, + subpat: None, + })), + colon_token: Token![:](Span::call_site()), + ty: Box::new(f.ty.clone()), + }), + ) + }) + .unzip(); + + let calling_args = Punctuated::from_iter(arg_names); + let variant_name = Expr::Path(ExprPath { + attrs: Vec::new(), + qself: None, + path: variant_path, + }); + + let variant_expr = ExprCall { + attrs: Vec::new(), + func: Box::new(variant_name), + paren_token: Paren(Span::call_site()), + args: calling_args, + }; + + (Punctuated::from_iter(arg_types), variant_expr.into()) + } + Fields::Unit => { + // Unit variant: MyVariant + let variant_expr = Expr::Path(ExprPath { + attrs: Vec::new(), + qself: None, + path: variant_path, + }); + + (Punctuated::new(), variant_expr) + } + } + }) + .unzip(); + + quote! { + #[automatically_derived] + #[allow(non_snake_case)] + impl #struct_name { + #(pub fn #variant_names(#variant_field_function_args) -> Self { + use hashconsing::HashConsign; + Self(#factory_name.mk(#variant_field_calling_args)) + })* + } + } + } + _ => abort_call_site!("unsupported syntax: hashconsing expected an enum definition"), + }; + + let output = if args.no_constructors.is_present() && args.no_factory.is_present() { + quote! { + #hash_struct + } + } else if args.no_constructors.is_present() { + quote! { + #hash_struct + + #hash_factory + } + } else { + quote! { + #hash_struct + + #hash_factory + + #hash_impl + } + }; + + /* println!("{output}"); */ + + input.extend::(output.into()); + + input +}