Skip to content

Commit ba3b9ea

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

5 files changed

Lines changed: 584 additions & 180 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
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")]
@@ -233,7 +237,9 @@ impl From<MatchError> for RetryFailError {
233237
// or with higher level control flow logic. For example, the
234238
// backtracker's wrapper will never hand out a backtracker engine
235239
// when the haystack would be too long.
236-
HaystackTooLong { .. } | UnsupportedAnchored { .. } => {
240+
HaystackTooLong { .. }
241+
| UnsupportedAnchored { .. }
242+
| NoEngine { .. } => {
237243
unreachable!("found impossible error in meta engine: {merr}")
238244
}
239245
}

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

Lines changed: 226 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,61 @@ 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. The engines with bounded runtime (the
553+
/// one-pass DFA and the bounded backtracker) still pick those up, and with
554+
/// the PikeVM disabled the backtracker is given every search it can serve
555+
/// rather than only the short haystacks it is normally reserved for. Only
556+
/// once nothing is left to run does this routine report a [`MatchError`],
557+
/// which is to say only when the haystack outgrows the backtracker's
558+
/// visited bitset (see [`Config::backtrack_visited_capacity`]).
559+
///
560+
/// A `Regex` built with the PikeVM enabled (the default) never returns an
561+
/// error here.
562+
///
563+
/// # Example
564+
///
565+
/// ```
566+
/// use regex_automata::meta::Regex;
567+
///
568+
/// let re = Regex::builder()
569+
/// .configure(Regex::config().pikevm(false))
570+
/// .build(r"\b\w+\b")?;
571+
/// let mut cache = re.create_cache();
572+
///
573+
/// assert_eq!(Ok(true), re.try_is_match_with(&mut cache, "quux"));
574+
/// // A Unicode word boundary against a non-ASCII haystack makes the lazy
575+
/// // DFA quit, but the backtracker still answers it.
576+
/// let haystack = "☃".repeat(100);
577+
/// assert_eq!(Ok(false), re.try_is_match_with(&mut cache, &haystack));
578+
/// // Past the backtracker's bound there is no engine left to run.
579+
/// let haystack = "☃".repeat(10_000);
580+
/// assert!(re.try_is_match_with(&mut cache, &haystack).is_err());
581+
///
582+
/// # Ok::<(), Box<dyn std::error::Error>>(())
583+
/// ```
584+
#[inline]
585+
pub fn try_is_match_with<'h, I: Into<Input<'h>>>(
586+
&self,
587+
cache: &mut Cache,
588+
input: I,
589+
) -> Result<bool, MatchError> {
590+
let input = input.into().earliest(true);
591+
if self.imp.info.is_impossible(&input) {
592+
return Ok(false);
593+
}
594+
self.imp
595+
.strat
596+
.try_is_match(cache, &input)
597+
.map_err(|err| MatchError::no_engine(err.offset()))
598+
}
599+
543600
/// Executes a leftmost search and returns the first match that is found,
544601
/// if one exists.
545602
///
@@ -1257,6 +1314,30 @@ impl Regex {
12571314
self.imp.strat.search(cache, input)
12581315
}
12591316

1317+
/// This is like [`Regex::search_with`], but returns an error instead of
1318+
/// falling back to the PikeVM.
1319+
///
1320+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1321+
/// once the DFAs and the bounded engines have all given up on a search.
1322+
/// This routine reports that as a [`MatchError`], where the infallible
1323+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1324+
/// never returns an error here.
1325+
pub fn try_search_with(
1326+
&self,
1327+
cache: &mut Cache,
1328+
input: &Input<'_>,
1329+
) -> Result<Option<Match>, MatchError> {
1330+
if self.imp.info.captures_disabled()
1331+
|| self.imp.info.is_impossible(input)
1332+
{
1333+
return Ok(None);
1334+
}
1335+
self.imp
1336+
.strat
1337+
.try_search(cache, input)
1338+
.map_err(|err| MatchError::no_engine(err.offset()))
1339+
}
1340+
12601341
/// This is like [`Regex::search_half`], but requires the caller to
12611342
/// explicitly pass a [`Cache`].
12621343
///
@@ -1301,6 +1382,30 @@ impl Regex {
13011382
self.imp.strat.search_half(cache, input)
13021383
}
13031384

1385+
/// This is like [`Regex::search_half_with`], but returns an error instead of
1386+
/// falling back to the PikeVM.
1387+
///
1388+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1389+
/// once the DFAs and the bounded engines have all given up on a search.
1390+
/// This routine reports that as a [`MatchError`], where the infallible
1391+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1392+
/// never returns an error here.
1393+
pub fn try_search_half_with(
1394+
&self,
1395+
cache: &mut Cache,
1396+
input: &Input<'_>,
1397+
) -> Result<Option<HalfMatch>, MatchError> {
1398+
if self.imp.info.captures_disabled()
1399+
|| self.imp.info.is_impossible(input)
1400+
{
1401+
return Ok(None);
1402+
}
1403+
self.imp
1404+
.strat
1405+
.try_search_half(cache, input)
1406+
.map_err(|err| MatchError::no_engine(err.offset()))
1407+
}
1408+
13041409
/// This is like [`Regex::search_captures`], but requires the caller to
13051410
/// explicitly pass a [`Cache`].
13061411
///
@@ -1394,6 +1499,27 @@ impl Regex {
13941499
caps.set_pattern(pid);
13951500
}
13961501

1502+
/// This is like [`Regex::search_captures_with`], but returns an error instead of
1503+
/// falling back to the PikeVM.
1504+
///
1505+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1506+
/// once the DFAs and the bounded engines have all given up on a search.
1507+
/// This routine reports that as a [`MatchError`], where the infallible
1508+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1509+
/// never returns an error here.
1510+
pub fn try_search_captures_with(
1511+
&self,
1512+
cache: &mut Cache,
1513+
input: &Input<'_>,
1514+
caps: &mut Captures,
1515+
) -> Result<(), MatchError> {
1516+
caps.set_pattern(None);
1517+
let pid =
1518+
self.try_search_slots_with(cache, input, caps.slots_mut())?;
1519+
caps.set_pattern(pid);
1520+
Ok(())
1521+
}
1522+
13971523
/// This is like [`Regex::search_slots`], but requires the caller to
13981524
/// explicitly pass a [`Cache`].
13991525
///
@@ -1456,6 +1582,31 @@ impl Regex {
14561582
self.imp.strat.search_slots(cache, input, slots)
14571583
}
14581584

1585+
/// This is like [`Regex::search_slots_with`], but returns an error instead of
1586+
/// falling back to the PikeVM.
1587+
///
1588+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1589+
/// once the DFAs and the bounded engines have all given up on a search.
1590+
/// This routine reports that as a [`MatchError`], where the infallible
1591+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1592+
/// never returns an error here.
1593+
pub fn try_search_slots_with(
1594+
&self,
1595+
cache: &mut Cache,
1596+
input: &Input<'_>,
1597+
slots: &mut [Option<NonMaxUsize>],
1598+
) -> Result<Option<PatternID>, MatchError> {
1599+
if self.imp.info.captures_disabled()
1600+
|| self.imp.info.is_impossible(input)
1601+
{
1602+
return Ok(None);
1603+
}
1604+
self.imp
1605+
.strat
1606+
.try_search_slots(cache, input, slots)
1607+
.map_err(|err| MatchError::no_engine(err.offset()))
1608+
}
1609+
14591610
/// This is like [`Regex::which_overlapping_matches`], but requires the
14601611
/// caller to explicitly pass a [`Cache`].
14611612
///
@@ -1504,6 +1655,29 @@ impl Regex {
15041655
}
15051656
self.imp.strat.which_overlapping_matches(cache, input, patset)
15061657
}
1658+
1659+
/// This is like [`Regex::which_overlapping_matches_with`], but returns an error instead of
1660+
/// falling back to the PikeVM.
1661+
///
1662+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1663+
/// once the DFAs and the bounded engines have all given up on a search.
1664+
/// This routine reports that as a [`MatchError`], where the infallible
1665+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1666+
/// never returns an error here.
1667+
pub fn try_which_overlapping_matches_with(
1668+
&self,
1669+
cache: &mut Cache,
1670+
input: &Input<'_>,
1671+
patset: &mut PatternSet,
1672+
) -> Result<(), MatchError> {
1673+
if self.imp.info.is_impossible(input) {
1674+
return Ok(());
1675+
}
1676+
self.imp
1677+
.strat
1678+
.try_which_overlapping_matches(cache, input, patset)
1679+
.map_err(|err| MatchError::no_engine(err.offset()))
1680+
}
15071681
}
15081682

