Skip to content

Commit f1a48fc

Browse files
committed
automata: make the PikeVM optional in the meta engine
1 parent 2b52759 commit f1a48fc

4 files changed

Lines changed: 172 additions & 40 deletions

File tree

‎regex-automata/src/meta/error.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ impl RetryFailError {
205205
pub(crate) fn from_offset(offset: usize) -> RetryFailError {
206206
RetryFailError { offset }
207207
}
208+
209+
pub(crate) fn offset(&self) -> usize {
210+
self.offset
211+
}
208212
}
209213

210214
#[cfg(feature = "std")]

‎regex-automata/src/meta/regex.rs‎

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ use crate::{
2424
pool::{Pool, PoolGuard},
2525
prefilter::Prefilter,
2626
primitives::{NonMaxUsize, PatternID},
27-
search::{HalfMatch, Input, Match, MatchKind, PatternSet, Span},
27+
search::{
28+
HalfMatch, Input, Match, MatchError, MatchKind, PatternSet, Span,
29+
},
2830
},
2931
};
3032

@@ -540,6 +542,59 @@ impl Regex {
540542
result
541543
}
542544

545+
/// Executes a leftmost is-match search, returning an error instead of
546+
/// falling back to the PikeVM.
547+
///
548+
/// This is like [`Regex::is_match`], but for a `Regex` built with
549+
/// [`Config::pikevm`] disabled. The meta regex engine's fast engines (the
550+
/// DFAs) can give up on a search, either because a lazy DFA's cache is
551+
/// being used ineffectively or because a Unicode word boundary was seen
552+
/// alongside a non-ASCII byte. Without the PikeVM to fall back to, such a
553+
/// search has no engine left to run, and this routine reports that as a
554+
/// [`MatchError`] rather than paying for the PikeVM's NFA simulation.
555+
/// Fallbacks with bounded runtime (the one-pass DFA and the bounded
556+
/// backtracker) are still used when the meta engine would otherwise
557+
/// choose them.
558+
///
559+
/// A `Regex` built with the PikeVM enabled (the default) never returns an
560+
/// error here.
561+
///
562+
/// This is a `topk-io/regex` fork addition.
563+
///
564+
/// # Example
565+
///
566+
/// ```
567+
/// use regex_automata::meta::Regex;
568+
///
569+
/// let re = Regex::builder()
570+
/// .configure(Regex::config().pikevm(false))
571+
/// .build(r"\b\w+\b")?;
572+
/// let mut cache = re.create_cache();
573+
///
574+
/// assert_eq!(Ok(true), re.try_is_match_with(&mut cache, "quux"));
575+
/// // A Unicode word boundary against a long non-ASCII haystack makes the
576+
/// // lazy DFA quit, and the haystack is too long for the backtracker.
577+
/// let haystack = "☃".repeat(100);
578+
/// assert!(re.try_is_match_with(&mut cache, &haystack).is_err());
579+
///
580+
/// # Ok::<(), Box<dyn std::error::Error>>(())
581+
/// ```
582+
#[inline]
583+
pub fn try_is_match_with<'h, I: Into<Input<'h>>>(
584+
&self,
585+
cache: &mut Cache,
586+
input: I,
587+
) -> Result<bool, MatchError> {
588+
let input = input.into().earliest(true);
589+
if self.imp.info.is_impossible(&input) {
590+
return Ok(false);
591+
}
592+
self.imp
593+
.strat
594+
.try_is_match(cache, &input)
595+
.map_err(|err| MatchError::gave_up(err.offset()))
596+
}
597+
543598
/// Executes a leftmost search and returns the first match that is found,
544599
/// if one exists.
545600
///
@@ -2481,6 +2536,7 @@ pub struct Config {
24812536
dfa_state_limit: Option<Option<usize>>,
24822537
onepass: Option<bool>,
24832538
backtrack: Option<bool>,
2539+
pikevm: Option<bool>,
24842540
byte_classes: Option<bool>,
24852541
line_terminator: Option<u8>,
24862542
}
@@ -3097,6 +3153,26 @@ impl Config {
30973153
Config { backtrack: Some(yes), ..self }
30983154
}
30993155

