REAL
Regular Expression Algorithmic Library — constexpr C++20 regex
Loading...
Searching...
No Matches
regex_core.hpp
Go to the documentation of this file.
1
7#ifndef REAL_STD_REGEX_CORE_HPP
8#define REAL_STD_REGEX_CORE_HPP
9
10// Internal — do not include directly.
11// Users: #include <real/real.hpp> (or the documented opt-ins <real/dfa.hpp>, <real/std/regex.hpp>).
12
13#include <real/version.hpp>
14
15#include <cstddef>
16#include <mutex>
17#include <optional>
18#include <regex>
19#include <string>
20#include <string_view>
21#include <type_traits>
22#include <utility>
23#include <variant>
24#include <vector>
25
26#include <real/real.hpp>
27
28namespace real::compat {
29
33 namespace regex_constants {
34
36 enum syntax_option_type : unsigned
37 {
39 icase = 1U << 0U,
40 nosubs = 1U << 1U,
41 optimize = 1U << 2U,
42 collate = 1U << 3U,
43 multiline = 1U << 4U,
44 basic = 1U << 5U,
45 extended = 1U << 6U,
46 awk = 1U << 7U,
47 grep = 1U << 8U,
48 egrep = 1U << 9U,
49 };
50
52 syntax_option_type b) noexcept
53 {
54 return static_cast<syntax_option_type>(static_cast<unsigned>(a) | static_cast<unsigned>(b));
55 }
56
58 syntax_option_type b) noexcept
59 {
60 return static_cast<syntax_option_type>(static_cast<unsigned>(a) & static_cast<unsigned>(b));
61 }
62
64 enum match_flag_type : unsigned
65 {
67 match_not_bol = 1U << 0U,
68 match_not_eol = 1U << 1U,
69 match_not_bow = 1U << 2U,
70 match_not_eow = 1U << 3U,
71 match_any = 1U << 4U,
72 match_not_null = 1U << 5U,
73 match_continuous = 1U << 6U,
74 match_prev_avail = 1U << 7U,
76 format_sed = 1U << 8U,
77 format_no_copy = 1U << 9U,
78 format_first_only = 1U << 10U,
79 };
80
82 match_flag_type b) noexcept
83 {
84 return static_cast<match_flag_type>(static_cast<unsigned>(a) | static_cast<unsigned>(b));
85 }
86
88 match_flag_type b) noexcept
89 {
90 return static_cast<match_flag_type>(static_cast<unsigned>(a) & static_cast<unsigned>(b));
91 }
92
94 {
95 return static_cast<match_flag_type>(~static_cast<unsigned>(a));
96 }
97
99 using error_type = std::regex_constants::error_type;
100 } // namespace regex_constants
101
113 class regex_error : public std::regex_error
114 {
115 public:
116
118 explicit regex_error(const std::regex_error& error)
119 : std::regex_error(error.code()),
120 message_(error.what())
121 {}
122
125 regex_error(std::regex_constants::error_type code,
126 std::string message)
127 : std::regex_error(code),
128 message_(std::move(message))
129 {}
130
131 [[nodiscard]] const char* what() const noexcept override
132 {
133 return message_.c_str();
134 }
135
136 private:
137
138 std::string message_;
139 };
140
147 enum class policy : std::uint8_t
148 {
149 strict,
150 fallback,
151 };
152
153 namespace detail {
154
159 template <typename CharT, typename Traits>
160 inline constexpr bool real_eligible =
161 std::is_same_v<CharT, char> && std::is_same_v<Traits, std::regex_traits<char>>;
162
167 {
168 using namespace regex_constants;
169 return (f & (basic | extended | awk | grep | egrep)) != ECMAScript
170 || (f & collate) != ECMAScript
171 || (f & nosubs) != ECMAScript;
172 }
173
182 [[nodiscard]] inline bool pattern_forces_std(std::string_view p) noexcept
183 {
184 const auto is_flag_or_dash = [](char c) {
185 return c == '-' || c == 'i' || c == 'm' || c == 's' || c == 'x' || c == 'a';
186 };
187 for (std::size_t i = 0; i < p.size(); ++i) {
188 if (p[i] == '\\') {
189 if (i + 2 < p.size() && p[i + 1] == '0' && p[i + 2] >= '0' && p[i + 2] <= '9') {
190 return true;
191 }
192 ++i; // consume the escaped character (so `\\0` is an escaped backslash, not `\0`)
193 continue;
194 }
195 // An inline-flags group `(?imsxa:...)` / `(?-...:...)` / a bare `(?imsxa)`: real accepts these
196 // (Python semantics), but ECMAScript has no inline flags, so std rejects them. Route to std so
197 // compat stays ≡ std — the char after `(?` is a flag letter or `-` only for a flag construct
198 // (`(?:` `(?=` `(?!` `(?<` `(?P` start with other characters). Over-routing here is safe.
199 if (p[i] == '(' && i + 2 < p.size() && p[i + 1] == '?' && is_flag_or_dash(p[i + 2])) {
200 return true;
201 }
202 }
203 return false;
204 }
205
210 [[nodiscard]] inline bool format_forces_std(std::string_view fmt) noexcept
211 {
212 for (std::size_t i = 0; i < fmt.size(); ++i) {
213 if (fmt[i] == '$' && i + 1 < fmt.size()) {
214 if (fmt[i + 1] == '$') {
215 ++i; // `$$` — escaped literal dollar
216 }
217 else if (fmt[i + 1] == '0') {
218 return true; // `$0…` platform-variant
219 }
220 }
221 }
222 return false;
223 }
224
227 inline std::string posix_class_ranges(std::string_view name)
228 {
229 if (name == "alpha") { return "A-Za-z"; }
230 if (name == "digit") { return "0-9"; }
231 if (name == "alnum") { return "0-9A-Za-z"; }
232 if (name == "upper") { return "A-Z"; }
233 if (name == "lower") { return "a-z"; }
234 if (name == "xdigit") { return "0-9A-Fa-f"; }
235 if (name == "space") { return "\\t\\n\\x0b\\f\\r "; }
236 if (name == "blank") { return "\\t "; }
237 if (name == "cntrl") { return "\\x00-\\x1f\\x7f"; }
238 if (name == "print") { return "\\x20-\\x7e"; }
239 if (name == "graph") { return "\\x21-\\x7e"; }
240 if (name == "punct") { return "!-/:-@\\[-`{-~"; }
241 return {};
242 }
243
249 [[nodiscard]] inline bool translate_bracket(std::string_view p,
250 std::size_t& i,
251 std::string& out)
252 {
253 const std::size_t n {p.size()};
254 std::string cls {'['};
255 i += 1;
256 if (i < n && p[i] == '^') { cls += '^'; i += 1; }
257 if (i < n && p[i] == ']') { cls += "\\]"; i += 1; } // a leading `]` is a literal member in POSIX — escape
258 // it for REAL, where a bare `[]` opens an empty class
259 while (i < n && p[i] != ']') {
260 if (p[i] == '[' && i + 1 < n && p[i + 1] == ':') {
261 const std::size_t close {p.find(":]", i + 2)};
262 if (close == std::string_view::npos) { return false; }
263 const std::string ranges {posix_class_ranges(p.substr(i + 2, close - (i + 2)))};
264 if (ranges.empty()) { return false; }
265 cls += ranges;
266 i = close + 2;
267 }
268 else if (p[i] == '[' && i + 1 < n && (p[i + 1] == '.' || p[i + 1] == '=')) {
269 return false; // collating [.x.] / equivalence [=x=]
270 }
271 else {
272 cls += p[i];
273 i += 1;
274 }
275 }
276 if (i >= n) { return false; } // unterminated class
277 cls += ']';
278 i += 1;
279 out += cls;
280 return true;
281 }
282
288 [[nodiscard]] inline bool append_awk_escape(std::string_view p,
289 std::size_t& i,
290 std::string& out)
291 {
292 const std::size_t n {p.size()};
293 const char d {p[i + 1]};
294 const auto emit_hex {[&out](unsigned v) {
295 constexpr std::string_view hex {"0123456789abcdef"};
296 out += "\\x";
297 out += hex[(v >> 4U) & 0xFU];
298 out += hex[v & 0xFU];
299 }};
300 switch (d) {
301 case 'b': emit_hex(0x08U); i += 2; return true; // BACKSPACE, not a word boundary
302 case 'a': emit_hex(0x07U); i += 2; return true;
303 case 'n': emit_hex(0x0AU); i += 2; return true;
304 case 't': emit_hex(0x09U); i += 2; return true;
305 case 'r': emit_hex(0x0DU); i += 2; return true;
306 case 'f': emit_hex(0x0CU); i += 2; return true;
307 case 'v': emit_hex(0x0BU); i += 2; return true;
308 case '/': out += '/'; i += 2; return true; // an escaped delimiter -> a literal slash
309 case '"': out += '"'; i += 2; return true; // a literal quote
310 default: break;
311 }
312 if (d >= '0' && d <= '7') {
313 unsigned val {0};
314 std::size_t k {i + 1};
315 std::size_t digits {0};
316 while (k < n && digits < 3 && p[k] >= '0' && p[k] <= '7') {
317 val = (val * 8U) + static_cast<unsigned>(p[k] - '0');
318 ++k;
319 ++digits;
320 }
321 if (val > 0xFFU) { return false; } // an octal overflow (> 0377) -> std
322 emit_hex(val);
323 i = k;
324 return true;
325 }
326 return false; // `\d` `\w` `\s` … — no awk meaning; decline
327 }
328
336 [[nodiscard]] inline bool has_empty_alternation_branch(std::string_view p)
337 {
338 bool in_class {false};
339 for (std::size_t i = 0; i < p.size(); ++i) {
340 const char c {p[i]};
341 if (c == '\\') { ++i; continue; } // skip the escaped character
342 if (in_class) {
343 if (c == ']') { in_class = false; }
344 continue;
345 }
346 if (c == '[') { in_class = true; continue; }
347 if (c == '|') {
348 const bool left_empty {i == 0 || p[i - 1] == '(' || p[i - 1] == '|'};
349 const bool right_empty {i + 1 >= p.size() || p[i + 1] == ')' || p[i + 1] == '|'};
350 if (left_empty || right_empty) { return true; }
351 }
352 }
353 return false;
354 }
355
362 [[nodiscard]] inline std::optional<std::string> translate_ere(std::string_view p,
363 bool awk = false)
364 {
365 if (has_empty_alternation_branch(p)) { return std::nullopt; } // std rejects these in POSIX -> keep ≡ std
366 std::string out;
367 std::size_t i {0};
368 const std::size_t n {p.size()};
369 while (i < n) {
370 const char c {p[i]};
371 if (c == '\\') {
372 if (i + 1 >= n) { return std::nullopt; } // trailing backslash
373 const char d {p[i + 1]};
374 // `\]` / `\}` OUTSIDE a class are undefined in POSIX and the std libraries disagree — libc++ rejects
375 // `\]` but accepts `\}`, libstdc++ rejects both, AT&T matches — so decline (≡ its own std per platform,
376 // the same rule as the medial `^`/`$`). Ironically REAL was AT&T-conformant here, but the drop-in
377 // contract is ≡-std, not ≡-AT&T. (Inside a class, `\]` is handled by translate_bracket, unaffected.)
378 if (d == ']' || d == '}') { return std::nullopt; }
379 if (std::string_view {".[]{}()*+?|^$\\"}.find(d) != std::string_view::npos) {
380 out += '\\';
381 out += d;
382 i += 2;
383 continue; // an escaped metacharacter is a literal in both grammars
384 }
385 if (awk && append_awk_escape(p, i, out)) { continue; } // awk C-escapes (\b=BS, \n, octal, …)
386 return std::nullopt; // \d, \w, \b, … — no ERE meaning; fall back to std
387 }
388 if (c == '[') {
389 if (!translate_bracket(p, i, out)) { return std::nullopt; }
390 continue;
391 }
392 if (c == '{') {
393 // keep only a strict interval {n}/{n,}/{n,m}; an ambiguous `{` is literal in POSIX (it diverges).
394 std::size_t j {i + 1};
395 const std::size_t ds {j};
396 while (j < n && p[j] >= '0' && p[j] <= '9') { ++j; }
397 bool ok {j > ds};
398 if (ok && j < n && p[j] == ',') {
399 ++j;
400 while (j < n && p[j] >= '0' && p[j] <= '9') { ++j; }
401 }
402 ok = ok && j < n && p[j] == '}';
403 if (!ok) { return std::nullopt; }
404 out += p.substr(i, (j + 1) - i);
405 i = j + 1;
406 continue;
407 }
408 out += c; // common production (. * + ? | ( ) ^ $ literal) — read alike
409 i += 1;
410 }
411 return out;
412 }
413
419 [[nodiscard]] inline std::optional<std::string> translate_bre(std::string_view p)
420 {
421 std::string out;
422 std::size_t i {0};
423 const std::size_t n {p.size()};
424 bool at_start {true}; // pattern start or just after `\‍(` — where `*` is literal and `^` anchors
425 while (i < n) {
426 const char c {p[i]};
427 if (c == '\\') {
428 if (i + 1 >= n) { return std::nullopt; } // trailing backslash
429 const char d {p[i + 1]};
430 if (d == '(') { out += '('; i += 2; at_start = true; continue; } // \‍( -> group open
431 if (d == ')') { out += ')'; i += 2; at_start = false; continue; } // \‍) -> group close
432 if (d == '{') { // \{n\} / \{n,\} / \{n,m\} interval
433 std::size_t j {i + 2};
434 const std::size_t ds {j};
435 while (j < n && p[j] >= '0' && p[j] <= '9') { ++j; }
436 bool ok {j > ds};
437 if (ok && j < n && p[j] == ',') {
438 ++j;
439 while (j < n && p[j] >= '0' && p[j] <= '9') { ++j; }
440 }
441 if (!ok || j + 1 >= n || p[j] != '\\' || p[j + 1] != '}') { return std::nullopt; } // not a strict \{...\}
442 out += '{';
443 out += p.substr(i + 2, j - (i + 2));
444 out += '}';
445 i = j + 2;
446 at_start = false;
447 continue;
448 }
449 if (d == ']' || d == '}') { return std::nullopt; } // `\]` / `\}` outside a class: undefined, std-divergent
450 if (std::string_view {".[]*\\^$"}.find(d) != std::string_view::npos) {
451 out += '\\'; // an escaped metacharacter is a literal in both grammars
452 out += d;
453 i += 2;
454 at_start = false;
455 continue;
456 }
457 return std::nullopt; // \1-\9 backref, \d\w\s\b, \+ \? \| (not BRE), stray \} -> decline
458 }
459 if (c == '(' || c == ')' || c == '{' || c == '}' || c == '|' || c == '+' || c == '?') {
460 out += '\\'; // bare, these are literals in BRE -> escape for REAL
461 out += c;
462 i += 1;
463 at_start = false;
464 continue;
465 }
466 if (c == '^') {
467 if (i == 0) { out += '^'; i += 1; at_start = false; continue; } // anchor at the pattern head (libs agree)
468 return std::nullopt; // a medial `^` is a POSIX literal, but libstdc++ reads it as an anchor while
469 // libc++ reads it as a literal — decline so compat stays ≡ its own std
470 }
471 if (c == '$') {
472 if (i + 1 == n) { out += '$'; i += 1; at_start = false; continue; } // anchor at the tail (libs agree)
473 return std::nullopt; // a medial `$` — the same libstdc++/libc++ disagreement; decline to std
474 }
475 if (c == '*') {
476 if (at_start) { return std::nullopt; } // a leading `*` is a literal in POSIX BRE -> decline (rare)
477 out += '*'; // quantifier
478 i += 1;
479 continue;
480 }
481 if (c == '[') {
482 if (!translate_bracket(p, i, out)) { return std::nullopt; }
483 at_start = false;
484 continue;
485 }
486 out += c; // `.` and ordinary literals — read alike
487 i += 1;
488 at_start = false;
489 }
490 return out;
491 }
492
498 template <typename LineFn>
499 [[nodiscard]] inline std::optional<std::string> translate_newline_alt(std::string_view p,
500 LineFn translate_line)
501 {
502 std::string out;
503 std::size_t start {0};
504 bool first {true};
505 while (true) {
506 const std::size_t nl {p.find('\n', start)};
507 const std::string_view line {p.substr(start, (nl == std::string_view::npos ? p.size() : nl) - start)};
508 if (line.empty()) { return std::nullopt; } // a blank branch -> std
509 const std::optional<std::string> t {translate_line(line)};
510 if (!t) { return std::nullopt; }
511 if (!first) { out += '|'; }
512 out += *t;
513 first = false;
514 if (nl == std::string_view::npos) { break; }
515 start = nl + 1;
516 }
517 return out;
518 }
519
523 [[nodiscard]] inline std::optional<std::string> translate_posix(std::string_view p,
525 {
526 using namespace regex_constants;
527 if ((f & (collate | nosubs)) != ECMAScript) { return std::nullopt; }
528 int grammars {0};
529 if ((f & basic) != ECMAScript) { ++grammars; }
530 if ((f & extended) != ECMAScript) { ++grammars; }
531 if ((f & awk) != ECMAScript) { ++grammars; }
532 if ((f & grep) != ECMAScript) { ++grammars; }
533 if ((f & egrep) != ECMAScript) { ++grammars; }
534 if (grammars != 1) { return std::nullopt; } // ECMAScript (0), or a mix — not a single POSIX grammar
535 if ((f & extended) != ECMAScript) { return translate_ere(p); }
536 if ((f & basic) != ECMAScript) { return translate_bre(p); }
537 if ((f & awk) != ECMAScript) { return translate_ere(p, /*awk=*/ true); }
538 if ((f & grep) != ECMAScript) { return translate_newline_alt(p, [](std::string_view l) { return translate_bre(l); }); }
539 return translate_newline_alt(p, [](std::string_view l) { return translate_ere(l); }); // egrep
540 }
541
550
552 inline std::regex_constants::syntax_option_type to_std(regex_constants::syntax_option_type f) noexcept
553 {
554 namespace sc = std::regex_constants;
555 sc::syntax_option_type s {};
556 using namespace regex_constants;
557 if ((f & icase) != ECMAScript) { s |= sc::icase; }
558 if ((f & nosubs) != ECMAScript) { s |= sc::nosubs; }
559 if ((f & optimize) != ECMAScript) { s |= sc::optimize; }
560 if ((f & collate) != ECMAScript) { s |= sc::collate; }
561 if ((f & multiline) != ECMAScript) { s |= sc::multiline; }
562 if ((f & basic) != ECMAScript) { s |= sc::basic; }
563 else if ((f & extended) != ECMAScript) { s |= sc::extended; }
564 else if ((f & awk) != ECMAScript) { s |= sc::awk; }
565 else if ((f & grep) != ECMAScript) { s |= sc::grep; }
566 else if ((f & egrep) != ECMAScript) { s |= sc::egrep; }
567 else { s |= sc::ECMAScript; }
568 return s;
569 }
570 } // namespace detail
571
578 template <typename CharT = char, typename Traits = std::regex_traits<CharT>>
580 {
581 public:
582
583 using value_type = CharT;
585 using string_type = std::basic_string<CharT>;
586
587 basic_regex() = default; // the variant default-constructs to an empty std regex
588
589 explicit basic_regex(const CharT* pattern,
592 {
593 policy_ = pol;
594 assign(std::basic_string_view<CharT>(pattern), f);
595 }
596
597 explicit basic_regex(const string_type& pattern,
600 {
601 policy_ = pol;
602 assign(std::basic_string_view<CharT>(pattern), f);
603 }
604
605 basic_regex(const CharT* pattern,
606 std::size_t len,
609 {
610 policy_ = pol;
611 assign(std::basic_string_view<CharT>(pattern, len), f);
612 }
613
614 template <typename It>
615 basic_regex(It begin,
616 It end,
619 {
620 policy_ = pol;
621 const string_type pattern(begin, end);
622 assign(std::basic_string_view<CharT>(pattern), f);
623 }
624
626 [[nodiscard]] std::size_t mark_count() const noexcept
627 {
628 return mark_count_;
629 }
630
632 [[nodiscard]] flag_type flags() const noexcept
633 {
634 return flags_;
635 }
636
637 void swap(basic_regex& other) noexcept
638 {
639 engine_.swap(other.engine_);
640 std::swap(pattern_, other.pattern_);
641 std::swap(flags_, other.flags_);
642 std::swap(mark_count_, other.mark_count_);
643 std::swap(nullable_, other.nullable_);
644 std::swap(posix_longest_, other.posix_longest_);
645 std::swap(lazy_std_, other.lazy_std_);
646 std::swap(policy_, other.policy_);
647 }
648
650 [[nodiscard]] bool uses_real() const noexcept
651 {
652 return std::holds_alternative<real::regex>(engine_);
653 }
654
657 [[nodiscard]] bool uses_fallback() const noexcept
658 {
659 return !uses_real();
660 }
661
663 [[nodiscard]] compat::policy policy() const noexcept
664 {
665 return policy_;
666 }
667
669 [[nodiscard]] const std::variant<std::basic_regex<CharT, Traits>, real::regex>& engine() const noexcept
670 {
671 return engine_;
672 }
673
680 [[nodiscard]] bool nullable() const noexcept
681 {
682 return nullable_;
683 }
684
687 [[nodiscard]] bool posix_longest() const noexcept
688 {
689 return posix_longest_;
690 }
691
696 [[nodiscard]] bool uses_real_traversal() const noexcept
697 {
698 return uses_real() && !nullable_;
699 }
700
710 [[nodiscard]] const std::basic_regex<CharT, Traits>& std_engine() const
711 {
712 if (std::holds_alternative<std::basic_regex<CharT, Traits>>(engine_)) {
713 return std::get<std::basic_regex<CharT, Traits>>(engine_);
714 }
715 static std::mutex build_mutex; // one per basic_regex<CharT, Traits> instantiation
716 const std::lock_guard lock {build_mutex};
717 if (!lazy_std_.has_value()) {
718 try {
719 lazy_std_.emplace(pattern_.data(), pattern_.size(), detail::to_std(flags_));
720 }
721 catch (const std::regex_error& std_error) {
722 // A pattern real accepted but std cannot build (a real superset) reaches std only via a
723 // constraining flag / nullable replace-iterate. Surface it as a compat::regex_error, not a
724 // raw std one: an error, homogeneous with the ctor path, never a silent result.
725 throw regex_error(std_error);
726 }
727 }
728 return *lazy_std_;
729 }
730
731 private:
732
733 // std backend first so the variant is default-constructible (real::regex has no default ctor).
734 std::variant<std::basic_regex<CharT, Traits>, real::regex> engine_;
737 std::size_t mark_count_ {};
738 bool nullable_ {};
740 mutable std::optional<std::basic_regex<CharT, Traits>> lazy_std_;
742
743 void assign(std::basic_string_view<CharT> pattern,
744 flag_type f)
745 {
746 flags_ = f;
747 pattern_ = string_type(pattern);
748 lazy_std_.reset();
749 nullable_ = false;
750 posix_longest_ = false;
751 if constexpr (detail::real_eligible<CharT, Traits>) {
752 const std::string_view sv {pattern.data(), pattern.size()};
753 // PX1a/PX1b: a single POSIX grammar (extended -> ERE, basic -> BRE; awk/grep/egrep next) on the linear
754 // engine when the pattern translates — run it on REAL with leftmost-LONGEST bounds (the POSIX semantics,
755 // via search_longest / find_iter_longest) instead of delegating to std's backtracker. A `nullopt` (a
756 // wrong grammar mix, or an untranslatable construct) or a real reject falls through to the std path below;
757 // the fallback lives, and nothing that used to reach std stops reaching it.
759 if (const std::optional<std::string> translated {detail::translate_posix(sv, f)}) {
760 try {
761 real::regex compiled(*translated, detail::to_real(f));
762 mark_count_ = compiled.group_count();
764 posix_longest_ = true;
765 engine_.template emplace<real::regex>(std::move(compiled));
766 return;
767 }
768 catch (const real::regex_error&) {
769 // translated but real cannot represent it -> fall through to the std path
770 }
771 }
772 }
774 reject_or_fallback(sv, f, "the pattern uses a construct the linear engine does not represent "
775 "(a backreference, an unbounded lookaround, a POSIX class, or a "
776 "grammar that forces std)");
777 return;
778 }
779 try {
780 real::regex compiled(sv, detail::to_real(f));
781 mark_count_ = compiled.group_count();
783 engine_.template emplace<real::regex>(std::move(compiled));
784 // The std engine for a real-backed pattern (needed for a constraining flag or nullable
785 // replace/iterate) is built lazily and thread-safely by std_engine() under its build mutex,
786 // so no eager build is needed here (a real superset that std rejects surfaces the wrapped
787 // error only when the std-only operation is actually invoked; search/match stay on real).
788 }
789 catch (const real::regex_error& real_error) {
790 // real cannot represent it (backref / unbounded lookaround / POSIX class / non-ASCII in a
791 // class). strict rejects; fallback delegates to std (which may accept it). Invalid for both
792 // throws compat::regex_error (emplace_std wraps).
793 reject_or_fallback(sv, f, real_error.what());
794 }
795 }
796 else {
797 // wchar_t / char8/16/32 / custom traits: real is never eligible, so go straight to std. The
798 // real::regex variant alternative stays dead for this CharT (never emplaced), and real's
799 // char-only helpers are not instantiated. always-std => std parity by construction.
800 try {
801 engine_.template emplace<std::basic_regex<CharT, Traits>>(pattern.data(), pattern.size(),
802 detail::to_std(f));
803 mark_count_ = std::get<std::basic_regex<CharT, Traits>>(engine_).mark_count();
804 }
805 catch (const std::regex_error& std_error) {
806 throw regex_error(std_error); // homogeneous compat::regex_error on the wide/custom-traits path
807 }
808 }
809 }
810
814 void reject_or_fallback(std::string_view sv,
815 flag_type f,
816 const std::string& reason)
817 {
818 if (policy_ == policy::strict) {
819 // Distinguish "real cannot represent it, but the pattern is valid" (std accepts) from "invalid for
820 // both" (a syntax error). The first is a capability limit -> error_complexity; the second is a bad
821 // pattern -> std's own code, so a syntax error still reports as a syntax error, exactly like std.
822 if constexpr (detail::real_eligible<CharT, Traits>) {
823 try {
824 const std::basic_regex<CharT, Traits> probe(sv.data(), sv.size(), detail::to_std(f));
825 static_cast<void>(probe); // std accepts it: a real-only limitation, fall through to the throw below
826 }
827 catch (const std::regex_error& std_error) {
828 throw regex_error(std_error); // invalid for both -> std's exact code (drop-in ≡ std)
829 }
830 }
831 throw regex_error(std::regex_constants::error_complexity,
832 "real::compat (strict policy): this pattern requires a non-linear (backtracking) "
833 "engine — " + reason + ". Construct with policy::fallback to delegate it to "
834 "std::regex, forfeiting the linear-time / ReDoS-safe guarantee for it.");
835 }
836 emplace_std(sv, f);
837 }
838
839 void emplace_std(std::string_view sv,
840 flag_type f)
841 {
842 try {
843 auto& std_engine = engine_.template emplace<std::basic_regex<CharT, Traits>>(
844 sv.data(), sv.size(), detail::to_std(f));
845 mark_count_ = std_engine.mark_count();
846 }
847 catch (const std::regex_error& std_error) {
848 // Every std-only build path throws a compat::regex_error, homogeneous with the rest of the
849 // layer (never a raw std::regex_error leaking out of a compat entry point).
850 throw regex_error(std_error);
851 }
852 }
853 };
854
857} // namespace real::compat
858
859#endif // REAL_STD_REGEX_CORE_HPP
A compiled regular expression, parameterized on its storage policy.
Definition real.hpp:468
constexpr detail::program_view raw_program() const
The raw compiled program, for embedders (advanced).
Definition real.hpp:817
constexpr std::size_t group_count() const
Returns the number of capturing groups (excluding group 0).
Definition real.hpp:804
A std::basic_regex-compatible pattern, backed by real where proven, else std.
bool uses_fallback() const noexcept
True if this regex fell back to std::regex (a policy::fallback regex on an ineligible pattern) — so t...
string_type pattern_
Original pattern (for the lazy std build).
void emplace_std(std::string_view sv, flag_type f)
void assign(std::basic_string_view< CharT > pattern, flag_type f)
bool nullable_
empty_match_possible (real-backed).
std::optional< std::basic_regex< CharT, Traits > > lazy_std_
Lazy std for nullable replace/iterate.
basic_regex(const CharT *pattern, std::size_t len, flag_type f=regex_constants::ECMAScript, policy pol=policy::strict)
std::variant< std::basic_regex< CharT, Traits >, real::regex > engine_
compat::policy policy_
strict rejects ineligible, fallback delegates to std.
bool posix_longest() const noexcept
Whether this is a POSIX-ERE pattern routed to REAL: search/match must use leftmost-**longest** bounds...
basic_regex(const string_type &pattern, flag_type f=regex_constants::ECMAScript, policy pol=policy::strict)
const std::basic_regex< CharT, Traits > & std_engine() const
The std::regex for the std / lazy-std path (built once on demand for a real-backed pattern reached vi...
void swap(basic_regex &other) noexcept
void reject_or_fallback(std::string_view sv, flag_type f, const std::string &reason)
The policy branch for a pattern the linear engine cannot represent: strict throws regex_error with er...
CharT value_type
Character type.
std::size_t mark_count() const noexcept
Number of marked sub-expressions (excluding group 0), as std::basic_regex.
const std::variant< std::basic_regex< CharT, Traits >, real::regex > & engine() const noexcept
Access the active backend (engine-facing; used by the free functions).
flag_type flags() const noexcept
The flags this regex was built with.
std::basic_string< CharT > string_type
Pattern string type.
bool posix_longest_
POSIX ERE on REAL: search uses leftmost-longest bounds.
bool uses_real_traversal() const noexcept
Whether replace/iterate run on the real traversal (real-backed AND non-nullable). A nullable pattern ...
bool uses_real() const noexcept
True if this regex is backed by the real engine (vs the std fallback).
compat::policy policy() const noexcept
The drop-in policy this regex was constructed with.
basic_regex(const CharT *pattern, flag_type f=regex_constants::ECMAScript, policy pol=policy::strict)
bool nullable() const noexcept
Whether the pattern can match the empty string (real's empty_match_possible hint).
basic_regex(It begin, It end, flag_type f=regex_constants::ECMAScript, policy pol=policy::strict)
std::regex_error-compatible exception.
regex_error(const std::regex_error &error)
From a std backend error (the fallback path); keeps std's exact code.
const char * what() const noexcept override
regex_error(std::regex_constants::error_type code, std::string message)
With an explicit code and message — the strict-policy rejection of a pattern REAL cannot represent li...
std::string message_
The originating error's detailed message.
const char * what() const noexcept override
Returns the formatted error message (with position).
Definition program.hpp:142
bool has_empty_alternation_branch(std::string_view p)
Whether p has an empty alternation branch — a | with nothing on one side: (|, |), ||,...
std::optional< std::string > translate_ere(std::string_view p, bool awk=false)
Translates a POSIX extended (ERE) — or, with awk, an awk — pattern to an equivalent REAL pattern,...
bool append_awk_escape(std::string_view p, std::size_t &i, std::string &out)
Appends the REAL translation of an awk C-escape at i (which points at the backslash),...
std::optional< std::string > translate_newline_alt(std::string_view p, LineFn translate_line)
grep / egrep: a newline in the pattern is a top-level alternation of the lines (grep = BRE lines,...
std::optional< std::string > translate_posix(std::string_view p, regex_constants::syntax_option_type f)
Dispatches a single POSIX grammar to its translator, or nullopt (→ std). Exactly one grammar bit must...
bool format_forces_std(std::string_view fmt) noexcept
Replacement format text that must route to std: $0. $0 is platform-variant (libstdc++ = the whole mat...
bool translate_bracket(std::string_view p, std::size_t &i, std::string &out)
Translates a POSIX bracket expression [...] — identical syntax in BRE and ERE, so shared by both tran...
constexpr bool real_eligible
Whether real is even eligible for this basic_regex instantiation. real runs only the char path with d...
bool grammar_forces_std(regex_constants::syntax_option_type f) noexcept
Grammars/options that force the std backend (real implements default-traits ECMAScript,...
std::string posix_class_ranges(std::string_view name)
A POSIX bracket-class name to its ASCII (C-locale) range content, appended inside a [....
std::optional< std::string > translate_bre(std::string_view p)
Translates a POSIX basic (BRE) pattern to an equivalent REAL pattern, or nullopt. BRE differs from ER...
bool pattern_forces_std(std::string_view p) noexcept
Pattern text that real accepts but matches differently from libstdc++ — a both-accept silent divergen...
real::flags to_real(regex_constants::syntax_option_type f) noexcept
Maps compat options to real::flags (always with bytes|ecma for std-char alignment).
std::regex_constants::syntax_option_type to_std(regex_constants::syntax_option_type f) noexcept
Maps compat options to std::regex syntax flags (the fallback path).
constexpr syntax_option_type operator&(syntax_option_type a, syntax_option_type b) noexcept
syntax_option_type
Grammar / option flags (own bit values; mapped to real::flags or std at construction).
@ awk
awk grammar — std backend.
@ collate
Locale-sensitive ranges; forces the std backend.
@ egrep
egrep grammar — std backend.
@ nosubs
Do not expose sub-expressions (groups still computed).
@ grep
grep grammar — std backend.
@ ECMAScript
The default grammar.
@ icase
Case-insensitive (ASCII).
@ basic
POSIX basic — std backend.
@ optimize
Hint to favour matching speed; honoured as a no-op.
@ multiline
^/$ match at line boundaries.
@ extended
POSIX extended — std backend.
constexpr syntax_option_type operator|(syntax_option_type a, syntax_option_type b) noexcept
std::regex_constants::error_type error_type
Error categories. Aliased to std's so regex_error::code() is a true drop-in.
match_flag_type
Match-control flags: the common subset.
@ match_not_null
Do not match an empty sequence.
@ match_not_bol
^ does not match the start of the sequence.
@ format_sed
sed/POSIX replacement syntax (routes to std).
@ match_not_bow
\b does not match at the start.
@ match_not_eol
$ does not match the end of the sequence.
@ format_first_only
Replace only the first match.
@ format_no_copy
Do not copy the parts of the text that did not match.
@ match_continuous
The match must start at the first character.
@ match_not_eow
\b does not match at the end.
constexpr match_flag_type operator~(match_flag_type a) noexcept
policy
The drop-in policy for a pattern the linear engine cannot represent (backreferences,...
@ strict
Reject an ineligible pattern (throws regex_error with error_complexity). The default.
@ fallback
Delegate an ineligible pattern to std::regex (backtracking — not ReDoS-safe).
@ first
Leftmost-first (Perl / Python re / the crate): source-order thread priority decides....
flags
Compilation flags, mirroring Python's re.I, re.M and re.S.
Definition program.hpp:42
@ bytes
Binary mode: . and [^…] match raw bytes, not codepoints.
@ icase
Case-insensitive (ASCII).
@ ecma
ECMAScript compatibility: $ (no multiline) matches only at the very end (not before a final \n,...
@ multiline
^ and $ also match at line boundaries.
The public API: real::regex, real::static_regex and results.
bool empty_match_possible
The pattern can match the empty string (the nullable gate; conservative: assertions/lookarounds pass ...
Definition program.hpp:273
pattern_hints hints
Search-acceleration hints.
Definition program.hpp:360
REAL's version macros and the C++20 language-standard guard.