metrale_grammar/regex/
converter.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Core regex→EBNF converter — port of `class RegexConverter` and
4// `RegexConverter::Convert` from `cpp/regex_converter.cc`.
5
6use super::RegexError;
7use super::cursor::Cursor;
8use super::escape_handlers::handle_escape;
9use super::sub_handlers::quant_char;
10use crate::support::encoding::TCodepoint;
11use crate::support::escape::print_as_escaped;
12use std::collections::HashMap;
13
14/// Result of a successful conversion.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ConvertOutput {
17    /// The EBNF rule body (no `root ::=` prefix, no trailing newline).
18    pub ebnf: String,
19    /// Non-fatal warnings collected during conversion (stray `^`/`$`,
20    /// unrecognised escapes). The C++ prints these to stderr.
21    pub warnings: Vec<String>,
22}
23
24/// The regex→EBNF converter. Construct with [`RegexConverter::new`]
25/// then call [`RegexConverter::convert`].
26pub struct RegexConverter {
27    pub(super) cur: Cursor,
28    pub(super) result: String,
29    pub(super) parenthesis_level: i32,
30}
31
32impl RegexConverter {
33    /// Build a converter for `regex`. Fails if `regex` is not valid
34    /// UTF-8.
35    pub fn new(regex: &str) -> Result<Self, RegexError> {
36        Ok(RegexConverter {
37            cur: Cursor::new(regex)?,
38            result: String::new(),
39            parenthesis_level: 0,
40        })
41    }
42
43    /// Append `element` to the result, inserting a separating space
44    /// when the result is non-empty — port of `AddEBNFSegment`.
45    fn add_segment(&mut self, element: &str) {
46        if !self.result.is_empty() {
47            self.result.push(' ');
48        }
49        self.result.push_str(element);
50    }
51
52    /// Run the conversion — port of `RegexConverter::Convert`.
53    pub fn convert(mut self) -> Result<ConvertOutput, RegexError> {
54        let mut is_empty = true;
55        while !self.cur.at_end() {
56            let c = self.cur.peek();
57            match c {
58                _ if c == '^' as TCodepoint => {
59                    if !self.cur.at_start() {
60                        self.cur.warn(
61                            "'^' should be at the start of the regex, but found in the \
62                             middle. It is ignored.",
63                        );
64                    }
65                    self.cur.bump();
66                }
67                _ if c == '$' as TCodepoint => {
68                    if !self.cur.at_last() {
69                        self.cur.warn(
70                            "'$' should be at the end of the regex, but found in the \
71                             middle. It is ignored.",
72                        );
73                    }
74                    self.cur.bump();
75                }
76                _ if c == '[' as TCodepoint => {
77                    is_empty = false;
78                    let cls = self.handle_character_class()?;
79                    self.add_segment(&cls);
80                }
81                _ if c == '(' as TCodepoint => {
82                    is_empty = false;
83                    self.cur.bump();
84                    self.parenthesis_level += 1;
85                    self.add_segment("(");
86                    if !self.cur.at_end() && self.cur.peek() == '?' as TCodepoint {
87                        self.cur.bump();
88                        self.handle_group_modifier()?;
89                    }
90                }
91                _ if c == ')' as TCodepoint => {
92                    is_empty = false;
93                    if self.parenthesis_level == 0 {
94                        return Err(self.cur.error("Unmatched ')'"));
95                    }
96                    // If the previous char was '|', emit an empty
97                    // alternative so `(a|)` becomes `( "a" | "" )`.
98                    if self.cur.prev() == Some('|' as TCodepoint) {
99                        self.add_segment("\"\"");
100                    }
101                    self.parenthesis_level -= 1;
102                    self.add_segment(")");
103                    self.cur.bump();
104                }
105                _ if c == '*' as TCodepoint || c == '+' as TCodepoint || c == '?' as TCodepoint => {
106                    is_empty = false;
107                    self.result.push(quant_char(c));
108                    self.cur.bump();
109                    self.consume_optional_non_greedy();
110                    self.reject_consecutive_quantifier()?;
111                }
112                _ if c == '{' as TCodepoint => {
113                    is_empty = false;
114                    let rep = self.handle_repetition_range()?;
115                    self.result.push_str(&rep);
116                    self.consume_optional_non_greedy();
117                    self.reject_consecutive_quantifier()?;
118                }
119                _ if c == '|' as TCodepoint => {
120                    is_empty = false;
121                    self.add_segment("|");
122                    self.cur.bump();
123                }
124                _ if c == '\\' as TCodepoint => {
125                    is_empty = false;
126                    let esc = handle_escape(&mut self.cur)?;
127                    self.add_segment(&esc);
128                }
129                _ if c == '.' as TCodepoint => {
130                    is_empty = false;
131                    self.add_segment("[\\u0000-\\U0010FFFF]");
132                    self.cur.bump();
133                }
134                _ => {
135                    is_empty = false;
136                    let escaped = print_as_escaped(c, &HashMap::new());
137                    self.add_segment(&format!("\"{escaped}\""));
138                    self.cur.bump();
139                }
140            }
141        }
142        if self.parenthesis_level != 0 {
143            return Err(self.cur.error("The parenthesis is not closed."));
144        }
145        if is_empty {
146            self.add_segment("\"\"");
147        }
148        Ok(ConvertOutput {
149            ebnf: self.result,
150            warnings: self.cur.warnings,
151        })
152    }
153
154    /// Skip a `?` non-greedy modifier if present — our grammar treats
155    /// all repetition non-deterministically, so it is simply dropped.
156    fn consume_optional_non_greedy(&mut self) {
157        if !self.cur.at_end() && self.cur.peek() == '?' as TCodepoint {
158            self.cur.bump();
159        }
160    }
161
162    /// Reject a second repetition modifier directly following a
163    /// quantifier — port of the "Two consecutive repetition
164    /// modifiers" check.
165    fn reject_consecutive_quantifier(&self) -> Result<(), RegexError> {
166        if self.cur.at_end() {
167            return Ok(());
168        }
169        let c = self.cur.peek();
170        if c == '{' as TCodepoint
171            || c == '*' as TCodepoint
172            || c == '+' as TCodepoint
173            || c == '?' as TCodepoint
174        {
175            return Err(self
176                .cur
177                .error("Two consecutive repetition modifiers are not allowed."));
178        }
179        Ok(())
180    }
181}