3156+
/// Whether to permit the use of the PikeVM.
3157+
///
3158+
/// The PikeVM is the meta regex engine's engine of last resort: it can
3159+
/// handle any regex against any haystack, but its NFA simulation can be
3160+
/// orders of magnitude slower than the DFAs. Disabling it means a search
3161+
/// that the faster engines give up on has nothing left to fall back to.
3162+
/// Only [`Regex::try_is_match_with`] can report that condition, by
3163+
/// returning a [`MatchError`]; the other search routines require the
3164+
/// PikeVM and will panic if it is disabled.
3165+
///
3166+
/// This is useful for rejecting a pathological regex outright rather than
3167+
/// paying for it.
3168+
///
3169+
/// This is enabled by default.
3170+
///
3171+
/// This is a `topk-io/regex` fork addition.
3172+
pub fn pikevm(self, yes: bool) -> Config {
3173+
Config { pikevm: Some(yes), ..self }
3174+
}
3175+
31003176
/// Returns the match kind on this configuration, as set by
31013177
/// [`Config::match_kind`].
31023178
///
@@ -3270,6 +3346,13 @@ impl Config {
32703346
}
32713347
}
32723348

3349+
/// Returns whether the PikeVM may be used, as set by [`Config::pikevm`].
3350+
///
3351+
/// This is a `topk-io/regex` fork addition.
3352+
pub fn get_pikevm(&self) -> bool {
3353+
self.pikevm.unwrap_or(true)
3354+
}
3355+
32733356
/// Returns a "baseline" Thompson configuration for constructing NFAs based
32743357
/// on this configuration.
32753358
///
@@ -3315,6 +3398,7 @@ impl Config {
33153398
dfa_state_limit: o.dfa_state_limit.or(self.dfa_state_limit),
33163399
onepass: o.onepass.or(self.onepass),
33173400
backtrack: o.backtrack.or(self.backtrack),
3401+
pikevm: o.pikevm.or(self.pikevm),
33183402
byte_classes: o.byte_classes.or(self.byte_classes),
33193403
line_terminator: o.line_terminator.or(self.line_terminator),
33203404
}

‎regex-automata/src/meta/strategy.rs‎

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,18 @@ pub(super) trait Strategy:
6161
input: &Input<'_>,
6262
) -> Option<HalfMatch>;
6363

64-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool;
64+
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
65+
self.try_is_match(cache, input)
66+
.expect("PikeVM is disabled, use Regex::try_is_match_with")
67+
}
68+
69+
// Returns an error only when the search needed the PikeVM but it was
70+
// disabled via 'Config::pikevm'.
71+
fn try_is_match(
72+
&self,
73+
cache: &mut Cache,
74+
input: &Input<'_>,
75+
) -> Result<bool, RetryFailError>;
6576

6677
fn search_slots(
6778
&self,
@@ -414,8 +425,12 @@ impl<P: PrefilterI> Strategy for Pre<P> {
414425
}
415426

416427
#[cfg_attr(feature = "perf-inline", inline(always))]
417-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
418-
self.search(cache, input).is_some()
428+
fn try_is_match(
429+
&self,
430+
cache: &mut Cache,
431+
input: &Input<'_>,
432+
) -> Result<bool, RetryFailError> {
433+
Ok(self.search(cache, input).is_some())
419434
}
420435

421436
#[cfg_attr(feature = "perf-inline", inline(always))]
@@ -590,7 +605,7 @@ impl Core {
590605
e.search_slots(&mut cache.backtrack, input, caps.slots_mut())
591606
} else {
592607
trace!("using PikeVM for search at {:?}", input.get_span());
593-
let e = self.pikevm.get();
608+
let e = self.pikevm.get().expect("PikeVM is required");
594609
e.search_slots(&mut cache.pikevm, input, caps.slots_mut())
595610
};
596611
caps.set_pattern(pid);
@@ -633,31 +648,38 @@ impl Core {
633648
"using PikeVM for capture search at {:?}",
634649
input.get_span()
635650
);
636-
let e = self.pikevm.get();
651+
let e = self.pikevm.get().expect("PikeVM is required");
637652
e.search_slots(&mut cache.pikevm, input, slots)
638653
}
639654
}
640655

