regex_automata/hybrid/search.rs
1use crate::{
2 hybrid::{
3 dfa::{Cache, OverlappingState, DFA},
4 id::LazyStateID,
5 },
6 util::{
7 prefilter::Prefilter,
8 search::{HalfMatch, Input, MatchError, Span},
9 },
10};
11
12#[inline(never)]
13pub(crate) fn find_fwd(
14 dfa: &DFA,
15 cache: &mut Cache,
16 input: &Input<'_>,
17) -> Result<Option<HalfMatch>, MatchError> {
18 if input.is_done() {
19 return Ok(None);
20 }
21 let pre = if input.get_anchored().is_anchored() {
22 None
23 } else {
24 dfa.get_config().get_prefilter()
25 };
26 // So what we do here is specialize four different versions of 'find_fwd':
27 // one for each of the combinations for 'has prefilter' and 'is earliest
28 // search'. The reason for doing this is that both of these things require
29 // branches and special handling in some code that can be very hot,
30 // and shaving off as much as we can when we don't need it tends to be
31 // beneficial in ad hoc benchmarks. To see these differences, you often
32 // need a query with a high match count. In other words, specializing these
33 // four routines *tends* to help latency more than throughput.
34 if pre.is_some() {
35 if input.get_earliest() {
36 find_fwd_imp(dfa, cache, input, pre, true)
37 } else {
38 find_fwd_imp(dfa, cache, input, pre, false)
39 }
40 } else {
41 if input.get_earliest() {
42 find_fwd_imp(dfa, cache, input, None, true)
43 } else {
44 find_fwd_imp(dfa, cache, input, None, false)
45 }
46 }
47}
48
49#[cfg_attr(feature = "perf-inline", inline(always))]
50fn find_fwd_imp(
51 dfa: &DFA,
52 cache: &mut Cache,
53 input: &Input<'_>,
54 pre: Option<&'_ Prefilter>,
55 earliest: bool,
56) -> Result<Option<HalfMatch>, MatchError> {
57 // See 'prefilter_restart' docs for explanation.
58 let universal_start = dfa.get_nfa().look_set_prefix_any().is_empty();
59 let mut mat = None;
60 let mut sid = init_fwd(dfa, cache, input)?;
61 let mut at = input.start();
62 // This could just be a closure, but then I think it would be unsound
63 // because it would need to be safe to invoke. This way, the lack of safety
64 // is clearer in the code below.
65 macro_rules! next_unchecked {
66 ($sid:expr, $at:expr) => {{
67 let byte = *input.haystack().get_unchecked($at);
68 dfa.next_state_untagged_unchecked(cache, $sid, byte)
69 }};
70 }
71
72 if let Some(ref pre) = pre {
73 let span = Span::from(at..input.end());
74 match pre.find(input.haystack(), span) {
75 None => return Ok(mat),
76 Some(ref span) => {
77 at = span.start;
78 if !universal_start {
79 sid = prefilter_restart(dfa, cache, &input, at)?;
80 }
81 }
82 }
83 }
84 cache.search_start(at);
85 while at < input.end() {
86 if sid.is_tagged() {
87 cache.search_update(at);
88 sid = dfa
89 .next_state(cache, sid, input.haystack()[at])
90 .map_err(|_| gave_up(at))?;
91 } else {
92 // SAFETY: There are two safety invariants we need to uphold
93 // here in the loops below: that 'sid' and 'prev_sid' are valid
94 // state IDs for this DFA, and that 'at' is a valid index into
95 // 'haystack'. For the former, we rely on the invariant that
96 // next_state* and start_state_forward always returns a valid state
97 // ID (given a valid state ID in the former case), and that we are
98 // only at this place in the code if 'sid' is untagged. Moreover,
99 // every call to next_state_untagged_unchecked below is guarded by
100 // a check that sid is untagged. For the latter safety invariant,
101 // we always guard unchecked access with a check that 'at' is less
102 // than 'end', where 'end <= haystack.len()'. In the unrolled loop
103 // below, we ensure that 'at' is always in bounds.
104 //
105 // PERF: For justification of omitting bounds checks, it gives us a
106 // ~10% bump in search time. This was used for a benchmark:
107 //
108 // regex-cli find half hybrid -p '(?m)^.+$' -UBb bigfile
109 //
110 // PERF: For justification for the loop unrolling, we use a few
111 // different tests:
112 //
113 // regex-cli find half hybrid -p '\w{50}' -UBb bigfile
114 // regex-cli find half hybrid -p '(?m)^.+$' -UBb bigfile
115 // regex-cli find half hybrid -p 'ZQZQZQZQ' -UBb bigfile
116 //
117 // And there are three different configurations:
118 //
119 // nounroll: this entire 'else' block vanishes and we just
120 // always use 'dfa.next_state(..)'.
121 // unroll1: just the outer loop below
122 // unroll2: just the inner loop below
123 // unroll3: both the outer and inner loops below
124 //
125 // This results in a matrix of timings for each of the above
126 // regexes with each of the above unrolling configurations:
127 //
128 // '\w{50}' '(?m)^.+$' 'ZQZQZQZQ'
129 // nounroll 1.51s 2.34s 1.51s
130 // unroll1 1.53s 2.32s 1.56s
131 // unroll2 2.22s 1.50s 0.61s
132 // unroll3 1.67s 1.45s 0.61s
133 //
134 // Ideally we'd be able to find a configuration that yields the
135 // best time for all regexes, but alas we settle for unroll3 that
136 // gives us *almost* the best for '\w{50}' and the best for the
137 // other two regexes.
138 //
139 // So what exactly is going on here? The first unrolling (grouping
140 // together runs of untagged transitions) specifically targets
141 // our choice of representation. The second unrolling (grouping
142 // together runs of self-transitions) specifically targets a common
143 // DFA topology. Let's dig in a little bit by looking at our
144 // regexes:
145 //
146 // '\w{50}': This regex spends a lot of time outside of the DFA's
147 // start state matching some part of the '\w' repetition. This
148 // means that it's a bit of a worst case for loop unrolling that
149 // targets self-transitions since the self-transitions in '\w{50}'
150 // are not particularly active for this haystack. However, the
151 // first unrolling (grouping together untagged transitions)
152 // does apply quite well here since very few transitions hit
153 // match/dead/quit/unknown states. It is however worth mentioning
154 // that if start states are configured to be tagged (which you
155 // typically want to do if you have a prefilter), then this regex
156 // actually slows way down because it is constantly ping-ponging
157 // out of the unrolled loop and into the handling of a tagged start
158 // state below. But when start states aren't tagged, the unrolled
159 // loop stays hot. (This is why it's imperative that start state
160 // tagging be disabled when there isn't a prefilter!)
161 //
162 // '(?m)^.+$': There are two important aspects of this regex: 1)
163 // on this haystack, its match count is very high, much higher
164 // than the other two regex and 2) it spends the vast majority
165 // of its time matching '.+'. Since Unicode mode is disabled,
166 // this corresponds to repeatedly following self transitions for
167 // the vast majority of the input. This does benefit from the
168 // untagged unrolling since most of the transitions will be to
169 // untagged states, but the untagged unrolling does more work than
170 // what is actually required. Namely, it has to keep track of the
171 // previous and next state IDs, which I guess requires a bit more
172 // shuffling. This is supported by the fact that nounroll+unroll1
173 // are both slower than unroll2+unroll3, where the latter has a
174 // loop unrolling that specifically targets self-transitions.
175 //
176 // 'ZQZQZQZQ': This one is very similar to '(?m)^.+$' because it
177 // spends the vast majority of its time in self-transitions for
178 // the (implicit) unanchored prefix. The main difference with
179 // '(?m)^.+$' is that it has a much lower match count. So there
180 // isn't much time spent in the overhead of reporting matches. This
181 // is the primary explainer in the perf difference here. We include
182 // this regex and the former to make sure we have comparison points
183 // with high and low match counts.
184 //
185 // NOTE: I used 'OpenSubtitles2018.raw.sample.en' for 'bigfile'.
186 //
187 // NOTE: In a follow-up, it turns out that the "inner" loop
188 // mentioned above was a pretty big pessimization in some other
189 // cases. Namely, it resulted in too much ping-ponging into and out
190 // of the loop, which resulted in nearly ~2x regressions in search
191 // time when compared to the originally lazy DFA in the regex crate.
192 // So I've removed the second loop unrolling that targets the
193 // self-transition case.
194 let mut prev_sid = sid;
195 while at < input.end() {
196 prev_sid = unsafe { next_unchecked!(sid, at) };
197 if prev_sid.is_tagged() || at + 3 >= input.end() {
198 core::mem::swap(&mut prev_sid, &mut sid);
199 break;
200 }
201 at += 1;
202
203 sid = unsafe { next_unchecked!(prev_sid, at) };
204 if sid.is_tagged() {
205 break;
206 }
207 at += 1;
208
209 prev_sid = unsafe { next_unchecked!(sid, at) };
210 if prev_sid.is_tagged() {
211 core::mem::swap(&mut prev_sid, &mut sid);
212 break;
213 }
214 at += 1;
215
216 sid = unsafe { next_unchecked!(prev_sid, at) };
217 if sid.is_tagged() {
218 break;
219 }
220 at += 1;
221 }
222 // If we quit out of the code above with an unknown state ID at
223 // any point, then we need to re-compute that transition using
224 // 'next_state', which will do NFA powerset construction for us.
225 if sid.is_unknown() {
226 cache.search_update(at);
227 sid = dfa
228 .next_state(cache, prev_sid, input.haystack()[at])
229 .map_err(|_| gave_up(at))?;
230 }
231 }
232 if sid.is_tagged() {
233 if sid.is_start() {
234 // A prefilter finds possible starts of new matches. Once a
235 // match is pending, the DFA may still be resolving its
236 // priority. Skipping ahead here could discard that match and
237 // replace it with a later one.
238 if let Some(ref pre) = pre {
239 if mat.is_none() {
240 let span = Span::from(at..input.end());
241 match pre.find(input.haystack(), span) {
242 None => {
243 cache.search_finish(span.end);
244 return Ok(mat);
245 }
246 Some(ref span) => {
247 // We want to skip any update to 'at' below
248 // at the end of this iteration and just
249 // jump immediately back to the next state
250 // transition at the leading position of the
251 // candidate match.
252 //
253 // ... but only if we actually made progress
254 // with our prefilter, otherwise if the start
255 // state has a self-loop, we can get stuck.
256 if span.start > at {
257 at = span.start;
258 if !universal_start {
259 sid = prefilter_restart(
260 dfa, cache, &input, at,
261 )?;
262 }
263 continue;
264 }
265 }
266 }
267 }
268 }
269 } else if sid.is_match() {
270 let pattern = dfa.match_pattern(cache, sid, 0);
271 // Since slice ranges are inclusive at the beginning and
272 // exclusive at the end, and since forward searches report
273 // the end, we can return 'at' as-is. This only works because
274 // matches are delayed by 1 byte. So by the time we observe a
275 // match, 'at' has already been set to 1 byte past the actual
276 // match location, which is precisely the exclusive ending
277 // bound of the match.
278 mat = Some(HalfMatch::new(pattern, at));
279 if earliest {
280 cache.search_finish(at);
281 return Ok(mat);
282 }
283 } else if sid.is_dead() {
284 cache.search_finish(at);
285 return Ok(mat);
286 } else if sid.is_quit() {
287 cache.search_finish(at);
288 return Err(MatchError::quit(input.haystack()[at], at));
289 } else {
290 debug_assert!(sid.is_unknown());
291 unreachable!("sid being unknown is a bug");
292 }
293 }
294 at += 1;
295 }
296 eoi_fwd(dfa, cache, input, &mut sid, &mut mat)?;
297 cache.search_finish(input.end());
298 Ok(mat)
299}
300
301#[inline(never)]
302pub(crate) fn find_rev(
303 dfa: &DFA,
304 cache: &mut Cache,
305 input: &Input<'_>,
306) -> Result<Option<HalfMatch>, MatchError> {
307 if input.is_done() {
308 return Ok(None);
309 }
310 if input.get_earliest() {
311 find_rev_imp(dfa, cache, input, true)
312 } else {
313 find_rev_imp(dfa, cache, input, false)
314 }
315}
316
317#[cfg_attr(feature = "perf-inline", inline(always))]
318fn find_rev_imp(
319 dfa: &DFA,
320 cache: &mut Cache,
321 input: &Input<'_>,
322 earliest: bool,
323) -> Result<Option<HalfMatch>, MatchError> {
324 let mut mat = None;
325 let mut sid = init_rev(dfa, cache, input)?;
326 // In reverse search, the loop below can't handle the case of searching an
327 // empty slice. Ideally we could write something congruent to the forward
328 // search, i.e., 'while at >= start', but 'start' might be 0. Since we use
329 // an unsigned offset, 'at >= 0' is trivially always true. We could avoid
330 // this extra case handling by using a signed offset, but Rust makes it
331 // annoying to do. So... We just handle the empty case separately.
332 if input.start() == input.end() {
333 eoi_rev(dfa, cache, input, &mut sid, &mut mat)?;
334 return Ok(mat);
335 }
336
337 let mut at = input.end() - 1;
338 macro_rules! next_unchecked {
339 ($sid:expr, $at:expr) => {{
340 let byte = *input.haystack().get_unchecked($at);
341 dfa.next_state_untagged_unchecked(cache, $sid, byte)
342 }};
343 }
344 cache.search_start(at);
345 loop {
346 if sid.is_tagged() {
347 cache.search_update(at);
348 sid = dfa
349 .next_state(cache, sid, input.haystack()[at])
350 .map_err(|_| gave_up(at))?;
351 } else {
352 // SAFETY: See comments in 'find_fwd' for a safety argument.
353 //
354 // PERF: The comments in 'find_fwd' also provide a justification
355 // from a performance perspective as to 1) why we elide bounds
356 // checks and 2) why we do a specialized version of unrolling
357 // below. The reverse search does have a slightly different
358 // consideration in that most reverse searches tend to be
359 // anchored and on shorter haystacks. However, this still makes a
360 // difference. Take this command for example:
361 //
362 // regex-cli find match hybrid -p '(?m)^.+$' -UBb bigfile
363 //
364 // (Notice that we use 'find hybrid regex', not 'find hybrid dfa'
365 // like in the justification for the forward direction. The 'regex'
366 // sub-command will find start-of-match and thus run the reverse
367 // direction.)
368 //
369 // Without unrolling below, the above command takes around 3.76s.
370 // But with the unrolling below, we get down to 2.55s. If we keep
371 // the unrolling but add in bounds checks, then we get 2.86s.
372 //
373 // NOTE: I used 'OpenSubtitles2018.raw.sample.en' for 'bigfile'.
374 let mut prev_sid = sid;
375 while at >= input.start() {
376 prev_sid = unsafe { next_unchecked!(sid, at) };
377 if prev_sid.is_tagged()
378 || at <= input.start().saturating_add(3)
379 {
380 core::mem::swap(&mut prev_sid, &mut sid);
381 break;
382 }
383 at -= 1;
384
385 sid = unsafe { next_unchecked!(prev_sid, at) };
386 if sid.is_tagged() {
387 break;
388 }
389 at -= 1;
390
391 prev_sid = unsafe { next_unchecked!(sid, at) };
392 if prev_sid.is_tagged() {
393 core::mem::swap(&mut prev_sid, &mut sid);
394 break;
395 }
396 at -= 1;
397
398 sid = unsafe { next_unchecked!(prev_sid, at) };
399 if sid.is_tagged() {
400 break;
401 }
402 at -= 1;
403 }
404 // If we quit out of the code above with an unknown state ID at
405 // any point, then we need to re-compute that transition using
406 // 'next_state', which will do NFA powerset construction for us.
407 if sid.is_unknown() {
408 cache.search_update(at);
409 sid = dfa
410 .next_state(cache, prev_sid, input.haystack()[at])
411 .map_err(|_| gave_up(at))?;
412 }
413 }
414 if sid.is_tagged() {
415 if sid.is_start() {
416 // do nothing
417 } else if sid.is_match() {
418 let pattern = dfa.match_pattern(cache, sid, 0);
419 // Since reverse searches report the beginning of a match
420 // and the beginning is inclusive (not exclusive like the
421 // end of a match), we add 1 to make it inclusive.
422 mat = Some(HalfMatch::new(pattern, at + 1));
423 if earliest {
424 cache.search_finish(at);
425 return Ok(mat);
426 }
427 } else if sid.is_dead() {
428 cache.search_finish(at);
429 return Ok(mat);
430 } else if sid.is_quit() {
431 cache.search_finish(at);
432 return Err(MatchError::quit(input.haystack()[at], at));
433 } else {
434 debug_assert!(sid.is_unknown());
435 unreachable!("sid being unknown is a bug");
436 }
437 }
438 if at == input.start() {
439 break;
440 }
441 at -= 1;
442 }
443 cache.search_finish(input.start());
444 eoi_rev(dfa, cache, input, &mut sid, &mut mat)?;
445 Ok(mat)
446}
447
448#[inline(never)]
449pub(crate) fn find_overlapping_fwd(
450 dfa: &DFA,
451 cache: &mut Cache,
452 input: &Input<'_>,
453 state: &mut OverlappingState,
454) -> Result<(), MatchError> {
455 state.mat = None;
456 if input.is_done() {
457 return Ok(());
458 }
459 let pre = if input.get_anchored().is_anchored() {
460 None
461 } else {
462 dfa.get_config().get_prefilter()
463 };
464 if pre.is_some() {
465 find_overlapping_fwd_imp(dfa, cache, input, pre, state)
466 } else {
467 find_overlapping_fwd_imp(dfa, cache, input, None, state)
468 }
469}
470
471#[cfg_attr(feature = "perf-inline", inline(always))]
472fn find_overlapping_fwd_imp(
473 dfa: &DFA,
474 cache: &mut Cache,
475 input: &Input<'_>,
476 pre: Option<&'_ Prefilter>,
477 state: &mut OverlappingState,
478) -> Result<(), MatchError> {
479 // See 'prefilter_restart' docs for explanation.
480 let universal_start = dfa.get_nfa().look_set_prefix_any().is_empty();
481 let mut sid = match state.id {
482 None => {
483 state.at = input.start();
484 init_fwd(dfa, cache, input)?
485 }
486 Some(sid) => {
487 if let Some(match_index) = state.next_match_index {
488 let match_len = dfa.match_len(cache, sid);
489 if match_index < match_len {
490 state.next_match_index = Some(match_index + 1);
491 let pattern = dfa.match_pattern(cache, sid, match_index);
492 state.mat = Some(HalfMatch::new(pattern, state.at));
493 return Ok(());
494 }
495 }
496 // Once we've reported all matches at a given position, we need to
497 // advance the search to the next position.
498 state.at += 1;
499 if state.at > input.end() {
500 return Ok(());
501 }
502 sid
503 }
504 };
505
506 // NOTE: We don't optimize the crap out of this routine primarily because
507 // it seems like most overlapping searches will have higher match counts,
508 // and thus, throughput is perhaps not as important. But if you have a use
509 // case for something faster, feel free to file an issue.
510 cache.search_start(state.at);
511 while state.at < input.end() {
512 sid = dfa
513 .next_state(cache, sid, input.haystack()[state.at])
514 .map_err(|_| gave_up(state.at))?;
515 if sid.is_tagged() {
516 state.id = Some(sid);
517 if sid.is_start() {
518 if let Some(ref pre) = pre {
519 let span = Span::from(state.at..input.end());
520 match pre.find(input.haystack(), span) {
521 None => return Ok(()),
522 Some(ref span) => {
523 if span.start > state.at {
524 state.at = span.start;
525 if !universal_start {
526 sid = prefilter_restart(
527 dfa, cache, &input, state.at,
528 )?;
529 }
530 continue;
531 }
532 }
533 }
534 }
535 } else if sid.is_match() {
536 state.next_match_index = Some(1);
537 let pattern = dfa.match_pattern(cache, sid, 0);
538 state.mat = Some(HalfMatch::new(pattern, state.at));
539 cache.search_finish(state.at);
540 return Ok(());
541 } else if sid.is_dead() {
542 cache.search_finish(state.at);
543 return Ok(());
544 } else if sid.is_quit() {
545 cache.search_finish(state.at);
546 return Err(MatchError::quit(
547 input.haystack()[state.at],
548 state.at,
549 ));
550 } else {
551 debug_assert!(sid.is_unknown());
552 unreachable!("sid being unknown is a bug");
553 }
554 }
555 state.at += 1;
556 cache.search_update(state.at);
557 }
558
559 let result = eoi_fwd(dfa, cache, input, &mut sid, &mut state.mat);
560 state.id = Some(sid);
561 if state.mat.is_some() {
562 // '1' is always correct here since if we get to this point, this
563 // always corresponds to the first (index '0') match discovered at
564 // this position. So the next match to report at this position (if
565 // it exists) is at index '1'.
566 state.next_match_index = Some(1);
567 }
568 cache.search_finish(input.end());
569 result
570}
571
572#[inline(never)]
573pub(crate) fn find_overlapping_rev(
574 dfa: &DFA,
575 cache: &mut Cache,
576 input: &Input<'_>,
577 state: &mut OverlappingState,
578) -> Result<(), MatchError> {
579 state.mat = None;
580 if input.is_done() {
581 return Ok(());
582 }
583 let mut sid = match state.id {
584 None => {
585 let sid = init_rev(dfa, cache, input)?;
586 state.id = Some(sid);
587 if input.start() == input.end() {
588 state.rev_eoi = true;
589 } else {
590 state.at = input.end() - 1;
591 }
592 sid
593 }
594 Some(sid) => {
595 if let Some(match_index) = state.next_match_index {
596 let match_len = dfa.match_len(cache, sid);
597 if match_index < match_len {
598 state.next_match_index = Some(match_index + 1);
599 let pattern = dfa.match_pattern(cache, sid, match_index);
600 state.mat = Some(HalfMatch::new(pattern, state.at));
601 return Ok(());
602 }
603 }
604 // Once we've reported all matches at a given position, we need
605 // to advance the search to the next position. However, if we've
606 // already followed the EOI transition, then we know we're done
607 // with the search and there cannot be any more matches to report.
608 if state.rev_eoi {
609 return Ok(());
610 } else if state.at == input.start() {
611 // At this point, we should follow the EOI transition. This
612 // will cause us the skip the main loop below and fall through
613 // to the final 'eoi_rev' transition.
614 state.rev_eoi = true;
615 } else {
616 // We haven't hit the end of the search yet, so move on.
617 state.at -= 1;
618 }
619 sid
620 }
621 };
622 cache.search_start(state.at);
623 while !state.rev_eoi {
624 sid = dfa
625 .next_state(cache, sid, input.haystack()[state.at])
626 .map_err(|_| gave_up(state.at))?;
627 if sid.is_tagged() {
628 state.id = Some(sid);
629 if sid.is_start() {
630 // do nothing
631 } else if sid.is_match() {
632 state.next_match_index = Some(1);
633 let pattern = dfa.match_pattern(cache, sid, 0);
634 state.mat = Some(HalfMatch::new(pattern, state.at + 1));
635 cache.search_finish(state.at);
636 return Ok(());
637 } else if sid.is_dead() {
638 cache.search_finish(state.at);
639 return Ok(());
640 } else if sid.is_quit() {
641 cache.search_finish(state.at);
642 return Err(MatchError::quit(
643 input.haystack()[state.at],
644 state.at,
645 ));
646 } else {
647 debug_assert!(sid.is_unknown());
648 unreachable!("sid being unknown is a bug");
649 }
650 }
651 if state.at == input.start() {
652 break;
653 }
654 state.at -= 1;
655 cache.search_update(state.at);
656 }
657
658 let result = eoi_rev(dfa, cache, input, &mut sid, &mut state.mat);
659 state.rev_eoi = true;
660 state.id = Some(sid);
661 if state.mat.is_some() {
662 // '1' is always correct here since if we get to this point, this
663 // always corresponds to the first (index '0') match discovered at
664 // this position. So the next match to report at this position (if
665 // it exists) is at index '1'.
666 state.next_match_index = Some(1);
667 }
668 cache.search_finish(input.start());
669 result
670}
671
672#[cfg_attr(feature = "perf-inline", inline(always))]
673fn init_fwd(
674 dfa: &DFA,
675 cache: &mut Cache,
676 input: &Input<'_>,
677) -> Result<LazyStateID, MatchError> {
678 let sid = dfa.start_state_forward(cache, input)?;
679 // Start states can never be match states, since all matches are delayed
680 // by 1 byte.
681 debug_assert!(!sid.is_match());
682 Ok(sid)
683}
684
685#[cfg_attr(feature = "perf-inline", inline(always))]
686fn init_rev(
687 dfa: &DFA,
688 cache: &mut Cache,
689 input: &Input<'_>,
690) -> Result<LazyStateID, MatchError> {
691 let sid = dfa.start_state_reverse(cache, input)?;
692 // Start states can never be match states, since all matches are delayed
693 // by 1 byte.
694 debug_assert!(!sid.is_match());
695 Ok(sid)
696}
697
698#[cfg_attr(feature = "perf-inline", inline(always))]
699fn eoi_fwd(
700 dfa: &DFA,
701 cache: &mut Cache,
702 input: &Input<'_>,
703 sid: &mut LazyStateID,
704 mat: &mut Option<HalfMatch>,
705) -> Result<(), MatchError> {
706 let sp = input.get_span();
707 match input.haystack().get(sp.end) {
708 Some(&b) => {
709 *sid =
710 dfa.next_state(cache, *sid, b).map_err(|_| gave_up(sp.end))?;
711 if sid.is_match() {
712 let pattern = dfa.match_pattern(cache, *sid, 0);
713 *mat = Some(HalfMatch::new(pattern, sp.end));
714 } else if sid.is_quit() {
715 return Err(MatchError::quit(b, sp.end));
716 }
717 }
718 None => {
719 *sid = dfa
720 .next_eoi_state(cache, *sid)
721 .map_err(|_| gave_up(input.haystack().len()))?;
722 if sid.is_match() {
723 let pattern = dfa.match_pattern(cache, *sid, 0);
724 *mat = Some(HalfMatch::new(pattern, input.haystack().len()));
725 }
726 // N.B. We don't have to check 'is_quit' here because the EOI
727 // transition can never lead to a quit state.
728 debug_assert!(!sid.is_quit());
729 }
730 }
731 Ok(())
732}
733
734#[cfg_attr(feature = "perf-inline", inline(always))]
735fn eoi_rev(
736 dfa: &DFA,
737 cache: &mut Cache,
738 input: &Input<'_>,
739 sid: &mut LazyStateID,
740 mat: &mut Option<HalfMatch>,
741) -> Result<(), MatchError> {
742 let sp = input.get_span();
743 if sp.start > 0 {
744 let byte = input.haystack()[sp.start - 1];
745 *sid = dfa
746 .next_state(cache, *sid, byte)
747 .map_err(|_| gave_up(sp.start))?;
748 if sid.is_match() {
749 let pattern = dfa.match_pattern(cache, *sid, 0);
750 *mat = Some(HalfMatch::new(pattern, sp.start));
751 } else if sid.is_quit() {
752 return Err(MatchError::quit(byte, sp.start - 1));
753 }
754 } else {
755 *sid =
756 dfa.next_eoi_state(cache, *sid).map_err(|_| gave_up(sp.start))?;
757 if sid.is_match() {
758 let pattern = dfa.match_pattern(cache, *sid, 0);
759 *mat = Some(HalfMatch::new(pattern, 0));
760 }
761 // N.B. We don't have to check 'is_quit' here because the EOI
762 // transition can never lead to a quit state.
763 debug_assert!(!sid.is_quit());
764 }
765 Ok(())
766}
767
768/// Re-compute the starting state that a DFA should be in after finding a
769/// prefilter candidate match at the position `at`.
770///
771/// It is always correct to call this, but not always necessary. Namely,
772/// whenever the DFA has a universal start state, the DFA can remain in the
773/// start state that it was in when it ran the prefilter. Why? Because in that
774/// case, there is only one start state.
775///
776/// When does a DFA have a universal start state? In precisely cases where
777/// it has no look-around assertions in its prefix. So for example, `\bfoo`
778/// does not have a universal start state because the start state depends on
779/// whether the byte immediately before the start position is a word byte or
780/// not. However, `foo\b` does have a universal start state because the word
781/// boundary does not appear in the pattern's prefix.
782///
783/// So... most cases don't need this, but when a pattern doesn't have a
784/// universal start state, then after a prefilter candidate has been found, the
785/// current state *must* be re-litigated as if computing the start state at the
786/// beginning of the search because it might change. That is, not all start
787/// states are created equal.
788///
789/// Why avoid it? Because while it's not super expensive, it isn't a trivial
790/// operation to compute the start state. It is much better to avoid it and
791/// just state in the current state if you know it to be correct.
792#[cfg_attr(feature = "perf-inline", inline(always))]
793fn prefilter_restart(
794 dfa: &DFA,
795 cache: &mut Cache,
796 input: &Input<'_>,
797 at: usize,
798) -> Result<LazyStateID, MatchError> {
799 let mut input = input.clone();
800 input.set_start(at);
801 init_fwd(dfa, cache, &input)
802}
803
804/// A convenience routine for constructing a "gave up" match error.
805#[cfg_attr(feature = "perf-inline", inline(always))]
806fn gave_up(offset: usize) -> MatchError {
807 MatchError::gave_up(offset)
808}