metrale_grammar/regex/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Regex converter — port of `cpp/regex_converter.{h,cc}`.
4//
5// Converts a JavaScript-flavoured regex pattern string into an EBNF
6// grammar string (the `RegexToEBNF` entry point) and, as a
7// convenience, into a parsed `GrammarData` AST by feeding that EBNF
8// through the existing `grammar::parse_ebnf` parser.
9//
10// RELATION TO THE FSM REGEX BUILDER
11// ---------------------------------
12// `fsm::builder::RegexFsmBuilder` (ported by the W2 FSM agent) takes a
13// regex and builds an NFA/FSM directly. This module is the *other*
14// path: it lowers a regex to the EBNF grammar surface syntax, which
15// the EBNF parser then turns into a `GrammarData` AST. The two share
16// no code — they are independent lowerings of the same input, exactly
17// as in upstream xgrammar where `regex_converter.cc` and
18// `fsm_builder.cc` are separate translation units.
19//
20// The implementation refers to the regex described in
21// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions>.
22//
23// FAITHFULNESS / SIMPLIFICATIONS vs C++
24// -------------------------------------
25//  * C++ aborts the process (`XGRAMMAR_LOG(FATAL)`) on a malformed
26//    regex; we return `Err(RegexError)` instead — no panics, no
27//    `unsafe`.
28//  * C++ emits warnings to stderr for stray `^`/`$` and unknown
29//    escapes; we collect them into `ConvertOutput::warnings` rather
30//    than performing I/O (keeps the converter pure).
31//  * Backreferences, lookahead/lookbehind, named-group *flags*,
32//    unicode-property `\p{…}` and word boundaries are unsupported and
33//    reported as errors — identical to upstream.
34
35mod converter;
36mod cursor;
37mod escape_handlers;
38mod sub_handlers;
39
40pub use converter::{ConvertOutput, RegexConverter};
41
42use crate::grammar::{GrammarData, parse_ebnf};
43
44/// Error raised while converting a regex to EBNF.
45///
46/// `position` is 1-based, matching the C++ `RaiseError` message
47/// (`current_ - start_ + 1`).
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct RegexError {
50    /// 1-based codepoint offset into the regex where the error was
51    /// detected.
52    pub position: usize,
53    /// Human-readable description of the problem.
54    pub message: String,
55}
56
57impl std::fmt::Display for RegexError {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(
60            f,
61            "Regex parsing error at position {}: {}",
62            self.position, self.message
63        )
64    }
65}
66
67impl std::error::Error for RegexError {}
68
69/// Convert a regex string to an EBNF grammar string.
70///
71/// When `with_rule_name` is `true` the result is wrapped as
72/// `root ::= <body>\n`; otherwise just the rule body is returned.
73/// Faithful port of C++ `RegexToEBNF`.
74pub fn regex_to_ebnf(regex: &str, with_rule_name: bool) -> Result<String, RegexError> {
75    let body = RegexConverter::new(regex)?.convert()?.ebnf;
76    if with_rule_name {
77        Ok(format!("root ::= {body}\n"))
78    } else {
79        Ok(body)
80    }
81}
82
83/// Error from [`regex_to_grammar`]: either the regex itself was
84/// malformed, or the EBNF it lowered to failed to parse.
85#[derive(Debug)]
86pub enum RegexToGrammarError {
87    /// The regex pattern was malformed.
88    Regex(RegexError),
89    /// The lowered EBNF failed to parse (should not happen for a
90    /// well-formed regex; indicates an internal converter bug).
91    Ebnf(crate::grammar::ParseError),
92}
93
94impl std::fmt::Display for RegexToGrammarError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            RegexToGrammarError::Regex(e) => write!(f, "{e}"),
98            RegexToGrammarError::Ebnf(e) => write!(f, "lowered EBNF failed to parse: {e:?}"),
99        }
100    }
101}
102
103impl std::error::Error for RegexToGrammarError {}
104
105/// Convert a regex string straight to a parsed [`GrammarData`] AST.
106///
107/// This is the regex→grammar-AST path: it lowers the regex to EBNF
108/// (via [`regex_to_ebnf`]) and parses that with the existing EBNF
109/// parser, so the result is a fully-formed grammar usable by the rest
110/// of the crate. Upstream xgrammar's `Grammar::FromRegex` does the
111/// same composition.
112pub fn regex_to_grammar(regex: &str) -> Result<GrammarData, RegexToGrammarError> {
113    let ebnf = regex_to_ebnf(regex, true).map_err(RegexToGrammarError::Regex)?;
114    parse_ebnf(&ebnf, "root").map_err(RegexToGrammarError::Ebnf)
115}
116
117#[cfg(test)]
118mod tests;
119#[cfg(test)]
120mod tests_patterns;