Skip to main content

regex_automata/meta/
reverse_inner.rs

1/*!
2A module dedicated to plucking inner literals out of a regex pattern, and
3then constructing a prefilter for them. We also include a regex pattern
4"prefix" that corresponds to the bits of the regex that need to match before
5the literals do. The reverse inner optimization then proceeds by looking for
6matches of the inner literal(s), and then doing a reverse search of the prefix
7from the start of the literal match to find the overall start position of the
8match.
9
10The essential invariant we want to uphold here is that the literals we return
11reflect a set where *at least* one of them must match in order for the overall
12regex to match. We also need to maintain the invariant that the regex prefix
13returned corresponds to the entirety of the regex up until the literals we
14return.
15
16This somewhat limits what we can do. That is, if we a regex like
17`\w+(@!|%%)\w+`, then we can pluck the `{@!, %%}` out and build a prefilter
18from it. Then we just need to compile `\w+` in reverse. No fuss no muss. But if
19we have a regex like \d+@!|\w+%%`, then we get kind of stymied. Technically,
20we could still extract `{@!, %%}`, and it is true that at least of them must
21match. But then, what is our regex prefix? Again, in theory, that could be
22`\d+|\w+`, but that's not quite right, because the `\d+` only matches when `@!`
23matches, and `\w+` only matches when `%%` matches.
24
25All of that is technically possible to do, but it seemingly requires a lot of
26sophistication and machinery. Probably the way to tackle that is with some kind
27of formalism and approach this problem more generally.
28
29For now, the code below basically just looks for a top-level concatenation.
30And if it can find one, it looks for literals in each of the direct child
31sub-expressions of that concatenation. If some good ones are found, we return
32those and a concatenation of the Hir expressions seen up to that point.
33*/
34
35use alloc::vec::Vec;
36
37use regex_syntax::hir::{
38    self,
39    literal::{self, Literal},
40    Hir, HirKind,
41};
42
43use crate::{meta::prefix, util::prefilter::Prefilter, MatchKind};
44
45/// Returns true when it's impossible for an earlier match to be detected after
46/// a literal candidate (corresponding to anything in `literals`) has
47/// been found.
48///
49/// Specifically, that there is no earlier match than what a reverse scan of
50/// `concat_prefix` after a match of `literals` reports.
51///
52/// Since this requires a single `Hir`, this implies the reverse inner optimization
53/// only works with a single regex.
54pub(super) fn has_no_earlier_match(
55    concat_prefix: &Hir,
56    literals: &[Literal],
57) -> bool {
58    // let literals = prefix::LiteralSet::many(literals);
59    if literals.is_empty() || literals.iter().any(|lit| lit.is_empty()) {
60        debug!(
61            "reverse inner is not early return safe because \
62                 no non-empty inner literals were found"
63        );
64        return false;
65    }
66    // With one literal, an occurrence crossing the prefix boundary must
67    // overlap another occurrence of that same literal. Such an overlap
68    // requires the prefix to consume every distinct byte in the literal.
69    // This reasoning does not apply when one extracted literal can cross
70    // the boundary into a different extracted literal.
71    if literals.len() == 1 {
72        let prefix_may_contain = prefix::hir_can_contain_literal(
73            concat_prefix,
74            literals[0].as_bytes(),
75        );
76        debug!(
77            "reverse inner prefix can contain inner literals? \
78             {prefix_may_contain}"
79        );
80        if !prefix_may_contain {
81            return true;
82        }
83    }
84
85    let fixed_length = prefix::hir_has_fixed_length(concat_prefix);
86    debug!("reverse inner has fixed length prefix? {fixed_length}");
87    if fixed_length {
88        return true;
89    }
90
91    let class_separator =
92        prefix::has_disjoint_class_separator(concat_prefix, &literals);
93    debug!("reverse inner has disjoint class separator? {class_separator}");
94    if class_separator {
95        return true;
96    }
97
98    // We couldn't prove that the reverse inner optimization
99    // was safe, so bail out.
100    false
101}
102
103/// This attempts to extract an "inner" prefilter from the given HIR
104/// expressions. If one was found, then a concatenation of the HIR expressions
105/// that precede it is returned.
106///
107/// The idea here is that the prefilter returned can be used to find
108/// candidate matches. And then the HIR returned can be used to build a
109/// reverse regex matcher, which will find the start of the candidate
110/// match. Finally, the match still has to be confirmed with a normal
111/// anchored forward scan to find the end position of the match.
112///
113/// Note that this assumes leftmost-first match semantics, so callers must
114/// not call this otherwise.
115#[derive(Debug)]
116pub(crate) struct InnerPrefilter {
117    pub(crate) prefix: Hir,
118    /// The prefilter generated from `literals`.
119    pub(crate) pre: Prefilter,
120    /// The actual literals extracted and used to build `pre`.
121    ///
122    /// These are used by the meta strategy to prove that the inner prefilter
123    /// can return after the first confirmed candidate. If that proof fails, we
124    /// could try extracting a different set of literals. But we don't
125    /// currently do that.
126    pub(crate) literals: Vec<Literal>,
127}
128
129impl InnerPrefilter {
130    pub(crate) fn new(hirs: &[&Hir]) -> Option<InnerPrefilter> {
131        if hirs.len() != 1 {
132            debug!(
133                "skipping reverse inner optimization since it only \
134                 supports 1 pattern, {} were given",
135                hirs.len(),
136            );
137            return None;
138        }
139        let mut concat = match top_concat(hirs[0]) {
140            Some(concat) => concat,
141            None => {
142                debug!(
143                    "skipping reverse inner optimization because a top-level \
144                     concatenation could not found",
145                );
146                return None;
147            }
148        };
149        // We skip the first HIR because if it did have a prefix prefilter in
150        // it, we probably wouldn't be here looking for an inner prefilter.
151        for i in 1..concat.len() {
152            let hir = &concat[i];
153            let (pre, lits) = match prefilter_with_literals(hir) {
154                None => continue,
155                Some(pre) => pre,
156            };
157            // Even if we got a prefilter, if it isn't consider "fast," then
158            // we probably don't want to bother with it. Namely, since the
159            // reverse inner optimization requires some overhead, it likely
160            // only makes sense if the prefilter scan itself is (believed) to
161            // be much faster than the regex engine.
162            if !pre.is_fast() {
163                debug!(
164                    "skipping extracted inner prefilter because \
165                     it probably isn't fast"
166                );
167                continue;
168            }
169            let concat_suffix = Hir::concat(concat.split_off(i));
170            let concat_prefix = Hir::concat(concat);
171            // Look for a prefilter again. Why? Because above we only looked
172            // for a prefilter on the individual 'hir', but we might be able
173            // to find something better and more discriminatory by looking at
174            // the entire suffix. We don't do this above to avoid making this
175            // loop worst case quadratic in the length of 'concat'.
176            let (preinner, inner_literals) =
177                match prefilter_with_literals(&concat_suffix) {
178                    None => (pre, lits),
179                    Some((pre2, lits2)) => {
180                        if pre2.is_fast() {
181                            (pre2, lits2)
182                        } else {
183                            (pre, lits)
184                        }
185                    }
186                };
187            return Some(InnerPrefilter {
188                prefix: concat_prefix,
189                pre: preinner,
190                literals: inner_literals,
191            });
192        }
193        debug!(
194            "skipping reverse inner optimization because a top-level \
195             sub-expression with a fast prefilter could not be found"
196        );
197        None
198    }
199}
200
201/// Attempt to extract a prefilter from an HIR expression.
202///
203/// We do a little massaging here to do our best that the prefilter we get out
204/// of this is *probably* fast. Basically, the false positive rate has a much
205/// higher impact for things like the reverse inner optimization because more
206/// work needs to potentially be done for each candidate match.
207///
208/// Note that this assumes leftmost-first match semantics, so callers must
209/// not call this otherwise.
210fn prefilter_with_literals(hir: &Hir) -> Option<(Prefilter, Vec<Literal>)> {
211    let mut extractor = literal::Extractor::new();
212    extractor.kind(literal::ExtractKind::Prefix);
213    let mut prefixes = extractor.extract(hir);
214    debug!(
215        "inner prefixes (len={:?}) extracted before optimization: {:?}",
216        prefixes.len(),
217        prefixes
218    );
219    // Since these are inner literals, we know they cannot be exact. But the
220    // extractor doesn't know this. We mark them as inexact because this might
221    // impact literal optimization. Namely, optimization weights "all literals
222    // are exact" as very high, because it presumes that any match results in
223    // an overall match. But of course, that is not the case here.
224    //
225    // In practice, this avoids plucking out a ASCII-only \s as an alternation
226    // of single-byte whitespace characters.
227    prefixes.make_inexact();
228    prefixes.optimize_for_prefix_by_preference();
229    debug!(
230        "inner prefixes (len={:?}) extracted after optimization: {:?}",
231        prefixes.len(),
232        prefixes
233    );
234    let lits = prefixes.literals()?;
235    let pre = Prefilter::new(MatchKind::LeftmostFirst, lits)?;
236    Some((pre, lits.to_vec()))
237}
238
239/// Looks for a "top level" HirKind::Concat item in the given HIR. This will
240/// try to return one even if it's embedded in a capturing group, but is
241/// otherwise pretty conservative in what is returned.
242///
243/// The HIR returned is a complete copy of the concat with all capturing
244/// groups removed. In effect, the concat returned is "flattened" with respect
245/// to capturing groups. This makes the detection logic above for prefixes
246/// a bit simpler, and it works because 1) capturing groups never influence
247/// whether a match occurs or not and 2) capturing groups are not used when
248/// doing the reverse inner search to find the start of the match.
249fn top_concat(mut hir: &Hir) -> Option<Vec<Hir>> {
250    loop {
251        hir = match hir.kind() {
252            HirKind::Empty
253            | HirKind::Literal(_)
254            | HirKind::Class(_)
255            | HirKind::Look(_)
256            | HirKind::Repetition(_)
257            | HirKind::Alternation(_) => return None,
258            HirKind::Capture(hir::Capture { ref sub, .. }) => sub,
259            HirKind::Concat(ref subs) => {
260                // We are careful to only do the flattening/copy when we know
261                // we have a "top level" concat we can inspect. This avoids
262                // doing extra work in cases where we definitely won't use it.
263                // (This might still be wasted work if we can't go on to find
264                // some literals to extract.)
265                let concat =
266                    Hir::concat(subs.iter().map(|h| flatten(h)).collect());
267                return match concat.into_kind() {
268                    HirKind::Concat(xs) => Some(xs),
269                    // It is actually possible for this case to occur, because
270                    // 'Hir::concat' might simplify the expression to the point
271                    // that concatenations are actually removed. One wonders
272                    // whether this leads to other cases where we should be
273                    // extracting literals, but in theory, I believe if we do
274                    // get here, then it means that a "real" prefilter failed
275                    // to be extracted and we should probably leave well enough
276                    // alone. (A "real" prefilter is unbothered by "top-level
277                    // concats" and "capturing groups.")
278                    _ => return None,
279                };
280            }
281        };
282    }
283}
284
285/// Returns a copy of the given HIR but with all capturing groups removed.
286fn flatten(hir: &Hir) -> Hir {
287    match hir.kind() {
288        HirKind::Empty => Hir::empty(),
289        HirKind::Literal(hir::Literal(ref x)) => Hir::literal(x.clone()),
290        HirKind::Class(ref x) => Hir::class(x.clone()),
291        HirKind::Look(ref x) => Hir::look(x.clone()),
292        HirKind::Repetition(ref x) => Hir::repetition(x.with(flatten(&x.sub))),
293        // This is the interesting case. We just drop the group information
294        // entirely and use the child HIR itself.
295        HirKind::Capture(hir::Capture { ref sub, .. }) => flatten(sub),
296        HirKind::Alternation(ref xs) => {
297            Hir::alternation(xs.iter().map(|x| flatten(x)).collect())
298        }
299        HirKind::Concat(ref xs) => {
300            Hir::concat(xs.iter().map(|x| flatten(x)).collect())
301        }
302    }
303}