build(rust): vendor googletest crate
This commit was merged in pull request #76.
This commit is contained in:
1 parent
a6fe84190e
commit
6887b104d6
576 files changed
+284290
-2
No files matched your search
@@ -0,0 +1,691 @@
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
packed::{pattern::Patterns, rabinkarp::RabinKarp, teddy},
|
||||
util::search::{Match, Span},
|
||||
};
|
||||
|
||||
/// This is a limit placed on the total number of patterns we're willing to try
|
||||
/// and match at once. As more sophisticated algorithms are added, this number
|
||||
/// may be increased.
|
||||
const PATTERN_LIMIT: usize = 128;
|
||||
|
||||
/// A knob for controlling the match semantics of a packed multiple string
|
||||
/// searcher.
|
||||
///
|
||||
/// This differs from the [`MatchKind`](crate::MatchKind) type in the top-level
|
||||
/// crate module in that it doesn't support "standard" match semantics,
|
||||
/// and instead only supports leftmost-first or leftmost-longest. Namely,
|
||||
/// "standard" semantics cannot be easily supported by packed searchers.
|
||||
///
|
||||
/// For more information on the distinction between leftmost-first and
|
||||
/// leftmost-longest, see the docs on the top-level `MatchKind` type.
|
||||
///
|
||||
/// Unlike the top-level `MatchKind` type, the default match semantics for this
|
||||
/// type are leftmost-first.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum MatchKind {
|
||||
/// Use leftmost-first match semantics, which reports leftmost matches.
|
||||
/// When there are multiple possible leftmost matches, the match
|
||||
/// corresponding to the pattern that appeared earlier when constructing
|
||||
/// the automaton is reported.
|
||||
///
|
||||
/// This is the default.
|
||||
LeftmostFirst,
|
||||
/// Use leftmost-longest match semantics, which reports leftmost matches.
|
||||
/// When there are multiple possible leftmost matches, the longest match
|
||||
/// is chosen.
|
||||
LeftmostLongest,
|
||||
}
|
||||
|
||||
impl Default for MatchKind {
|
||||
fn default() -> MatchKind {
|
||||
MatchKind::LeftmostFirst
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a packed multiple pattern searcher.
|
||||
///
|
||||
/// The configuration is currently limited only to being able to select the
|
||||
/// match semantics (leftmost-first or leftmost-longest) of a searcher. In the
|
||||
/// future, more knobs may be made available.
|
||||
///
|
||||
/// A configuration produces a [`packed::Builder`](Builder), which in turn can
|
||||
/// be used to construct a [`packed::Searcher`](Searcher) for searching.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example shows how to use leftmost-longest semantics instead of the
|
||||
/// default (leftmost-first).
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{Config, MatchKind}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Config::new()
|
||||
/// .match_kind(MatchKind::LeftmostLongest)
|
||||
/// .builder()
|
||||
/// .add("foo")
|
||||
/// .add("foobar")
|
||||
/// .build()?;
|
||||
/// let matches: Vec<PatternID> = searcher
|
||||
/// .find_iter("foobar")
|
||||
/// .map(|mat| mat.pattern())
|
||||
/// .collect();
|
||||
/// assert_eq!(vec![PatternID::must(1)], matches);
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
kind: MatchKind,
|
||||
force: Option<ForceAlgorithm>,
|
||||
only_teddy_fat: Option<bool>,
|
||||
only_teddy_256bit: Option<bool>,
|
||||
heuristic_pattern_limits: bool,
|
||||
}
|
||||
|
||||
/// An internal option for forcing the use of a particular packed algorithm.
|
||||
///
|
||||
/// When an algorithm is forced, if a searcher could not be constructed for it,
|
||||
/// then no searcher will be returned even if an alternative algorithm would
|
||||
/// work.
|
||||
#[derive(Clone, Debug)]
|
||||
enum ForceAlgorithm {
|
||||
Teddy,
|
||||
RabinKarp,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new default configuration. A default configuration uses
|
||||
/// leftmost-first match semantics.
|
||||
pub fn new() -> Config {
|
||||
Config {
|
||||
kind: MatchKind::LeftmostFirst,
|
||||
force: None,
|
||||
only_teddy_fat: None,
|
||||
only_teddy_256bit: None,
|
||||
heuristic_pattern_limits: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a packed builder from this configuration. The builder can be
|
||||
/// used to accumulate patterns and create a [`Searcher`] from them.
|
||||
pub fn builder(&self) -> Builder {
|
||||
Builder::from_config(self.clone())
|
||||
}
|
||||
|
||||
/// Set the match semantics for this configuration.
|
||||
pub fn match_kind(&mut self, kind: MatchKind) -> &mut Config {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// An undocumented method for forcing the use of the Teddy algorithm.
|
||||
///
|
||||
/// This is only exposed for more precise testing and benchmarks. Callers
|
||||
/// should not use it as it is not part of the API stability guarantees of
|
||||
/// this crate.
|
||||
#[doc(hidden)]
|
||||
pub fn only_teddy(&mut self, yes: bool) -> &mut Config {
|
||||
if yes {
|
||||
self.force = Some(ForceAlgorithm::Teddy);
|
||||
} else {
|
||||
self.force = None;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// An undocumented method for forcing the use of the Fat Teddy algorithm.
|
||||
///
|
||||
/// This is only exposed for more precise testing and benchmarks. Callers
|
||||
/// should not use it as it is not part of the API stability guarantees of
|
||||
/// this crate.
|
||||
#[doc(hidden)]
|
||||
pub fn only_teddy_fat(&mut self, yes: Option<bool>) -> &mut Config {
|
||||
self.only_teddy_fat = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// An undocumented method for forcing the use of SSE (`Some(false)`) or
|
||||
/// AVX (`Some(true)`) algorithms.
|
||||
///
|
||||
/// This is only exposed for more precise testing and benchmarks. Callers
|
||||
/// should not use it as it is not part of the API stability guarantees of
|
||||
/// this crate.
|
||||
#[doc(hidden)]
|
||||
pub fn only_teddy_256bit(&mut self, yes: Option<bool>) -> &mut Config {
|
||||
self.only_teddy_256bit = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// An undocumented method for forcing the use of the Rabin-Karp algorithm.
|
||||
///
|
||||
/// This is only exposed for more precise testing and benchmarks. Callers
|
||||
/// should not use it as it is not part of the API stability guarantees of
|
||||
/// this crate.
|
||||
#[doc(hidden)]
|
||||
pub fn only_rabin_karp(&mut self, yes: bool) -> &mut Config {
|
||||
if yes {
|
||||
self.force = Some(ForceAlgorithm::RabinKarp);
|
||||
} else {
|
||||
self.force = None;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Request that heuristic limitations on the number of patterns be
|
||||
/// employed. This useful to disable for benchmarking where one wants to
|
||||
/// explore how Teddy performs on large number of patterns even if the
|
||||
/// heuristics would otherwise refuse construction.
|
||||
///
|
||||
/// This is enabled by default.
|
||||
pub fn heuristic_pattern_limits(&mut self, yes: bool) -> &mut Config {
|
||||
self.heuristic_pattern_limits = yes;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for constructing a packed searcher from a collection of patterns.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example shows how to use a builder to construct a searcher. By
|
||||
/// default, leftmost-first match semantics are used.
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{Builder, MatchKind}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Builder::new()
|
||||
/// .add("foobar")
|
||||
/// .add("foo")
|
||||
/// .build()?;
|
||||
/// let matches: Vec<PatternID> = searcher
|
||||
/// .find_iter("foobar")
|
||||
/// .map(|mat| mat.pattern())
|
||||
/// .collect();
|
||||
/// assert_eq!(vec![PatternID::ZERO], matches);
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Builder {
|
||||
/// The configuration of this builder and subsequent matcher.
|
||||
config: Config,
|
||||
/// Set to true if the builder detects that a matcher cannot be built.
|
||||
inert: bool,
|
||||
/// The patterns provided by the caller.
|
||||
patterns: Patterns,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Create a new builder for constructing a multi-pattern searcher. This
|
||||
/// constructor uses the default configuration.
|
||||
pub fn new() -> Builder {
|
||||
Builder::from_config(Config::new())
|
||||
}
|
||||
|
||||
fn from_config(config: Config) -> Builder {
|
||||
Builder { config, inert: false, patterns: Patterns::new() }
|
||||
}
|
||||
|
||||
/// Build a searcher from the patterns added to this builder so far.
|
||||
pub fn build(&self) -> Option<Searcher> {
|
||||
if self.inert || self.patterns.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut patterns = self.patterns.clone();
|
||||
patterns.set_match_kind(self.config.kind);
|
||||
let patterns = Arc::new(patterns);
|
||||
let rabinkarp = RabinKarp::new(&patterns);
|
||||
// Effectively, we only want to return a searcher if we can use Teddy,
|
||||
// since Teddy is our only fast packed searcher at the moment.
|
||||
// Rabin-Karp is only used when searching haystacks smaller than what
|
||||
// Teddy can support. Thus, the only way to get a Rabin-Karp searcher
|
||||
// is to force it using undocumented APIs (for tests/benchmarks).
|
||||
let (search_kind, minimum_len) = match self.config.force {
|
||||
None | Some(ForceAlgorithm::Teddy) => {
|
||||
debug!("trying to build Teddy packed matcher");
|
||||
let teddy = match self.build_teddy(Arc::clone(&patterns)) {
|
||||
None => return None,
|
||||
Some(teddy) => teddy,
|
||||
};
|
||||
let minimum_len = teddy.minimum_len();
|
||||
(SearchKind::Teddy(teddy), minimum_len)
|
||||
}
|
||||
Some(ForceAlgorithm::RabinKarp) => {
|
||||
debug!("using Rabin-Karp packed matcher");
|
||||
(SearchKind::RabinKarp, 0)
|
||||
}
|
||||
};
|
||||
Some(Searcher { patterns, rabinkarp, search_kind, minimum_len })
|
||||
}
|
||||
|
||||
fn build_teddy(&self, patterns: Arc<Patterns>) -> Option<teddy::Searcher> {
|
||||
teddy::Builder::new()
|
||||
.only_256bit(self.config.only_teddy_256bit)
|
||||
.only_fat(self.config.only_teddy_fat)
|
||||
.heuristic_pattern_limits(self.config.heuristic_pattern_limits)
|
||||
.build(patterns)
|
||||
}
|
||||
|
||||
/// Add the given pattern to this set to match.
|
||||
///
|
||||
/// The order in which patterns are added is significant. Namely, when
|
||||
/// using leftmost-first match semantics, then when multiple patterns can
|
||||
/// match at a particular location, the pattern that was added first is
|
||||
/// used as the match.
|
||||
///
|
||||
/// If the number of patterns added exceeds the amount supported by packed
|
||||
/// searchers, then the builder will stop accumulating patterns and render
|
||||
/// itself inert. At this point, constructing a searcher will always return
|
||||
/// `None`.
|
||||
pub fn add<P: AsRef<[u8]>>(&mut self, pattern: P) -> &mut Builder {
|
||||
if self.inert {
|
||||
return self;
|
||||
} else if self.patterns.len() >= PATTERN_LIMIT {
|
||||
self.inert = true;
|
||||
self.patterns.reset();
|
||||
return self;
|
||||
}
|
||||
// Just in case PATTERN_LIMIT increases beyond u16::MAX.
|
||||
assert!(self.patterns.len() <= core::u16::MAX as usize);
|
||||
|
||||
let pattern = pattern.as_ref();
|
||||
if pattern.is_empty() {
|
||||
self.inert = true;
|
||||
self.patterns.reset();
|
||||
return self;
|
||||
}
|
||||
self.patterns.add(pattern);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given iterator of patterns to this set to match.
|
||||
///
|
||||
/// The iterator must yield elements that can be converted into a `&[u8]`.
|
||||
///
|
||||
/// The order in which patterns are added is significant. Namely, when
|
||||
/// using leftmost-first match semantics, then when multiple patterns can
|
||||
/// match at a particular location, the pattern that was added first is
|
||||
/// used as the match.
|
||||
///
|
||||
/// If the number of patterns added exceeds the amount supported by packed
|
||||
/// searchers, then the builder will stop accumulating patterns and render
|
||||
/// itself inert. At this point, constructing a searcher will always return
|
||||
/// `None`.
|
||||
pub fn extend<I, P>(&mut self, patterns: I) -> &mut Builder
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
for p in patterns {
|
||||
self.add(p);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the number of patterns added to this builder.
|
||||
pub fn len(&self) -> usize {
|
||||
self.patterns.len()
|
||||
}
|
||||
|
||||
/// Returns the length, in bytes, of the shortest pattern added.
|
||||
pub fn minimum_len(&self) -> usize {
|
||||
self.patterns.minimum_len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed searcher for quickly finding occurrences of multiple patterns.
|
||||
///
|
||||
/// If callers need more flexible construction, or if one wants to change the
|
||||
/// match semantics (either leftmost-first or leftmost-longest), then one can
|
||||
/// use the [`Config`] and/or [`Builder`] types for more fine grained control.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example shows how to create a searcher from an iterator of patterns.
|
||||
/// By default, leftmost-first match semantics are used.
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{MatchKind, Searcher}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// let matches: Vec<PatternID> = searcher
|
||||
/// .find_iter("foobar")
|
||||
/// .map(|mat| mat.pattern())
|
||||
/// .collect();
|
||||
/// assert_eq!(vec![PatternID::ZERO], matches);
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Searcher {
|
||||
patterns: Arc<Patterns>,
|
||||
rabinkarp: RabinKarp,
|
||||
search_kind: SearchKind,
|
||||
minimum_len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum SearchKind {
|
||||
Teddy(teddy::Searcher),
|
||||
RabinKarp,
|
||||
}
|
||||
|
||||
impl Searcher {
|
||||
/// A convenience function for constructing a searcher from an iterator
|
||||
/// of things that can be converted to a `&[u8]`.
|
||||
///
|
||||
/// If a searcher could not be constructed (either because of an
|
||||
/// unsupported CPU or because there are too many patterns), then `None`
|
||||
/// is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{MatchKind, Searcher}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// let matches: Vec<PatternID> = searcher
|
||||
/// .find_iter("foobar")
|
||||
/// .map(|mat| mat.pattern())
|
||||
/// .collect();
|
||||
/// assert_eq!(vec![PatternID::ZERO], matches);
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new<I, P>(patterns: I) -> Option<Searcher>
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
Builder::new().extend(patterns).build()
|
||||
}
|
||||
|
||||
/// A convenience function for calling `Config::new()`.
|
||||
///
|
||||
/// This is useful for avoiding an additional import.
|
||||
pub fn config() -> Config {
|
||||
Config::new()
|
||||
}
|
||||
|
||||
/// A convenience function for calling `Builder::new()`.
|
||||
///
|
||||
/// This is useful for avoiding an additional import.
|
||||
pub fn builder() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
|
||||
/// Return the first occurrence of any of the patterns in this searcher,
|
||||
/// according to its match semantics, in the given haystack. The `Match`
|
||||
/// returned will include the identifier of the pattern that matched, which
|
||||
/// corresponds to the index of the pattern (starting from `0`) in which it
|
||||
/// was added.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{MatchKind, Searcher}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// let mat = searcher.find("foobar")?;
|
||||
/// assert_eq!(PatternID::ZERO, mat.pattern());
|
||||
/// assert_eq!(0, mat.start());
|
||||
/// assert_eq!(6, mat.end());
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn find<B: AsRef<[u8]>>(&self, haystack: B) -> Option<Match> {
|
||||
let haystack = haystack.as_ref();
|
||||
self.find_in(haystack, Span::from(0..haystack.len()))
|
||||
}
|
||||
|
||||
/// Return the first occurrence of any of the patterns in this searcher,
|
||||
/// according to its match semantics, in the given haystack starting from
|
||||
/// the given position.
|
||||
///
|
||||
/// The `Match` returned will include the identifier of the pattern that
|
||||
/// matched, which corresponds to the index of the pattern (starting from
|
||||
/// `0`) in which it was added. The offsets in the `Match` will be relative
|
||||
/// to the start of `haystack` (and not `at`).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When `span` does not correspond to a valid range in `haystack`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{MatchKind, Searcher}, PatternID, Span};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let haystack = "foofoobar";
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// let mat = searcher.find_in(haystack, Span::from(3..haystack.len()))?;
|
||||
/// assert_eq!(PatternID::ZERO, mat.pattern());
|
||||
/// assert_eq!(3, mat.start());
|
||||
/// assert_eq!(9, mat.end());
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn find_in<B: AsRef<[u8]>>(
|
||||
&self,
|
||||
haystack: B,
|
||||
span: Span,
|
||||
) -> Option<Match> {
|
||||
let haystack = haystack.as_ref();
|
||||
match self.search_kind {
|
||||
SearchKind::Teddy(ref teddy) => {
|
||||
if haystack[span].len() < teddy.minimum_len() {
|
||||
return self.find_in_slow(haystack, span);
|
||||
}
|
||||
teddy.find(&haystack[..span.end], span.start)
|
||||
}
|
||||
SearchKind::RabinKarp => {
|
||||
self.rabinkarp.find_at(&haystack[..span.end], span.start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator of non-overlapping occurrences of the patterns in
|
||||
/// this searcher, according to its match semantics, in the given haystack.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::{packed::{MatchKind, Searcher}, PatternID};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// let matches: Vec<PatternID> = searcher
|
||||
/// .find_iter("foobar fooba foofoo")
|
||||
/// .map(|mat| mat.pattern())
|
||||
/// .collect();
|
||||
/// assert_eq!(vec![
|
||||
/// PatternID::must(0),
|
||||
/// PatternID::must(1),
|
||||
/// PatternID::must(1),
|
||||
/// PatternID::must(1),
|
||||
/// ], matches);
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn find_iter<'a, 'b, B: ?Sized + AsRef<[u8]>>(
|
||||
&'a self,
|
||||
haystack: &'b B,
|
||||
) -> FindIter<'a, 'b> {
|
||||
let haystack = haystack.as_ref();
|
||||
let span = Span::from(0..haystack.len());
|
||||
FindIter { searcher: self, haystack, span }
|
||||
}
|
||||
|
||||
/// Returns the match kind used by this packed searcher.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```
|
||||
/// use aho_corasick::packed::{MatchKind, Searcher};
|
||||
///
|
||||
/// # fn example() -> Option<()> {
|
||||
/// let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
/// // leftmost-first is the default.
|
||||
/// assert_eq!(&MatchKind::LeftmostFirst, searcher.match_kind());
|
||||
/// # Some(()) }
|
||||
/// # if cfg!(all(feature = "std", any(
|
||||
/// # target_arch = "x86_64", target_arch = "aarch64",
|
||||
/// # ))) {
|
||||
/// # example().unwrap()
|
||||
/// # } else {
|
||||
/// # assert!(example().is_none());
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn match_kind(&self) -> &MatchKind {
|
||||
self.patterns.match_kind()
|
||||
}
|
||||
|
||||
/// Returns the minimum length of a haystack that is required in order for
|
||||
/// packed searching to be effective.
|
||||
///
|
||||
/// In some cases, the underlying packed searcher may not be able to search
|
||||
/// very short haystacks. When that occurs, the implementation will defer
|
||||
/// to a slower non-packed searcher (which is still generally faster than
|
||||
/// Aho-Corasick for a small number of patterns). However, callers may
|
||||
/// want to avoid ever using the slower variant, which one can do by
|
||||
/// never passing a haystack shorter than the minimum length returned by
|
||||
/// this method.
|
||||
#[inline]
|
||||
pub fn minimum_len(&self) -> usize {
|
||||
self.minimum_len
|
||||
}
|
||||
|
||||
/// Returns the approximate total amount of heap used by this searcher, in
|
||||
/// units of bytes.
|
||||
#[inline]
|
||||
pub fn memory_usage(&self) -> usize {
|
||||
self.patterns.memory_usage()
|
||||
+ self.rabinkarp.memory_usage()
|
||||
+ self.search_kind.memory_usage()
|
||||
}
|
||||
|
||||
/// Use a slow (non-packed) searcher.
|
||||
///
|
||||
/// This is useful when a packed searcher could be constructed, but could
|
||||
/// not be used to search a specific haystack. For example, if Teddy was
|
||||
/// built but the haystack is smaller than ~34 bytes, then Teddy might not
|
||||
/// be able to run.
|
||||
fn find_in_slow(&self, haystack: &[u8], span: Span) -> Option<Match> {
|
||||
self.rabinkarp.find_at(&haystack[..span.end], span.start)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchKind {
|
||||
fn memory_usage(&self) -> usize {
|
||||
match *self {
|
||||
SearchKind::Teddy(ref ted) => ted.memory_usage(),
|
||||
SearchKind::RabinKarp => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over non-overlapping matches from a packed searcher.
|
||||
///
|
||||
/// The lifetime `'s` refers to the lifetime of the underlying [`Searcher`],
|
||||
/// while the lifetime `'h` refers to the lifetime of the haystack being
|
||||
/// searched.
|
||||
#[derive(Debug)]
|
||||
pub struct FindIter<'s, 'h> {
|
||||
searcher: &'s Searcher,
|
||||
haystack: &'h [u8],
|
||||
span: Span,
|
||||
}
|
||||
|
||||
impl<'s, 'h> Iterator for FindIter<'s, 'h> {
|
||||
type Item = Match;
|
||||
|
||||
fn next(&mut self) -> Option<Match> {
|
||||
if self.span.start > self.span.end {
|
||||
return None;
|
||||
}
|
||||
match self.searcher.find_in(self.haystack, self.span) {
|
||||
None => None,
|
||||
Some(m) => {
|
||||
self.span.start = m.end();
|
||||
Some(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/// A trait for adding some helper routines to pointers.
|
||||
pub(crate) trait Pointer {
|
||||
/// Returns the distance, in units of `T`, between `self` and `origin`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Same as `ptr::offset_from` in addition to `self >= origin`.
|
||||
unsafe fn distance(self, origin: Self) -> usize;
|
||||
|
||||
/// Casts this pointer to `usize`.
|
||||
///
|
||||
/// Callers should not convert the `usize` back to a pointer if at all
|
||||
/// possible. (And if you believe it's necessary, open an issue to discuss
|
||||
/// why. Otherwise, it has the potential to violate pointer provenance.)
|
||||
/// The purpose of this function is just to be able to do arithmetic, i.e.,
|
||||
/// computing offsets or alignments.
|
||||
fn as_usize(self) -> usize;
|
||||
}
|
||||
|
||||
impl<T> Pointer for *const T {
|
||||
unsafe fn distance(self, origin: *const T) -> usize {
|
||||
// TODO: Replace with `ptr::sub_ptr` once stabilized.
|
||||
usize::try_from(self.offset_from(origin)).unwrap_unchecked()
|
||||
}
|
||||
|
||||
fn as_usize(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Pointer for *mut T {
|
||||
unsafe fn distance(self, origin: *mut T) -> usize {
|
||||
(self as *const T).distance(origin as *const T)
|
||||
}
|
||||
|
||||
fn as_usize(self) -> usize {
|
||||
(self as *const T).as_usize()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*!
|
||||
Provides packed multiple substring search, principally for a small number of
|
||||
patterns.
|
||||
|
||||
This sub-module provides vectorized routines for quickly finding
|
||||
matches of a small number of patterns. In general, users of this crate
|
||||
shouldn't need to interface with this module directly, as the primary
|
||||
[`AhoCorasick`](crate::AhoCorasick) searcher will use these routines
|
||||
automatically as a prefilter when applicable. However, in some cases, callers
|
||||
may want to bypass the Aho-Corasick machinery entirely and use this vectorized
|
||||
searcher directly.
|
||||
|
||||
# Overview
|
||||
|
||||
The primary types in this sub-module are:
|
||||
|
||||
* [`Searcher`] executes the actual search algorithm to report matches in a
|
||||
haystack.
|
||||
* [`Builder`] accumulates patterns incrementally and can construct a
|
||||
`Searcher`.
|
||||
* [`Config`] permits tuning the searcher, and itself will produce a `Builder`
|
||||
(which can then be used to build a `Searcher`). Currently, the only tuneable
|
||||
knob are the match semantics, but this may be expanded in the future.
|
||||
|
||||
# Examples
|
||||
|
||||
This example shows how to create a searcher from an iterator of patterns.
|
||||
By default, leftmost-first match semantics are used. (See the top-level
|
||||
[`MatchKind`] type for more details about match semantics, which apply
|
||||
similarly to packed substring search.)
|
||||
|
||||
```
|
||||
use aho_corasick::{packed::{MatchKind, Searcher}, PatternID};
|
||||
|
||||
# fn example() -> Option<()> {
|
||||
let searcher = Searcher::new(["foobar", "foo"].iter().cloned())?;
|
||||
let matches: Vec<PatternID> = searcher
|
||||
.find_iter("foobar")
|
||||
.map(|mat| mat.pattern())
|
||||
.collect();
|
||||
assert_eq!(vec![PatternID::ZERO], matches);
|
||||
# Some(()) }
|
||||
# if cfg!(all(feature = "std", any(
|
||||
# target_arch = "x86_64", target_arch = "aarch64",
|
||||
# ))) {
|
||||
# example().unwrap()
|
||||
# } else {
|
||||
# assert!(example().is_none());
|
||||
# }
|
||||
```
|
||||
|
||||
This example shows how to use [`Config`] to change the match semantics to
|
||||
leftmost-longest:
|
||||
|
||||
```
|
||||
use aho_corasick::{packed::{Config, MatchKind}, PatternID};
|
||||
|
||||
# fn example() -> Option<()> {
|
||||
let searcher = Config::new()
|
||||
.match_kind(MatchKind::LeftmostLongest)
|
||||
.builder()
|
||||
.add("foo")
|
||||
.add("foobar")
|
||||
.build()?;
|
||||
let matches: Vec<PatternID> = searcher
|
||||
.find_iter("foobar")
|
||||
.map(|mat| mat.pattern())
|
||||
.collect();
|
||||
assert_eq!(vec![PatternID::must(1)], matches);
|
||||
# Some(()) }
|
||||
# if cfg!(all(feature = "std", any(
|
||||
# target_arch = "x86_64", target_arch = "aarch64",
|
||||
# ))) {
|
||||
# example().unwrap()
|
||||
# } else {
|
||||
# assert!(example().is_none());
|
||||
# }
|
||||
```
|
||||
|
||||
# Packed substring searching
|
||||
|
||||
Packed substring searching refers to the use of SIMD (Single Instruction,
|
||||
Multiple Data) to accelerate the detection of matches in a haystack. Unlike
|
||||
conventional algorithms, such as Aho-Corasick, SIMD algorithms for substring
|
||||
search tend to do better with a small number of patterns, where as Aho-Corasick
|
||||
generally maintains reasonably consistent performance regardless of the number
|
||||
of patterns you give it. Because of this, the vectorized searcher in this
|
||||
sub-module cannot be used as a general purpose searcher, since building the
|
||||
searcher may fail even when given a small number of patterns. However, in
|
||||
exchange, when searching for a small number of patterns, searching can be quite
|
||||
a bit faster than Aho-Corasick (sometimes by an order of magnitude).
|
||||
|
||||
The key take away here is that constructing a searcher from a list of patterns
|
||||
is a fallible operation with no clear rules for when it will fail. While the
|
||||
precise conditions under which building a searcher can fail is specifically an
|
||||
implementation detail, here are some common reasons:
|
||||
|
||||
* Too many patterns were given. Typically, the limit is on the order of 100 or
|
||||
so, but this limit may fluctuate based on available CPU features.
|
||||
* The available packed algorithms require CPU features that aren't available.
|
||||
For example, currently, this crate only provides packed algorithms for
|
||||
`x86_64` and `aarch64`. Therefore, constructing a packed searcher on any
|
||||
other target will always fail.
|
||||
* Zero patterns were given, or one of the patterns given was empty. Packed
|
||||
searchers require at least one pattern and that all patterns are non-empty.
|
||||
* Something else about the nature of the patterns (typically based on
|
||||
heuristics) suggests that a packed searcher would perform very poorly, so
|
||||
no searcher is built.
|
||||
*/
|
||||
|
||||
pub use crate::packed::api::{Builder, Config, FindIter, MatchKind, Searcher};
|
||||
|
||||
mod api;
|
||||
mod ext;
|
||||
mod pattern;
|
||||
mod rabinkarp;
|
||||
mod teddy;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
mod vector;
|
||||
@@ -0,0 +1,480 @@
|
||||
use core::{cmp, fmt, mem, u16, usize};
|
||||
|
||||
use alloc::{boxed::Box, string::String, vec, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
packed::{api::MatchKind, ext::Pointer},
|
||||
PatternID,
|
||||
};
|
||||
|
||||
/// A non-empty collection of non-empty patterns to search for.
|
||||
///
|
||||
/// This collection of patterns is what is passed around to both execute
|
||||
/// searches and to construct the searchers themselves. Namely, this permits
|
||||
/// searches to avoid copying all of the patterns, and allows us to keep only
|
||||
/// one copy throughout all packed searchers.
|
||||
///
|
||||
/// Note that this collection is not a set. The same pattern can appear more
|
||||
/// than once.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Patterns {
|
||||
/// The match semantics supported by this collection of patterns.
|
||||
///
|
||||
/// The match semantics determines the order of the iterator over patterns.
|
||||
/// For leftmost-first, patterns are provided in the same order as were
|
||||
/// provided by the caller. For leftmost-longest, patterns are provided in
|
||||
/// descending order of length, with ties broken by the order in which they
|
||||
/// were provided by the caller.
|
||||
kind: MatchKind,
|
||||
/// The collection of patterns, indexed by their identifier.
|
||||
by_id: Vec<Vec<u8>>,
|
||||
/// The order of patterns defined for iteration, given by pattern
|
||||
/// identifiers. The order of `by_id` and `order` is always the same for
|
||||
/// leftmost-first semantics, but may be different for leftmost-longest
|
||||
/// semantics.
|
||||
order: Vec<PatternID>,
|
||||
/// The length of the smallest pattern, in bytes.
|
||||
minimum_len: usize,
|
||||
/// The total number of pattern bytes across the entire collection. This
|
||||
/// is used for reporting total heap usage in constant time.
|
||||
total_pattern_bytes: usize,
|
||||
}
|
||||
|
||||
// BREADCRUMBS: I think we want to experiment with a different bucket
|
||||
// representation. Basically, each bucket is just a Range<usize> to a single
|
||||
// contiguous allocation? Maybe length-prefixed patterns or something? The
|
||||
// idea is to try to get rid of the pointer chasing in verification. I don't
|
||||
// know that that is the issue, but I suspect it is.
|
||||
|
||||
impl Patterns {
|
||||
/// Create a new collection of patterns for the given match semantics. The
|
||||
/// ID of each pattern is the index of the pattern at which it occurs in
|
||||
/// the `by_id` slice.
|
||||
///
|
||||
/// If any of the patterns in the slice given are empty, then this panics.
|
||||
/// Similarly, if the number of patterns given is zero, then this also
|
||||
/// panics.
|
||||
pub(crate) fn new() -> Patterns {
|
||||
Patterns {
|
||||
kind: MatchKind::default(),
|
||||
by_id: vec![],
|
||||
order: vec![],
|
||||
minimum_len: usize::MAX,
|
||||
total_pattern_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a pattern to this collection.
|
||||
///
|
||||
/// This panics if the pattern given is empty.
|
||||
pub(crate) fn add(&mut self, bytes: &[u8]) {
|
||||
assert!(!bytes.is_empty());
|
||||
assert!(self.by_id.len() <= u16::MAX as usize);
|
||||
|
||||
let id = PatternID::new(self.by_id.len()).unwrap();
|
||||
self.order.push(id);
|
||||
self.by_id.push(bytes.to_vec());
|
||||
self.minimum_len = cmp::min(self.minimum_len, bytes.len());
|
||||
self.total_pattern_bytes += bytes.len();
|
||||
}
|
||||
|
||||
/// Set the match kind semantics for this collection of patterns.
|
||||
///
|
||||
/// If the kind is not set, then the default is leftmost-first.
|
||||
pub(crate) fn set_match_kind(&mut self, kind: MatchKind) {
|
||||
self.kind = kind;
|
||||
match self.kind {
|
||||
MatchKind::LeftmostFirst => {
|
||||
self.order.sort();
|
||||
}
|
||||
MatchKind::LeftmostLongest => {
|
||||
let (order, by_id) = (&mut self.order, &mut self.by_id);
|
||||
order.sort_by(|&id1, &id2| {
|
||||
by_id[id1].len().cmp(&by_id[id2].len()).reverse()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the number of patterns in this collection.
|
||||
///
|
||||
/// This is guaranteed to be greater than zero.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.by_id.len()
|
||||
}
|
||||
|
||||
/// Returns true if and only if this collection of patterns is empty.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the approximate total amount of heap used by these patterns, in
|
||||
/// units of bytes.
|
||||
pub(crate) fn memory_usage(&self) -> usize {
|
||||
self.order.len() * mem::size_of::<PatternID>()
|
||||
+ self.by_id.len() * mem::size_of::<Vec<u8>>()
|
||||
+ self.total_pattern_bytes
|
||||
}
|
||||
|
||||
/// Clears all heap memory associated with this collection of patterns and
|
||||
/// resets all state such that it is a valid empty collection.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.kind = MatchKind::default();
|
||||
self.by_id.clear();
|
||||
self.order.clear();
|
||||
self.minimum_len = usize::MAX;
|
||||
}
|
||||
|
||||
/// Returns the length, in bytes, of the smallest pattern.
|
||||
///
|
||||
/// This is guaranteed to be at least one.
|
||||
pub(crate) fn minimum_len(&self) -> usize {
|
||||
self.minimum_len
|
||||
}
|
||||
|
||||
/// Returns the match semantics used by these patterns.
|
||||
pub(crate) fn match_kind(&self) -> &MatchKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Return the pattern with the given identifier. If such a pattern does
|
||||
/// not exist, then this panics.
|
||||
pub(crate) fn get(&self, id: PatternID) -> Pattern<'_> {
|
||||
Pattern(&self.by_id[id])
|
||||
}
|
||||
|
||||
/// Return the pattern with the given identifier without performing bounds
|
||||
/// checks.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers must ensure that a pattern with the given identifier exists
|
||||
/// before using this method.
|
||||
pub(crate) unsafe fn get_unchecked(&self, id: PatternID) -> Pattern<'_> {
|
||||
Pattern(self.by_id.get_unchecked(id.as_usize()))
|
||||
}
|
||||
|
||||
/// Return an iterator over all the patterns in this collection, in the
|
||||
/// order in which they should be matched.
|
||||
///
|
||||
/// Specifically, in a naive multi-pattern matcher, the following is
|
||||
/// guaranteed to satisfy the match semantics of this collection of
|
||||
/// patterns:
|
||||
///
|
||||
/// ```ignore
|
||||
/// for i in 0..haystack.len():
|
||||
/// for p in patterns.iter():
|
||||
/// if haystack[i..].starts_with(p.bytes()):
|
||||
/// return Match(p.id(), i, i + p.bytes().len())
|
||||
/// ```
|
||||
///
|
||||
/// Namely, among the patterns in a collection, if they are matched in
|
||||
/// the order provided by this iterator, then the result is guaranteed
|
||||
/// to satisfy the correct match semantics. (Either leftmost-first or
|
||||
/// leftmost-longest.)
|
||||
pub(crate) fn iter(&self) -> PatternIter<'_> {
|
||||
PatternIter { patterns: self, i: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the patterns in the `Patterns` collection.
|
||||
///
|
||||
/// The order of the patterns provided by this iterator is consistent with the
|
||||
/// match semantics of the originating collection of patterns.
|
||||
///
|
||||
/// The lifetime `'p` corresponds to the lifetime of the collection of patterns
|
||||
/// this is iterating over.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PatternIter<'p> {
|
||||
patterns: &'p Patterns,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl<'p> Iterator for PatternIter<'p> {
|
||||
type Item = (PatternID, Pattern<'p>);
|
||||
|
||||
fn next(&mut self) -> Option<(PatternID, Pattern<'p>)> {
|
||||
if self.i >= self.patterns.len() {
|
||||
return None;
|
||||
}
|
||||
let id = self.patterns.order[self.i];
|
||||
let p = self.patterns.get(id);
|
||||
self.i += 1;
|
||||
Some((id, p))
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern that is used in packed searching.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Pattern<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> fmt::Debug for Pattern<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pattern")
|
||||
.field("lit", &String::from_utf8_lossy(self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'p> Pattern<'p> {
|
||||
/// Returns the length of this pattern, in bytes.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns the bytes of this pattern.
|
||||
pub(crate) fn bytes(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the first `len` low nybbles from this pattern. If this pattern
|
||||
/// is shorter than `len`, then this panics.
|
||||
pub(crate) fn low_nybbles(&self, len: usize) -> Box<[u8]> {
|
||||
let mut nybs = vec![0; len].into_boxed_slice();
|
||||
for (i, byte) in self.bytes().iter().take(len).enumerate() {
|
||||
nybs[i] = byte & 0xF;
|
||||
}
|
||||
nybs
|
||||
}
|
||||
|
||||
/// Returns true if this pattern is a prefix of the given bytes.
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_prefix(&self, bytes: &[u8]) -> bool {
|
||||
is_prefix(bytes, self.bytes())
|
||||
}
|
||||
|
||||
/// Returns true if this pattern is a prefix of the haystack given by the
|
||||
/// raw `start` and `end` pointers.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// * It must be the case that `start < end` and that the distance between
|
||||
/// them is at least equal to `V::BYTES`. That is, it must always be valid
|
||||
/// to do at least an unaligned load of `V` at `start`.
|
||||
/// * Both `start` and `end` must be valid for reads.
|
||||
/// * Both `start` and `end` must point to an initialized value.
|
||||
/// * Both `start` and `end` must point to the same allocated object and
|
||||
/// must either be in bounds or at most one byte past the end of the
|
||||
/// allocated object.
|
||||
/// * Both `start` and `end` must be _derived from_ a pointer to the same
|
||||
/// object.
|
||||
/// * The distance between `start` and `end` must not overflow `isize`.
|
||||
/// * The distance being in bounds must not rely on "wrapping around" the
|
||||
/// address space.
|
||||
#[inline(always)]
|
||||
pub(crate) unsafe fn is_prefix_raw(
|
||||
&self,
|
||||
start: *const u8,
|
||||
end: *const u8,
|
||||
) -> bool {
|
||||
let patlen = self.bytes().len();
|
||||
let haylen = end.distance(start);
|
||||
if patlen > haylen {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: We've checked that the haystack has length at least equal
|
||||
// to this pattern. All other safety concerns are the responsibility
|
||||
// of the caller.
|
||||
is_equal_raw(start, self.bytes().as_ptr(), patlen)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if `needle` is a prefix of `haystack`.
|
||||
///
|
||||
/// This uses a latency optimized variant of `memcmp` internally which *might*
|
||||
/// make this faster for very short strings.
|
||||
///
|
||||
/// # Inlining
|
||||
///
|
||||
/// This routine is marked `inline(always)`. If you want to call this function
|
||||
/// in a way that is not always inlined, you'll need to wrap a call to it in
|
||||
/// another function that is marked as `inline(never)` or just `inline`.
|
||||
#[inline(always)]
|
||||
fn is_prefix(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
if needle.len() > haystack.len() {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: Our pointers are derived directly from borrowed slices which
|
||||
// uphold all of our safety guarantees except for length. We account for
|
||||
// length with the check above.
|
||||
unsafe { is_equal_raw(haystack.as_ptr(), needle.as_ptr(), needle.len()) }
|
||||
}
|
||||
|
||||
/// Compare corresponding bytes in `x` and `y` for equality.
|
||||
///
|
||||
/// That is, this returns true if and only if `x.len() == y.len()` and
|
||||
/// `x[i] == y[i]` for all `0 <= i < x.len()`.
|
||||
///
|
||||
/// Note that this isn't used. We only use it in tests as a convenient way
|
||||
/// of testing `is_equal_raw`.
|
||||
///
|
||||
/// # Inlining
|
||||
///
|
||||
/// This routine is marked `inline(always)`. If you want to call this function
|
||||
/// in a way that is not always inlined, you'll need to wrap a call to it in
|
||||
/// another function that is marked as `inline(never)` or just `inline`.
|
||||
///
|
||||
/// # Motivation
|
||||
///
|
||||
/// Why not use slice equality instead? Well, slice equality usually results in
|
||||
/// a call out to the current platform's `libc` which might not be inlineable
|
||||
/// or have other overhead. This routine isn't guaranteed to be a win, but it
|
||||
/// might be in some cases.
|
||||
#[cfg(test)]
|
||||
#[inline(always)]
|
||||
fn is_equal(x: &[u8], y: &[u8]) -> bool {
|
||||
if x.len() != y.len() {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: Our pointers are derived directly from borrowed slices which
|
||||
// uphold all of our safety guarantees except for length. We account for
|
||||
// length with the check above.
|
||||
unsafe { is_equal_raw(x.as_ptr(), y.as_ptr(), x.len()) }
|
||||
}
|
||||
|
||||
/// Compare `n` bytes at the given pointers for equality.
|
||||
///
|
||||
/// This returns true if and only if `*x.add(i) == *y.add(i)` for all
|
||||
/// `0 <= i < n`.
|
||||
///
|
||||
/// # Inlining
|
||||
///
|
||||
/// This routine is marked `inline(always)`. If you want to call this function
|
||||
/// in a way that is not always inlined, you'll need to wrap a call to it in
|
||||
/// another function that is marked as `inline(never)` or just `inline`.
|
||||
///
|
||||
/// # Motivation
|
||||
///
|
||||
/// Why not use slice equality instead? Well, slice equality usually results in
|
||||
/// a call out to the current platform's `libc` which might not be inlineable
|
||||
/// or have other overhead. This routine isn't guaranteed to be a win, but it
|
||||
/// might be in some cases.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// * Both `x` and `y` must be valid for reads of up to `n` bytes.
|
||||
/// * Both `x` and `y` must point to an initialized value.
|
||||
/// * Both `x` and `y` must each point to an allocated object and
|
||||
/// must either be in bounds or at most one byte past the end of the
|
||||
/// allocated object. `x` and `y` do not need to point to the same allocated
|
||||
/// object, but they may.
|
||||
/// * Both `x` and `y` must be _derived from_ a pointer to their respective
|
||||
/// allocated objects.
|
||||
/// * The distance between `x` and `x+n` must not overflow `isize`. Similarly
|
||||
/// for `y` and `y+n`.
|
||||
/// * The distance being in bounds must not rely on "wrapping around" the
|
||||
/// address space.
|
||||
#[inline(always)]
|
||||
unsafe fn is_equal_raw(mut x: *const u8, mut y: *const u8, n: usize) -> bool {
|
||||
// If we don't have enough bytes to do 4-byte at a time loads, then
|
||||
// handle each possible length specially. Note that I used to have a
|
||||
// byte-at-a-time loop here and that turned out to be quite a bit slower
|
||||
// for the memmem/pathological/defeat-simple-vector-alphabet benchmark.
|
||||
if n < 4 {
|
||||
return match n {
|
||||
0 => true,
|
||||
1 => x.read() == y.read(),
|
||||
2 => {
|
||||
x.cast::<u16>().read_unaligned()
|
||||
== y.cast::<u16>().read_unaligned()
|
||||
}
|
||||
// I also tried copy_nonoverlapping here and it looks like the
|
||||
// codegen is the same.
|
||||
3 => x.cast::<[u8; 3]>().read() == y.cast::<[u8; 3]>().read(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
// When we have 4 or more bytes to compare, then proceed in chunks of 4 at
|
||||
// a time using unaligned loads.
|
||||
//
|
||||
// Also, why do 4 byte loads instead of, say, 8 byte loads? The reason is
|
||||
// that this particular version of memcmp is likely to be called with tiny
|
||||
// needles. That means that if we do 8 byte loads, then a higher proportion
|
||||
// of memcmp calls will use the slower variant above. With that said, this
|
||||
// is a hypothesis and is only loosely supported by benchmarks. There's
|
||||
// likely some improvement that could be made here. The main thing here
|
||||
// though is to optimize for latency, not throughput.
|
||||
|
||||
// SAFETY: The caller is responsible for ensuring the pointers we get are
|
||||
// valid and readable for at least `n` bytes. We also do unaligned loads,
|
||||
// so there's no need to ensure we're aligned. (This is justified by this
|
||||
// routine being specifically for short strings.)
|
||||
let xend = x.add(n.wrapping_sub(4));
|
||||
let yend = y.add(n.wrapping_sub(4));
|
||||
while x < xend {
|
||||
let vx = x.cast::<u32>().read_unaligned();
|
||||
let vy = y.cast::<u32>().read_unaligned();
|
||||
if vx != vy {
|
||||
return false;
|
||||
}
|
||||
x = x.add(4);
|
||||
y = y.add(4);
|
||||
}
|
||||
let vx = xend.cast::<u32>().read_unaligned();
|
||||
let vy = yend.cast::<u32>().read_unaligned();
|
||||
vx == vy
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn equals_different_lengths() {
|
||||
assert!(!is_equal(b"", b"a"));
|
||||
assert!(!is_equal(b"a", b""));
|
||||
assert!(!is_equal(b"ab", b"a"));
|
||||
assert!(!is_equal(b"a", b"ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_mismatch() {
|
||||
let one_mismatch = [
|
||||
(&b"a"[..], &b"x"[..]),
|
||||
(&b"ab"[..], &b"ax"[..]),
|
||||
(&b"abc"[..], &b"abx"[..]),
|
||||
(&b"abcd"[..], &b"abcx"[..]),
|
||||
(&b"abcde"[..], &b"abcdx"[..]),
|
||||
(&b"abcdef"[..], &b"abcdex"[..]),
|
||||
(&b"abcdefg"[..], &b"abcdefx"[..]),
|
||||
(&b"abcdefgh"[..], &b"abcdefgx"[..]),
|
||||
(&b"abcdefghi"[..], &b"abcdefghx"[..]),
|
||||
(&b"abcdefghij"[..], &b"abcdefghix"[..]),
|
||||
(&b"abcdefghijk"[..], &b"abcdefghijx"[..]),
|
||||
(&b"abcdefghijkl"[..], &b"abcdefghijkx"[..]),
|
||||
(&b"abcdefghijklm"[..], &b"abcdefghijklx"[..]),
|
||||
(&b"abcdefghijklmn"[..], &b"abcdefghijklmx"[..]),
|
||||
];
|
||||
for (x, y) in one_mismatch {
|
||||
assert_eq!(x.len(), y.len(), "lengths should match");
|
||||
assert!(!is_equal(x, y));
|
||||
assert!(!is_equal(y, x));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_yes() {
|
||||
assert!(is_equal(b"", b""));
|
||||
assert!(is_equal(b"a", b"a"));
|
||||
assert!(is_equal(b"ab", b"ab"));
|
||||
assert!(is_equal(b"abc", b"abc"));
|
||||
assert!(is_equal(b"abcd", b"abcd"));
|
||||
assert!(is_equal(b"abcde", b"abcde"));
|
||||
assert!(is_equal(b"abcdef", b"abcdef"));
|
||||
assert!(is_equal(b"abcdefg", b"abcdefg"));
|
||||
assert!(is_equal(b"abcdefgh", b"abcdefgh"));
|
||||
assert!(is_equal(b"abcdefghi", b"abcdefghi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix() {
|
||||
assert!(is_prefix(b"", b""));
|
||||
assert!(is_prefix(b"a", b""));
|
||||
assert!(is_prefix(b"ab", b""));
|
||||
assert!(is_prefix(b"foo", b"foo"));
|
||||
assert!(is_prefix(b"foobar", b"foo"));
|
||||
|
||||
assert!(!is_prefix(b"foo", b"fob"));
|
||||
assert!(!is_prefix(b"foobar", b"fob"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
use alloc::{sync::Arc, vec, vec::Vec};
|
||||
|
||||
use crate::{packed::pattern::Patterns, util::search::Match, PatternID};
|
||||
|
||||
/// The type of the rolling hash used in the Rabin-Karp algorithm.
|
||||
type Hash = usize;
|
||||
|
||||
/// The number of buckets to store our patterns in. We don't want this to be
|
||||
/// too big in order to avoid wasting memory, but we don't want it to be too
|
||||
/// small either to avoid spending too much time confirming literals.
|
||||
///
|
||||
/// The number of buckets MUST be a power of two. Otherwise, determining the
|
||||
/// bucket from a hash will slow down the code considerably. Using a power
|
||||
/// of two means `hash % NUM_BUCKETS` can compile down to a simple `and`
|
||||
/// instruction.
|
||||
const NUM_BUCKETS: usize = 64;
|
||||
|
||||
/// An implementation of the Rabin-Karp algorithm. The main idea of this
|
||||
/// algorithm is to maintain a rolling hash as it moves through the input, and
|
||||
/// then check whether that hash corresponds to the same hash for any of the
|
||||
/// patterns we're looking for.
|
||||
///
|
||||
/// A draw back of naively scaling Rabin-Karp to multiple patterns is that
|
||||
/// it requires all of the patterns to be the same length, which in turn
|
||||
/// corresponds to the number of bytes to hash. We adapt this to work for
|
||||
/// multiple patterns of varying size by fixing the number of bytes to hash
|
||||
/// to be the length of the smallest pattern. We also split the patterns into
|
||||
/// several buckets to hopefully make the confirmation step faster.
|
||||
///
|
||||
/// Wikipedia has a decent explanation, if a bit heavy on the theory:
|
||||
/// https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm
|
||||
///
|
||||
/// But ESMAJ provides something a bit more concrete:
|
||||
/// https://www-igm.univ-mlv.fr/~lecroq/string/node5.html
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RabinKarp {
|
||||
/// The patterns we're searching for.
|
||||
patterns: Arc<Patterns>,
|
||||
/// The order of patterns in each bucket is significant. Namely, they are
|
||||
/// arranged such that the first one to match is the correct match. This
|
||||
/// may not necessarily correspond to the order provided by the caller.
|
||||
/// For example, if leftmost-longest semantics are used, then the patterns
|
||||
/// are sorted by their length in descending order. If leftmost-first
|
||||
/// semantics are used, then the patterns are sorted by their pattern ID
|
||||
/// in ascending order (which corresponds to the caller's order).
|
||||
buckets: Vec<Vec<(Hash, PatternID)>>,
|
||||
/// The length of the hashing window. Generally, this corresponds to the
|
||||
/// length of the smallest pattern.
|
||||
hash_len: usize,
|
||||
/// The factor to subtract out of a hash before updating it with a new
|
||||
/// byte.
|
||||
hash_2pow: usize,
|
||||
}
|
||||
|
||||
impl RabinKarp {
|
||||
/// Compile a new Rabin-Karp matcher from the patterns given.
|
||||
///
|
||||
/// This panics if any of the patterns in the collection are empty, or if
|
||||
/// the collection is itself empty.
|
||||
pub(crate) fn new(patterns: &Arc<Patterns>) -> RabinKarp {
|
||||
assert!(patterns.len() >= 1);
|
||||
let hash_len = patterns.minimum_len();
|
||||
assert!(hash_len >= 1);
|
||||
|
||||
let mut hash_2pow = 1usize;
|
||||
for _ in 1..hash_len {
|
||||
hash_2pow = hash_2pow.wrapping_shl(1);
|
||||
}
|
||||
|
||||
let mut rk = RabinKarp {
|
||||
patterns: Arc::clone(patterns),
|
||||
buckets: vec![vec![]; NUM_BUCKETS],
|
||||
hash_len,
|
||||
hash_2pow,
|
||||
};
|
||||
for (id, pat) in patterns.iter() {
|
||||
let hash = rk.hash(&pat.bytes()[..rk.hash_len]);
|
||||
let bucket = hash % NUM_BUCKETS;
|
||||
rk.buckets[bucket].push((hash, id));
|
||||
}
|
||||
rk
|
||||
}
|
||||
|
||||
/// Return the first matching pattern in the given haystack, begining the
|
||||
/// search at `at`.
|
||||
pub(crate) fn find_at(
|
||||
&self,
|
||||
haystack: &[u8],
|
||||
mut at: usize,
|
||||
) -> Option<Match> {
|
||||
assert_eq!(NUM_BUCKETS, self.buckets.len());
|
||||
|
||||
if at + self.hash_len > haystack.len() {
|
||||
return None;
|
||||
}
|
||||
let mut hash = self.hash(&haystack[at..at + self.hash_len]);
|
||||
loop {
|
||||
let bucket = &self.buckets[hash % NUM_BUCKETS];
|
||||
for &(phash, pid) in bucket {
|
||||
if phash == hash {
|
||||
if let Some(c) = self.verify(pid, haystack, at) {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if at + self.hash_len >= haystack.len() {
|
||||
return None;
|
||||
}
|
||||
hash = self.update_hash(
|
||||
hash,
|
||||
haystack[at],
|
||||
haystack[at + self.hash_len],
|
||||
);
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the approximate total amount of heap used by this searcher, in
|
||||
/// units of bytes.
|
||||
pub(crate) fn memory_usage(&self) -> usize {
|
||||
self.buckets.len() * core::mem::size_of::<Vec<(Hash, PatternID)>>()
|
||||
+ self.patterns.len() * core::mem::size_of::<(Hash, PatternID)>()
|
||||
}
|
||||
|
||||
/// Verify whether the pattern with the given id matches at
|
||||
/// `haystack[at..]`.
|
||||
///
|
||||
/// We tag this function as `cold` because it helps improve codegen.
|
||||
/// Intuitively, it would seem like inlining it would be better. However,
|
||||
/// the only time this is called and a match is not found is when there
|
||||
/// there is a hash collision, or when a prefix of a pattern matches but
|
||||
/// the entire pattern doesn't match. This is hopefully fairly rare, and
|
||||
/// if it does occur a lot, it's going to be slow no matter what we do.
|
||||
#[cold]
|
||||
fn verify(
|
||||
&self,
|
||||
id: PatternID,
|
||||
haystack: &[u8],
|
||||
at: usize,
|
||||
) -> Option<Match> {
|
||||
let pat = self.patterns.get(id);
|
||||
if pat.is_prefix(&haystack[at..]) {
|
||||
Some(Match::new(id, at..at + pat.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash the given bytes.
|
||||
fn hash(&self, bytes: &[u8]) -> Hash {
|
||||
assert_eq!(self.hash_len, bytes.len());
|
||||
|
||||
let mut hash = 0usize;
|
||||
for &b in bytes {
|
||||
hash = hash.wrapping_shl(1).wrapping_add(b as usize);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
/// Update the hash given based on removing `old_byte` at the beginning
|
||||
/// of some byte string, and appending `new_byte` to the end of that same
|
||||
/// byte string.
|
||||
fn update_hash(&self, prev: Hash, old_byte: u8, new_byte: u8) -> Hash {
|
||||
prev.wrapping_sub((old_byte as usize).wrapping_mul(self.hash_2pow))
|
||||
.wrapping_shl(1)
|
||||
.wrapping_add(new_byte as usize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
Teddy is a SIMD accelerated multiple substring matching algorithm. The name
|
||||
and the core ideas in the algorithm were learned from the [Hyperscan][1_u]
|
||||
project. The implementation in this repository was mostly motivated for use in
|
||||
accelerating regex searches by searching for small sets of required literals
|
||||
extracted from the regex.
|
||||
|
||||
|
||||
# Background
|
||||
|
||||
The key idea of Teddy is to do *packed* substring matching. In the literature,
|
||||
packed substring matching is the idea of examining multiple bytes in a haystack
|
||||
at a time to detect matches. Implementations of, for example, memchr (which
|
||||
detects matches of a single byte) have been doing this for years. Only
|
||||
recently, with the introduction of various SIMD instructions, has this been
|
||||
extended to substring matching. The PCMPESTRI instruction (and its relatives),
|
||||
for example, implements substring matching in hardware. It is, however, limited
|
||||
to substrings of length 16 bytes or fewer, but this restriction is fine in a
|
||||
regex engine, since we rarely care about the performance difference between
|
||||
searching for a 16 byte literal and a 16 + N literal; 16 is already long
|
||||
enough. The key downside of the PCMPESTRI instruction, on current (2016) CPUs
|
||||
at least, is its latency and throughput. As a result, it is often faster to
|
||||
do substring search with a Boyer-Moore (or Two-Way) variant and a well placed
|
||||
memchr to quickly skip through the haystack.
|
||||
|
||||
There are fewer results from the literature on packed substring matching,
|
||||
and even fewer for packed multiple substring matching. Ben-Kiki et al. [2]
|
||||
describes use of PCMPESTRI for substring matching, but is mostly theoretical
|
||||
and hand-waves performance. There is other theoretical work done by Bille [3]
|
||||
as well.
|
||||
|
||||
The rest of the work in the field, as far as I'm aware, is by Faro and Kulekci
|
||||
and is generally focused on multiple pattern search. Their first paper [4a]
|
||||
introduces the concept of a fingerprint, which is computed for every block of
|
||||
N bytes in every pattern. The haystack is then scanned N bytes at a time and
|
||||
a fingerprint is computed in the same way it was computed for blocks in the
|
||||
patterns. If the fingerprint corresponds to one that was found in a pattern,
|
||||
then a verification step follows to confirm that one of the substrings with the
|
||||
corresponding fingerprint actually matches at the current location. Various
|
||||
implementation tricks are employed to make sure the fingerprint lookup is fast;
|
||||
typically by truncating the fingerprint. (This may, of course, provoke more
|
||||
steps in the verification process, so a balance must be struck.)
|
||||
|
||||
The main downside of [4a] is that the minimum substring length is 32 bytes,
|
||||
presumably because of how the algorithm uses certain SIMD instructions. This
|
||||
essentially makes it useless for general purpose regex matching, where a small
|
||||
number of short patterns is far more likely.
|
||||
|
||||
Faro and Kulekci published another paper [4b] that is conceptually very similar
|
||||
to [4a]. The key difference is that it uses the CRC32 instruction (introduced
|
||||
as part of SSE 4.2) to compute fingerprint values. This also enables the
|
||||
algorithm to work effectively on substrings as short as 7 bytes with 4 byte
|
||||
windows. 7 bytes is unfortunately still too long. The window could be
|
||||
technically shrunk to 2 bytes, thereby reducing minimum length to 3, but the
|
||||
small window size ends up negating most performance benefits—and it's likely
|
||||
the common case in a general purpose regex engine.
|
||||
|
||||
Faro and Kulekci also published [4c] that appears to be intended as a
|
||||
replacement to using PCMPESTRI. In particular, it is specifically motivated by
|
||||
the high throughput/latency time of PCMPESTRI and therefore chooses other SIMD
|
||||
instructions that are faster. While this approach works for short substrings,
|
||||
I personally couldn't see a way to generalize it to multiple substring search.
|
||||
|
||||
Faro and Kulekci have another paper [4d] that I haven't been able to read
|
||||
because it is behind a paywall.
|
||||
|
||||
|
||||
# Teddy
|
||||
|
||||
Finally, we get to Teddy. If the above literature review is complete, then it
|
||||
appears that Teddy is a novel algorithm. More than that, in my experience, it
|
||||
completely blows away the competition for short substrings, which is exactly
|
||||
what we want in a general purpose regex engine. Again, the algorithm appears
|
||||
to be developed by the authors of [Hyperscan][1_u]. Hyperscan was open sourced
|
||||
late 2015, and no earlier history could be found. Therefore, tracking the exact
|
||||
provenance of the algorithm with respect to the published literature seems
|
||||
difficult.
|
||||
|
||||
At a high level, Teddy works somewhat similarly to the fingerprint algorithms
|
||||
published by Faro and Kulekci, but Teddy does it in a way that scales a bit
|
||||
better. Namely:
|
||||
|
||||
1. Teddy's core algorithm scans the haystack in 16 (for SSE, or 32 for AVX)
|
||||
byte chunks. 16 (or 32) is significant because it corresponds to the number
|
||||
of bytes in a SIMD vector.
|
||||
2. Bitwise operations are performed on each chunk to discover if any region of
|
||||
it matches a set of precomputed fingerprints from the patterns. If there are
|
||||
matches, then a verification step is performed. In this implementation, our
|
||||
verification step is naive. This can be improved upon.
|
||||
|
||||
The details to make this work are quite clever. First, we must choose how to
|
||||
pick our fingerprints. In Hyperscan's implementation, I *believe* they use the
|
||||
last N bytes of each substring, where N must be at least the minimum length of
|
||||
any substring in the set being searched. In this implementation, we use the
|
||||
first N bytes of each substring. (The tradeoffs between these choices aren't
|
||||
yet clear to me.) We then must figure out how to quickly test whether an
|
||||
occurrence of any fingerprint from the set of patterns appears in a 16 byte
|
||||
block from the haystack. To keep things simple, let's assume N = 1 and examine
|
||||
some examples to motivate the approach. Here are our patterns:
|
||||
|
||||
```ignore
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
```
|
||||
|
||||
The corresponding fingerprints, for N = 1, are `f`, `b` and `b`. Now let's set
|
||||
our 16 byte block to:
|
||||
|
||||
```ignore
|
||||
bat cat foo bump
|
||||
xxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
To cut to the chase, Teddy works by using bitsets. In particular, Teddy creates
|
||||
a mask that allows us to quickly compute membership of a fingerprint in a 16
|
||||
byte block that also tells which pattern the fingerprint corresponds to. In
|
||||
this case, our fingerprint is a single byte, so an appropriate abstraction is
|
||||
a map from a single byte to a list of patterns that contain that fingerprint:
|
||||
|
||||
```ignore
|
||||
f |--> foo
|
||||
b |--> bar, baz
|
||||
```
|
||||
|
||||
Now, all we need to do is figure out how to represent this map in vector space
|
||||
and use normal SIMD operations to perform a lookup. The first simplification
|
||||
we can make is to represent our patterns as bit fields occupying a single
|
||||
byte. This is important, because a single SIMD vector can store 16 bytes.
|
||||
|
||||
```ignore
|
||||
f |--> 00000001
|
||||
b |--> 00000010, 00000100
|
||||
```
|
||||
|
||||
How do we perform lookup though? It turns out that SSSE3 introduced a very cool
|
||||
instruction called PSHUFB. The instruction takes two SIMD vectors, `A` and `B`,
|
||||
and returns a third vector `C`. All vectors are treated as 16 8-bit integers.
|
||||
`C` is formed by `C[i] = A[B[i]]`. (This is a bit of a simplification, but true
|
||||
for the purposes of this algorithm. For full details, see [Intel's Intrinsics
|
||||
Guide][5_u].) This essentially lets us use the values in `B` to lookup values
|
||||
in `A`.
|
||||
|
||||
If we could somehow cause `B` to contain our 16 byte block from the haystack,
|
||||
and if `A` could contain our bitmasks, then we'd end up with something like
|
||||
this for `A`:
|
||||
|
||||
```ignore
|
||||
0x00 0x01 ... 0x62 ... 0x66 ... 0xFF
|
||||
A = 0 0 00000110 00000001 0
|
||||
```
|
||||
|
||||
And if `B` contains our window from our haystack, we could use shuffle to take
|
||||
the values from `B` and use them to look up our bitsets in `A`. But of course,
|
||||
we can't do this because `A` in the above example contains 256 bytes, which
|
||||
is much larger than the size of a SIMD vector.
|
||||
|
||||
Nybbles to the rescue! A nybble is 4 bits. Instead of one mask to hold all of
|
||||
our bitsets, we can use two masks, where one mask corresponds to the lower four
|
||||
bits of our fingerprint and the other mask corresponds to the upper four bits.
|
||||
So our map now looks like:
|
||||
|
||||
```ignore
|
||||
'f' & 0xF = 0x6 |--> 00000001
|
||||
'f' >> 4 = 0x6 |--> 00000111
|
||||
'b' & 0xF = 0x2 |--> 00000110
|
||||
'b' >> 4 = 0x6 |--> 00000111
|
||||
```
|
||||
|
||||
Notice that the bitsets for each nybble correspond to the union of all
|
||||
fingerprints that contain that nybble. For example, both `f` and `b` have the
|
||||
same upper 4 bits but differ on the lower 4 bits. Putting this together, we
|
||||
have `A0`, `A1` and `B`, where `A0` is our mask for the lower nybble, `A1` is
|
||||
our mask for the upper nybble and `B` is our 16 byte block from the haystack:
|
||||
|
||||
```ignore
|
||||
0x00 0x01 0x02 0x03 ... 0x06 ... 0xF
|
||||
A0 = 0 0 00000110 0 00000001 0
|
||||
A1 = 0 0 0 0 00000111 0
|
||||
B = b a t _ t p
|
||||
B = 0x62 0x61 0x74 0x20 0x74 0x70
|
||||
```
|
||||
|
||||
But of course, we can't use `B` with `PSHUFB` yet, since its values are 8 bits,
|
||||
and we need indexes that are at most 4 bits (corresponding to one of 16
|
||||
values). We can apply the same transformation to split `B` into lower and upper
|
||||
nybbles as we did `A`. As before, `B0` corresponds to the lower nybbles and
|
||||
`B1` corresponds to the upper nybbles:
|
||||
|
||||
```ignore
|
||||
b a t _ c a t _ f o o _ b u m p
|
||||
B0 = 0x2 0x1 0x4 0x0 0x3 0x1 0x4 0x0 0x6 0xF 0xF 0x0 0x2 0x5 0xD 0x0
|
||||
B1 = 0x6 0x6 0x7 0x2 0x6 0x6 0x7 0x2 0x6 0x6 0x6 0x2 0x6 0x7 0x6 0x7
|
||||
```
|
||||
|
||||
And now we have a nice correspondence. `B0` can index `A0` and `B1` can index
|
||||
`A1`. Here's what we get when we apply `C0 = PSHUFB(A0, B0)`:
|
||||
|
||||
```ignore
|
||||
b a ... f o ... p
|
||||
A0[0x2] A0[0x1] A0[0x6] A0[0xF] A0[0x0]
|
||||
C0 = 00000110 0 00000001 0 0
|
||||
```
|
||||
|
||||
And `C1 = PSHUFB(A1, B1)`:
|
||||
|
||||
```ignore
|
||||
b a ... f o ... p
|
||||
A1[0x6] A1[0x6] A1[0x6] A1[0x6] A1[0x7]
|
||||
C1 = 00000111 00000111 00000111 00000111 0
|
||||
```
|
||||
|
||||
Notice how neither one of `C0` or `C1` is guaranteed to report fully correct
|
||||
results all on its own. For example, `C1` claims that `b` is a fingerprint for
|
||||
the pattern `foo` (since `A1[0x6] = 00000111`), and that `o` is a fingerprint
|
||||
for all of our patterns. But if we combined `C0` and `C1` with an `AND`
|
||||
operation:
|
||||
|
||||
```ignore
|
||||
b a ... f o ... p
|
||||
C = 00000110 0 00000001 0 0
|
||||
```
|
||||
|
||||
Then we now have that `C[i]` contains a bitset corresponding to the matching
|
||||
fingerprints in a haystack's 16 byte block, where `i` is the `ith` byte in that
|
||||
block.
|
||||
|
||||
Once we have that, we can look for the position of the least significant bit
|
||||
in `C`. (Least significant because we only target little endian here. Thus,
|
||||
the least significant bytes correspond to bytes in our haystack at a lower
|
||||
address.) That position, modulo `8`, gives us the pattern that the fingerprint
|
||||
matches. That position, integer divided by `8`, also gives us the byte offset
|
||||
that the fingerprint occurs in inside the 16 byte haystack block. Using those
|
||||
two pieces of information, we can run a verification procedure that tries
|
||||
to match all substrings containing that fingerprint at that position in the
|
||||
haystack.
|
||||
|
||||
|
||||
# Implementation notes
|
||||
|
||||
The problem with the algorithm as described above is that it uses a single byte
|
||||
for a fingerprint. This will work well if the fingerprints are rare in the
|
||||
haystack (e.g., capital letters or special characters in normal English text),
|
||||
but if the fingerprints are common, you'll wind up spending too much time in
|
||||
the verification step, which effectively negates the performance benefits of
|
||||
scanning 16 bytes at a time. Remember, the key to the performance of this
|
||||
algorithm is to do as little work as possible per 16 (or 32) bytes.
|
||||
|
||||
This algorithm can be extrapolated in a relatively straight-forward way to use
|
||||
larger fingerprints. That is, instead of a single byte prefix, we might use a
|
||||
two or three byte prefix. The implementation here implements N = {1, 2, 3}
|
||||
and always picks the largest N possible. The rationale is that the bigger the
|
||||
fingerprint, the fewer verification steps we'll do. Of course, if N is too
|
||||
large, then we'll end up doing too much on each step.
|
||||
|
||||
The way to extend it is:
|
||||
|
||||
1. Add a mask for each byte in the fingerprint. (Remember that each mask is
|
||||
composed of two SIMD vectors.) This results in a value of `C` for each byte
|
||||
in the fingerprint while searching.
|
||||
2. When testing each 16 (or 32) byte block, each value of `C` must be shifted
|
||||
so that they are aligned. Once aligned, they should all be `AND`'d together.
|
||||
This will give you only the bitsets corresponding to the full match of the
|
||||
fingerprint. To do this, one needs to save the last byte (for N=2) or last
|
||||
two bytes (for N=3) from the previous iteration, and then line them up with
|
||||
the first one or two bytes of the next iteration.
|
||||
|
||||
## Verification
|
||||
|
||||
Verification generally follows the procedure outlined above. The tricky parts
|
||||
are in the right formulation of operations to get our bits out of our vectors.
|
||||
We have a limited set of operations available to us on SIMD vectors as 128-bit
|
||||
or 256-bit numbers, so we wind up needing to rip out 2 (or 4) 64-bit integers
|
||||
from our vectors, and then run our verification step on each of those. The
|
||||
verification step looks at the least significant bit set, and from its
|
||||
position, we can derive the byte offset and bucket. (Again, as described
|
||||
above.) Once we know the bucket, we do a fairly naive exhaustive search for
|
||||
every literal in that bucket. (Hyperscan is a bit smarter here and uses a hash
|
||||
table, but I haven't had time to thoroughly explore that. A few initial
|
||||
half-hearted attempts resulted in worse performance.)
|
||||
|
||||
## AVX
|
||||
|
||||
The AVX version of Teddy extrapolates almost perfectly from the SSE version.
|
||||
The only hickup is that PALIGNR is used to align chunks in the 16-bit version,
|
||||
and there is no equivalent instruction in AVX. AVX does have VPALIGNR, but it
|
||||
only works within 128-bit lanes. So there's a bit of tomfoolery to get around
|
||||
this by shuffling the vectors before calling VPALIGNR.
|
||||
|
||||
The only other aspect to AVX is that since our masks are still fundamentally
|
||||
16-bytes (0x0-0xF), they are duplicated to 32-bytes, so that they can apply to
|
||||
32-byte chunks.
|
||||
|
||||
## Fat Teddy
|
||||
|
||||
In the version of Teddy described above, 8 buckets are used to group patterns
|
||||
that we want to search for. However, when AVX is available, we can extend the
|
||||
number of buckets to 16 by permitting each byte in our masks to use 16-bits
|
||||
instead of 8-bits to represent the buckets it belongs to. (This variant is also
|
||||
in Hyperscan.) However, what we give up is the ability to scan 32 bytes at a
|
||||
time, even though we're using AVX. Instead, we have to scan 16 bytes at a time.
|
||||
What we gain, though, is (hopefully) less work in our verification routine.
|
||||
It patterns are more spread out across more buckets, then there should overall
|
||||
be fewer false positives. In general, Fat Teddy permits us to grow our capacity
|
||||
a bit and search for more literals before Teddy gets overwhelmed.
|
||||
|
||||
The tricky part of Fat Teddy is in how we adjust our masks and our verification
|
||||
procedure. For the masks, we simply represent the first 8 buckets in each of
|
||||
the low 16 bytes, and then the second 8 buckets in each of the high 16 bytes.
|
||||
Then, in the search loop, instead of loading 32 bytes from the haystack, we
|
||||
load the same 16 bytes from the haystack into both the low and high 16 byte
|
||||
portions of our 256-bit vector. So for example, a mask might look like this:
|
||||
|
||||
bits: 00100001 00000000 ... 11000000 00000000 00000001 ... 00000000
|
||||
byte: 31 30 16 15 14 0
|
||||
offset: 15 14 0 15 14 0
|
||||
buckets: 8-15 8-15 8-15 0-7 0-7 0-7
|
||||
|
||||
Where `byte` is the position in the vector (higher numbers corresponding to
|
||||
more significant bits), `offset` is the corresponding position in the haystack
|
||||
chunk, and `buckets` corresponds to the bucket assignments for that particular
|
||||
byte.
|
||||
|
||||
In particular, notice that the bucket assignments for offset `0` are spread
|
||||
out between bytes `0` and `16`. This works well for the chunk-by-chunk search
|
||||
procedure, but verification really wants to process all bucket assignments for
|
||||
each offset at once. Otherwise, we might wind up finding a match at offset
|
||||
`1` in one the first 8 buckets, when we really should have reported a match
|
||||
at offset `0` in one of the second 8 buckets. (Because we want the leftmost
|
||||
match.)
|
||||
|
||||
Thus, for verification, we rearrange the above vector such that it is a
|
||||
sequence of 16-bit integers, where the least significant 16-bit integer
|
||||
corresponds to all of the bucket assignments for offset `0`. So with the
|
||||
above vector, the least significant 16-bit integer would be
|
||||
|
||||
11000000 000000
|
||||
|
||||
which was taken from bytes `16` and `0`. Then the verification step pretty much
|
||||
runs as described, except with 16 buckets instead of 8.
|
||||
|
||||
|
||||
# References
|
||||
|
||||
- **[1]** [Hyperscan on GitHub](https://github.com/intel/hyperscan),
|
||||
[webpage](https://www.hyperscan.io/)
|
||||
- **[2a]** Ben-Kiki, O., Bille, P., Breslauer, D., Gasieniec, L., Grossi, R.,
|
||||
& Weimann, O. (2011).
|
||||
_Optimal packed string matching_.
|
||||
In LIPIcs-Leibniz International Proceedings in Informatics (Vol. 13).
|
||||
Schloss Dagstuhl-Leibniz-Zentrum fuer Informatik.
|
||||
DOI: 10.4230/LIPIcs.FSTTCS.2011.423.
|
||||
[PDF](https://drops.dagstuhl.de/opus/volltexte/2011/3355/pdf/37.pdf).
|
||||
- **[2b]** Ben-Kiki, O., Bille, P., Breslauer, D., Ga̧sieniec, L., Grossi, R.,
|
||||
& Weimann, O. (2014).
|
||||
_Towards optimal packed string matching_.
|
||||
Theoretical Computer Science, 525, 111-129.
|
||||
DOI: 10.1016/j.tcs.2013.06.013.
|
||||
[PDF](https://www.cs.haifa.ac.il/~oren/Publications/bpsm.pdf).
|
||||
- **[3]** Bille, P. (2011).
|
||||
_Fast searching in packed strings_.
|
||||
Journal of Discrete Algorithms, 9(1), 49-56.
|
||||
DOI: 10.1016/j.jda.2010.09.003.
|
||||
[PDF](https://www.sciencedirect.com/science/article/pii/S1570866710000353).
|
||||
- **[4a]** Faro, S., & Külekci, M. O. (2012, October).
|
||||
_Fast multiple string matching using streaming SIMD extensions technology_.
|
||||
In String Processing and Information Retrieval (pp. 217-228).
|
||||
Springer Berlin Heidelberg.
|
||||
DOI: 10.1007/978-3-642-34109-0_23.
|
||||
[PDF](https://www.dmi.unict.it/faro/papers/conference/faro32.pdf).
|
||||
- **[4b]** Faro, S., & Külekci, M. O. (2013, September).
|
||||
_Towards a Very Fast Multiple String Matching Algorithm for Short Patterns_.
|
||||
In Stringology (pp. 78-91).
|
||||
[PDF](https://www.dmi.unict.it/faro/papers/conference/faro36.pdf).
|
||||
- **[4c]** Faro, S., & Külekci, M. O. (2013, January).
|
||||
_Fast packed string matching for short patterns_.
|
||||
In Proceedings of the Meeting on Algorithm Engineering & Expermiments
|
||||
(pp. 113-121).
|
||||
Society for Industrial and Applied Mathematics.
|
||||
[PDF](https://arxiv.org/pdf/1209.6449.pdf).
|
||||
- **[4d]** Faro, S., & Külekci, M. O. (2014).
|
||||
_Fast and flexible packed string matching_.
|
||||
Journal of Discrete Algorithms, 28, 61-72.
|
||||
DOI: 10.1016/j.jda.2014.07.003.
|
||||
|
||||
[1_u]: https://github.com/intel/hyperscan
|
||||
[5_u]: https://software.intel.com/sites/landingpage/IntrinsicsGuide
|
||||
@@ -0,0 +1,792 @@
|
||||
use core::{
|
||||
fmt::Debug,
|
||||
panic::{RefUnwindSafe, UnwindSafe},
|
||||
};
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::packed::{ext::Pointer, pattern::Patterns, teddy::generic::Match};
|
||||
|
||||
/// A builder for constructing a Teddy matcher.
|
||||
///
|
||||
/// The builder primarily permits fine grained configuration of the Teddy
|
||||
/// matcher. Most options are made only available for testing/benchmarking
|
||||
/// purposes. In reality, options are automatically determined by the nature
|
||||
/// and number of patterns given to the builder.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Builder {
|
||||
/// When none, this is automatically determined. Otherwise, `false` means
|
||||
/// slim Teddy is used (8 buckets) and `true` means fat Teddy is used
|
||||
/// (16 buckets). Fat Teddy requires AVX2, so if that CPU feature isn't
|
||||
/// available and Fat Teddy was requested, no matcher will be built.
|
||||
only_fat: Option<bool>,
|
||||
/// When none, this is automatically determined. Otherwise, `false` means
|
||||
/// that 128-bit vectors will be used (up to SSSE3 instructions) where as
|
||||
/// `true` means that 256-bit vectors will be used. As with `fat`, if
|
||||
/// 256-bit vectors are requested and they aren't available, then a
|
||||
/// searcher will not be built.
|
||||
only_256bit: Option<bool>,
|
||||
/// When true (the default), the number of patterns will be used as a
|
||||
/// heuristic for refusing construction of a Teddy searcher. The point here
|
||||
/// is that too many patterns can overwhelm Teddy. But this can be disabled
|
||||
/// in cases where the caller knows better.
|
||||
heuristic_pattern_limits: bool,
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Create a new builder for configuring a Teddy matcher.
|
||||
pub(crate) fn new() -> Builder {
|
||||
Builder {
|
||||
only_fat: None,
|
||||
only_256bit: None,
|
||||
heuristic_pattern_limits: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a matcher for the set of patterns given. If a matcher could not
|
||||
/// be built, then `None` is returned.
|
||||
///
|
||||
/// Generally, a matcher isn't built if the necessary CPU features aren't
|
||||
/// available, an unsupported target or if the searcher is believed to be
|
||||
/// slower than standard techniques (i.e., if there are too many literals).
|
||||
pub(crate) fn build(&self, patterns: Arc<Patterns>) -> Option<Searcher> {
|
||||
self.build_imp(patterns)
|
||||
}
|
||||
|
||||
/// Require the use of Fat (true) or Slim (false) Teddy. Fat Teddy uses
|
||||
/// 16 buckets where as Slim Teddy uses 8 buckets. More buckets are useful
|
||||
/// for a larger set of literals.
|
||||
///
|
||||
/// `None` is the default, which results in an automatic selection based
|
||||
/// on the number of literals and available CPU features.
|
||||
pub(crate) fn only_fat(&mut self, yes: Option<bool>) -> &mut Builder {
|
||||
self.only_fat = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Request the use of 256-bit vectors (true) or 128-bit vectors (false).
|
||||
/// Generally, a larger vector size is better since it either permits
|
||||
/// matching more patterns or matching more bytes in the haystack at once.
|
||||
///
|
||||
/// `None` is the default, which results in an automatic selection based on
|
||||
/// the number of literals and available CPU features.
|
||||
pub(crate) fn only_256bit(&mut self, yes: Option<bool>) -> &mut Builder {
|
||||
self.only_256bit = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Request that heuristic limitations on the number of patterns be
|
||||
/// employed. This useful to disable for benchmarking where one wants to
|
||||
/// explore how Teddy performs on large number of patterns even if the
|
||||
/// heuristics would otherwise refuse construction.
|
||||
///
|
||||
/// This is enabled by default.
|
||||
pub(crate) fn heuristic_pattern_limits(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> &mut Builder {
|
||||
self.heuristic_pattern_limits = yes;
|
||||
self
|
||||
}
|
||||
|
||||
fn build_imp(&self, patterns: Arc<Patterns>) -> Option<Searcher> {
|
||||
let patlimit = self.heuristic_pattern_limits;
|
||||
// There's no particular reason why we limit ourselves to little endian
|
||||
// here, but it seems likely that some parts of Teddy as they are
|
||||
// currently written (e.g., the uses of `trailing_zeros`) are likely
|
||||
// wrong on non-little-endian targets. Such things are likely easy to
|
||||
// fix, but at the time of writing (2023/09/18), I actually do not know
|
||||
// how to test this code on a big-endian target. So for now, we're
|
||||
// conservative and just bail out.
|
||||
if !cfg!(target_endian = "little") {
|
||||
debug!("skipping Teddy because target isn't little endian");
|
||||
return None;
|
||||
}
|
||||
// Too many patterns will overwhelm Teddy and likely lead to slow
|
||||
// downs, typically in the verification step.
|
||||
if patlimit && patterns.len() > 64 {
|
||||
debug!("skipping Teddy because of too many patterns");
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
|
||||
{
|
||||
use self::x86_64::{FatAVX2, SlimAVX2, SlimSSSE3};
|
||||
|
||||
let mask_len = core::cmp::min(4, patterns.minimum_len());
|
||||
let beefy = patterns.len() > 32;
|
||||
let has_avx2 = self::x86_64::is_available_avx2();
|
||||
let has_ssse3 = has_avx2 || self::x86_64::is_available_ssse3();
|
||||
let use_avx2 = if self.only_256bit == Some(true) {
|
||||
if !has_avx2 {
|
||||
debug!(
|
||||
"skipping Teddy because avx2 was demanded but unavailable"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
true
|
||||
} else if self.only_256bit == Some(false) {
|
||||
if !has_ssse3 {
|
||||
debug!(
|
||||
"skipping Teddy because ssse3 was demanded but unavailable"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
false
|
||||
} else if !has_ssse3 && !has_avx2 {
|
||||
debug!(
|
||||
"skipping Teddy because ssse3 and avx2 are unavailable"
|
||||
);
|
||||
return None;
|
||||
} else {
|
||||
has_avx2
|
||||
};
|
||||
let fat = match self.only_fat {
|
||||
None => use_avx2 && beefy,
|
||||
Some(false) => false,
|
||||
Some(true) if !use_avx2 => {
|
||||
debug!(
|
||||
"skipping Teddy because fat was demanded, but fat \
|
||||
Teddy requires avx2 which is unavailable"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(true) => true,
|
||||
};
|
||||
// Just like for aarch64, it's possible that too many patterns will
|
||||
// overhwelm Teddy. Unlike aarch64 though, we have Fat teddy which
|
||||
// helps things scale a bit more by spreading patterns over more
|
||||
// buckets.
|
||||
//
|
||||
// These thresholds were determined by looking at the measurements
|
||||
// for the rust/aho-corasick/packed/leftmost-first and
|
||||
// rust/aho-corasick/dfa/leftmost-first engines on the `teddy/`
|
||||
// benchmarks.
|
||||
if patlimit && mask_len == 1 && patterns.len() > 16 {
|
||||
debug!(
|
||||
"skipping Teddy (mask len: 1) because there are \
|
||||
too many patterns",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
match (mask_len, use_avx2, fat) {
|
||||
(1, false, _) => {
|
||||
debug!("Teddy choice: 128-bit slim, 1 byte");
|
||||
SlimSSSE3::<1>::new(&patterns)
|
||||
}
|
||||
(1, true, false) => {
|
||||
debug!("Teddy choice: 256-bit slim, 1 byte");
|
||||
SlimAVX2::<1>::new(&patterns)
|
||||
}
|
||||
(1, true, true) => {
|
||||
debug!("Teddy choice: 256-bit fat, 1 byte");
|
||||
FatAVX2::<1>::new(&patterns)
|
||||
}
|
||||
(2, false, _) => {
|
||||
debug!("Teddy choice: 128-bit slim, 2 bytes");
|
||||
SlimSSSE3::<2>::new(&patterns)
|
||||
}
|
||||
(2, true, false) => {
|
||||
debug!("Teddy choice: 256-bit slim, 2 bytes");
|
||||
SlimAVX2::<2>::new(&patterns)
|
||||
}
|
||||
(2, true, true) => {
|
||||
debug!("Teddy choice: 256-bit fat, 2 bytes");
|
||||
FatAVX2::<2>::new(&patterns)
|
||||
}
|
||||
(3, false, _) => {
|
||||
debug!("Teddy choice: 128-bit slim, 3 bytes");
|
||||
SlimSSSE3::<3>::new(&patterns)
|
||||
}
|
||||
(3, true, false) => {
|
||||
debug!("Teddy choice: 256-bit slim, 3 bytes");
|
||||
SlimAVX2::<3>::new(&patterns)
|
||||
}
|
||||
(3, true, true) => {
|
||||
debug!("Teddy choice: 256-bit fat, 3 bytes");
|
||||
FatAVX2::<3>::new(&patterns)
|
||||
}
|
||||
(4, false, _) => {
|
||||
debug!("Teddy choice: 128-bit slim, 4 bytes");
|
||||
SlimSSSE3::<4>::new(&patterns)
|
||||
}
|
||||
(4, true, false) => {
|
||||
debug!("Teddy choice: 256-bit slim, 4 bytes");
|
||||
SlimAVX2::<4>::new(&patterns)
|
||||
}
|
||||
(4, true, true) => {
|
||||
debug!("Teddy choice: 256-bit fat, 4 bytes");
|
||||
FatAVX2::<4>::new(&patterns)
|
||||
}
|
||||
_ => {
|
||||
debug!("no supported Teddy configuration found");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(all(
|
||||
target_arch = "aarch64",
|
||||
target_feature = "neon",
|
||||
target_endian = "little"
|
||||
))]
|
||||
{
|
||||
use self::aarch64::SlimNeon;
|
||||
|
||||
let mask_len = core::cmp::min(4, patterns.minimum_len());
|
||||
if self.only_256bit == Some(true) {
|
||||
debug!(
|
||||
"skipping Teddy because 256-bits were demanded \
|
||||
but unavailable"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if self.only_fat == Some(true) {
|
||||
debug!(
|
||||
"skipping Teddy because fat was demanded but unavailable"
|
||||
);
|
||||
}
|
||||
// Since we don't have Fat teddy in aarch64 (I think we'd want at
|
||||
// least 256-bit vectors for that), we need to be careful not to
|
||||
// allow too many patterns as it might overwhelm Teddy. Generally
|
||||
// speaking, as the mask length goes up, the more patterns we can
|
||||
// handle because the mask length results in fewer candidates
|
||||
// generated.
|
||||
//
|
||||
// These thresholds were determined by looking at the measurements
|
||||
// for the rust/aho-corasick/packed/leftmost-first and
|
||||
// rust/aho-corasick/dfa/leftmost-first engines on the `teddy/`
|
||||
// benchmarks.
|
||||
match mask_len {
|
||||
1 => {
|
||||
if patlimit && patterns.len() > 16 {
|
||||
debug!(
|
||||
"skipping Teddy (mask len: 1) because there are \
|
||||
too many patterns",
|
||||
);
|
||||
}
|
||||
debug!("Teddy choice: 128-bit slim, 1 byte");
|
||||
SlimNeon::<1>::new(&patterns)
|
||||
}
|
||||
2 => {
|
||||
if patlimit && patterns.len() > 32 {
|
||||
debug!(
|
||||
"skipping Teddy (mask len: 2) because there are \
|
||||
too many patterns",
|
||||
);
|
||||
}
|
||||
debug!("Teddy choice: 128-bit slim, 2 bytes");
|
||||
SlimNeon::<2>::new(&patterns)
|
||||
}
|
||||
3 => {
|
||||
if patlimit && patterns.len() > 48 {
|
||||
debug!(
|
||||
"skipping Teddy (mask len: 3) because there are \
|
||||
too many patterns",
|
||||
);
|
||||
}
|
||||
debug!("Teddy choice: 128-bit slim, 3 bytes");
|
||||
SlimNeon::<3>::new(&patterns)
|
||||
}
|
||||
4 => {
|
||||
debug!("Teddy choice: 128-bit slim, 4 bytes");
|
||||
SlimNeon::<4>::new(&patterns)
|
||||
}
|
||||
_ => {
|
||||
debug!("no supported Teddy configuration found");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "x86_64", target_feature = "sse2"),
|
||||
all(
|
||||
target_arch = "aarch64",
|
||||
target_feature = "neon",
|
||||
target_endian = "little"
|
||||
)
|
||||
)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A searcher that dispatches to one of several possible Teddy variants.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Searcher {
|
||||
/// The Teddy variant we use. We use dynamic dispatch under the theory that
|
||||
/// it results in better codegen then a enum, although this is a specious
|
||||
/// claim.
|
||||
///
|
||||
/// This `Searcher` is essentially a wrapper for a `SearcherT` trait
|
||||
/// object. We just make `memory_usage` and `minimum_len` available without
|
||||
/// going through dynamic dispatch.
|
||||
imp: Arc<dyn SearcherT>,
|
||||
/// Total heap memory used by the Teddy variant.
|
||||
memory_usage: usize,
|
||||
/// The minimum haystack length this searcher can handle. It is intended
|
||||
/// for callers to use some other search routine (such as Rabin-Karp) in
|
||||
/// cases where the haystack (or remainer of the haystack) is too short.
|
||||
minimum_len: usize,
|
||||
}
|
||||
|
||||
impl Searcher {
|
||||
/// Look for the leftmost occurrence of any pattern in this search in the
|
||||
/// given haystack starting at the given position.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This panics when `haystack[at..].len()` is less than the minimum length
|
||||
/// for this haystack.
|
||||
#[inline(always)]
|
||||
pub(crate) fn find(
|
||||
&self,
|
||||
haystack: &[u8],
|
||||
at: usize,
|
||||
) -> Option<crate::Match> {
|
||||
// SAFETY: The Teddy implementations all require a minimum haystack
|
||||
// length, and this is required for safety. Therefore, we assert it
|
||||
// here in order to make this method sound.
|
||||
assert!(haystack[at..].len() >= self.minimum_len);
|
||||
let hayptr = haystack.as_ptr();
|
||||
// SAFETY: Construction of the searcher guarantees that we are able
|
||||
// to run it in the current environment (i.e., we won't get an AVX2
|
||||
// searcher on a x86-64 CPU without AVX2 support). Also, the pointers
|
||||
// are valid as they are derived directly from a borrowed slice.
|
||||
let teddym = unsafe {
|
||||
self.imp.find(hayptr.add(at), hayptr.add(haystack.len()))?
|
||||
};
|
||||
let start = teddym.start().as_usize().wrapping_sub(hayptr.as_usize());
|
||||
let end = teddym.end().as_usize().wrapping_sub(hayptr.as_usize());
|
||||
let span = crate::Span { start, end };
|
||||
// OK because we won't permit the construction of a searcher that
|
||||
// could report a pattern ID bigger than what can fit in the crate-wide
|
||||
// PatternID type.
|
||||
let pid = crate::PatternID::new_unchecked(teddym.pattern().as_usize());
|
||||
let m = crate::Match::new(pid, span);
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Returns the approximate total amount of heap used by this type, in
|
||||
/// units of bytes.
|
||||
#[inline(always)]
|
||||
pub(crate) fn memory_usage(&self) -> usize {
|
||||
self.memory_usage
|
||||
}
|
||||
|
||||
/// Returns the minimum length, in bytes, that a haystack must be in order
|
||||
/// to use it with this searcher.
|
||||
#[inline(always)]
|
||||
pub(crate) fn minimum_len(&self) -> usize {
|
||||
self.minimum_len
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides dynamic dispatch over the different possible Teddy
|
||||
/// variants on the same algorithm.
|
||||
///
|
||||
/// On `x86_64` for example, it isn't known until runtime which of 12 possible
|
||||
/// variants will be used. One might use one of the four slim 128-bit vector
|
||||
/// variants, or one of the four 256-bit vector variants or even one of the
|
||||
/// four fat 256-bit vector variants.
|
||||
///
|
||||
/// Since this choice is generally made when the Teddy searcher is constructed
|
||||
/// and this choice is based on the patterns given and what the current CPU
|
||||
/// supports, it follows that there must be some kind of indirection at search
|
||||
/// time that "selects" the variant chosen at build time.
|
||||
///
|
||||
/// There are a few different ways to go about this. One approach is to use an
|
||||
/// enum. It works fine, but in my experiments, this generally results in worse
|
||||
/// codegen. Another approach, which is what we use here, is dynamic dispatch
|
||||
/// via a trait object. We basically implement this trait for each possible
|
||||
/// variant, select the variant we want at build time and convert it to a
|
||||
/// trait object for use at search time.
|
||||
///
|
||||
/// Another approach is to use function pointers and stick each of the possible
|
||||
/// variants into a union. This is essentially isomorphic to the dynamic
|
||||
/// dispatch approach, but doesn't require any allocations. Since this crate
|
||||
/// requires `alloc`, there's no real reason (AFAIK) to go down this path. (The
|
||||
/// `memchr` crate does this.)
|
||||
trait SearcherT:
|
||||
Debug + Send + Sync + UnwindSafe + RefUnwindSafe + 'static
|
||||
{
|
||||
/// Execute a search on the given haystack (identified by `start` and `end`
|
||||
/// raw pointers).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Essentially, the `start` and `end` pointers must be valid and point
|
||||
/// to a haystack one can read. As long as you derive them from, for
|
||||
/// example, a `&[u8]`, they should automatically satisfy all of the safety
|
||||
/// obligations:
|
||||
///
|
||||
/// * Both `start` and `end` must be valid for reads.
|
||||
/// * Both `start` and `end` must point to an initialized value.
|
||||
/// * Both `start` and `end` must point to the same allocated object and
|
||||
/// must either be in bounds or at most one byte past the end of the
|
||||
/// allocated object.
|
||||
/// * Both `start` and `end` must be _derived from_ a pointer to the same
|
||||
/// object.
|
||||
/// * The distance between `start` and `end` must not overflow `isize`.
|
||||
/// * The distance being in bounds must not rely on "wrapping around" the
|
||||
/// address space.
|
||||
/// * It must be the case that `start <= end`.
|
||||
/// * `end - start` must be greater than the minimum length for this
|
||||
/// searcher.
|
||||
///
|
||||
/// Also, it is expected that implementations of this trait will tag this
|
||||
/// method with a `target_feature` attribute. Callers must ensure that
|
||||
/// they are executing this method in an environment where that attribute
|
||||
/// is valid.
|
||||
unsafe fn find(&self, start: *const u8, end: *const u8) -> Option<Match>;
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
|
||||
mod x86_64 {
|
||||
use core::arch::x86_64::{__m128i, __m256i};
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::packed::{
|
||||
ext::Pointer,
|
||||
pattern::Patterns,
|
||||
teddy::generic::{self, Match},
|
||||
};
|
||||
|
||||
use super::{Searcher, SearcherT};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SlimSSSE3<const BYTES: usize> {
|
||||
slim128: generic::Slim<__m128i, BYTES>,
|
||||
}
|
||||
|
||||
// Defines SlimSSSE3 wrapper functions for 1, 2, 3 and 4 bytes.
|
||||
macro_rules! slim_ssse3 {
|
||||
($len:expr) => {
|
||||
impl SlimSSSE3<$len> {
|
||||
/// Creates a new searcher using "slim" Teddy with 128-bit
|
||||
/// vectors. If SSSE3 is not available in the current
|
||||
/// environment, then this returns `None`.
|
||||
pub(super) fn new(
|
||||
patterns: &Arc<Patterns>,
|
||||
) -> Option<Searcher> {
|
||||
if !is_available_ssse3() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { SlimSSSE3::<$len>::new_unchecked(patterns) })
|
||||
}
|
||||
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors without checking whether SSSE3 is available or not.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers must ensure that SSSE3 is available in the current
|
||||
/// environment.
|
||||
#[target_feature(enable = "ssse3")]
|
||||
unsafe fn new_unchecked(patterns: &Arc<Patterns>) -> Searcher {
|
||||
let slim128 = generic::Slim::<__m128i, $len>::new(
|
||||
Arc::clone(patterns),
|
||||
);
|
||||
let memory_usage = slim128.memory_usage();
|
||||
let minimum_len = slim128.minimum_len();
|
||||
let imp = Arc::new(SlimSSSE3 { slim128 });
|
||||
Searcher { imp, memory_usage, minimum_len }
|
||||
}
|
||||
}
|
||||
|
||||
impl SearcherT for SlimSSSE3<$len> {
|
||||
#[target_feature(enable = "ssse3")]
|
||||
#[inline]
|
||||
unsafe fn find(
|
||||
&self,
|
||||
start: *const u8,
|
||||
end: *const u8,
|
||||
) -> Option<Match> {
|
||||
// SAFETY: All obligations except for `target_feature` are
|
||||
// passed to the caller. Our use of `target_feature` is
|
||||
// safe because construction of this type requires that the
|
||||
// requisite target features are available.
|
||||
self.slim128.find(start, end)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
slim_ssse3!(1);
|
||||
slim_ssse3!(2);
|
||||
slim_ssse3!(3);
|
||||
slim_ssse3!(4);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SlimAVX2<const BYTES: usize> {
|
||||
slim128: generic::Slim<__m128i, BYTES>,
|
||||
slim256: generic::Slim<__m256i, BYTES>,
|
||||
}
|
||||
|
||||
// Defines SlimAVX2 wrapper functions for 1, 2, 3 and 4 bytes.
|
||||
macro_rules! slim_avx2 {
|
||||
($len:expr) => {
|
||||
impl SlimAVX2<$len> {
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors. If AVX2 is not available in the current
|
||||
/// environment, then this returns `None`.
|
||||
pub(super) fn new(
|
||||
patterns: &Arc<Patterns>,
|
||||
) -> Option<Searcher> {
|
||||
if !is_available_avx2() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { SlimAVX2::<$len>::new_unchecked(patterns) })
|
||||
}
|
||||
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors without checking whether AVX2 is available or not.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers must ensure that AVX2 is available in the current
|
||||
/// environment.
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn new_unchecked(patterns: &Arc<Patterns>) -> Searcher {
|
||||
let slim128 = generic::Slim::<__m128i, $len>::new(
|
||||
Arc::clone(&patterns),
|
||||
);
|
||||
let slim256 = generic::Slim::<__m256i, $len>::new(
|
||||
Arc::clone(&patterns),
|
||||
);
|
||||
let memory_usage =
|
||||
slim128.memory_usage() + slim256.memory_usage();
|
||||
let minimum_len = slim128.minimum_len();
|
||||
let imp = Arc::new(SlimAVX2 { slim128, slim256 });
|
||||
Searcher { imp, memory_usage, minimum_len }
|
||||
}
|
||||
}
|
||||
|
||||
impl SearcherT for SlimAVX2<$len> {
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[inline]
|
||||
unsafe fn find(
|
||||
&self,
|
||||
start: *const u8,
|
||||
end: *const u8,
|
||||
) -> Option<Match> {
|
||||
// SAFETY: All obligations except for `target_feature` are
|
||||
// passed to the caller. Our use of `target_feature` is
|
||||
// safe because construction of this type requires that the
|
||||
// requisite target features are available.
|
||||
let len = end.distance(start);
|
||||
if len < self.slim256.minimum_len() {
|
||||
self.slim128.find(start, end)
|
||||
} else {
|
||||
self.slim256.find(start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
slim_avx2!(1);
|
||||
slim_avx2!(2);
|
||||
slim_avx2!(3);
|
||||
slim_avx2!(4);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FatAVX2<const BYTES: usize> {
|
||||
fat256: generic::Fat<__m256i, BYTES>,
|
||||
}
|
||||
|
||||
// Defines SlimAVX2 wrapper functions for 1, 2, 3 and 4 bytes.
|
||||
macro_rules! fat_avx2 {
|
||||
($len:expr) => {
|
||||
impl FatAVX2<$len> {
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors. If AVX2 is not available in the current
|
||||
/// environment, then this returns `None`.
|
||||
pub(super) fn new(
|
||||
patterns: &Arc<Patterns>,
|
||||
) -> Option<Searcher> {
|
||||
if !is_available_avx2() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { FatAVX2::<$len>::new_unchecked(patterns) })
|
||||
}
|
||||
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors without checking whether AVX2 is available or not.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers must ensure that AVX2 is available in the current
|
||||
/// environment.
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn new_unchecked(patterns: &Arc<Patterns>) -> Searcher {
|
||||
let fat256 = generic::Fat::<__m256i, $len>::new(
|
||||
Arc::clone(&patterns),
|
||||
);
|
||||
let memory_usage = fat256.memory_usage();
|
||||
let minimum_len = fat256.minimum_len();
|
||||
let imp = Arc::new(FatAVX2 { fat256 });
|
||||
Searcher { imp, memory_usage, minimum_len }
|
||||
}
|
||||
}
|
||||
|
||||
impl SearcherT for FatAVX2<$len> {
|
||||
#[target_feature(enable = "avx2")]
|
||||
#[inline]
|
||||
unsafe fn find(
|
||||
&self,
|
||||
start: *const u8,
|
||||
end: *const u8,
|
||||
) -> Option<Match> {
|
||||
// SAFETY: All obligations except for `target_feature` are
|
||||
// passed to the caller. Our use of `target_feature` is
|
||||
// safe because construction of this type requires that the
|
||||
// requisite target features are available.
|
||||
self.fat256.find(start, end)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fat_avx2!(1);
|
||||
fat_avx2!(2);
|
||||
fat_avx2!(3);
|
||||
fat_avx2!(4);
|
||||
|
||||
#[inline]
|
||||
pub(super) fn is_available_ssse3() -> bool {
|
||||
#[cfg(not(target_feature = "sse2"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
#[cfg(target_feature = "sse2")]
|
||||
{
|
||||
#[cfg(target_feature = "ssse3")]
|
||||
{
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_feature = "ssse3"))]
|
||||
{
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
std::is_x86_feature_detected!("ssse3")
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn is_available_avx2() -> bool {
|
||||
#[cfg(not(target_feature = "sse2"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
#[cfg(target_feature = "sse2")]
|
||||
{
|
||||
#[cfg(target_feature = "avx2")]
|
||||
{
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_feature = "avx2"))]
|
||||
{
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
std::is_x86_feature_detected!("avx2")
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
target_arch = "aarch64",
|
||||
target_feature = "neon",
|
||||
target_endian = "little"
|
||||
))]
|
||||
mod aarch64 {
|
||||
use core::arch::aarch64::uint8x16_t;
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::packed::{
|
||||
pattern::Patterns,
|
||||
teddy::generic::{self, Match},
|
||||
};
|
||||
|
||||
use super::{Searcher, SearcherT};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SlimNeon<const BYTES: usize> {
|
||||
slim128: generic::Slim<uint8x16_t, BYTES>,
|
||||
}
|
||||
|
||||
// Defines SlimSSSE3 wrapper functions for 1, 2, 3 and 4 bytes.
|
||||
macro_rules! slim_neon {
|
||||
($len:expr) => {
|
||||
impl SlimNeon<$len> {
|
||||
/// Creates a new searcher using "slim" Teddy with 128-bit
|
||||
/// vectors. If SSSE3 is not available in the current
|
||||
/// environment, then this returns `None`.
|
||||
pub(super) fn new(
|
||||
patterns: &Arc<Patterns>,
|
||||
) -> Option<Searcher> {
|
||||
Some(unsafe { SlimNeon::<$len>::new_unchecked(patterns) })
|
||||
}
|
||||
|
||||
/// Creates a new searcher using "slim" Teddy with 256-bit
|
||||
/// vectors without checking whether SSSE3 is available or not.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers must ensure that SSSE3 is available in the current
|
||||
/// environment.
|
||||
#[target_feature(enable = "neon")]
|
||||
unsafe fn new_unchecked(patterns: &Arc<Patterns>) -> Searcher {
|
||||
let slim128 = generic::Slim::<uint8x16_t, $len>::new(
|
||||
Arc::clone(patterns),
|
||||
);
|
||||
let memory_usage = slim128.memory_usage();
|
||||
let minimum_len = slim128.minimum_len();
|
||||
let imp = Arc::new(SlimNeon { slim128 });
|
||||
Searcher { imp, memory_usage, minimum_len }
|
||||
}
|
||||
}
|
||||
|
||||
impl SearcherT for SlimNeon<$len> {
|
||||
#[target_feature(enable = "neon")]
|
||||
#[inline]
|
||||
unsafe fn find(
|
||||
&self,
|
||||
start: *const u8,
|
||||
end: *const u8,
|
||||
) -> Option<Match> {
|
||||
// SAFETY: All obligations except for `target_feature` are
|
||||
// passed to the caller. Our use of `target_feature` is
|
||||
// safe because construction of this type requires that the
|
||||
// requisite target features are available.
|
||||
self.slim128.find(start, end)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
slim_neon!(1);
|
||||
slim_neon!(2);
|
||||
slim_neon!(3);
|
||||
slim_neon!(4);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
// Regrettable, but Teddy stuff just isn't used on all targets. And for some
|
||||
// targets, like aarch64, only "slim" Teddy is used and so "fat" Teddy gets a
|
||||
// bunch of dead-code warnings. Just not worth trying to squash them. Blech.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub(crate) use self::builder::{Builder, Searcher};
|
||||
|
||||
mod builder;
|
||||
mod generic;
|
||||
@@ -0,0 +1,583 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use alloc::{
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
packed::{Config, MatchKind},
|
||||
util::search::Match,
|
||||
};
|
||||
|
||||
/// A description of a single test against a multi-pattern searcher.
|
||||
///
|
||||
/// A single test may not necessarily pass on every configuration of a
|
||||
/// searcher. The tests are categorized and grouped appropriately below.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct SearchTest {
|
||||
/// The name of this test, for debugging.
|
||||
name: &'static str,
|
||||
/// The patterns to search for.
|
||||
patterns: &'static [&'static str],
|
||||
/// The text to search.
|
||||
haystack: &'static str,
|
||||
/// Each match is a triple of (pattern_index, start, end), where
|
||||
/// pattern_index is an index into `patterns` and `start`/`end` are indices
|
||||
/// into `haystack`.
|
||||
matches: &'static [(usize, usize, usize)],
|
||||
}
|
||||
|
||||
struct SearchTestOwned {
|
||||
offset: usize,
|
||||
name: String,
|
||||
patterns: Vec<String>,
|
||||
haystack: String,
|
||||
matches: Vec<(usize, usize, usize)>,
|
||||
}
|
||||
|
||||
impl SearchTest {
|
||||
fn variations(&self) -> Vec<SearchTestOwned> {
|
||||
let count = if cfg!(miri) { 1 } else { 261 };
|
||||
let mut tests = vec![];
|
||||
for i in 0..count {
|
||||
tests.push(self.offset_prefix(i));
|
||||
tests.push(self.offset_suffix(i));
|
||||
tests.push(self.offset_both(i));
|
||||
}
|
||||
tests
|
||||
}
|
||||
|
||||
fn offset_both(&self, off: usize) -> SearchTestOwned {
|
||||
SearchTestOwned {
|
||||
offset: off,
|
||||
name: self.name.to_string(),
|
||||
patterns: self.patterns.iter().map(|s| s.to_string()).collect(),
|
||||
haystack: format!(
|
||||
"{}{}{}",
|
||||
"Z".repeat(off),
|
||||
self.haystack,
|
||||
"Z".repeat(off)
|
||||
),
|
||||
matches: self
|
||||
.matches
|
||||
.iter()
|
||||
.map(|&(id, s, e)| (id, s + off, e + off))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_prefix(&self, off: usize) -> SearchTestOwned {
|
||||
SearchTestOwned {
|
||||
offset: off,
|
||||
name: self.name.to_string(),
|
||||
patterns: self.patterns.iter().map(|s| s.to_string()).collect(),
|
||||
haystack: format!("{}{}", "Z".repeat(off), self.haystack),
|
||||
matches: self
|
||||
.matches
|
||||
.iter()
|
||||
.map(|&(id, s, e)| (id, s + off, e + off))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_suffix(&self, off: usize) -> SearchTestOwned {
|
||||
SearchTestOwned {
|
||||
offset: off,
|
||||
name: self.name.to_string(),
|
||||
patterns: self.patterns.iter().map(|s| s.to_string()).collect(),
|
||||
haystack: format!("{}{}", self.haystack, "Z".repeat(off)),
|
||||
matches: self.matches.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Short-hand constructor for SearchTest. We use it a lot below.
|
||||
macro_rules! t {
|
||||
($name:ident, $patterns:expr, $haystack:expr, $matches:expr) => {
|
||||
SearchTest {
|
||||
name: stringify!($name),
|
||||
patterns: $patterns,
|
||||
haystack: $haystack,
|
||||
matches: $matches,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A collection of test groups.
|
||||
type TestCollection = &'static [&'static [SearchTest]];
|
||||
|
||||
// Define several collections corresponding to the different type of match
|
||||
// semantics supported. These collections have some overlap, but each
|
||||
// collection should have some tests that no other collection has.
|
||||
|
||||
/// Tests for leftmost-first match semantics.
|
||||
const PACKED_LEFTMOST_FIRST: TestCollection =
|
||||
&[BASICS, LEFTMOST, LEFTMOST_FIRST, REGRESSION, TEDDY];
|
||||
|
||||
/// Tests for leftmost-longest match semantics.
|
||||
const PACKED_LEFTMOST_LONGEST: TestCollection =
|
||||
&[BASICS, LEFTMOST, LEFTMOST_LONGEST, REGRESSION, TEDDY];
|
||||
|
||||
// Now define the individual tests that make up the collections above.
|
||||
|
||||
/// A collection of tests for the that should always be true regardless of
|
||||
/// match semantics. That is, all combinations of leftmost-{first, longest}
|
||||
/// should produce the same answer.
|
||||
const BASICS: &'static [SearchTest] = &[
|
||||
t!(basic001, &["a"], "", &[]),
|
||||
t!(basic010, &["a"], "a", &[(0, 0, 1)]),
|
||||
t!(basic020, &["a"], "aa", &[(0, 0, 1), (0, 1, 2)]),
|
||||
t!(basic030, &["a"], "aaa", &[(0, 0, 1), (0, 1, 2), (0, 2, 3)]),
|
||||
t!(basic040, &["a"], "aba", &[(0, 0, 1), (0, 2, 3)]),
|
||||
t!(basic050, &["a"], "bba", &[(0, 2, 3)]),
|
||||
t!(basic060, &["a"], "bbb", &[]),
|
||||
t!(basic070, &["a"], "bababbbba", &[(0, 1, 2), (0, 3, 4), (0, 8, 9)]),
|
||||
t!(basic100, &["aa"], "", &[]),
|
||||
t!(basic110, &["aa"], "aa", &[(0, 0, 2)]),
|
||||
t!(basic120, &["aa"], "aabbaa", &[(0, 0, 2), (0, 4, 6)]),
|
||||
t!(basic130, &["aa"], "abbab", &[]),
|
||||
t!(basic140, &["aa"], "abbabaa", &[(0, 5, 7)]),
|
||||
t!(basic150, &["aaa"], "aaa", &[(0, 0, 3)]),
|
||||
t!(basic200, &["abc"], "abc", &[(0, 0, 3)]),
|
||||
t!(basic210, &["abc"], "zazabzabcz", &[(0, 6, 9)]),
|
||||
t!(basic220, &["abc"], "zazabczabcz", &[(0, 3, 6), (0, 7, 10)]),
|
||||
t!(basic230, &["abcd"], "abcd", &[(0, 0, 4)]),
|
||||
t!(basic240, &["abcd"], "zazabzabcdz", &[(0, 6, 10)]),
|
||||
t!(basic250, &["abcd"], "zazabcdzabcdz", &[(0, 3, 7), (0, 8, 12)]),
|
||||
t!(basic300, &["a", "b"], "", &[]),
|
||||
t!(basic310, &["a", "b"], "z", &[]),
|
||||
t!(basic320, &["a", "b"], "b", &[(1, 0, 1)]),
|
||||
t!(basic330, &["a", "b"], "a", &[(0, 0, 1)]),
|
||||
t!(
|
||||
basic340,
|
||||
&["a", "b"],
|
||||
"abba",
|
||||
&[(0, 0, 1), (1, 1, 2), (1, 2, 3), (0, 3, 4),]
|
||||
),
|
||||
t!(
|
||||
basic350,
|
||||
&["b", "a"],
|
||||
"abba",
|
||||
&[(1, 0, 1), (0, 1, 2), (0, 2, 3), (1, 3, 4),]
|
||||
),
|
||||
t!(basic360, &["abc", "bc"], "xbc", &[(1, 1, 3),]),
|
||||
t!(basic400, &["foo", "bar"], "", &[]),
|
||||
t!(basic410, &["foo", "bar"], "foobar", &[(0, 0, 3), (1, 3, 6),]),
|
||||
t!(basic420, &["foo", "bar"], "barfoo", &[(1, 0, 3), (0, 3, 6),]),
|
||||
t!(basic430, &["foo", "bar"], "foofoo", &[(0, 0, 3), (0, 3, 6),]),
|
||||
t!(basic440, &["foo", "bar"], "barbar", &[(1, 0, 3), (1, 3, 6),]),
|
||||
t!(basic450, &["foo", "bar"], "bafofoo", &[(0, 4, 7),]),
|
||||
t!(basic460, &["bar", "foo"], "bafofoo", &[(1, 4, 7),]),
|
||||
t!(basic470, &["foo", "bar"], "fobabar", &[(1, 4, 7),]),
|
||||
t!(basic480, &["bar", "foo"], "fobabar", &[(0, 4, 7),]),
|
||||
t!(basic700, &["yabcdef", "abcdezghi"], "yabcdefghi", &[(0, 0, 7),]),
|
||||
t!(basic710, &["yabcdef", "abcdezghi"], "yabcdezghi", &[(1, 1, 10),]),
|
||||
t!(
|
||||
basic720,
|
||||
&["yabcdef", "bcdeyabc", "abcdezghi"],
|
||||
"yabcdezghi",
|
||||
&[(2, 1, 10),]
|
||||
),
|
||||
t!(basic810, &["abcd", "bcd", "cd"], "abcd", &[(0, 0, 4),]),
|
||||
t!(basic820, &["bcd", "cd", "abcd"], "abcd", &[(2, 0, 4),]),
|
||||
t!(basic830, &["abc", "bc"], "zazabcz", &[(0, 3, 6),]),
|
||||
t!(
|
||||
basic840,
|
||||
&["ab", "ba"],
|
||||
"abababa",
|
||||
&[(0, 0, 2), (0, 2, 4), (0, 4, 6),]
|
||||
),
|
||||
t!(basic850, &["foo", "foo"], "foobarfoo", &[(0, 0, 3), (0, 6, 9),]),
|
||||
];
|
||||
|
||||
/// Tests for leftmost match semantics. These should pass for both
|
||||
/// leftmost-first and leftmost-longest match kinds. Stated differently, among
|
||||
/// ambiguous matches, the longest match and the match that appeared first when
|
||||
/// constructing the automaton should always be the same.
|
||||
const LEFTMOST: &'static [SearchTest] = &[
|
||||
t!(leftmost000, &["ab", "ab"], "abcd", &[(0, 0, 2)]),
|
||||
t!(leftmost030, &["a", "ab"], "aa", &[(0, 0, 1), (0, 1, 2)]),
|
||||
t!(leftmost031, &["ab", "a"], "aa", &[(1, 0, 1), (1, 1, 2)]),
|
||||
t!(leftmost032, &["ab", "a"], "xayabbbz", &[(1, 1, 2), (0, 3, 5)]),
|
||||
t!(leftmost300, &["abcd", "bce", "b"], "abce", &[(1, 1, 4)]),
|
||||
t!(leftmost310, &["abcd", "ce", "bc"], "abce", &[(2, 1, 3)]),
|
||||
t!(leftmost320, &["abcd", "bce", "ce", "b"], "abce", &[(1, 1, 4)]),
|
||||
t!(leftmost330, &["abcd", "bce", "cz", "bc"], "abcz", &[(3, 1, 3)]),
|
||||
t!(leftmost340, &["bce", "cz", "bc"], "bcz", &[(2, 0, 2)]),
|
||||
t!(leftmost350, &["abc", "bd", "ab"], "abd", &[(2, 0, 2)]),
|
||||
t!(
|
||||
leftmost360,
|
||||
&["abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(2, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
leftmost370,
|
||||
&["abcdefghi", "cde", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(3, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
leftmost380,
|
||||
&["abcdefghi", "hz", "abcdefgh", "a"],
|
||||
"abcdefghz",
|
||||
&[(2, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
leftmost390,
|
||||
&["b", "abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(3, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
leftmost400,
|
||||
&["h", "abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(3, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
leftmost410,
|
||||
&["z", "abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(3, 0, 8), (0, 8, 9),]
|
||||
),
|
||||
];
|
||||
|
||||
/// Tests for non-overlapping leftmost-first match semantics. These tests
|
||||
/// should generally be specific to leftmost-first, which means they should
|
||||
/// generally fail under leftmost-longest semantics.
|
||||
const LEFTMOST_FIRST: &'static [SearchTest] = &[
|
||||
t!(leftfirst000, &["ab", "abcd"], "abcd", &[(0, 0, 2)]),
|
||||
t!(leftfirst020, &["abcd", "ab"], "abcd", &[(0, 0, 4)]),
|
||||
t!(leftfirst030, &["ab", "ab"], "abcd", &[(0, 0, 2)]),
|
||||
t!(leftfirst040, &["a", "ab"], "xayabbbz", &[(0, 1, 2), (0, 3, 4)]),
|
||||
t!(leftfirst100, &["abcdefg", "bcde", "bcdef"], "abcdef", &[(1, 1, 5)]),
|
||||
t!(leftfirst110, &["abcdefg", "bcdef", "bcde"], "abcdef", &[(1, 1, 6)]),
|
||||
t!(leftfirst300, &["abcd", "b", "bce"], "abce", &[(1, 1, 2)]),
|
||||
t!(
|
||||
leftfirst310,
|
||||
&["abcd", "b", "bce", "ce"],
|
||||
"abce",
|
||||
&[(1, 1, 2), (3, 2, 4),]
|
||||
),
|
||||
t!(
|
||||
leftfirst320,
|
||||
&["a", "abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(0, 0, 1), (2, 7, 9),]
|
||||
),
|
||||
t!(leftfirst330, &["a", "abab"], "abab", &[(0, 0, 1), (0, 2, 3)]),
|
||||
t!(
|
||||
leftfirst340,
|
||||
&["abcdef", "x", "x", "x", "x", "x", "x", "abcde"],
|
||||
"abcdef",
|
||||
&[(0, 0, 6)]
|
||||
),
|
||||
];
|
||||
|
||||
/// Tests for non-overlapping leftmost-longest match semantics. These tests
|
||||
/// should generally be specific to leftmost-longest, which means they should
|
||||
/// generally fail under leftmost-first semantics.
|
||||
const LEFTMOST_LONGEST: &'static [SearchTest] = &[
|
||||
t!(leftlong000, &["ab", "abcd"], "abcd", &[(1, 0, 4)]),
|
||||
t!(leftlong010, &["abcd", "bcd", "cd", "b"], "abcd", &[(0, 0, 4),]),
|
||||
t!(leftlong040, &["a", "ab"], "a", &[(0, 0, 1)]),
|
||||
t!(leftlong050, &["a", "ab"], "ab", &[(1, 0, 2)]),
|
||||
t!(leftlong060, &["ab", "a"], "a", &[(1, 0, 1)]),
|
||||
t!(leftlong070, &["ab", "a"], "ab", &[(0, 0, 2)]),
|
||||
t!(leftlong100, &["abcdefg", "bcde", "bcdef"], "abcdef", &[(2, 1, 6)]),
|
||||
t!(leftlong110, &["abcdefg", "bcdef", "bcde"], "abcdef", &[(1, 1, 6)]),
|
||||
t!(leftlong300, &["abcd", "b", "bce"], "abce", &[(2, 1, 4)]),
|
||||
t!(
|
||||
leftlong310,
|
||||
&["a", "abcdefghi", "hz", "abcdefgh"],
|
||||
"abcdefghz",
|
||||
&[(3, 0, 8),]
|
||||
),
|
||||
t!(leftlong320, &["a", "abab"], "abab", &[(1, 0, 4)]),
|
||||
t!(leftlong330, &["abcd", "b", "ce"], "abce", &[(1, 1, 2), (2, 2, 4),]),
|
||||
t!(leftlong340, &["a", "ab"], "xayabbbz", &[(0, 1, 2), (1, 3, 5)]),
|
||||
];
|
||||
|
||||
/// Regression tests that are applied to all combinations.
|
||||
///
|
||||
/// If regression tests are needed for specific match semantics, then add them
|
||||
/// to the appropriate group above.
|
||||
const REGRESSION: &'static [SearchTest] = &[
|
||||
t!(regression010, &["inf", "ind"], "infind", &[(0, 0, 3), (1, 3, 6),]),
|
||||
t!(regression020, &["ind", "inf"], "infind", &[(1, 0, 3), (0, 3, 6),]),
|
||||
t!(
|
||||
regression030,
|
||||
&["libcore/", "libstd/"],
|
||||
"libcore/char/methods.rs",
|
||||
&[(0, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
regression040,
|
||||
&["libstd/", "libcore/"],
|
||||
"libcore/char/methods.rs",
|
||||
&[(1, 0, 8),]
|
||||
),
|
||||
t!(
|
||||
regression050,
|
||||
&["\x00\x00\x01", "\x00\x00\x00"],
|
||||
"\x00\x00\x00",
|
||||
&[(1, 0, 3),]
|
||||
),
|
||||
t!(
|
||||
regression060,
|
||||
&["\x00\x00\x00", "\x00\x00\x01"],
|
||||
"\x00\x00\x00",
|
||||
&[(0, 0, 3),]
|
||||
),
|
||||
];
|
||||
|
||||
const TEDDY: &'static [SearchTest] = &[
|
||||
t!(
|
||||
teddy010,
|
||||
&["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"],
|
||||
"abcdefghijk",
|
||||
&[
|
||||
(0, 0, 1),
|
||||
(1, 1, 2),
|
||||
(2, 2, 3),
|
||||
(3, 3, 4),
|
||||
(4, 4, 5),
|
||||
(5, 5, 6),
|
||||
(6, 6, 7),
|
||||
(7, 7, 8),
|
||||
(8, 8, 9),
|
||||
(9, 9, 10),
|
||||
(10, 10, 11)
|
||||
]
|
||||
),
|
||||
t!(
|
||||
teddy020,
|
||||
&["ab", "bc", "cd", "de", "ef", "fg", "gh", "hi", "ij", "jk", "kl"],
|
||||
"abcdefghijk",
|
||||
&[(0, 0, 2), (2, 2, 4), (4, 4, 6), (6, 6, 8), (8, 8, 10),]
|
||||
),
|
||||
t!(
|
||||
teddy030,
|
||||
&["abc"],
|
||||
"abcdefghijklmnopqrstuvwxyzabcdefghijk",
|
||||
&[(0, 0, 3), (0, 26, 29)]
|
||||
),
|
||||
];
|
||||
|
||||
// Now define a test for each combination of things above that we want to run.
|
||||
// Since there are a few different combinations for each collection of tests,
|
||||
// we define a couple of macros to avoid repetition drudgery. The testconfig
|
||||
// macro constructs the automaton from a given match kind, and runs the search
|
||||
// tests one-by-one over the given collection. The `with` parameter allows one
|
||||
// to configure the config with additional parameters. The testcombo macro
|
||||
// invokes testconfig in precisely this way: it sets up several tests where
|
||||
// each one turns a different knob on Config.
|
||||
|
||||
macro_rules! testconfig {
|
||||
($name:ident, $collection:expr, $with:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
run_search_tests($collection, |test| {
|
||||
let mut config = Config::new();
|
||||
$with(&mut config);
|
||||
let mut builder = config.builder();
|
||||
builder.extend(test.patterns.iter().map(|p| p.as_bytes()));
|
||||
let searcher = match builder.build() {
|
||||
Some(searcher) => searcher,
|
||||
None => {
|
||||
// For x86-64 and aarch64, not building a searcher is
|
||||
// probably a bug, so be loud.
|
||||
if cfg!(any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64"
|
||||
)) {
|
||||
panic!("failed to build packed searcher")
|
||||
}
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(searcher.find_iter(&test.haystack).collect())
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testconfig!(
|
||||
search_default_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|_: &mut Config| {}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_default_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.match_kind(MatchKind::LeftmostLongest);
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true);
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true).match_kind(MatchKind::LeftmostLongest);
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_ssse3_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("ssse3") {
|
||||
c.only_teddy_256bit(Some(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_ssse3_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true).match_kind(MatchKind::LeftmostLongest);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("ssse3") {
|
||||
c.only_teddy_256bit(Some(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_avx2_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("avx2") {
|
||||
c.only_teddy_256bit(Some(true));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_avx2_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true).match_kind(MatchKind::LeftmostLongest);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("avx2") {
|
||||
c.only_teddy_256bit(Some(true));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_fat_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("avx2") {
|
||||
c.only_teddy_fat(Some(true));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_teddy_fat_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.only_teddy(true).match_kind(MatchKind::LeftmostLongest);
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if std::is_x86_feature_detected!("avx2") {
|
||||
c.only_teddy_fat(Some(true));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_rabinkarp_leftmost_first,
|
||||
PACKED_LEFTMOST_FIRST,
|
||||
|c: &mut Config| {
|
||||
c.only_rabin_karp(true);
|
||||
}
|
||||
);
|
||||
|
||||
testconfig!(
|
||||
search_rabinkarp_leftmost_longest,
|
||||
PACKED_LEFTMOST_LONGEST,
|
||||
|c: &mut Config| {
|
||||
c.only_rabin_karp(true).match_kind(MatchKind::LeftmostLongest);
|
||||
}
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn search_tests_have_unique_names() {
|
||||
let assert = |constname, tests: &[SearchTest]| {
|
||||
let mut seen = HashMap::new(); // map from test name to position
|
||||
for (i, test) in tests.iter().enumerate() {
|
||||
if !seen.contains_key(test.name) {
|
||||
seen.insert(test.name, i);
|
||||
} else {
|
||||
let last = seen[test.name];
|
||||
panic!(
|
||||
"{} tests have duplicate names at positions {} and {}",
|
||||
constname, last, i
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
assert("BASICS", BASICS);
|
||||
assert("LEFTMOST", LEFTMOST);
|
||||
assert("LEFTMOST_FIRST", LEFTMOST_FIRST);
|
||||
assert("LEFTMOST_LONGEST", LEFTMOST_LONGEST);
|
||||
assert("REGRESSION", REGRESSION);
|
||||
assert("TEDDY", TEDDY);
|
||||
}
|
||||
|
||||
fn run_search_tests<F: FnMut(&SearchTestOwned) -> Option<Vec<Match>>>(
|
||||
which: TestCollection,
|
||||
mut f: F,
|
||||
) {
|
||||
let get_match_triples =
|
||||
|matches: Vec<Match>| -> Vec<(usize, usize, usize)> {
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|m| (m.pattern().as_usize(), m.start(), m.end()))
|
||||
.collect()
|
||||
};
|
||||
for &tests in which {
|
||||
for spec in tests {
|
||||
for test in spec.variations() {
|
||||
let results = match f(&test) {
|
||||
None => continue,
|
||||
Some(results) => results,
|
||||
};
|
||||
assert_eq!(
|
||||
test.matches,
|
||||
get_match_triples(results).as_slice(),
|
||||
"test: {}, patterns: {:?}, haystack(len={:?}): {:?}, \
|
||||
offset: {:?}",
|
||||
test.name,
|
||||
test.patterns,
|
||||
test.haystack.len(),
|
||||
test.haystack,
|
||||
test.offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user