Skip to main content

trex/
builder.rs

1//! A pattern built with its readings named, rather than parsed and taken as
2//! it comes.
3//!
4//! The regex crate's `RegexBuilder` carries twelve knobs. Most of them decide
5//! how bytes are grouped into the things a pattern matches - whether a dot
6//! crosses a newline, what counts as a line terminator, whether a character
7//! class folds case, whether whitespace in the pattern is significant. Over
8//! tokens those questions are already answered, and not by a pattern flag:
9//! the lexer decides what a token is, and it decided before any pattern was
10//! compiled.
11//!
12//! So this builder carries the knobs that still mean something once the
13//! alphabet is tokens:
14//!
15//! - **case folding**, because comparing a literal to a token is a comparison
16//!   and an equivalence can be chosen for it. It is [`OrbitGroup::Case`],
17//!   the same one `"Cat"~case` names per literal, applied to every literal at
18//!   once.
19//! - **swapped greed**, because a quantifier's preference is expressed by the
20//!   order the engine queues its threads, which is a property of the pattern
21//!   and not of the alphabet.
22//! - **nesting depth**, because the parser descends recursively, so depth is
23//!   stack and a caller on a small stack may want a smaller ceiling than
24//!   [`crate::parser::NEST_LIMIT`].
25//! - **the empty-loop reading**, which decides what a repetition whose body
26//!   matches nothing does, and which trex already exposes because the two
27//!   readings genuinely disagree.
28//!
29//! The other regex knobs have no counterpart here, and it is worth being
30//! exact about why rather than listing them as missing. `ignore_whitespace`
31//! is meaningless because whitespace already separates atoms in a trex
32//! pattern rather than being matchable text. `dot_matches_new_line`,
33//! `multi_line`, `crlf` and `line_terminator` are lexer questions: `.` is one
34//! token and never a byte, and `^` anchors to a line the lexer has already
35//! cut. `octal` describes an escape syntax this language does not have.
36//! `unicode` selects character classes, where a token's kind comes from the
37//! lexer. `size_limit` and `dfa_size_limit` bound a compiled automaton that
38//! is never built - the engine simulates the program over the token stream
39//! and its memory is a function of the pattern's size, not of a table.
40
41use crate::ast::{EmptyLoop, Greed, Pattern};
42use crate::orbit::OrbitGroup;
43use crate::parser::ParseError;
44
45/// A pattern's source together with the readings it is to be parsed under.
46pub struct PatternBuilder<'s> {
47    src: &'s str,
48    orbit: Option<OrbitGroup>,
49    swap_greed: bool,
50    empty: EmptyLoop,
51    nest_limit: u32,
52}
53
54impl<'s> PatternBuilder<'s> {
55    /// A builder over `src` with every reading at its default, so
56    /// `PatternBuilder::new(s).build()` is [`crate::parse`].
57    #[must_use]
58    pub fn new(src: &'s str) -> Self {
59        PatternBuilder {
60            src,
61            orbit: None,
62            swap_greed: false,
63            empty: EmptyLoop::Thompson,
64            nest_limit: crate::parser::NEST_LIMIT,
65        }
66    }
67
68    /// The pattern text this builder was given.
69    ///
70    /// The nearest thing to the regex crate's `as_str`, which exists because a
71    /// compiled `Regex` is opaque: once built, the source is the only window
72    /// into what it matches. A [`Pattern`] is a public tree the caller can walk
73    /// and match on, so the question `as_str` answers does not arise for one -
74    /// and a builder is the only place in this crate that holds the text after
75    /// parsing, so it is the only place the accessor belongs.
76    #[must_use]
77    pub fn as_str(&self) -> &'s str {
78        self.src
79    }
80
81    /// Compare every literal in the pattern under case folding, so `"Cat"`
82    /// matches `cat` and `CAT`.
83    ///
84    /// A register-equality atom keeps whichever group its own spelling gave
85    /// it: `=case x` is a comparison the author named, and widening it here
86    /// would make the pattern mean something other than what was written.
87    #[must_use]
88    pub fn case_insensitive(mut self, yes: bool) -> Self {
89        self.orbit = yes.then_some(OrbitGroup::Case);
90        self
91    }
92
93    /// Compare every literal in the pattern under `group`.
94    ///
95    /// [`Self::case_insensitive`] is this with [`OrbitGroup::Case`], and it is
96    /// the only rung the regex crate has a name for. The others have no
97    /// counterpart there because a regular expression compares bytes and
98    /// these are equivalences over tokens:
99    ///
100    /// - [`OrbitGroup::Notation`] folds case and notation together, so
101    ///   `theta`, `\theta` and the Greek letter are one token.
102    /// - [`OrbitGroup::Shape`] compares a token's consonant-vowel-digit
103    ///   shape, so `"cat"` matches `dog` and `bat`. A whole pattern under
104    ///   this rung is a structural search: find anything shaped like this,
105    ///   whatever it says.
106    /// - [`OrbitGroup::E8`] compares the token's eight-channel profile
107    ///   quotiented by the E8 reflection group, which is the one rung whose
108    ///   equivalence is not a string relation at all.
109    ///
110    /// A register-equality atom keeps whichever group its own spelling gave
111    /// it, at every rung: `=shape x` is a comparison the author named.
112    #[must_use]
113    pub fn orbit(mut self, group: OrbitGroup) -> Self {
114        self.orbit = Some(group);
115        self
116    }
117
118    /// Swap every quantifier's preference, so `*` prefers the shortest match
119    /// and `*?` the longest.
120    #[must_use]
121    pub fn swap_greed(mut self, yes: bool) -> Self {
122        self.swap_greed = yes;
123        self
124    }
125
126    /// Refuse a pattern nesting deeper than `limit` groups.
127    #[must_use]
128    pub fn nest_limit(mut self, limit: u32) -> Self {
129        self.nest_limit = limit;
130        self
131    }
132
133    /// Which reading a repetition whose body can match nothing takes.
134    #[must_use]
135    pub fn empty_loop(mut self, empty: EmptyLoop) -> Self {
136        self.empty = empty;
137        self
138    }
139
140    /// The pattern, or the error that says why the source is not one.
141    ///
142    /// # Errors
143    ///
144    /// A malformed pattern, or one nesting deeper than the limit.
145    pub fn build(self) -> Result<Pattern, ParseError> {
146        let (src, _) = crate::parser::split_empty_loop(self.src)?;
147        let shapes = crate::custom::ShapeSet::new();
148        let mut pat = crate::parser::parse_with_shapes_to_depth(src, &shapes, self.nest_limit)?;
149        if let Some(group) = self.orbit {
150            crate::parser::set_orbit(&mut pat, group);
151        }
152        if self.swap_greed {
153            swap_greed(&mut pat);
154        }
155        Ok(pat)
156    }
157
158    /// The empty-loop reading a scan of this pattern should take: the one the
159    /// source named with `(?empty:...)`, or the one this builder was given.
160    ///
161    /// The source wins because it is part of the pattern, and a pattern that
162    /// says how its own empty loops read is making a claim about what it
163    /// means rather than about how some caller wants it run.
164    ///
165    /// # Errors
166    ///
167    /// A malformed `(?empty:...)` directive.
168    pub fn reading(&self) -> Result<EmptyLoop, ParseError> {
169        let (_, named) = crate::parser::split_empty_loop(self.src)?;
170        if self.src.trim_start().starts_with("(?empty:") {
171            return Ok(named);
172        }
173        Ok(self.empty)
174    }
175}
176
177/// Flip the preference of every quantifier in `pat`.
178fn swap_greed(pat: &mut Pattern) {
179    let flip = |g: &mut Greed| {
180        *g = match *g {
181            Greed::Greedy => Greed::Lazy,
182            Greed::Lazy => Greed::Greedy,
183        };
184    };
185    match pat {
186        Pattern::Star(p, g) | Pattern::Plus(p, g) | Pattern::Opt(p, g) => {
187            flip(g);
188            swap_greed(p);
189        }
190        Pattern::Repeat(p, _, _, g) => {
191            flip(g);
192            swap_greed(p);
193        }
194        Pattern::Bind(_, _, p)
195        | Pattern::Balanced(_, p)
196        | Pattern::Field(_, p)
197        | Pattern::Atomic(p)
198        | Pattern::Assert(p, _, _) => swap_greed(p),
199        Pattern::Concat(v) | Pattern::Alt(v, _) => {
200            for p in v {
201                swap_greed(p);
202            }
203        }
204        Pattern::Empty
205        | Pattern::Atom(_)
206        | Pattern::Within(..)
207        | Pattern::Guard(..)
208        | Pattern::Anchor(_) => {}
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn a_builder_at_its_defaults_is_the_ordinary_parse() {
218        for src in ["\"alpha\"", "\\W \"=\" \\N", "(\"a\" | \"b\")*", "\\W:x \"=\" =x"] {
219            let built = PatternBuilder::new(src).build().expect("builds");
220            let plain = crate::parse(src).expect("parses");
221            assert_eq!(built, plain, "{src}");
222        }
223    }
224
225    #[test]
226    fn the_source_names_the_empty_loop_reading_and_the_builder_defers_to_it() {
227        // A pattern that says how its own empty loops read is making a claim
228        // about what it means; a builder default is a caller's preference,
229        // and the claim wins.
230        let named = PatternBuilder::new("(?empty:perl)\\W*").empty_loop(EmptyLoop::Thompson);
231        assert_eq!(named.reading().expect("the directive is well formed"), EmptyLoop::Perl);
232        named.build().expect("a source with a directive still builds");
233
234        let unnamed = PatternBuilder::new("\\W*").empty_loop(EmptyLoop::Perl);
235        assert_eq!(unnamed.reading().expect("no directive to malform"), EmptyLoop::Perl);
236    }
237
238    #[test]
239    fn case_folding_makes_a_literal_match_the_other_spellings() {
240        let input = b"the Cat sat on the CAT and the cat" as &[u8];
241        let folded = PatternBuilder::new("\"cat\"").case_insensitive(true).build().expect("builds");
242        let exact = PatternBuilder::new("\"cat\"").build().expect("builds");
243        assert_eq!(crate::scan(&folded, input).len(), 3, "Cat, CAT and cat");
244        assert_eq!(crate::scan(&exact, input).len(), 1, "only the exact one");
245    }
246
247    #[test]
248    fn a_whole_pattern_under_the_shape_orbit_is_a_structural_search() {
249        // The rung with no regular-expression counterpart: the literal stops
250        // asking for its own characters and asks for its consonant-vowel
251        // shape, so the pattern finds anything built the same way.
252        let input = b"the cat and the dog and the bat and the a1 thing" as &[u8];
253        let shaped = PatternBuilder::new("\"cat\"").orbit(OrbitGroup::Shape).build().expect("builds");
254        let exact = PatternBuilder::new("\"cat\"").build().expect("builds");
255        let found = crate::scan(&shaped, input);
256        assert!(found.len() > crate::scan(&exact, input).len(), "the shape matches more than one word");
257        for s in &found {
258            let word = &input[s.range()];
259            assert_eq!(word.len(), 3, "every match is three letters like cat: {:?}", word);
260        }
261    }
262
263    #[test]
264    fn case_folding_leaves_a_named_register_comparison_alone() {
265        // `=x` with no group named compares exactly; folding the pattern must
266        // not widen a comparison the author wrote, or `\W:x "=" =x` would
267        // start matching `A = a`.
268        let folded = PatternBuilder::new("\\W:x \"=\" =x")
269            .case_insensitive(true)
270            .build()
271            .expect("builds");
272        assert!(crate::scan(&folded, b"alpha = alpha").len() == 1, "the same word still matches");
273        assert!(crate::scan(&folded, b"Alpha = alpha").is_empty(), "a differing case does not");
274    }
275
276    #[test]
277    fn swapping_greed_turns_the_longest_preference_into_the_shortest() {
278        let input = b"a b c d ;" as &[u8];
279        let greedy = PatternBuilder::new("\\W+").build().expect("builds");
280        let lazy = PatternBuilder::new("\\W+").swap_greed(true).build().expect("builds");
281        let g = crate::scan(&greedy, input);
282        let l = crate::scan(&lazy, input);
283        assert_eq!(g.len(), 1, "greedy takes all four words as one match");
284        assert_eq!(l.len(), 4, "swapped, each word is its own match");
285    }
286
287    #[test]
288    fn swapping_greed_twice_is_swapping_it_not_at_all() {
289        for src in ["\\W+", "\\W*?", "\\W{2,5}", "(\\W | \\N)+?"] {
290            let once = PatternBuilder::new(src).swap_greed(true).build().expect("builds");
291            let mut twice = once.clone();
292            super::swap_greed(&mut twice);
293            assert_eq!(twice, crate::parse(src).expect("parses"), "{src}");
294        }
295    }
296
297    #[test]
298    fn a_pattern_nested_past_the_limit_is_an_error_and_not_a_crash() {
299        // The case the limit exists for: without it this recurses until the
300        // stack is gone, which a caller cannot catch.
301        let deep = format!("{}\"a\"{}", "(".repeat(5_000), ")".repeat(5_000));
302        let e = PatternBuilder::new(&deep).build().expect_err("must refuse");
303        assert!(e.msg.contains("nests deeper"), "the reason names the depth: {}", e.msg);
304        assert!(crate::parse(&deep).is_err(), "the ordinary parse refuses it too");
305    }
306
307    #[test]
308    fn a_pattern_inside_the_limit_still_parses() {
309        // Half the limit, read from the limit rather than written down, since
310        // the limit is what the build's stack holds and a debug build holds
311        // far less than a release one.
312        let depth = (crate::parser::NEST_LIMIT / 2) as usize;
313        let ok = format!("{}\"a\"{}", "(".repeat(depth), ")".repeat(depth));
314        PatternBuilder::new(&ok).build().expect("half the limit is fine");
315        // And a limit a caller chose is the one that applies.
316        let e = PatternBuilder::new(&ok).nest_limit(10).build().expect_err("must refuse at ten");
317        assert!(e.msg.contains("nests deeper"), "{}", e.msg);
318    }
319}