15091683
/// Various non-search routines for querying properties of a `Regex` and
@@ -2481,6 +2655,8 @@ pub struct Config {
24812655
dfa_state_limit: Option<Option<usize>>,
24822656
onepass: Option<bool>,
24832657
backtrack: Option<bool>,
2658+
backtrack_visited_capacity: Option<usize>,
2659+
pikevm: Option<bool>,
24842660
byte_classes: Option<bool>,
24852661
line_terminator: Option<u8>,
24862662
}
@@ -3097,6 +3273,39 @@ impl Config {
30973273
Config { backtrack: Some(yes), ..self }
30983274
}
30993275

3276+
/// Sets the visited capacity, in bytes, used by the bounded backtracker.
3277+
///
3278+
/// The backtracker refuses to run when the haystack is longer than its
3279+
/// visited bitset can track, which is roughly this capacity divided by the
3280+
/// number of NFA states. Raising it lets the backtracker serve longer
3281+
/// haystacks, which matters when [`Config::pikevm`] is disabled and the
3282+
/// backtracker is the last engine standing.
3283+
///
3284+
/// When unset, the backtracker's own default is used.
3285+
pub fn backtrack_visited_capacity(self, capacity: usize) -> Config {
3286+
Config { backtrack_visited_capacity: Some(capacity), ..self }
3287+
}
3288+
3289+
/// Whether to permit the use of the PikeVM.
3290+
///
3291+
/// The PikeVM is the meta regex engine's engine of last resort: it can
3292+
/// handle any regex against any haystack, but its NFA simulation can be
3293+
/// orders of magnitude slower than the DFAs. It is what makes the
3294+
/// infallible search routines infallible.
3295+
///
3296+
/// Disabling it turns this into a regex engine that can fail: a search
3297+
/// that the faster engines give up on has nothing left to run it. The
3298+
/// `try_` search routines report that condition as a [`MatchError`] with
3299+
/// a [`MatchErrorKind::NoEngine`](crate::MatchErrorKind::NoEngine) kind,
3300+
/// while the infallible routines panic. This is useful for rejecting a
3301+
/// pathological regex outright rather than paying for it.
3302+
///
3303+
/// This is enabled by default, and when it is, no search routine can
3304+
/// fail or panic.
3305+
pub fn pikevm(self, yes: bool) -> Config {
3306+
Config { pikevm: Some(yes), ..self }
3307+
}
3308+
31003309
/// Returns the match kind on this configuration, as set by
31013310
/// [`Config::match_kind`].
31023311
///
@@ -3270,6 +3479,18 @@ impl Config {
32703479
}
32713480
}
32723481