641-
fn is_match_nofail(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
656+
fn try_is_match_nofail(
657+
&self,
658+
cache: &mut Cache,
659+
input: &Input<'_>,
660+
) -> Result<bool, RetryFailError> {
642661
if let Some(ref e) = self.onepass.get(input) {
643662
trace!(
644663
"using OnePass for is-match search at {:?}",
645664
input.get_span()
646665
);
647-
e.search_slots(&mut cache.onepass, input, &mut []).is_some()
666+
Ok(e.search_slots(&mut cache.onepass, input, &mut []).is_some())
648667
} else if let Some(ref e) = self.backtrack.get(input) {
649668
trace!(
650669
"using BoundedBacktracker for is-match search at {:?}",
651670
input.get_span()
652671
);
653-
e.is_match(&mut cache.backtrack, input)
672+
Ok(e.is_match(&mut cache.backtrack, input))
654673
} else {
655674
trace!(
656675
"using PikeVM for is-match search at {:?}",
657676
input.get_span()
658677
);
659-
let e = self.pikevm.get();
660-
e.is_match(&mut cache.pikevm, input)
678+
let e = self
679+
.pikevm
680+
.get()
681+
.ok_or_else(|| RetryFailError::from_offset(input.start()))?;
682+
Ok(e.is_match(&mut cache.pikevm, input))
661683
}
662684
}
663685

@@ -769,17 +791,21 @@ impl Strategy for Core {
769791
}
770792

771793
#[cfg_attr(feature = "perf-inline", inline(always))]
772-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
794+
fn try_is_match(
795+
&self,
796+
cache: &mut Cache,
797+
input: &Input<'_>,
798+
) -> Result<bool, RetryFailError> {
773799
if let Some(e) = self.dfa.get(input) {
774800
trace!(
775801
"using full DFA for is-match search at {:?}",
776802
input.get_span()
777803
);
778804
match e.try_search_half_fwd(input) {
779-
Ok(x) => x.is_some(),
805+
Ok(x) => Ok(x.is_some()),
780806
Err(_err) => {
781807
trace!("full DFA half search failed: {_err}");
782-
self.is_match_nofail(cache, input)
808+
self.try_is_match_nofail(cache, input)
783809
}
784810
}
785811
} else if let Some(e) = self.hybrid.get(input) {
@@ -788,14 +814,14 @@ impl Strategy for Core {
788814
input.get_span()
789815
);
790816
match e.try_search_half_fwd(&mut cache.hybrid, input) {
791-
Ok(x) => x.is_some(),
817+
Ok(x) => Ok(x.is_some()),
792818
Err(_err) => {
793819
trace!("lazy DFA half search failed: {_err}");
794-
self.is_match_nofail(cache, input)
820+
self.try_is_match_nofail(cache, input)
795821
}
796822
}
797823
} else {
798-
self.is_match_nofail(cache, input)
824+
self.try_is_match_nofail(cache, input)
799825
}
800826
}
801827

@@ -902,7 +928,7 @@ impl Strategy for Core {
902928
"using PikeVM for overlapping search at {:?}",
903929
input.get_span()
904930
);
905-
let e = self.pikevm.get();
931+
let e = self.pikevm.get().expect("PikeVM is required");
906932
e.which_overlapping_matches(&mut cache.pikevm, input, patset)
907933
}
908934
}
@@ -1061,17 +1087,21 @@ impl Strategy for ReverseAnchored {
10611087
}
10621088

