Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
26 changes: 18 additions & 8 deletions src/fast_automaton/convert/to_regex/state_elimination/builder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use ahash::HashMapExt;

use super::*;

impl Gnfa {
Expand All @@ -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,
};

Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/fast_automaton/generate.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<u32>>;
type RangeIds<'a> = HashMap<&'a Condition, Option<u32>>;

/// How [`FastAutomaton::generate_strings`] schedules the *paths* of a
/// language: one at a time, or interleaved so that every shape the pattern
Expand Down Expand Up @@ -519,7 +523,7 @@ fn resolve_ranges<'a>(
charset: Option<&CharRange>,
) -> Result<(Vec<CharRange>, RangeIds<'a>), EngineError> {
let mut range_pool: Vec<CharRange> = 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) {
Expand Down
1 change: 0 additions & 1 deletion src/fast_automaton/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::error::EngineError;
use ahash::{AHashMap, HashSetExt};
use condition::Condition;
use regex_charclass::CharacterClass;
use spanning_set::SpanningSet;
Expand Down
7 changes: 6 additions & 1 deletion src/fast_automaton/operation/determinize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
13 changes: 9 additions & 4 deletions src/fast_automaton/operation/intersection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, EngineError> {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/fast_automaton/operation/minimize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>> = vec![Vec::new(); max_states];
let mut touched_partitions: Vec<usize> = Vec::with_capacity(max_states);
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions src/fast_automaton/operation/union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ impl FastAutomaton {
new_states: &mut IntMap<usize, usize>,
condition_converter: Option<&ConditionConverter>,
) -> Result<IntSet<usize>, 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
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 7 additions & 2 deletions src/fast_automaton/spanning_set/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -194,7 +198,8 @@ impl SpanningSet {
fn sweep_wide(input_count: usize, events: &[Event]) -> (Vec<CharRange>, Vec<(u32, u32)>) {
let mut active = vec![0u64; input_count.div_ceil(64)];
let mut active_count = 0usize;
let mut atoms: AHashMap<Vec<u64>, Vec<(u32, u32)>> = AHashMap::new();
let mut atoms: HashMap<Vec<u64>, Vec<(u32, u32)>> =
HashMap::with_capacity_and_hasher(0, Default::default());
let mut covered: Vec<(u32, u32)> = Vec::new();

let mut i = 0;
Expand Down