Skip to main content

regex_automata/meta/
prefix.rs

1/*!
2Cheap HIR proofs that literal candidate order is compatible with regex match
3order.
4
5The reverse suffix and reverse inner strategies both scan for a required
6literal before confirming the surrounding regex. These proofs are all
7conservative. If they cannot establish that the first confirmed candidate is
8the match the regex engine would report, then the corresponding reverse
9strategy is not used.
10*/
11
12use regex_syntax::hir::{literal::Literal, Class, Hir, HirKind};
13
14/// Return true when `hir` can match some string containing every byte in
15/// `lit`.
16///
17/// This is deliberately low precision. If every byte in a literal can be
18/// consumed somewhere in the HIR, this gives up and reports that the
19/// literal might occur internally. That is, this returning true does not
20/// necessarily mean that the `Hir` provided definitively matches `lit`.
21pub(super) fn hir_can_contain_literal(hir: &Hir, lit: &[u8]) -> bool {
22    lit.is_empty() || lit.iter().all(|&byte| hir_can_consume_byte(hir, byte))
23}
24
25/// Returns true when the given `Hir` has a fixed length.
26///
27/// That is, when its minimum and maximum lengths are both finite and
28/// equivalent.
29pub(super) fn hir_has_fixed_length(hir: &Hir) -> bool {
30    let props = hir.properties();
31    props
32        .minimum_len()
33        .and_then(|min| props.maximum_len().map(|max| min == max))
34        .unwrap_or(false)
35}
36
37/// Return true when an edge of `prefix` provides a required separator between
38/// the start of a match and each literal candidate.
39///
40/// More precisely, this looks at the first and last consuming children of a
41/// top-level concatenation. One of those children must be either a character
42/// class or a repetition of a character class that matches at least once. Let
43/// that child be `S`. This returns true when both of the following hold:
44///
45/// * `S` is disjoint from everything consumed by the other children in
46///   `prefix`.
47/// * `S` is disjoint from every literal in `literals`.
48///
49/// In that case, `S` acts as a separator whose characters cannot be mistaken
50/// for characters belonging to either the rest of the prefix or a literal
51/// candidate. A reverse search therefore cannot slide `S` across either one
52/// and produce a later match start from an earlier literal candidate.
53///
54/// For example, consider this prefix and literal:
55///
56/// ```text
57/// prefix  = \w+\s+
58/// literal = Holmes
59/// ```
60///
61/// The trailing `\s+` is required, is disjoint from `\w+` and cannot match
62/// anything in `Holmes`. Thus this proves the reverse suffix optimization safe
63/// for `\w+\s+Holmes`. It also works with multiple literals, such as `Holmes`
64/// and `Watson`, provided the separator is disjoint from all of them.
65///
66/// The separator may instead be the first consuming child:
67///
68/// ```text
69/// prefix  = \s[A-Za-z]{0,12}
70/// literal = ing
71/// ```
72///
73/// Here, the leading `\s` is disjoint from both `[A-Za-z]{0,12}` and `ing`.
74///
75/// This returns false when the possible separator is optional, overlaps
76/// another prefix component or can match a character in any literal. For
77/// example, neither `\s*` in `[A-Za-z]*\s*` nor `\w+` in `\w+\w+` provides
78/// the required separator.
79pub(super) fn has_disjoint_class_separator(
80    prefix: &Hir,
81    literals: &[Literal],
82) -> bool {
83    let hirs = match uncapture(prefix).kind() {
84        HirKind::Concat(hirs) if hirs.len() >= 2 => hirs,
85        _ => return false,
86    };
87    let Some(first) = hirs.iter().position(|hir| !hir_matches_empty_only(hir))
88    else {
89        return false;
90    };
91    let last =
92        hirs.iter().rposition(|hir| !hir_matches_empty_only(hir)).unwrap();
93    has_disjoint_class_separator_at(hirs, last, literals)
94        || (first != last
95            && has_disjoint_class_separator_at(hirs, first, literals))
96}
97
98fn has_disjoint_class_separator_at(
99    hirs: &[Hir],
100    separator: usize,
101    literals: &[Literal],
102) -> bool {
103    let Some(separator_class) = required_class(&hirs[separator]) else {
104        return false;
105    };
106    class_is_disjoint_from_literals(separator_class, literals)
107        && hirs.iter().enumerate().all(|(i, hir)| {
108            i == separator || hir_is_disjoint_from_class(hir, separator_class)
109        })
110}
111
112fn hir_matches_empty_only(hir: &Hir) -> bool {
113    hir.properties().maximum_len() == Some(0)
114}
115
116fn required_class(hir: &Hir) -> Option<&Class> {
117    let hir = uncapture(hir);
118    match hir.kind() {
119        HirKind::Class(cls) => Some(cls),
120        HirKind::Repetition(rep) if rep.min > 0 => {
121            match uncapture(&rep.sub).kind() {
122                HirKind::Class(cls) => Some(cls),
123                _ => None,
124            }
125        }
126        _ => None,
127    }
128}
129
130fn hir_is_disjoint_from_class(hir: &Hir, separator: &Class) -> bool {
131    match hir.kind() {
132        HirKind::Empty | HirKind::Look(_) => true,
133        HirKind::Literal(lit) => {
134            class_is_disjoint_from_literal(separator, &lit.0)
135        }
136        HirKind::Class(cls) => classes_are_disjoint(cls, separator),
137        HirKind::Repetition(rep) => {
138            hir_is_disjoint_from_class(&rep.sub, separator)
139        }
140        HirKind::Capture(capture) => {
141            hir_is_disjoint_from_class(&capture.sub, separator)
142        }
143        HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {
144            hirs.iter().all(|hir| hir_is_disjoint_from_class(hir, separator))
145        }
146    }
147}
148
149fn uncapture(mut hir: &Hir) -> &Hir {
150    while let HirKind::Capture(capture) = hir.kind() {
151        hir = &capture.sub;
152    }
153    hir
154}
155
156fn classes_are_disjoint(left: &Class, right: &Class) -> bool {
157    match (left, right) {
158        (Class::Bytes(left), Class::Bytes(right)) => {
159            let (mut intersection, other) =
160                if left.ranges().len() <= right.ranges().len() {
161                    (left.clone(), right)
162                } else {
163                    (right.clone(), left)
164                };
165            intersection.intersect(other);
166            intersection.ranges().is_empty()
167        }
168        (Class::Unicode(left), Class::Unicode(right)) => {
169            let (mut intersection, other) =
170                if left.ranges().len() <= right.ranges().len() {
171                    (left.clone(), right)
172                } else {
173                    (right.clone(), left)
174                };
175            intersection.intersect(other);
176            intersection.ranges().is_empty()
177        }
178        _ => false,
179    }
180}
181
182fn class_is_disjoint_from_literals(cls: &Class, literals: &[Literal]) -> bool {
183    literals
184        .iter()
185        .all(|lit| class_is_disjoint_from_literal(cls, lit.as_bytes()))
186}
187
188fn class_is_disjoint_from_literal(cls: &Class, lit: &[u8]) -> bool {
189    match cls {
190        Class::Bytes(cls) => {
191            lit.iter().all(|&byte| !byte_class_contains(cls, byte))
192        }
193        Class::Unicode(cls) => core::str::from_utf8(lit)
194            .map_or(false, |lit| {
195                lit.chars().all(|ch| !unicode_class_contains(cls, ch))
196            }),
197    }
198}
199
200fn hir_can_consume_byte(hir: &Hir, byte: u8) -> bool {
201    match hir.kind() {
202        HirKind::Empty | HirKind::Look(_) => false,
203        HirKind::Literal(lit) => lit.0.contains(&byte),
204        HirKind::Class(Class::Bytes(cls)) => byte_class_contains(cls, byte),
205        HirKind::Class(Class::Unicode(cls)) => {
206            // We don't check literals based on codepoints, so if
207            // we have a non-ASCII byte, we assume here that any
208            // Unicode class will match it. In practice, given
209            // the prevalence of `\w`, this is not a terrible
210            // approximation.
211            if byte > 0x7F {
212                return true;
213            }
214            let ch = char::from(byte);
215            unicode_class_contains(cls, ch)
216        }
217        HirKind::Repetition(rep) => hir_can_consume_byte(&rep.sub, byte),
218        HirKind::Capture(capture) => hir_can_consume_byte(&capture.sub, byte),
219        HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {
220            hirs.iter().any(|hir| hir_can_consume_byte(hir, byte))
221        }
222    }
223}
224
225fn byte_class_contains(cls: &regex_syntax::hir::ClassBytes, byte: u8) -> bool {
226    cls.ranges()
227        .iter()
228        .any(|range| range.start() <= byte && byte <= range.end())
229}
230
231fn unicode_class_contains(
232    cls: &regex_syntax::hir::ClassUnicode,
233    ch: char,
234) -> bool {
235    cls.ranges().iter().any(|range| range.start() <= ch && ch <= range.end())
236}