10631089
#[cfg_attr(feature = "perf-inline", inline(always))]
1064-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
1090+
fn try_is_match(
1091+
&self,
1092+
cache: &mut Cache,
1093+
input: &Input<'_>,
1094+
) -> Result<bool, RetryFailError> {
10651095
if input.get_anchored().is_anchored() {
1066-
return self.core.is_match(cache, input);
1096+
return self.core.try_is_match(cache, input);
10671097
}
10681098
match self.try_search_half_anchored_rev(cache, input) {
10691099
Err(_err) => {
10701100
trace!("fast reverse anchored search failed: {_err}");
1071-
self.core.is_match_nofail(cache, input)
1101+
self.core.try_is_match_nofail(cache, input)
10721102
}
1073-
Ok(None) => false,
1074-
Ok(Some(_)) => true,
1103+
Ok(None) => Ok(false),
1104+
Ok(Some(_)) => Ok(true),
10751105
}
10761106
}
10771107

@@ -1446,23 +1476,27 @@ impl Strategy for ReverseSuffix {
14461476
}
14471477

14481478
#[cfg_attr(feature = "perf-inline", inline(always))]
1449-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
1479+
fn try_is_match(
1480+
&self,
1481+
cache: &mut Cache,
1482+
input: &Input<'_>,
1483+
) -> Result<bool, RetryFailError> {
14501484
if input.get_anchored().is_anchored() {
1451-
return self.core.is_match(cache, input);
1485+
return self.core.try_is_match(cache, input);
14521486
}
14531487
match self.try_search_half_start(cache, input) {
14541488
Err(RetryError::Quadratic(_err)) => {
14551489
trace!("reverse suffix half optimization failed: {_err}");
1456-
self.core.is_match_nofail(cache, input)
1490+
self.core.try_is_match_nofail(cache, input)
14571491
}
14581492
Err(RetryError::Fail(_err)) => {
14591493
trace!(
14601494
"reverse suffix reverse fast half search failed: {_err}"
14611495
);
1462-
self.core.is_match_nofail(cache, input)
1496+
self.core.try_is_match_nofail(cache, input)
14631497
}
1464-
Ok(None) => false,
1465-
Ok(Some(_)) => true,
1498+
Ok(None) => Ok(false),
1499+
Ok(Some(_)) => Ok(true),
14661500
}
14671501
}
14681502

@@ -1851,21 +1885,25 @@ impl Strategy for ReverseInner {
18511885
}
18521886

18531887
#[cfg_attr(feature = "perf-inline", inline(always))]
1854-
fn is_match(&self, cache: &mut Cache, input: &Input<'_>) -> bool {
1888+
fn try_is_match(
1889+
&self,
1890+
cache: &mut Cache,
1891+
input: &Input<'_>,
1892+
) -> Result<bool, RetryFailError> {
18551893
if input.get_anchored().is_anchored() {
1856-
return self.core.is_match(cache, input);
1894+
return self.core.try_is_match(cache, input);
18571895
}
18581896
match self.try_search_full(cache, input) {
18591897
Err(RetryError::Quadratic(_err)) => {
18601898
trace!("reverse inner half optimization failed: {_err}");
1861-
self.core.is_match_nofail(cache, input)
1899+
self.core.try_is_match_nofail(cache, input)
18621900
}
18631901
Err(RetryError::Fail(_err)) => {
18641902
trace!("reverse inner fast half search failed: {_err}");
1865-
self.core.is_match_nofail(cache, input)
1903+
self.core.try_is_match_nofail(cache, input)
18661904
}
1867-
Ok(None) => false,
1868-
Ok(Some(_)) => true,
1905+
Ok(None) => Ok(false),
1906+
Ok(Some(_)) => Ok(true),
18691907
}
18701908
}
18711909

0 commit comments

Comments
 (0)