re_parser/ast.rs
1/// A parsed regex AST node.
2#[derive(Debug, Clone, PartialEq)]
3pub enum Regex {
4 /// Matches a single literal character, e.g. `a`.
5 Literal(char),
6
7 /// Matches any character except newline (or including it with `s` flag): `.`
8 AnyChar,
9
10 /// Zero-width assertion: `^`, `$`, `\b`, `\B`.
11 Anchor(Anchor),
12
13 /// Predefined character class shorthand: `\d`, `\w`, `\s` and their negations.
14 EscapeClass(EscapeClass),
15
16 /// A character class expression: `[abc]`, `[^a-z]`, `[\d\w]`.
17 CharClass(CharClass),
18
19 /// A group: capturing, non-capturing, or a lookaround assertion.
20 Group(Box<Regex>, GroupKind),
21
22 /// A quantifier applied to a sub-expression. The `bool` is `true` for greedy.
23 Quantifier(Box<Regex>, QuantKind, bool),
24
25 /// A sequence of sub-expressions matched one after the other.
26 Concat(Vec<Regex>),
27
28 /// An alternation of sub-expressions: `a|b|c`.
29 Alternation(Vec<Regex>),
30}
31
32/// Zero-width boundary assertions.
33#[derive(Debug, Clone, PartialEq)]
34pub enum Anchor {
35 /// `^` — start of string (or line in multiline mode).
36 Start,
37 /// `$` — end of string (or line in multiline mode).
38 End,
39 /// `\b` — word boundary.
40 WordBoundary,
41 /// `\B` — non-word boundary.
42 NonWordBoundary,
43}
44
45/// Predefined shorthand character classes.
46#[derive(Debug, Clone, PartialEq)]
47pub enum EscapeClass {
48 /// `\d` — any ASCII digit `[0-9]`.
49 Digit,
50 /// `\D` — any non-digit.
51 NonDigit,
52 /// `\w` — word character `[a-zA-Z0-9_]`.
53 Word,
54 /// `\W` — non-word character.
55 NonWord,
56 /// `\s` — whitespace `[ \t\n\r\f\v]`.
57 Space,
58 /// `\S` — non-whitespace.
59 NonSpace,
60}
61
62/// A character class `[...]`.
63#[derive(Debug, Clone, PartialEq)]
64pub struct CharClass {
65 /// Items inside the brackets.
66 pub items: Vec<CharClassItem>,
67 /// `true` when the class is negated with `^`, e.g. `[^abc]`.
68 pub negated: bool,
69}
70
71/// A single item inside `[...]`.
72#[derive(Debug, Clone, PartialEq)]
73pub enum CharClassItem {
74 /// A single literal character.
75 Literal(char),
76 /// A character range, e.g. `a-z`. Invariant: `start <= end`.
77 Range(char, char),
78 /// An escape-class shorthand reused inside a bracket, e.g. `[\d]`.
79 EscapeClass(EscapeClass),
80}
81
82/// How a group should be treated by a regex engine.
83#[derive(Debug, Clone, PartialEq)]
84pub enum GroupKind {
85 /// Plain `(...)` — captures and numbers the match.
86 Capturing,
87 /// `(?P<name>...)` — captures with a name.
88 Named(String),
89 /// `(?:...)` — groups without capturing.
90 NonCapturing,
91 /// `(?=...)` — positive lookahead.
92 LookaheadPos,
93 /// `(?!...)` — negative lookahead.
94 LookaheadNeg,
95 /// `(?<=...)` — positive lookbehind.
96 LookbehindPos,
97 /// `(?<!...)` — negative lookbehind.
98 LookbehindNeg,
99}
100
101/// The kind of repetition applied by a quantifier.
102#[derive(Debug, Clone, PartialEq)]
103pub enum QuantKind {
104 /// `*` — zero or more.
105 ZeroOrMore,
106 /// `+` — one or more.
107 OneOrMore,
108 /// `?` — zero or one.
109 ZeroOrOne,
110 /// `{n}` — exactly *n* repetitions.
111 Exactly(usize),
112 /// `{n,}` — at least *n* repetitions.
113 AtLeast(usize),
114 /// `{n,m}` — between *n* and *m* repetitions (inclusive).
115 Between(usize, usize),
116}
117
118// ── width analysis ────────────────────────────────────────────────────────────
119
120impl Regex {
121 /// Returns the minimum number of characters this node can match.
122 ///
123 /// Anchors (`^`, `$`, `\b`) and lookarounds contribute **zero** because
124 /// they are zero-width assertions.
125 ///
126 /// ```rust
127 /// use re_parser::parse;
128 ///
129 /// assert_eq!(parse(r"\d{2,4}").unwrap().min_width(), 2);
130 /// assert_eq!(parse(r"^abc$").unwrap().min_width(), 3); // anchors are zero-width
131 /// assert_eq!(parse(r"a*").unwrap().min_width(), 0);
132 /// ```
133 pub fn min_width(&self) -> usize {
134 match self {
135 Regex::Literal(_) | Regex::AnyChar | Regex::EscapeClass(_) | Regex::CharClass(_) => 1,
136
137 Regex::Anchor(_) => 0,
138
139 Regex::Group(inner, kind) => match kind {
140 GroupKind::LookaheadPos
141 | GroupKind::LookaheadNeg
142 | GroupKind::LookbehindPos
143 | GroupKind::LookbehindNeg => 0,
144 _ => inner.min_width(),
145 },
146
147 Regex::Quantifier(inner, kind, _) => match kind {
148 QuantKind::ZeroOrMore | QuantKind::ZeroOrOne => 0,
149 QuantKind::OneOrMore => inner.min_width(),
150 QuantKind::Exactly(n) => inner.min_width().saturating_mul(*n),
151 QuantKind::AtLeast(n) => inner.min_width().saturating_mul(*n),
152 QuantKind::Between(n, _) => inner.min_width().saturating_mul(*n),
153 },
154
155 Regex::Concat(nodes) => nodes.iter().map(Regex::min_width).sum(),
156
157 Regex::Alternation(nodes) => {
158 nodes.iter().map(Regex::min_width).min().unwrap_or(0)
159 }
160 }
161 }
162
163 /// Returns the maximum number of characters this node can match, or `None`
164 /// if the match length is unbounded (the node contains `*`, `+`, or
165 /// `{n,}`).
166 ///
167 /// ```rust
168 /// use re_parser::parse;
169 ///
170 /// assert_eq!(parse(r"\d{2,4}").unwrap().max_width(), Some(4));
171 /// assert_eq!(parse(r"a+").unwrap().max_width(), None); // unbounded
172 /// assert_eq!(parse(r"foo(?=bar)").unwrap().max_width(), Some(3)); // lookahead is zero-width
173 /// ```
174 pub fn max_width(&self) -> Option<usize> {
175 match self {
176 Regex::Literal(_) | Regex::AnyChar | Regex::EscapeClass(_) | Regex::CharClass(_) => {
177 Some(1)
178 }
179
180 Regex::Anchor(_) => Some(0),
181
182 Regex::Group(inner, kind) => match kind {
183 GroupKind::LookaheadPos
184 | GroupKind::LookaheadNeg
185 | GroupKind::LookbehindPos
186 | GroupKind::LookbehindNeg => Some(0),
187 _ => inner.max_width(),
188 },
189
190 Regex::Quantifier(inner, kind, _) => match kind {
191 QuantKind::ZeroOrMore | QuantKind::OneOrMore | QuantKind::AtLeast(_) => None,
192 QuantKind::ZeroOrOne => inner.max_width(),
193 QuantKind::Exactly(n) => inner.max_width()?.checked_mul(*n),
194 QuantKind::Between(_, m) => inner.max_width()?.checked_mul(*m),
195 },
196
197 // None (unbounded) propagates: if any child is unbounded, the
198 // whole concat is unbounded.
199 Regex::Concat(nodes) => nodes
200 .iter()
201 .try_fold(0usize, |acc, n| acc.checked_add(n.max_width()?)),
202
203 // None propagates: if any branch is unbounded, the alternation is
204 // unbounded.
205 Regex::Alternation(nodes) => nodes
206 .iter()
207 .try_fold(0usize, |acc, n| Some(acc.max(n.max_width()?))),
208 }
209 }
210}