diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ba303..6274a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this crate are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- `ahash` is now an optional dependency, falling back to `std::collections::HashMap` if not enabled. It is included in the `default` features. + ## [1.0.1] - 2026-09-07 A maintenance release covering dependencies and packaging. The public API is unchanged, and no operation returns a different result. diff --git a/Cargo.toml b/Cargo.toml index 96d9033..0dbc663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ include = [ [dependencies] tracing = "0.1" -ahash = "0.8.11" +ahash = { version = "0.8.11", optional = true } regex-syntax = "0.8.11" # `ucd-16` has to match the Unicode database `regex-syntax` carries: a pattern is # parsed into ranges by `regex-syntax` and named back into a class by @@ -39,13 +39,15 @@ bit-set = "0.11.1" indexmap = "2.13.0" [features] -default = ["parallel"] +default = ["parallel", "ahash"] parallel = ["dep:rayon"] +ahash = ["dep:ahash"] [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } proptest = "1" regex = "1.13.1" +ahash = "0.8.11" [package.metadata.docs.rs] all-features = true diff --git a/src/fast_automaton/convert/to_regex/state_elimination/builder.rs b/src/fast_automaton/convert/to_regex/state_elimination/builder.rs index 568e9ae..45908b1 100644 --- a/src/fast_automaton/convert/to_regex/state_elimination/builder.rs +++ b/src/fast_automaton/convert/to_regex/state_elimination/builder.rs @@ -1,5 +1,3 @@ -use ahash::HashMapExt; - use super::*; impl Gnfa { @@ -8,8 +6,14 @@ impl Gnfa { start_state: 0, // start_state is not set yet accept_state: 0, // accept_state is not set yet transitions: Vec::with_capacity(automaton.number_of_states()), - transitions_in: IntMap::with_capacity(automaton.number_of_states()), - removed_states: IntSet::with_capacity(automaton.number_of_states()), + transitions_in: IntMap::with_capacity_and_hasher( + automaton.number_of_states(), + Default::default(), + ), + removed_states: IntSet::with_capacity_and_hasher( + automaton.number_of_states(), + Default::default(), + ), empty: false, }; @@ -18,7 +22,8 @@ impl Gnfa { return Ok(state_elimination_automaton); } - let mut states_map = IntMap::with_capacity(automaton.number_of_states()); + let mut states_map = + IntMap::with_capacity_and_hasher(automaton.number_of_states(), Default::default()); for from_state in automaton.states() { let new_from_state = *states_map @@ -81,12 +86,17 @@ impl Gnfa { fn new_state(&mut self) -> usize { if let Some(&new_state) = self.removed_states.iter().next() { self.removed_states.remove(&new_state); - self.transitions_in.insert(new_state, IntSet::new()); + self.transitions_in.insert( + new_state, + IntSet::with_capacity_and_hasher(0, Default::default()), + ); new_state } else { self.transitions.push(IntMap::default()); - self.transitions_in - .insert(self.transitions.len() - 1, IntSet::new()); + self.transitions_in.insert( + self.transitions.len() - 1, + IntSet::with_capacity_and_hasher(0, Default::default()), + ); self.transitions.len() - 1 } } diff --git a/src/fast_automaton/generate.rs b/src/fast_automaton/generate.rs index a81ee8c..099b937 100644 --- a/src/fast_automaton/generate.rs +++ b/src/fast_automaton/generate.rs @@ -1,7 +1,11 @@ use crate::{EngineError, execution_profile::ExecutionProfile}; -use ahash::RandomState; use indexmap::IndexSet; +#[cfg(feature = "ahash")] +use ahash::{AHashMap as HashMap, RandomState}; +#[cfg(not(feature = "ahash"))] +use std::collections::hash_map::RandomState; + use super::*; use std::cmp::Ordering; use std::collections::BinaryHeap; @@ -10,7 +14,7 @@ use std::ops::Range; /// Each transition condition's index into the range pool the generation /// resolved, the charset already taken out; `None` for the conditions the /// charset leaves nothing of. -type RangeIds<'a> = AHashMap<&'a Condition, Option>; +type RangeIds<'a> = HashMap<&'a Condition, Option>; /// How [`FastAutomaton::generate_strings`] schedules the *paths* of a /// language: one at a time, or interleaved so that every shape the pattern @@ -519,7 +523,7 @@ fn resolve_ranges<'a>( charset: Option<&CharRange>, ) -> Result<(Vec, RangeIds<'a>), EngineError> { let mut range_pool: Vec = Vec::new(); - let mut range_ids: RangeIds = AHashMap::with_capacity(automaton.transitions.len()); + let mut range_ids: RangeIds = HashMap::with_capacity(automaton.transitions.len()); for state in automaton.states() { for (cond, _) in automaton.transitions_from(state) { diff --git a/src/fast_automaton/mod.rs b/src/fast_automaton/mod.rs index bdad488..c677043 100644 --- a/src/fast_automaton/mod.rs +++ b/src/fast_automaton/mod.rs @@ -1,5 +1,4 @@ use crate::error::EngineError; -use ahash::{AHashMap, HashSetExt}; use condition::Condition; use regex_charclass::CharacterClass; use spanning_set::SpanningSet; diff --git a/src/fast_automaton/operation/determinize.rs b/src/fast_automaton/operation/determinize.rs index 92d6406..bae52ae 100644 --- a/src/fast_automaton/operation/determinize.rs +++ b/src/fast_automaton/operation/determinize.rs @@ -4,6 +4,11 @@ use crate::{EngineError, execution_profile::ExecutionProfile}; use super::*; +#[cfg(feature = "ahash")] +use ahash::AHashMap as HashMap; +#[cfg(not(feature = "ahash"))] +use std::collections::HashMap; + impl FastAutomaton { /// [`determinize`](Self::determinize) on behalf of an operation that /// requires a deterministic automaton: when the execution profile @@ -30,7 +35,7 @@ impl FastAutomaton { let mut worklist = VecDeque::with_capacity(self.number_of_states()); let map_capacity = (self.number_of_states() as f64 / 0.75).ceil() as usize; - let mut new_states = AHashMap::with_capacity(map_capacity); + let mut new_states = HashMap::with_capacity(map_capacity); let mut accept_states = BitSet::new(); for &state in &self.accept_states { diff --git a/src/fast_automaton/operation/intersection.rs b/src/fast_automaton/operation/intersection.rs index 0ba1fce..ecdb7fa 100644 --- a/src/fast_automaton/operation/intersection.rs +++ b/src/fast_automaton/operation/intersection.rs @@ -6,6 +6,11 @@ use crate::{error::EngineError, execution_profile::ExecutionProfile}; use super::*; +#[cfg(feature = "ahash")] +use ahash::AHashMap as HashMap; +#[cfg(not(feature = "ahash"))] +use std::collections::HashMap; + impl FastAutomaton { /// Computes the intersection between `self` and `other`. pub fn intersection(&self, other: &FastAutomaton) -> Result { @@ -103,8 +108,8 @@ impl FastAutomaton { let mut new_automaton = FastAutomaton::new_empty(); let mut worklist = VecDeque::with_capacity(self.number_of_states() + other.number_of_states()); - let mut new_states: AHashMap<(usize, usize), (usize, usize, usize), _> = - AHashMap::with_capacity(self.number_of_states() + other.number_of_states()); + let mut new_states: HashMap<(usize, usize), (usize, usize, usize), _> = + HashMap::with_capacity(self.number_of_states() + other.number_of_states()); let initial_pair = ( new_automaton.start_state, @@ -198,8 +203,8 @@ impl FastAutomaton { let mut new_automaton = FastAutomaton::new_empty(); let mut worklist = VecDeque::with_capacity(self.number_of_states() + other.number_of_states()); - let mut new_states: AHashMap<(usize, usize), (usize, usize, usize), _> = - AHashMap::with_capacity(self.number_of_states() + other.number_of_states()); + let mut new_states: HashMap<(usize, usize), (usize, usize, usize), _> = + HashMap::with_capacity(self.number_of_states() + other.number_of_states()); let initial_pair = ( new_automaton.start_state, diff --git a/src/fast_automaton/operation/minimize.rs b/src/fast_automaton/operation/minimize.rs index f33f555..a484376 100644 --- a/src/fast_automaton/operation/minimize.rs +++ b/src/fast_automaton/operation/minimize.rs @@ -71,7 +71,7 @@ impl FastAutomaton { } } - let mut x = IntSet::with_capacity(self.number_of_states()); + let mut x = IntSet::with_capacity_and_hasher(self.number_of_states(), Default::default()); let mut intersection_states: Vec> = vec![Vec::new(); max_states]; let mut touched_partitions: Vec = Vec::with_capacity(max_states); @@ -120,7 +120,8 @@ impl FastAutomaton { // A split happens! 'int_states' becomes the new partition. let new_idx = partitions.len(); - let mut new_part = IntSet::with_capacity(int_states.len()); + let mut new_part = + IntSet::with_capacity_and_hasher(int_states.len(), Default::default()); for &state in int_states.iter() { partitions[p_idx].remove(&state); // Remove from original (forming the difference) diff --git a/src/fast_automaton/operation/union.rs b/src/fast_automaton/operation/union.rs index 2e135d1..ee54945 100644 --- a/src/fast_automaton/operation/union.rs +++ b/src/fast_automaton/operation/union.rs @@ -84,7 +84,10 @@ impl FastAutomaton { new_states: &mut IntMap, condition_converter: Option<&ConditionConverter>, ) -> Result, EngineError> { - let mut imcomplete_states = IntSet::with_capacity(other.out_degree(other.start_state) + 1); + let mut imcomplete_states = IntSet::with_capacity_and_hasher( + other.out_degree(other.start_state) + 1, + Default::default(), + ); // If `other` accepts the empty string we must make the union's *entry* // state accepting, but only after the start state is finalized below. // Marking the current start eagerly is wrong when it has incoming edges @@ -317,7 +320,7 @@ impl FastAutomaton { // Track which 'other' states are already mapped in the start phase // so we don't double-count them when calculating accept state savings. - let mut mapped_other_states = IntSet::new(); + let mut mapped_other_states = IntSet::with_capacity_and_hasher(0, Default::default()); mapped_other_states.insert(other.start_state); if other_in != 0 { diff --git a/src/fast_automaton/spanning_set/mod.rs b/src/fast_automaton/spanning_set/mod.rs index 2580177..a762bd6 100644 --- a/src/fast_automaton/spanning_set/mod.rs +++ b/src/fast_automaton/spanning_set/mod.rs @@ -1,11 +1,15 @@ use std::slice::Iter; -use ahash::AHashMap; use regex_charclass::irange::RangeSet; use super::{from_scalar, scalar}; use crate::CharRange; +#[cfg(feature = "ahash")] +use ahash::AHashMap as HashMap; +#[cfg(not(feature = "ahash"))] +use std::collections::HashMap; + /// Converts merged, ascending scalar segments (see [`scalar`](super::scalar)) /// back into a [`CharRange`]. The segments are disjoint and non-adjacent, so /// the flat bound list is already the canonical representation the set @@ -194,7 +198,8 @@ impl SpanningSet { fn sweep_wide(input_count: usize, events: &[Event]) -> (Vec, Vec<(u32, u32)>) { let mut active = vec![0u64; input_count.div_ceil(64)]; let mut active_count = 0usize; - let mut atoms: AHashMap, Vec<(u32, u32)>> = AHashMap::new(); + let mut atoms: HashMap, Vec<(u32, u32)>> = + HashMap::with_capacity_and_hasher(0, Default::default()); let mut covered: Vec<(u32, u32)> = Vec::new(); let mut i = 0;