Skip to main content

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}