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}