Skip to main content

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}