3482+
/// Returns the backtracker's visited capacity, as set by
3483+
/// [`Config::backtrack_visited_capacity`]. `None` means the backtracker's
3484+
/// own default is used.
3485+
pub fn get_backtrack_visited_capacity(&self) -> Option<usize> {
3486+
self.backtrack_visited_capacity
3487+
}
3488+
3489+
/// Returns whether the PikeVM may be used, as set by [`Config::pikevm`].
3490+
pub fn get_pikevm(&self) -> bool {
3491+
self.pikevm.unwrap_or(true)
3492+
}
3493+
32733494
/// Returns a "baseline" Thompson configuration for constructing NFAs based
32743495
/// on this configuration.
32753496
///
@@ -3315,6 +3536,10 @@ impl Config {
33153536
dfa_state_limit: o.dfa_state_limit.or(self.dfa_state_limit),
33163537
onepass: o.onepass.or(self.onepass),
33173538
backtrack: o.backtrack.or(self.backtrack),
3539+
backtrack_visited_capacity: o
3540+
.backtrack_visited_capacity
3541+
.or(self.backtrack_visited_capacity),
3542+
pikevm: o.pikevm.or(self.pikevm),
33183543
byte_classes: o.byte_classes.or(self.byte_classes),
33193544
line_terminator: o.line_terminator.or(self.line_terminator),
33203545
}

0 commit comments

Comments
 (0)