diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aec0024..b857e7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,13 +23,13 @@ jobs: with: rustflags: "" - name: Build - run: cargo build --verbose + run: cargo build --workspace --all-features --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --workspace --all-features --verbose - name: Build doc - run: cargo doc --verbose + run: cargo doc --workspace --all-features --verbose - name: Run clippy - run: cargo clippy --verbose + run: cargo clippy --workspace --all-features --verbose - name: Run rustfmt run: cargo fmt --verbose --check - name: Build release diff --git a/.gitignore b/.gitignore index 63a000b..f0aafa3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ Cargo.lock # Ignore target directory. -/target +**/target # Ignore crappy mac files. # Ignore files ending in ".nogit". diff --git a/Cargo.toml b/Cargo.toml index d406671..cd7a9d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,17 @@ +[workspace] +members = [ + "hashconsing_derive", +] + +resolver = "2" + +[workspace.package] +version = "1.7.0" +edition = "2021" + [package] name = "hashconsing" -version = "1.7.0" +version.workspace = true authors = [ "Adrien Champion ", "Leni Aniva ", @@ -19,7 +30,7 @@ categories = [ ] keywords = ["hashconsing", "hash", "consing", "sharing", "caching"] license = "MIT/Apache-2.0" -edition = "2021" +edition.workspace = true rust-version = "1.60" [package.metadata.docs.rs] @@ -29,9 +40,11 @@ features = ["unstable_docrs"] with_ahash = ["ahash"] unstable_docrs = ["with_ahash"] weak-table = ["dep:weak-table"] +derive = ["hashconsing-derive"] [dependencies] lazy_static = "1.*" +hashconsing-derive = { version = "1.7.0", optional = true, path = "hashconsing_derive" } [dependencies.ahash] version = "^0.8.3" @@ -43,6 +56,5 @@ optional = true [dev-dependencies] crossbeam-utils = "^0.8" -trybuild = "^1.0" rayon = "^1.5" rand = "0.8" diff --git a/hashconsing_derive/Cargo.toml b/hashconsing_derive/Cargo.toml new file mode 100644 index 0000000..d49d3bd --- /dev/null +++ b/hashconsing_derive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hashconsing-derive" +version.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +convert_case = "0.11" +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 = "..", features = ["derive"]} diff --git a/hashconsing_derive/src/lib.rs b/hashconsing_derive/src/lib.rs new file mode 100644 index 0000000..70e1f79 --- /dev/null +++ b/hashconsing_derive/src/lib.rs @@ -0,0 +1,356 @@ +//! 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()); +//! +//! # // Verify derived traits are inherited by the generated wrapper type +//! # fn assert_traits() {} +//! # assert_traits::(); +//! ``` +//! +//! Custom factory capacity: +//! ```rust +//! use hashconsing::hcons; +//! +//! #[hcons(name = "Expr", capacity = 1000)] +//! #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +//! pub enum ActualExpr { +//! Lit(i64), +//! Add(Expr, Expr), +//! } +//! +//! let lit = Expr::lit(42); +//! let sum = Expr::add(lit.clone(), lit); +//! +//! # // Note capacity is imprecise and allowed to allocate more space than expected +//! # assert!(Expr_FACTORY.read().unwrap().capacity() >= 1000); +//! ``` + +use convert_case::{Case, Casing}; +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, + #[darling(default)] + capacity: Option, +} + +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] + #[repr(transparent)] + #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 capacity = args.capacity.unwrap_or(50); + let hash_factory = quote! { + hashconsing::consign! { + let #factory_name = consign(#capacity) for #ident ; + } + }; + + let hash_impl = match data { + Data::Enum(DataEnum { variants, .. }) => { + let variant_names = variants + .iter() + .map(|v| format_ident!("{}", v.ident.to_string().to_case(Case::Snake))); + 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] + 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 +} diff --git a/src/coll.rs b/src/coll.rs index 2855128..d656a6d 100644 --- a/src/coll.rs +++ b/src/coll.rs @@ -178,7 +178,7 @@ where } /// An iterator visiting all elements. #[inline] - pub fn iter<'a>(&'a self) -> ::std::collections::hash_set::Iter<'a, HConsed> { + pub fn iter(&self) -> ::std::collections::hash_set::Iter<'_, HConsed> { self.set.iter() } } @@ -322,14 +322,12 @@ where } /// An iterator visiting all elements. #[inline] - pub fn iter<'a>(&'a self) -> ::std::collections::hash_map::Iter<'a, HConsed, 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<'a>( - &'a mut self, - ) -> ::std::collections::hash_map::IterMut<'a, HConsed, V> { + pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut<'_, HConsed, V> { self.map.iter_mut() } } diff --git a/src/hash_coll.rs b/src/hash_coll.rs index a79d87c..65943e5 100644 --- a/src/hash_coll.rs +++ b/src/hash_coll.rs @@ -322,7 +322,7 @@ where { /// An iterator visiting all elements. #[inline] - pub fn iter<'a>(&'a self) -> ::std::collections::hash_set::Iter<'a, HConsed> { + pub fn iter(&self) -> ::std::collections::hash_set::Iter<'_, HConsed> { self.set.iter() } } @@ -520,14 +520,12 @@ where { /// An iterator visiting all elements. #[inline] - pub fn iter<'a>(&'a self) -> ::std::collections::hash_map::Iter<'a, HConsed, 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<'a>( - &'a mut self, - ) -> ::std::collections::hash_map::IterMut<'a, HConsed, V> { + pub fn iter_mut(&mut self) -> ::std::collections::hash_map::IterMut<'_, HConsed, V> { self.map.iter_mut() } } diff --git a/src/lib.rs b/src/lib.rs index ecb0876..e07ef09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -227,6 +227,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; @@ -240,8 +248,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 { (