Coverage Report

Created: 2025-06-23 13:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/cargo-vendor-dir/regex-automata-0.4.9/src/hybrid/search.rs
Line
Count
Source
1
use 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)]
13
300
pub(crate) fn find_fwd(
14
300
    dfa: &DFA,
15
300
    cache: &mut Cache,
16
300
    input: &Input<'_>,
17
300
) -> Result<Option<HalfMatch>, MatchError> {
18
300
    if input.is_done() {
19
0
        return Ok(None);
20
300
    }
21
300
    let pre = if input.get_anchored().is_anchored() {
22
0
        None
23
    } else {
24
300
        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
300
    if pre.is_some() {
35
0
        if input.get_earliest() {
36
0
            find_fwd_imp(dfa, cache, input, pre, true)
37
        } else {
38
0
            find_fwd_imp(dfa, cache, input, pre, false)
39
        }
40
    } else {
41
300
        if input.get_earliest() {
42
300
            find_fwd_imp(dfa, cache, input, None, true)
43
        } else {
44
0
            find_fwd_imp(dfa, cache, input, None, false)
45
        }
46
    }
47
300
}
48
49
#[cfg_attr(feature = "perf-inline", inline(always))]
50
300
fn find_fwd_imp(
51
300
    dfa: &DFA,
52
300
    cache: &mut Cache,
53
300
    input: &Input<'_>,
54
300
    pre: Option<&'_ Prefilter>,
55
300
    earliest: bool,
56
300
) -> Result<Option<HalfMatch>, MatchError> {
57
300
    // See 'prefilter_restart' docs for explanation.
58
300
    let universal_start = dfa.get_nfa().look_set_prefix_any().is_empty();
59
300
    let mut mat = None;
60
300
    let mut sid = init_fwd(dfa, cache, input)
?0
;
61
300
    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
300
    if let Some(
ref pre0
) = pre {
73
0
        let span = Span::from(at..input.end());
74
0
        match pre.find(input.haystack(), span) {
75
0
            None => return Ok(mat),
76
0
            Some(ref span) => {
77
0
                at = span.start;
78
0
                if !universal_start {
79
0
                    sid = prefilter_restart(dfa, cache, &input, at)?;
80
0
                }
81
            }
82
        }
83
300
    }
84
300
    cache.search_start(at);
85
431
    while at < input.end() {
86
373
        if sid.is_tagged() {
87
0
            cache.search_update(at);
88
0
            sid = dfa
89
0
                .next_state(cache, sid, input.haystack()[at])
90
0
                .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
373
            let mut prev_sid = sid;
195
650
            while at < input.end() {
196
643
                prev_sid = unsafe { next_unchecked!(sid, at) };
197
643
                if prev_sid.is_tagged() || 
at + 3 >= input.end()381
{
198
355
                    core::mem::swap(&mut prev_sid, &mut sid);
199
355
                    break;
200
288
                }
201
288
                at += 1;
202
288
203
288
                sid = unsafe { next_unchecked!(prev_sid, at) };
204
288
                if sid.is_tagged() {
205
8
                    break;
206
280
                }
207
280
                at += 1;
208
280
209
280
                prev_sid = unsafe { next_unchecked!(sid, at) };
210
280
                if prev_sid.is_tagged() {
211
1
                    core::mem::swap(&mut prev_sid, &mut sid);
212
1
                    break;
213
279
                }
214
279
                at += 1;
215
279
216
279
                sid = unsafe { next_unchecked!(prev_sid, at) };
217
279
                if sid.is_tagged() {
218
2
                    break;
219
277
                }
220
277
                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
373
            if sid.is_unknown() {
226
47
                cache.search_update(at);
227
47
                sid = dfa
228
47
                    .next_state(cache, prev_sid, input.haystack()[at])
229
47
                    .map_err(|_| 
gave_up(at)0
)
?0
;
230
326
            }
231
        }
232
373
        if sid.is_tagged() {
233
242
            if sid.is_start() {
234
0
                if let Some(ref pre) = pre {
235
0
                    let span = Span::from(at..input.end());
236
0
                    match pre.find(input.haystack(), span) {
237
                        None => {
238
0
                            cache.search_finish(span.end);
239
0
                            return Ok(mat);
240
                        }
241
0
                        Some(ref span) => {
242
0
                            // We want to skip any update to 'at' below
243
0
                            // at the end of this iteration and just
244
0
                            // jump immediately back to the next state
245
0
                            // transition at the leading position of the
246
0
                            // candidate match.
247
0
                            //
248
0
                            // ... but only if we actually made progress
249
0
                            // with our prefilter, otherwise if the start
250
0
                            // state has a self-loop, we can get stuck.
251
0
                            if span.start > at {
252
0
                                at = span.start;
253
0
                                if !universal_start {
254
0
                                    sid = prefilter_restart(
255
0
                                        dfa, cache, &input, at,
256
0
                                    )?;
257
0
                                }
258
0
                                continue;
259
0
                            }
260
                        }
261
                    }
262
0
                }
263
242
            } else if sid.is_match() {
264
150
                let pattern = dfa.match_pattern(cache, sid, 0);
265
150
                // Since slice ranges are inclusive at the beginning and
266
150
                // exclusive at the end, and since forward searches report
267
150
                // the end, we can return 'at' as-is. This only works because
268
150
                // matches are delayed by 1 byte. So by the time we observe a
269
150
                // match, 'at' has already been set to 1 byte past the actual
270
150
                // match location, which is precisely the exclusive ending
271
150
                // bound of the match.
272
150
                mat = Some(HalfMatch::new(pattern, at));
273
150
                if earliest {
274
150
                    cache.search_finish(at);
275
150
                    return Ok(mat);
276
0
                }
277
92
            } else if sid.is_dead() {
278
92
                cache.search_finish(at);
279
92
                return Ok(mat);
280
0
            } else if sid.is_quit() {
281
0
                cache.search_finish(at);
282
0
                return Err(MatchError::quit(input.haystack()[at], at));
283
            } else {
284
0
                debug_assert!(sid.is_unknown());
285
0
                unreachable!("sid being unknown is a bug");
286
            }
287
131
        }
288
131
        at += 1;
289
    }
290
58
    eoi_fwd(dfa, cache, input, &mut sid, &mut mat)
?0
;
291
58
    cache.search_finish(input.end());
292
58
    Ok(mat)
293
300
}
294
295
#[inline(never)]
296
0
pub(crate) fn find_rev(
297
0
    dfa: &DFA,
298
0
    cache: &mut Cache,
299
0
    input: &Input<'_>,
300
0
) -> Result<Option<HalfMatch>, MatchError> {
301
0
    if input.is_done() {
302
0
        return Ok(None);
303
0
    }
304
0
    if input.get_earliest() {
305
0
        find_rev_imp(dfa, cache, input, true)
306
    } else {
307
0
        find_rev_imp(dfa, cache, input, false)
308
    }
309
0
}
310
311
#[cfg_attr(feature = "perf-inline", inline(always))]
312
0
fn find_rev_imp(
313
0
    dfa: &DFA,
314
0
    cache: &mut Cache,
315
0
    input: &Input<'_>,
316
0
    earliest: bool,
317
0
) -> Result<Option<HalfMatch>, MatchError> {
318
0
    let mut mat = None;
319
0
    let mut sid = init_rev(dfa, cache, input)?;
320
    // In reverse search, the loop below can't handle the case of searching an
321
    // empty slice. Ideally we could write something congruent to the forward
322
    // search, i.e., 'while at >= start', but 'start' might be 0. Since we use
323
    // an unsigned offset, 'at >= 0' is trivially always true. We could avoid
324
    // this extra case handling by using a signed offset, but Rust makes it
325
    // annoying to do. So... We just handle the empty case separately.
326
0
    if input.start() == input.end() {
327
0
        eoi_rev(dfa, cache, input, &mut sid, &mut mat)?;
328
0
        return Ok(mat);
329
0
    }
330
0
331
0
    let mut at = input.end() - 1;
332
    macro_rules! next_unchecked {
333
        ($sid:expr, $at:expr) => {{
334
            let byte = *input.haystack().get_unchecked($at);
335
            dfa.next_state_untagged_unchecked(cache, $sid, byte)
336
        }};
337
    }
338
0
    cache.search_start(at);
339
    loop {
340
0
        if sid.is_tagged() {
341
0
            cache.search_update(at);
342
0
            sid = dfa
343
0
                .next_state(cache, sid, input.haystack()[at])
344
0
                .map_err(|_| gave_up(at))?;
345
        } else {
346
            // SAFETY: See comments in 'find_fwd' for a safety argument.
347
            //
348
            // PERF: The comments in 'find_fwd' also provide a justification
349
            // from a performance perspective as to 1) why we elide bounds
350
            // checks and 2) why we do a specialized version of unrolling
351
            // below. The reverse search does have a slightly different
352
            // consideration in that most reverse searches tend to be
353
            // anchored and on shorter haystacks. However, this still makes a
354
            // difference. Take this command for example:
355
            //
356
            //     regex-cli find match hybrid -p '(?m)^.+$' -UBb bigfile
357
            //
358
            // (Notice that we use 'find hybrid regex', not 'find hybrid dfa'
359
            // like in the justification for the forward direction. The 'regex'
360
            // sub-command will find start-of-match and thus run the reverse
361
            // direction.)
362
            //
363
            // Without unrolling below, the above command takes around 3.76s.
364
            // But with the unrolling below, we get down to 2.55s. If we keep
365
            // the unrolling but add in bounds checks, then we get 2.86s.
366
            //
367
            // NOTE: I used 'OpenSubtitles2018.raw.sample.en' for 'bigfile'.
368
0
            let mut prev_sid = sid;
369
0
            while at >= input.start() {
370
0
                prev_sid = unsafe { next_unchecked!(sid, at) };
371
0
                if prev_sid.is_tagged()
372
0
                    || at <= input.start().saturating_add(3)
373
                {
374
0
                    core::mem::swap(&mut prev_sid, &mut sid);
375
0
                    break;
376
0
                }
377
0
                at -= 1;
378
0
379
0
                sid = unsafe { next_unchecked!(prev_sid, at) };
380
0
                if sid.is_tagged() {
381
0
                    break;
382
0
                }
383
0
                at -= 1;
384
0
385
0
                prev_sid = unsafe { next_unchecked!(sid, at) };
386
0
                if prev_sid.is_tagged() {
387
0
                    core::mem::swap(&mut prev_sid, &mut sid);
388
0
                    break;
389
0
                }
390
0
                at -= 1;
391
0
392
0
                sid = unsafe { next_unchecked!(prev_sid, at) };
393
0
                if sid.is_tagged() {
394
0
                    break;
395
0
                }
396
0
                at -= 1;
397
            }
398
            // If we quit out of the code above with an unknown state ID at
399
            // any point, then we need to re-compute that transition using
400
            // 'next_state', which will do NFA powerset construction for us.
401
0
            if sid.is_unknown() {
402
0
                cache.search_update(at);
403
0
                sid = dfa
404
0
                    .next_state(cache, prev_sid, input.haystack()[at])
405
0
                    .map_err(|_| gave_up(at))?;
406
0
            }
407
        }
408
0
        if sid.is_tagged() {
409
0
            if sid.is_start() {
410
0
                // do nothing
411
0
            } else if sid.is_match() {
412
0
                let pattern = dfa.match_pattern(cache, sid, 0);
413
0
                // Since reverse searches report the beginning of a match
414
0
                // and the beginning is inclusive (not exclusive like the
415
0
                // end of a match), we add 1 to make it inclusive.
416
0
                mat = Some(HalfMatch::new(pattern, at + 1));
417
0
                if earliest {
418
0
                    cache.search_finish(at);
419
0
                    return Ok(mat);
420
0
                }
421
0
            } else if sid.is_dead() {
422
0
                cache.search_finish(at);
423
0
                return Ok(mat);
424
0
            } else if sid.is_quit() {
425
0
                cache.search_finish(at);
426
0
                return Err(MatchError::quit(input.haystack()[at], at));
427
            } else {
428
0
                debug_assert!(sid.is_unknown());
429
0
                unreachable!("sid being unknown is a bug");
430
            }
431
0
        }
432
0
        if at == input.start() {
433
0
            break;
434
0
        }
435
0
        at -= 1;
436
    }
437
0
    cache.search_finish(input.start());
438
0
    eoi_rev(dfa, cache, input, &mut sid, &mut mat)?;
439
0
    Ok(mat)
440
0
}
441
442
#[inline(never)]
443
0
pub(crate) fn find_overlapping_fwd(
444
0
    dfa: &DFA,
445
0
    cache: &mut Cache,
446
0
    input: &Input<'_>,
447
0
    state: &mut OverlappingState,
448
0
) -> Result<(), MatchError> {
449
0
    state.mat = None;
450
0
    if input.is_done() {
451
0
        return Ok(());
452
0
    }
453
0
    let pre = if input.get_anchored().is_anchored() {
454
0
        None
455
    } else {
456
0
        dfa.get_config().get_prefilter()
457
    };
458
0
    if pre.is_some() {
459
0
        find_overlapping_fwd_imp(dfa, cache, input, pre, state)
460
    } else {
461
0
        find_overlapping_fwd_imp(dfa, cache, input, None, state)
462
    }
463
0
}
464
465
#[cfg_attr(feature = "perf-inline", inline(always))]
466
0
fn find_overlapping_fwd_imp(
467
0
    dfa: &DFA,
468
0
    cache: &mut Cache,
469
0
    input: &Input<'_>,
470
0
    pre: Option<&'_ Prefilter>,
471
0
    state: &mut OverlappingState,
472
0
) -> Result<(), MatchError> {
473
0
    // See 'prefilter_restart' docs for explanation.
474
0
    let universal_start = dfa.get_nfa().look_set_prefix_any().is_empty();
475
0
    let mut sid = match state.id {
476
        None => {
477
0
            state.at = input.start();
478
0
            init_fwd(dfa, cache, input)?
479
        }
480
0
        Some(sid) => {
481
0
            if let Some(match_index) = state.next_match_index {
482
0
                let match_len = dfa.match_len(cache, sid);
483
0
                if match_index < match_len {
484
0
                    state.next_match_index = Some(match_index + 1);
485
0
                    let pattern = dfa.match_pattern(cache, sid, match_index);
486
0
                    state.mat = Some(HalfMatch::new(pattern, state.at));
487
0
                    return Ok(());
488
0
                }
489
0
            }
490
            // Once we've reported all matches at a given position, we need to
491
            // advance the search to the next position.
492
0
            state.at += 1;
493
0
            if state.at > input.end() {
494
0
                return Ok(());
495
0
            }
496
0
            sid
497
        }
498
    };
499
500
    // NOTE: We don't optimize the crap out of this routine primarily because
501
    // it seems like most overlapping searches will have higher match counts,
502
    // and thus, throughput is perhaps not as important. But if you have a use
503
    // case for something faster, feel free to file an issue.
504
0
    cache.search_start(state.at);
505
0
    while state.at < input.end() {
506
0
        sid = dfa
507
0
            .next_state(cache, sid, input.haystack()[state.at])
508
0
            .map_err(|_| gave_up(state.at))?;
509
0
        if sid.is_tagged() {
510
0
            state.id = Some(sid);
511
0
            if sid.is_start() {
512
0
                if let Some(ref pre) = pre {
513
0
                    let span = Span::from(state.at..input.end());
514
0
                    match pre.find(input.haystack(), span) {
515
0
                        None => return Ok(()),
516
0
                        Some(ref span) => {
517
0
                            if span.start > state.at {
518
0
                                state.at = span.start;
519
0
                                if !universal_start {
520
0
                                    sid = prefilter_restart(
521
0
                                        dfa, cache, &input, state.at,
522
0
                                    )?;
523
0
                                }
524
0
                                continue;
525
0
                            }
526
                        }
527
                    }
528
0
                }
529
0
            } else if sid.is_match() {
530
0
                state.next_match_index = Some(1);
531
0
                let pattern = dfa.match_pattern(cache, sid, 0);
532
0
                state.mat = Some(HalfMatch::new(pattern, state.at));
533
0
                cache.search_finish(state.at);
534
0
                return Ok(());
535
0
            } else if sid.is_dead() {
536
0
                cache.search_finish(state.at);
537
0
                return Ok(());
538
0
            } else if sid.is_quit() {
539
0
                cache.search_finish(state.at);
540
0
                return Err(MatchError::quit(
541
0
                    input.haystack()[state.at],
542
0
                    state.at,
543
0
                ));
544
            } else {
545
0
                debug_assert!(sid.is_unknown());
546
0
                unreachable!("sid being unknown is a bug");
547
            }
548
0
        }
549
0
        state.at += 1;
550
0
        cache.search_update(state.at);
551
    }
552
553
0
    let result = eoi_fwd(dfa, cache, input, &mut sid, &mut state.mat);
554
0
    state.id = Some(sid);
555
0
    if state.mat.is_some() {
556
0
        // '1' is always correct here since if we get to this point, this
557
0
        // always corresponds to the first (index '0') match discovered at
558
0
        // this position. So the next match to report at this position (if
559
0
        // it exists) is at index '1'.
560
0
        state.next_match_index = Some(1);
561
0
    }
562
0
    cache.search_finish(input.end());
563
0
    result
564
0
}
565
566
#[inline(never)]
567
0
pub(crate) fn find_overlapping_rev(
568
0
    dfa: &DFA,
569
0
    cache: &mut Cache,
570
0
    input: &Input<'_>,
571
0
    state: &mut OverlappingState,
572
0
) -> Result<(), MatchError> {
573
0
    state.mat = None;
574
0
    if input.is_done() {
575
0
        return Ok(());
576
0
    }
577
0
    let mut sid = match state.id {
578
        None => {
579
0
            let sid = init_rev(dfa, cache, input)?;
580
0
            state.id = Some(sid);
581
0
            if input.start() == input.end() {
582
0
                state.rev_eoi = true;
583
0
            } else {
584
0
                state.at = input.end() - 1;
585
0
            }
586
0
            sid
587
        }
588
0
        Some(sid) => {
589
0
            if let Some(match_index) = state.next_match_index {
590
0
                let match_len = dfa.match_len(cache, sid);
591
0
                if match_index < match_len {
592
0
                    state.next_match_index = Some(match_index + 1);
593
0
                    let pattern = dfa.match_pattern(cache, sid, match_index);
594
0
                    state.mat = Some(HalfMatch::new(pattern, state.at));
595
0
                    return Ok(());
596
0
                }
597
0
            }
598
            // Once we've reported all matches at a given position, we need
599
            // to advance the search to the next position. However, if we've
600
            // already followed the EOI transition, then we know we're done
601
            // with the search and there cannot be any more matches to report.
602
0
            if state.rev_eoi {
603
0
                return Ok(());
604
0
            } else if state.at == input.start() {
605
0
                // At this point, we should follow the EOI transition. This
606
0
                // will cause us the skip the main loop below and fall through
607
0
                // to the final 'eoi_rev' transition.
608
0
                state.rev_eoi = true;
609
0
            } else {
610
0
                // We haven't hit the end of the search yet, so move on.
611
0
                state.at -= 1;
612
0
            }
613
0
            sid
614
        }
615
    };
616
0
    cache.search_start(state.at);
617
0
    while !state.rev_eoi {
618
0
        sid = dfa
619
0
            .next_state(cache, sid, input.haystack()[state.at])
620
0
            .map_err(|_| gave_up(state.at))?;
621
0
        if sid.is_tagged() {
622
0
            state.id = Some(sid);
623
0
            if sid.is_start() {
624
0
                // do nothing
625
0
            } else if sid.is_match() {
626
0
                state.next_match_index = Some(1);
627
0
                let pattern = dfa.match_pattern(cache, sid, 0);
628
0
                state.mat = Some(HalfMatch::new(pattern, state.at + 1));
629
0
                cache.search_finish(state.at);
630
0
                return Ok(());
631
0
            } else if sid.is_dead() {
632
0
                cache.search_finish(state.at);
633
0
                return Ok(());
634
0
            } else if sid.is_quit() {
635
0
                cache.search_finish(state.at);
636
0
                return Err(MatchError::quit(
637
0
                    input.haystack()[state.at],
638
0
                    state.at,
639
0
                ));
640
            } else {
641
0
                debug_assert!(sid.is_unknown());
642
0
                unreachable!("sid being unknown is a bug");
643
            }
644
0
        }
645
0
        if state.at == input.start() {
646
0
            break;
647
0
        }
648
0
        state.at -= 1;
649
0
        cache.search_update(state.at);
650
    }
651
652
0
    let result = eoi_rev(dfa, cache, input, &mut sid, &mut state.mat);
653
0
    state.rev_eoi = true;
654
0
    state.id = Some(sid);
655
0
    if state.mat.is_some() {
656
0
        // '1' is always correct here since if we get to this point, this
657
0
        // always corresponds to the first (index '0') match discovered at
658
0
        // this position. So the next match to report at this position (if
659
0
        // it exists) is at index '1'.
660
0
        state.next_match_index = Some(1);
661
0
    }
662
0
    cache.search_finish(input.start());
663
0
    result
664
0
}
665
666
#[cfg_attr(feature = "perf-inline", inline(always))]
667
300
fn init_fwd(
668
300
    dfa: &DFA,
669
300
    cache: &mut Cache,
670
300
    input: &Input<'_>,
671
300
) -> Result<LazyStateID, MatchError> {
672
300
    let sid = dfa.start_state_forward(cache, input)
?0
;
673
    // Start states can never be match states, since all matches are delayed
674
    // by 1 byte.
675
300
    debug_assert!(!sid.is_match());
676
300
    Ok(sid)
677
300
}
678
679
#[cfg_attr(feature = "perf-inline", inline(always))]
680
0
fn init_rev(
681
0
    dfa: &DFA,
682
0
    cache: &mut Cache,
683
0
    input: &Input<'_>,
684
0
) -> Result<LazyStateID, MatchError> {
685
0
    let sid = dfa.start_state_reverse(cache, input)?;
686
    // Start states can never be match states, since all matches are delayed
687
    // by 1 byte.
688
0
    debug_assert!(!sid.is_match());
689
0
    Ok(sid)
690
0
}
691
692
#[cfg_attr(feature = "perf-inline", inline(always))]
693
58
fn eoi_fwd(
694
58
    dfa: &DFA,
695
58
    cache: &mut Cache,
696
58
    input: &Input<'_>,
697
58
    sid: &mut LazyStateID,
698
58
    mat: &mut Option<HalfMatch>,
699
58
) -> Result<(), MatchError> {
700
58
    let sp = input.get_span();
701
58
    match input.haystack().get(sp.end) {
702
0
        Some(&b) => {
703
0
            *sid =
704
0
                dfa.next_state(cache, *sid, b).map_err(|_| gave_up(sp.end))?;
705
0
            if sid.is_match() {
706
0
                let pattern = dfa.match_pattern(cache, *sid, 0);
707
0
                *mat = Some(HalfMatch::new(pattern, sp.end));
708
0
            } else if sid.is_quit() {
709
0
                return Err(MatchError::quit(b, sp.end));
710
0
            }
711
        }
712
        None => {
713
58
            *sid = dfa
714
58
                .next_eoi_state(cache, *sid)
715
58
                .map_err(|_| 
gave_up(input.haystack().len())0
)
?0
;
716
58
            if sid.is_match() {
717
58
                let pattern = dfa.match_pattern(cache, *sid, 0);
718
58
                *mat = Some(HalfMatch::new(pattern, input.haystack().len()));
719
58
            
}0
720
            // N.B. We don't have to check 'is_quit' here because the EOI
721
            // transition can never lead to a quit state.
722
58
            debug_assert!(!sid.is_quit());
723
        }
724
    }
725
58
    Ok(())
726
58
}
727
728
#[cfg_attr(feature = "perf-inline", inline(always))]
729
0
fn eoi_rev(
730
0
    dfa: &DFA,
731
0
    cache: &mut Cache,
732
0
    input: &Input<'_>,
733
0
    sid: &mut LazyStateID,
734
0
    mat: &mut Option<HalfMatch>,
735
0
) -> Result<(), MatchError> {
736
0
    let sp = input.get_span();
737
0
    if sp.start > 0 {
738
0
        let byte = input.haystack()[sp.start - 1];
739
0
        *sid = dfa
740
0
            .next_state(cache, *sid, byte)
741
0
            .map_err(|_| gave_up(sp.start))?;
742
0
        if sid.is_match() {
743
0
            let pattern = dfa.match_pattern(cache, *sid, 0);
744
0
            *mat = Some(HalfMatch::new(pattern, sp.start));
745
0
        } else if sid.is_quit() {
746
0
            return Err(MatchError::quit(byte, sp.start - 1));
747
0
        }
748
    } else {
749
        *sid =
750
0
            dfa.next_eoi_state(cache, *sid).map_err(|_| gave_up(sp.start))?;
751
0
        if sid.is_match() {
752
0
            let pattern = dfa.match_pattern(cache, *sid, 0);
753
0
            *mat = Some(HalfMatch::new(pattern, 0));
754
0
        }
755
        // N.B. We don't have to check 'is_quit' here because the EOI
756
        // transition can never lead to a quit state.
757
0
        debug_assert!(!sid.is_quit());
758
    }
759
0
    Ok(())
760
0
}
761
762
/// Re-compute the starting state that a DFA should be in after finding a
763
/// prefilter candidate match at the position `at`.
764
///
765
/// It is always correct to call this, but not always necessary. Namely,
766
/// whenever the DFA has a universal start state, the DFA can remain in the
767
/// start state that it was in when it ran the prefilter. Why? Because in that
768
/// case, there is only one start state.
769
///
770
/// When does a DFA have a universal start state? In precisely cases where
771
/// it has no look-around assertions in its prefix. So for example, `\bfoo`
772
/// does not have a universal start state because the start state depends on
773
/// whether the byte immediately before the start position is a word byte or
774
/// not. However, `foo\b` does have a universal start state because the word
775
/// boundary does not appear in the pattern's prefix.
776
///
777
/// So... most cases don't need this, but when a pattern doesn't have a
778
/// universal start state, then after a prefilter candidate has been found, the
779
/// current state *must* be re-litigated as if computing the start state at the
780
/// beginning of the search because it might change. That is, not all start
781
/// states are created equal.
782
///
783
/// Why avoid it? Because while it's not super expensive, it isn't a trivial
784
/// operation to compute the start state. It is much better to avoid it and
785
/// just state in the current state if you know it to be correct.
786
#[cfg_attr(feature = "perf-inline", inline(always))]
787
0
fn prefilter_restart(
788
0
    dfa: &DFA,
789
0
    cache: &mut Cache,
790
0
    input: &Input<'_>,
791
0
    at: usize,
792
0
) -> Result<LazyStateID, MatchError> {
793
0
    let mut input = input.clone();
794
0
    input.set_start(at);
795
0
    init_fwd(dfa, cache, &input)
796
0
}
797
798
/// A convenience routine for constructing a "gave up" match error.
799
#[cfg_attr(feature = "perf-inline", inline(always))]
800
0
fn gave_up(offset: usize) -> MatchError {
801
0
    MatchError::gave_up(offset)
802
0
}