1use std::sync::LazyLock;
9
10use super::ast::{self, At, Category, ClassItem, Flags, Op, RepeatKind};
11use super::categories::{
12 CATEGORY_DIGIT_INTERVALS, CATEGORY_SPACE_INTERVALS, CATEGORY_WORD_INTERVALS,
13};
14use super::fold::expand_ignorecase;
15use super::intervals::IntervalSet;
16
17pub const PRINTABLE: &[char] = &[
20 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
21 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
22 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
23 'V', 'W', 'X', 'Y', 'Z', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.',
24 '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~', ' ',
25 '\t', '\n', '\r', '\x0b', '\x0c',
26];
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PairingAtom {
31 pub intervals: IntervalSet,
32 pub allows_zero: bool,
33 pub unbounded: bool,
34 pub max_repeat: Option<u32>,
35 pub variable_bounded: bool,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct NonPairingSlot {
41 pub is_boundary: bool,
42 pub inner: Option<Vec<Vec<Slot>>>,
43 pub unbounded: bool,
44 pub max_repeat: Option<u32>,
45 pub variable_bounded: bool,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Slot {
51 Pairing(PairingAtom),
52 NonPairing(NonPairingSlot),
53}
54
55static ASCII_DIGIT: LazyLock<IntervalSet> = LazyLock::new(|| IntervalSet::from_range(0x30, 0x39));
56static ASCII_WORD: LazyLock<IntervalSet> =
57 LazyLock::new(|| IntervalSet::new(&[(0x30, 0x39), (0x41, 0x5A), (0x5F, 0x5F), (0x61, 0x7A)]));
58static ASCII_SPACE: LazyLock<IntervalSet> =
59 LazyLock::new(|| IntervalSet::new(&[(0x09, 0x0D), (0x20, 0x20)]));
60
61fn category_intervals(category: Category, flags: Flags) -> IntervalSet {
62 let ascii = flags.ascii;
63 match category {
64 Category::Digit => {
65 if ascii {
66 ASCII_DIGIT.clone()
67 } else {
68 IntervalSet::from_normalized(CATEGORY_DIGIT_INTERVALS.to_vec())
69 }
70 }
71 Category::Space => {
72 if ascii {
73 ASCII_SPACE.clone()
74 } else {
75 IntervalSet::from_normalized(CATEGORY_SPACE_INTERVALS.to_vec())
76 }
77 }
78 Category::Word => {
79 if ascii {
80 ASCII_WORD.clone()
81 } else {
82 IntervalSet::from_normalized(CATEGORY_WORD_INTERVALS.to_vec())
83 }
84 }
85 Category::NotDigit => if ascii {
86 ASCII_DIGIT.clone()
87 } else {
88 IntervalSet::from_normalized(CATEGORY_DIGIT_INTERVALS.to_vec())
89 }
90 .complement(),
91 Category::NotSpace => if ascii {
92 ASCII_SPACE.clone()
93 } else {
94 IntervalSet::from_normalized(CATEGORY_SPACE_INTERVALS.to_vec())
95 }
96 .complement(),
97 Category::NotWord => if ascii {
98 ASCII_WORD.clone()
99 } else {
100 IntervalSet::from_normalized(CATEGORY_WORD_INTERVALS.to_vec())
101 }
102 .complement(),
103 }
104}
105
106fn apply_ignorecase(intervals: &IntervalSet, flags: Flags) -> IntervalSet {
107 if !flags.ignorecase {
108 return intervals.clone();
109 }
110 expand_ignorecase(intervals, flags.ascii)
111}
112
113fn member_intervals(item: &ClassItem, flags: Flags) -> IntervalSet {
114 match item {
115 ClassItem::Category(category) => category_intervals(*category, flags),
116 ClassItem::Range(low, high) => {
117 apply_ignorecase(&IntervalSet::from_range(*low, *high), flags)
118 }
119 ClassItem::Literal(cp) => apply_ignorecase(&IntervalSet::single(*cp), flags),
120 ClassItem::Negate => IntervalSet::empty(),
121 }
122}
123
124#[must_use]
126pub fn in_intervals(items: &[ClassItem], flags: Flags) -> IntervalSet {
127 let mut negate = false;
128 let mut member = IntervalSet::empty();
129 for item in items {
130 if matches!(item, ClassItem::Negate) {
131 negate = true;
132 }
133 member = member.union(&member_intervals(item, flags));
136 }
137 if negate { member.complement() } else { member }
138}
139
140fn any_intervals(flags: Flags) -> IntervalSet {
141 if flags.dotall {
142 IntervalSet::full()
143 } else {
144 IntervalSet::full().difference(&IntervalSet::single(u32::from('\n')))
145 }
146}
147
148#[must_use]
150pub fn node_intervals(op: &Op, flags: Flags) -> IntervalSet {
151 match op {
152 Op::NotLiteral(cp) => apply_ignorecase(&IntervalSet::single(*cp), flags).complement(),
153 Op::In(items) => in_intervals(items, flags),
154 Op::Any => any_intervals(flags),
155 Op::Category(category) => category_intervals(*category, flags),
156 Op::Literal(cp) => apply_ignorecase(&IntervalSet::single(*cp), flags),
157 _ => IntervalSet::empty(),
158 }
159}
160
161fn pairing_atom(
162 op: &Op,
163 flags: Flags,
164 allows_zero: bool,
165 unbounded: bool,
166 max_repeat: Option<u32>,
167 variable_bounded: bool,
168) -> Slot {
169 Slot::Pairing(PairingAtom {
170 intervals: node_intervals(op, flags),
171 allows_zero,
172 unbounded,
173 max_repeat,
174 variable_bounded,
175 })
176}
177
178fn nonpairing_slot(
179 op: &Op,
180 flags: Flags,
181 allows_zero: bool,
182 unbounded: bool,
183 max_repeat: Option<u32>,
184 variable_bounded: bool,
185) -> Slot {
186 match op {
187 Op::Branch(alternatives) => Slot::NonPairing(NonPairingSlot {
188 is_boundary: !allows_zero,
189 inner: Some(
190 alternatives
191 .iter()
192 .map(|alt| walk_sequence(alt, flags))
193 .collect(),
194 ),
195 unbounded,
196 max_repeat,
197 variable_bounded,
198 }),
199 Op::SubPattern { add, del, body, .. } => {
200 let mut child_flags = flags;
201 child_flags.apply_delta(add, del);
202 Slot::NonPairing(NonPairingSlot {
203 is_boundary: !allows_zero,
204 inner: Some(sequence_to_alternatives(body, child_flags)),
205 unbounded,
206 max_repeat,
207 variable_bounded,
208 })
209 }
210 Op::Assert { body, .. } => Slot::NonPairing(NonPairingSlot {
211 is_boundary: false,
212 inner: Some(sequence_to_alternatives(body, flags)),
213 unbounded: false,
214 max_repeat: None,
215 variable_bounded: false,
216 }),
217 Op::At(At::Boundary) | Op::At(At::NonBoundary) | Op::At(_) => {
218 Slot::NonPairing(NonPairingSlot {
219 is_boundary: false,
220 inner: None,
221 unbounded: false,
222 max_repeat: None,
223 variable_bounded: false,
224 })
225 }
226 _ => Slot::NonPairing(NonPairingSlot {
227 is_boundary: true,
228 inner: None,
229 unbounded: false,
230 max_repeat: None,
231 variable_bounded: false,
232 }),
233 }
234}
235
236fn unrepeated_slot(op: &Op, flags: Flags) -> Slot {
237 if op.is_pairing() {
238 return pairing_atom(op, flags, false, false, None, false);
239 }
240 nonpairing_slot(op, flags, false, false, None, false)
241}
242
243fn repeat_slot(kind: RepeatKind, low: u32, high: Option<u32>, body: &[Op], flags: Flags) -> Slot {
244 let _ = kind;
245 let allows_zero = low == 0;
246 let unbounded = high.is_none();
247 let variable_bounded = !unbounded && low < high.unwrap_or(low);
248 let max_repeat = high;
249 if body.len() == 1 {
250 let inner = &body[0];
251 if inner.is_pairing() {
252 return pairing_atom(
253 inner,
254 flags,
255 allows_zero,
256 unbounded,
257 max_repeat,
258 variable_bounded,
259 );
260 }
261 return nonpairing_slot(
262 inner,
263 flags,
264 allows_zero,
265 unbounded,
266 max_repeat,
267 variable_bounded,
268 );
269 }
270 Slot::NonPairing(NonPairingSlot {
271 is_boundary: !allows_zero,
272 inner: Some(sequence_to_alternatives(body, flags)),
273 unbounded,
274 max_repeat,
275 variable_bounded,
276 })
277}
278
279fn slot_for_flat_item(op: &Op, flags: Flags) -> Slot {
280 match op {
281 Op::Repeat {
282 kind,
283 low,
284 high,
285 body,
286 } => repeat_slot(*kind, *low, *high, body, flags),
287 other => unrepeated_slot(other, flags),
288 }
289}
290
291#[must_use]
293pub fn walk_sequence(ops: &[Op], flags: Flags) -> Vec<Slot> {
294 ops.iter().map(|op| slot_for_flat_item(op, flags)).collect()
295}
296
297#[must_use]
299pub fn sequence_to_alternatives(ops: &[Op], flags: Flags) -> Vec<Vec<Slot>> {
300 if let [op] = ops
301 && let Op::Branch(alternatives) = op
302 {
303 return alternatives
304 .iter()
305 .map(|alt| walk_sequence(alt, flags))
306 .collect();
307 }
308 vec![walk_sequence(ops, flags)]
309}
310
311#[must_use]
313pub fn pattern_slots(pattern: &str, flags: Flags) -> Option<Vec<Slot>> {
314 let (ops, final_flags) = ast::parse(pattern, flags).ok()?;
315 Some(walk_sequence(&ops, final_flags))
316}
317
318#[must_use]
321pub fn candidate_chars_for_atom_text(atom_text: &str, flags: Flags) -> Vec<char> {
322 let Ok((ops, final_flags)) = ast::parse(atom_text, flags) else {
323 return Vec::new();
324 };
325 if ops.len() != 1 {
326 return Vec::new();
327 }
328 node_intervals(&ops[0], final_flags)
329 .component_first_members()
330 .iter()
331 .filter_map(|cp| char::from_u32(*cp))
332 .collect()
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn expect_pairing(slot: &Slot) -> &PairingAtom {
341 match slot {
342 Slot::Pairing(atom) => atom,
343 other => panic!("expected pairing, got {other:?}"),
344 }
345 }
346
347 #[test]
348 fn expect_pairing_rejects_non_pairing_slots() {
349 let non = Slot::NonPairing(NonPairingSlot {
350 is_boundary: true,
351 inner: None,
352 unbounded: false,
353 max_repeat: None,
354 variable_bounded: false,
355 });
356 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
357 expect_pairing(&non);
358 }));
359 assert!(result.is_err(), "non-pairing slots must be rejected");
360 }
361
362 fn expect_non_pairing(slot: &Slot) -> &NonPairingSlot {
364 match slot {
365 Slot::NonPairing(non) => non,
366 other => panic!("expected non-pairing, got {other:?}"),
367 }
368 }
369
370 #[test]
371 fn candidate_chars_for_a_single_class_are_component_starts() {
372 let digit_starts = candidate_chars_for_atom_text(r"\d", Flags::default());
375 assert_eq!(digit_starts.first().copied(), Some('0'));
376 assert_eq!(digit_starts.len(), 71);
377 assert_eq!(
378 candidate_chars_for_atom_text("[a-c]", Flags::default()),
379 vec!['a']
380 );
381 }
382
383 #[test]
384 fn candidate_chars_empty_for_parse_failures_and_multi_node_texts() {
385 assert!(candidate_chars_for_atom_text("[oops", Flags::default()).is_empty());
386 assert!(candidate_chars_for_atom_text("ab", Flags::default()).is_empty());
387 }
388
389 #[test]
390 fn candidate_chars_reflect_inline_dotall() {
391 let chars = candidate_chars_for_atom_text("(?s).", Flags::default());
394 assert_eq!(chars, vec!['\0']);
395 let chars = candidate_chars_for_atom_text(".", Flags::default());
396 assert_eq!(chars, vec!['\0', '\u{b}']);
397 }
398
399 #[test]
400 fn pattern_slots_none_on_a_parse_failure() {
401 assert!(pattern_slots("[unterminated", Flags::default()).is_none());
402 }
403
404 #[test]
405 fn negated_class_is_a_pairing_atom_with_the_complement() {
406 let slots = pattern_slots("[^a]", Flags::default()).expect("parses");
407 assert_eq!(slots.len(), 1);
408 let atom = expect_pairing(&slots[0]);
409 assert!(!atom.allows_zero);
410 assert!(!atom.unbounded);
411 let expected = IntervalSet::full().difference(&IntervalSet::single(u32::from('a')));
412 assert_eq!(atom.intervals, expected);
413 }
414
415 #[test]
416 fn dotall_widens_any_to_the_full_alphabet() {
417 let dotted = pattern_slots("(?s).", Flags::default()).expect("parses");
418 let plain = pattern_slots(".", Flags::default()).expect("parses");
419 let dotted_atom = expect_pairing(&dotted[0]);
420 let plain_atom = expect_pairing(&plain[0]);
421 assert_eq!(dotted_atom.intervals, IntervalSet::full());
422 assert_eq!(
423 plain_atom.intervals,
424 IntervalSet::full().difference(&IntervalSet::single(u32::from('\n')))
425 );
426 }
427
428 #[test]
429 fn backreference_is_a_hard_boundary_slot() {
430 let slots = pattern_slots(r"(a)\1", Flags::default()).expect("parses");
431 assert_eq!(slots.len(), 2);
432 let non = expect_non_pairing(&slots[1]);
433 assert!(non.is_boundary);
434 assert!(non.inner.is_none());
435 }
436
437 #[test]
438 fn quantified_wrapped_group_keeps_its_inner_slots() {
439 let slots = pattern_slots(r"(\s)+", Flags::default()).expect("parses");
440 let non = expect_non_pairing(&slots[0]);
441 assert!(non.unbounded);
442 assert!(non.is_boundary);
445 let inner = non.inner.as_ref().expect("group body");
446 assert_eq!(inner.len(), 1);
447 }
448
449 #[test]
450 fn bounded_repeat_carries_max_repeat_and_variability() {
451 let slots = pattern_slots(r"a{2,5}", Flags::default()).expect("parses");
452 let atom = expect_pairing(&slots[0]);
453 assert_eq!(atom.max_repeat, Some(5));
454 assert!(atom.variable_bounded);
455 assert!(!atom.unbounded);
456 }
457
458 #[test]
459 fn zero_low_repeat_allows_zero() {
460 let slots = pattern_slots(r"a*", Flags::default()).expect("parses");
461 let atom = expect_pairing(&slots[0]);
462 assert!(atom.allows_zero);
463 assert!(atom.unbounded);
464 }
465
466 #[test]
467 fn assertions_are_non_boundary_slots() {
468 let slots = pattern_slots(r"(?=a)", Flags::default()).expect("parses");
469 let non = expect_non_pairing(&slots[0]);
470 assert!(!non.is_boundary);
471 assert!(non.inner.is_some());
472 }
473
474 #[test]
475 fn anchors_and_failure_slots() {
476 let slots = pattern_slots(r"\b", Flags::default()).expect("parses");
477 let non = expect_non_pairing(&slots[0]);
478 assert!(!non.is_boundary);
479 assert!(non.inner.is_none());
480 let slots = pattern_slots(r"(?!)", Flags::default()).expect("parses");
481 let non = expect_non_pairing(&slots[0]);
482 assert!(non.is_boundary);
483 }
484
485 #[test]
486 fn sequence_to_alternatives_splits_top_level_branches() {
487 let (ops, _) = crate::redos::ast::parse("a|b", Flags::default()).expect("parses");
488 let alternatives = sequence_to_alternatives(&ops, Flags::default());
489 assert_eq!(alternatives.len(), 2);
490 let (ops, _) = crate::redos::ast::parse("ab", Flags::default()).expect("parses");
491 let alternatives = sequence_to_alternatives(&ops, Flags::default());
492 assert_eq!(alternatives.len(), 1);
493 }
494
495 #[test]
496 fn printable_alphabet_matches_the_reference_layout() {
497 assert_eq!(PRINTABLE.first(), Some(&'0'));
499 assert_eq!(PRINTABLE[10], 'a');
500 assert_eq!(PRINTABLE[36], 'A');
501 assert_eq!(PRINTABLE[62], '!');
502 assert_eq!(PRINTABLE.last(), Some(&'\x0c'));
503 assert_eq!(PRINTABLE.len(), 100);
504 }
505
506 #[test]
507 fn node_intervals_handles_category_nodes_directly() {
508 let intervals = node_intervals(&Op::Category(Category::Digit), Flags::default());
509 assert!(intervals.contains(u32::from('5')));
510 assert!(!intervals.contains(u32::from('x')));
511 }
512
513 #[test]
514 fn node_intervals_folds_not_literal_under_ignorecase() {
515 let flags = Flags {
516 ignorecase: true,
517 ..Flags::default()
518 };
519 let intervals = node_intervals(&Op::NotLiteral(u32::from('a')), flags);
520 assert!(!intervals.contains(u32::from('a')));
521 assert!(!intervals.contains(u32::from('A')));
522 }
523
524 #[test]
525 fn node_intervals_literal_folds_under_ignorecase() {
526 let flags = Flags {
527 ignorecase: true,
528 ..Flags::default()
529 };
530 let intervals = node_intervals(&Op::Literal(u32::from('a')), flags);
531 assert!(intervals.contains(u32::from('A')));
532 }
533
534 #[test]
535 fn ascii_flag_narrows_the_digit_category() {
536 let flags = Flags {
537 ascii: true,
538 ..Flags::default()
539 };
540 let intervals = node_intervals(&Op::Category(Category::Digit), flags);
541 assert!(intervals.contains(u32::from('5')));
542 assert!(!intervals.contains(0x0660));
543 }
544
545 #[test]
546 fn in_intervals_negates_after_the_member_union() {
547 let items = vec![ClassItem::Negate, ClassItem::Literal(u32::from('a'))];
548 let intervals = in_intervals(&items, Flags::default());
549 assert!(intervals.contains(u32::from('b')));
550 assert!(!intervals.contains(u32::from('a')));
551 }
552
553 #[test]
554 #[should_panic(expected = "expected pairing")]
555 fn expect_pairing_rejects_other_variants() {
556 let _ = expect_pairing(&Slot::NonPairing(NonPairingSlot {
557 is_boundary: false,
558 inner: None,
559 unbounded: false,
560 max_repeat: None,
561 variable_bounded: false,
562 }));
563 }
564
565 #[test]
566 #[should_panic(expected = "expected non-pairing")]
567 fn expect_non_pairing_rejects_other_variants() {
568 let _ = expect_non_pairing(&Slot::Pairing(PairingAtom {
569 intervals: IntervalSet::empty(),
570 allows_zero: false,
571 unbounded: false,
572 max_repeat: None,
573 variable_bounded: false,
574 }));
575 }
576
577 #[test]
578 fn ascii_flags_pick_the_ascii_category_tables() {
579 let ascii = crate::redos::ast::Flags {
580 ascii: true,
581 ..Flags::default()
582 };
583 for pattern in [r"\d", r"\s", r"\w", r"\D", r"\S", r"\W"] {
584 let parsed = pattern_slots(pattern, ascii).expect("parses");
585 assert_eq!(parsed.len(), 1, "{pattern}");
586 let atom = expect_pairing(&parsed[0]);
587 assert!(!atom.intervals.is_empty(), "{pattern}");
588 }
589 }
590
591 #[test]
592 fn negated_categories_complement_the_unicode_tables() {
593 for pattern in [r"\D", r"\S", r"\W"] {
594 let parsed = pattern_slots(pattern, Flags::default()).expect("parses");
595 assert_eq!(parsed.len(), 1, "{pattern}");
596 let atom = expect_pairing(&parsed[0]);
597 assert!(!atom.intervals.is_empty(), "{pattern}");
598 }
599 }
600
601 #[test]
602 fn negated_classes_start_from_an_empty_set() {
603 let parsed = pattern_slots(r"[^ab]", Flags::default()).expect("parses");
605 assert_eq!(parsed.len(), 1);
606 }
607
608 #[test]
609 fn node_intervals_of_non_class_ops_is_empty() {
610 assert_eq!(
611 node_intervals(&Op::At(crate::redos::ast::At::Beginning), Flags::default()),
612 IntervalSet::empty()
613 );
614 assert_eq!(
615 node_intervals(&Op::GroupRef(1), Flags::default()),
616 IntervalSet::empty()
617 );
618 }
619}