[libc-commits] [libc] [libc][regex] Add ERE {m, n} interval quantifiers (PR #236569)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Sat Oct 31 13:36:10 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/236569
>From 369776e8be39b3c51cbbddb10399acdf2311fe43 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Wed, 28 Oct 2026 14:42:38 +0000
Subject: [PATCH 1/2] [libc][regex] Add ERE {m,n} interval quantifiers
POSIX Extended Regular Expressions support bounded and lower-bounded
interval quantifiers ({m}, {m,}, and {m,n}) with repetition counts up
to RE_DUP_MAX (255), while treating an unescaped '{' not followed by an
ASCII digit as a literal character. Unrolling counted repetitions at
parse time duplicates subexpressions up to 255 times per quantifier, and
unanchored derivative stepping across high-count intervals builds
quadratic alternation chains as active repetition states accumulate at
each input byte.
Represent interval quantifiers directly with a 16-byte Repeat(body, min,
max) AST node and bound cumulative nested repetition expansion in
parse_ere to reject multiplicative state blowup with REG_ESPACE. In
ExprPool::make_alt, coalesce overlapping or adjacent Repeat intervals
(including shared-suffix Concat nodes) into a single interval so
unanchored matching advances in O(1) nodes per byte. Also push r and
Star(r) as separate atom-stack entries for '+' in parse_concat
(follow-up to commit adb80317aa28) so right-to-left folding builds
r . (r* . rest) once without leaving an orphaned Concat spine in the
pool.
External compliance suites: Henry Spencer suite passes 268/456 (+50)
boolean/error cases and 84/456 (+11) full-position cases; AT&T testregex
passes 399/400 (+66) ERE boolean/error cases (100% of case-sensitive
ERE) and 401/473 (+66) overall boolean/error cases with 0 regressions.
Assisted-by: Automated tooling, human reviewed.
---
libc/include/limits.yaml | 4 +
libc/include/llvm-libc-macros/limits-macros.h | 7 +
libc/src/__support/regex/CMakeLists.txt | 1 +
libc/src/__support/regex/regex_ast.h | 25 +++
libc/src/__support/regex/regex_expr_pool.cpp | 96 ++++++++++-
libc/src/__support/regex/regex_expr_pool.h | 2 +
libc/src/__support/regex/regex_matcher.cpp | 17 ++
libc/src/__support/regex/regex_parser.cpp | 109 +++++++++++--
libc/test/src/__support/regex/expr_test.cpp | 31 ++++
.../test/src/__support/regex/matcher_test.cpp | 149 ++++++++++++++++--
libc/test/src/regex/regex_basic_test.cpp | 25 ++-
11 files changed, 437 insertions(+), 29 deletions(-)
diff --git a/libc/include/limits.yaml b/libc/include/limits.yaml
index 1d619984ff5c38..aa3677fa9432bf 100644
--- a/libc/include/limits.yaml
+++ b/libc/include/limits.yaml
@@ -92,6 +92,10 @@ macros:
macro_header: limits-macros.h
- macro_name: NZERO
macro_header: limits-macros.h
+ - macro_name: _POSIX2_RE_DUP_MAX
+ macro_header: limits-macros.h
+ - macro_name: RE_DUP_MAX
+ macro_header: limits-macros.h
types: []
enums: []
objects: []
diff --git a/libc/include/llvm-libc-macros/limits-macros.h b/libc/include/llvm-libc-macros/limits-macros.h
index 64f1b507ab3861..c7aa6087c32c49 100644
--- a/libc/include/llvm-libc-macros/limits-macros.h
+++ b/libc/include/llvm-libc-macros/limits-macros.h
@@ -298,4 +298,11 @@
#define NZERO 20
#endif // NZERO
+#ifndef _POSIX2_RE_DUP_MAX
+#define _POSIX2_RE_DUP_MAX 255
+#endif
+#ifndef RE_DUP_MAX
+#define RE_DUP_MAX _POSIX2_RE_DUP_MAX
+#endif
+
#endif // LLVM_LIBC_MACROS_LIMITS_MACROS_H
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index a9113005c08fe7..6d9db1eef5a253 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -58,6 +58,7 @@ add_object_library(
regex_parser.h
DEPENDS
.regex_expr_pool
+ libc.hdr.limits_macros
libc.hdr.regex_macros
libc.src.__support.alloc_checker
libc.src.__support.CPP.array
diff --git a/libc/src/__support/regex/regex_ast.h b/libc/src/__support/regex/regex_ast.h
index 5c5fa006bec76b..c87146a350f4e5 100644
--- a/libc/src/__support/regex/regex_ast.h
+++ b/libc/src/__support/regex/regex_ast.h
@@ -28,6 +28,10 @@ inline constexpr ExprId INVALID_EXPR_ID = 0;
inline constexpr ExprId EMPTY_SET_ID = 1;
inline constexpr ExprId EMPTY_STR_ID = 2;
+/// Sentinel upper bound representing unbounded repetition in Repeat(body, m,
+/// ∞).
+inline constexpr uint16_t REPEAT_INF = 0xFFFF;
+
/// One bit per possible byte value (2^CHAR_BIT = 256), allowing O(1)
/// membership tests for wildcards and bracket expressions.
inline constexpr size_t BYTE_SET_BITS = size_t(1) << CHAR_BIT;
@@ -57,6 +61,8 @@ enum class ExprKind : uint8_t {
Alt,
/// Zero or more repetitions of a sub-expression (body*).
Star,
+ /// Bounded or lower-bounded repetitions of a sub-expression (body{min,max}).
+ Repeat,
};
/// A node in the Regular Expression Abstract Syntax Tree.
@@ -81,6 +87,12 @@ struct Expr {
ExprId left;
ExprId right;
} bin = {INVALID_EXPR_ID, INVALID_EXPR_ID};
+ /// Sub-expression and bounds for Repeat nodes.
+ struct {
+ ExprId body;
+ uint16_t min;
+ uint16_t max;
+ } rep;
/// Sub-expression for Star nodes.
ExprId body;
/// Index into ExprPool::byte_sets for ByteSet nodes.
@@ -108,6 +120,11 @@ struct Expr {
/// Create a Star node.
constexpr Expr(ExprId b, bool anchors)
: kind(ExprKind::Star), nullable(true), has_anchors(anchors), body(b) {}
+ /// Create a Repeat node.
+ constexpr Expr(ExprId b, uint16_t min_rep, uint16_t max_rep, bool is_null,
+ bool anchors)
+ : kind(ExprKind::Repeat), nullable(is_null), has_anchors(anchors),
+ rep{b, min_rep, max_rep} {}
/// Create a binary node (Concat or Alt).
constexpr Expr(ExprKind k, bool is_null, bool anchors, ExprId l, ExprId r)
: kind(k), nullable(is_null), has_anchors(anchors), bin{l, r} {}
@@ -127,6 +144,11 @@ struct Expr {
static constexpr Expr make_star(ExprId b, bool anchors = false) {
return Expr(b, anchors);
}
+ static constexpr Expr make_repeat(ExprId b, uint16_t min_rep,
+ uint16_t max_rep, bool is_null = false,
+ bool anchors = false) {
+ return Expr(b, min_rep, max_rep, is_null, anchors);
+ }
static constexpr Expr make_concat(ExprId l, ExprId r, bool is_null = false,
bool anchors = false) {
return Expr(ExprKind::Concat, is_null, anchors, l, r);
@@ -152,6 +174,9 @@ struct Expr {
return byte_set_id == other.byte_set_id;
case ExprKind::Star:
return body == other.body;
+ case ExprKind::Repeat:
+ return rep.body == other.rep.body && rep.min == other.rep.min &&
+ rep.max == other.rep.max;
case ExprKind::Concat:
case ExprKind::Alt:
return bin.left == other.bin.left && bin.right == other.bin.right;
diff --git a/libc/src/__support/regex/regex_expr_pool.cpp b/libc/src/__support/regex/regex_expr_pool.cpp
index 7f68619c4b272a..47083f77ddf01b 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -48,6 +48,11 @@ uint32_t hash_expr(const Expr &e) {
case ExprKind::Star:
hasher.update(&e.body, sizeof(e.body));
break;
+ case ExprKind::Repeat:
+ hasher.update(&e.rep.body, sizeof(e.rep.body));
+ hasher.update(&e.rep.min, sizeof(e.rep.min));
+ hasher.update(&e.rep.max, sizeof(e.rep.max));
+ break;
case ExprKind::Concat:
case ExprKind::Alt:
hasher.update(&e.bin.left, sizeof(e.bin.left));
@@ -287,12 +292,35 @@ ErrorOr<ExprId> ExprPool::make_star(ExprId body) {
// Ø* = ε and ε* = ε
if (body == EMPTY_SET_ID || body == EMPTY_STR_ID)
return empty_str();
- // (R*)* = R* (Idempotency)
- if (get(body).kind == ExprKind::Star)
+ // (R*)* = R* and (R{m,n})* = R* for m <= 1.
+ const Expr be = get(body);
+ if (be.kind == ExprKind::Star)
return body;
+ if (be.kind == ExprKind::Repeat && be.rep.min <= 1)
+ return make_star(be.rep.body);
return intern(Expr::make_star(body, has_anchors(body)));
}
+ErrorOr<ExprId> ExprPool::make_repeat(ExprId body, uint16_t min, uint16_t max) {
+ if (body == INVALID_EXPR_ID || min > max)
+ return Error(REG_BADPAT);
+ // Normalize R{0,0} = ε, ε{m,n} = ε, Ø{m,n}, R{1,1} = R, and R{0,∞} = R*.
+ if (max == 0 || body == EMPTY_STR_ID)
+ return empty_str();
+ if (body == EMPTY_SET_ID)
+ return (min == 0) ? empty_str() : empty_set();
+ if (min == 1 && max == 1)
+ return body;
+ const Expr be = get(body);
+ if ((min == 0 && max == REPEAT_INF) || be.kind == ExprKind::Star)
+ return make_star(body);
+ if (be.kind == ExprKind::Repeat && be.rep.min == 1 &&
+ be.rep.max == REPEAT_INF && min == 1 && max == REPEAT_INF)
+ return body;
+ return intern(Expr::make_repeat(
+ body, min, max, (min == 0) || is_nullable(body), has_anchors(body)));
+}
+
ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
return Error(REG_BADPAT);
@@ -317,6 +345,56 @@ ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
has_anchors(l) || has_anchors(r)));
}
+namespace {
+
+// Coalesces R{m1,n1} | R{m2,n2} (and shared-suffix Concat nodes) when their
+// repetition intervals overlap or are adjacent. Returns INVALID_EXPR_ID when
+// the pair cannot be coalesced.
+ErrorOr<ExprId> coalesce_repeat_pair(ExprId a, ExprId b, ExprPool &pool) {
+ const Expr ae = pool.get(a);
+ const Expr be = pool.get(b);
+ if (ae.kind == ExprKind::Concat && be.kind == ExprKind::Concat &&
+ ae.bin.right == be.bin.right) {
+ ErrorOr<ExprId> left = coalesce_repeat_pair(ae.bin.left, be.bin.left, pool);
+ if (!left.has_value() || *left == INVALID_EXPR_ID)
+ return left;
+ return pool.make_concat(*left, ae.bin.right);
+ }
+ if (ae.kind != ExprKind::Repeat && be.kind != ExprKind::Repeat)
+ return INVALID_EXPR_ID;
+ ExprId body = (ae.kind == ExprKind::Repeat) ? ae.rep.body : be.rep.body;
+ auto bounds = [&](ExprId id, const Expr &e, uint16_t &lo,
+ uint16_t &hi) -> bool {
+ if (e.kind == ExprKind::Repeat && e.rep.body == body) {
+ lo = e.rep.min;
+ hi = e.rep.max;
+ return true;
+ }
+ if (id == body) {
+ lo = 1;
+ hi = 1;
+ return true;
+ }
+ if (id == EMPTY_STR_ID) {
+ lo = 0;
+ hi = 0;
+ return true;
+ }
+ return false;
+ };
+ uint16_t lo1 = 0, hi1 = 0, lo2 = 0, hi2 = 0;
+ if (!bounds(a, ae, lo1, hi1) || !bounds(b, be, lo2, hi2))
+ return INVALID_EXPR_ID;
+ uint16_t max_lo = (lo1 > lo2) ? lo1 : lo2;
+ uint16_t min_hi = (hi1 < hi2) ? hi1 : hi2;
+ if (min_hi != REPEAT_INF && max_lo > min_hi + 1)
+ return INVALID_EXPR_ID;
+ return pool.make_repeat(body, (lo1 < lo2) ? lo1 : lo2,
+ (hi1 > hi2) ? hi1 : hi2);
+}
+
+} // namespace
+
ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
return Error(REG_BADPAT);
@@ -333,6 +411,9 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
const Expr le = get(l);
const Expr re = get(r);
if (le.kind != ExprKind::Alt && re.kind != ExprKind::Alt) {
+ ErrorOr<ExprId> coalesced = coalesce_repeat_pair(l, r, *this);
+ if (!coalesced.has_value() || *coalesced != INVALID_EXPR_ID)
+ return coalesced;
if (l > r)
cpp::swap(l, r);
return intern(Expr::make_alt(l, r, is_nullable(l) || is_nullable(r),
@@ -341,11 +422,20 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
// Merge two right-associated sorted Alt spines in ascending ExprId order so
// multi-way alternations canonicalize to a single deduplicated tree and
- // derivative state spaces under '*' stay finite.
+ // derivative state spaces under '*' and '{m,n}' stay finite.
ExprId hl = (le.kind == ExprKind::Alt) ? le.bin.left : l;
ExprId tl = (le.kind == ExprKind::Alt) ? le.bin.right : EMPTY_SET_ID;
ExprId hr = (re.kind == ExprKind::Alt) ? re.bin.left : r;
ExprId tr = (re.kind == ExprKind::Alt) ? re.bin.right : EMPTY_SET_ID;
+ ErrorOr<ExprId> coalesced = coalesce_repeat_pair(hl, hr, *this);
+ if (!coalesced.has_value())
+ return coalesced;
+ if (*coalesced != INVALID_EXPR_ID) {
+ ErrorOr<ExprId> tails = make_alt(tl, tr);
+ if (!tails.has_value())
+ return tails;
+ return make_alt(*coalesced, *tails);
+ }
ExprId head = hl;
ErrorOr<ExprId> rest = EMPTY_SET_ID;
if (hl == hr) {
diff --git a/libc/src/__support/regex/regex_expr_pool.h b/libc/src/__support/regex/regex_expr_pool.h
index 37fc585e2e2387..cade66d14c3716 100644
--- a/libc/src/__support/regex/regex_expr_pool.h
+++ b/libc/src/__support/regex/regex_expr_pool.h
@@ -167,6 +167,8 @@ class ExprPool {
ErrorOr<ExprId> make_byte_set(const ByteSet &bs);
/// Creates or returns a normalized Kleene star node (body*).
ErrorOr<ExprId> make_star(ExprId body);
+ /// Creates or returns a normalized Repeat node (body{min,max}).
+ ErrorOr<ExprId> make_repeat(ExprId body, uint16_t min, uint16_t max);
/// Creates or returns a right-associated Concatenation node (l . r).
ErrorOr<ExprId> make_concat(ExprId l, ExprId r);
/// Creates or returns a canonical deduplicated Alternation node (l | r).
diff --git a/libc/src/__support/regex/regex_matcher.cpp b/libc/src/__support/regex/regex_matcher.cpp
index c58daf83294f60..003735ffc5121f 100644
--- a/libc/src/__support/regex/regex_matcher.cpp
+++ b/libc/src/__support/regex/regex_matcher.cpp
@@ -55,6 +55,8 @@ bool nullable(ExprId id, uint8_t anchor_ctx, const ExprPool &pool) {
case ExprKind::Alt:
return nullable(e.bin.left, anchor_ctx, pool) ||
nullable(e.bin.right, anchor_ctx, pool);
+ case ExprKind::Repeat:
+ return (e.rep.min == 0) || nullable(e.rep.body, anchor_ctx, pool);
default:
return e.nullable;
}
@@ -107,6 +109,21 @@ ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool,
return d_body;
return pool.make_concat(*d_body, id);
}
+ case ExprKind::Repeat: {
+ // Bounded Repetition Rule: D_c(R{m,n}) = D_c(R) . R{max(0,m-1), n-1}
+ ErrorOr<ExprId> d_body = derivative(e.rep.body, c, pool, ctx_before);
+ if (!d_body.has_value() || *d_body == EMPTY_SET_ID)
+ return d_body;
+ uint16_t next_min =
+ (e.rep.min > 0) ? static_cast<uint16_t>(e.rep.min - 1) : uint16_t(0);
+ uint16_t next_max = (e.rep.max == REPEAT_INF)
+ ? REPEAT_INF
+ : static_cast<uint16_t>(e.rep.max - 1);
+ ErrorOr<ExprId> tail = pool.make_repeat(e.rep.body, next_min, next_max);
+ if (!tail.has_value())
+ return tail;
+ return pool.make_concat(*d_body, *tail);
+ }
}
__builtin_unreachable();
}
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index f1864afd41171a..80c3ce37ac855d 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -13,13 +13,16 @@
#include "src/__support/regex/regex_parser.h"
+#include "hdr/limits_macros.h"
#include "hdr/regex_macros.h"
#include "src/__support/CPP/array.h"
#include "src/__support/CPP/new.h"
+#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/scope.h"
#include "src/__support/CPP/span.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/alloc-checker.h"
+#include "src/__support/ctype_utils.h"
#include "src/__support/error_or.h"
#include "src/__support/macros/config.h"
#include "src/__support/regex/regex_ast.h"
@@ -165,17 +168,91 @@ ErrorOr<ExprId> parse_bracket_expr(cpp::string_view &rem, int cflags,
// Maximum parenthesized group nesting depth to prevent parser stack overflow.
constexpr size_t MAX_GROUP_DEPTH = 128;
+// Maximum cumulative nested repetition multiplier to bound state expansion.
+constexpr size_t MAX_REPEAT_EXPANSION = 32768;
+
+size_t repeat_expansion(ExprId id, const ExprPool &pool) {
+ const Expr &e = pool.get(id);
+ if (e.kind == ExprKind::Repeat) {
+ uint16_t bound = (e.rep.max == REPEAT_INF) ? e.rep.min : e.rep.max;
+ return (bound > 1 ? size_t(bound) : size_t(1)) *
+ repeat_expansion(e.rep.body, pool);
+ }
+ if (e.kind == ExprKind::Star)
+ return repeat_expansion(e.body, pool);
+ if (e.kind == ExprKind::Concat || e.kind == ExprKind::Alt) {
+ size_t l = repeat_expansion(e.bin.left, pool);
+ size_t r =
+ (e.bin.right == e.bin.left) ? l : repeat_expansion(e.bin.right, pool);
+ return (l > r) ? l : r;
+ }
+ return 1;
+}
+
+// Parses a decimal repetition bound in [0, RE_DUP_MAX] from the prefix of spec.
+cpp::optional<uint16_t> consume_bound(cpp::string_view &spec) {
+ if (!internal::isdigit(spec.front()))
+ return cpp::nullopt;
+ uint32_t val = 0;
+ while (!spec.empty() && internal::isdigit(spec.front())) {
+ val = val * 10 + static_cast<uint32_t>(spec.front() - '0');
+ if (val > RE_DUP_MAX)
+ return cpp::nullopt;
+ spec.remove_prefix(1);
+ }
+ return static_cast<uint16_t>(val);
+}
+
+// Parses a trailing '{m}', '{m,}', or '{m,n}' interval quantifier after '{' has
+// been matched with a leading digit.
+ErrorOr<ExprId> parse_repeat_bounds(cpp::string_view &rem, ExprId atom,
+ ExprPool &pool) {
+ rem.remove_prefix(1);
+ size_t close_pos = 0;
+ while (close_pos < rem.size() && rem[close_pos] != '}')
+ ++close_pos;
+ if (close_pos == rem.size())
+ return Error(REG_EBRACE);
+ cpp::string_view spec = rem.substr(0, close_pos);
+ rem.remove_prefix(close_pos + 1);
+
+ cpp::optional<uint16_t> min_opt = consume_bound(spec);
+ if (!min_opt.has_value())
+ return Error(REG_BADBR);
+ uint16_t min_val = *min_opt;
+ uint16_t max_val = min_val;
+ if (spec.starts_with(',')) {
+ spec.remove_prefix(1);
+ if (spec.empty()) {
+ max_val = REPEAT_INF;
+ } else {
+ cpp::optional<uint16_t> max_opt = consume_bound(spec);
+ if (!max_opt.has_value() || !spec.empty() || min_val > *max_opt)
+ return Error(REG_BADBR);
+ max_val = *max_opt;
+ }
+ } else if (!spec.empty()) {
+ return Error(REG_BADBR);
+ }
+
+ uint16_t bound = (max_val == REPEAT_INF) ? min_val : max_val;
+ if (bound > 1 && repeat_expansion(atom, pool) * bound > MAX_REPEAT_EXPANSION)
+ return Error(REG_ESPACE);
+ return pool.make_repeat(atom, min_val, max_val);
+}
+
struct ParsedAtomStack;
ErrorOr<ExprId> parse_alt(cpp::string_view &rem, int cflags, ExprPool &pool,
size_t depth, size_t &re_nsub,
ParsedAtomStack &stack);
-// Parses a single ERE atom and an optional trailing '*', '+', or '?'
+// Parses a single ERE atom and an optional trailing '*', '+', '?', or '{m,n}'
// quantifier.
ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
ExprPool &pool, size_t depth,
- size_t &re_nsub, ParsedAtomStack &stack) {
+ size_t &re_nsub, ParsedAtomStack &stack,
+ ExprId &prefix_atom) {
char c = rem.front();
rem.remove_prefix(1);
ErrorOr<ExprId> atom = INVALID_EXPR_ID;
@@ -244,9 +321,10 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
case '?':
return Error(REG_BADRPT);
case '{':
- if (!rem.empty() && rem.front() >= '0' && rem.front() <= '9')
+ if (!rem.empty() && internal::isdigit(rem.front()))
return Error(REG_BADRPT);
- return Error(REG_BADPAT);
+ atom = pool.make_lit('{');
+ break;
case '[':
atom = parse_bracket_expr(rem, cflags, pool);
break;
@@ -282,17 +360,15 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
}
if (rem.starts_with('+')) {
rem.remove_prefix(1);
- ErrorOr<ExprId> star = pool.make_star(*atom);
- if (!star.has_value())
- return star;
- return pool.make_concat(*atom, *star);
+ prefix_atom = *atom;
+ return pool.make_star(*atom);
}
if (rem.starts_with('?')) {
rem.remove_prefix(1);
return pool.make_alt(EMPTY_STR_ID, *atom);
}
- if (rem.starts_with('{'))
- return Error(REG_BADPAT);
+ if (rem.size() >= 2 && rem[0] == '{' && internal::isdigit(rem[1]))
+ return parse_repeat_bounds(rem, *atom, pool);
return atom;
}
@@ -342,15 +418,20 @@ ErrorOr<ExprId> parse_concat(cpp::string_view &rem, int cflags, ExprPool &pool,
size_t depth, size_t &re_nsub,
ParsedAtomStack &stack) {
// Collect quantified atoms for this branch on the shared stack, then fold
- // right-to-left so right-associative make_concat stays O(1) without orphaned
- // prefixes.
+ // right-to-left so non-Concat atoms link in O(1) without re-association.
size_t atom_count = 0;
while (!rem.empty() && !rem.starts_with('|') &&
!(depth > 0 && rem.starts_with(')'))) {
- ErrorOr<ExprId> atom =
- parse_quantified_atom(rem, cflags, pool, depth, re_nsub, stack);
+ ExprId prefix_atom = INVALID_EXPR_ID;
+ ErrorOr<ExprId> atom = parse_quantified_atom(rem, cflags, pool, depth,
+ re_nsub, stack, prefix_atom);
if (!atom.has_value())
return atom;
+ if (prefix_atom != INVALID_EXPR_ID) {
+ if (!stack.push(prefix_atom))
+ return Error(REG_ESPACE);
+ ++atom_count;
+ }
if (!stack.push(*atom))
return Error(REG_ESPACE);
++atom_count;
diff --git a/libc/test/src/__support/regex/expr_test.cpp b/libc/test/src/__support/regex/expr_test.cpp
index 990a67d0cc87c3..26074c4767461f 100644
--- a/libc/test/src/__support/regex/expr_test.cpp
+++ b/libc/test/src/__support/regex/expr_test.cpp
@@ -24,6 +24,7 @@ using LIBC_NAMESPACE::regex::ExprId;
using LIBC_NAMESPACE::regex::ExprKind;
using LIBC_NAMESPACE::regex::ExprPool;
using LIBC_NAMESPACE::regex::INVALID_EXPR_ID;
+using LIBC_NAMESPACE::regex::REPEAT_INF;
TEST(LlvmLibcRegexExprTest, Interning) {
ExprPool pool;
@@ -223,6 +224,36 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAltAndStar) {
EXPECT_TRUE(pool.has_anchors(*pool.make_star(*pool.make_anchor(ANCHOR_BOL))));
EXPECT_TRUE(Expr::make_star(lit_a) == Expr::make_star(lit_a));
EXPECT_FALSE(Expr::make_star(lit_a) == Expr::make_star(lit_b));
+
+ // Repeat identities and interval coalescing in make_alt.
+ const ExprId plus_a = *pool.make_repeat(lit_a, 1, REPEAT_INF);
+ EXPECT_EQ(pool.make_repeat(INVALID_EXPR_ID, 1, 2).error(), REG_BADPAT);
+ EXPECT_EQ(pool.make_repeat(lit_a, 3, 2).error(), REG_BADPAT);
+ EXPECT_EQ(*pool.make_repeat(lit_a, 0, 0), empty_str);
+ EXPECT_EQ(*pool.make_repeat(empty_str, 2, 4), empty_str);
+ EXPECT_EQ(*pool.make_repeat(empty_set, 0, 3), empty_str);
+ EXPECT_EQ(*pool.make_repeat(empty_set, 1, 3), empty_set);
+ EXPECT_EQ(*pool.make_repeat(lit_a, 1, 1), lit_a);
+ EXPECT_EQ(*pool.make_repeat(lit_a, 0, REPEAT_INF), star_a);
+ EXPECT_EQ(*pool.make_repeat(star_a, 2, 4), star_a);
+ EXPECT_EQ(*pool.make_star(plus_a), star_a);
+ EXPECT_EQ(*pool.make_repeat(plus_a, 1, REPEAT_INF), plus_a);
+
+ const ExprId rep_2_3 = *pool.make_repeat(lit_a, 2, 3);
+ const ExprId rep_4_5 = *pool.make_repeat(lit_a, 4, 5);
+ EXPECT_EQ(*pool.make_alt(rep_2_3, rep_4_5), *pool.make_repeat(lit_a, 2, 5));
+ EXPECT_EQ(*pool.make_alt(lit_a, rep_2_3), *pool.make_repeat(lit_a, 1, 3));
+ EXPECT_EQ(*pool.make_alt(empty_str, plus_a), star_a);
+ EXPECT_EQ(*pool.make_alt(*pool.make_concat(rep_2_3, lit_b),
+ *pool.make_concat(rep_4_5, lit_b)),
+ *pool.make_concat(*pool.make_repeat(lit_a, 2, 5), lit_b));
+ EXPECT_TRUE(pool.is_nullable(*pool.make_repeat(lit_a, 0, 2)));
+ EXPECT_FALSE(pool.is_nullable(rep_2_3));
+ EXPECT_TRUE(
+ pool.has_anchors(*pool.make_repeat(*pool.make_anchor(ANCHOR_BOL), 2, 3)));
+ EXPECT_TRUE(Expr::make_repeat(lit_a, 2, 3) == Expr::make_repeat(lit_a, 2, 3));
+ EXPECT_FALSE(Expr::make_repeat(lit_a, 2, 3) ==
+ Expr::make_repeat(lit_a, 2, 4));
}
TEST(LlvmLibcRegexExprTest, HermeticHundredPools) {
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index d0d3c4c39c4739..ee001da472162a 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -252,15 +252,32 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
ASSERT_FALSE(trailing_escape.has_value());
EXPECT_EQ(trailing_escape.error(), REG_EESCAPE);
- const string_view bad_rpt_patterns[] = {"*", "+", "?", "{1}", "^*",
- "^+", "^?", "^{1}", "$*", "a**",
- "a*+", "a*?", "a*{1}"};
+ const string_view bad_rpt_patterns[] = {
+ "*", "+", "?", "{1}", "{1", "^*", "^+",
+ "^?", "^{1}", "$*", "${1}", "a**", "a*+", "a*?",
+ "a*{1}", "a+{1}", "a?{1}", "a{2}{2}", "a{2}*", "a{2}+", "a{2}?"};
for (string_view bad_rpt : bad_rpt_patterns) {
ErrorOr<ExprId> err = parse_ere(bad_rpt, REG_EXTENDED, pool);
ASSERT_FALSE(err.has_value());
EXPECT_EQ(err.error(), REG_BADRPT);
}
+ const string_view ebrace_patterns[] = {"a{1", "a{1a", "a{1,2", "a{1,x"};
+ for (string_view pat : ebrace_patterns) {
+ ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_EBRACE);
+ }
+
+ const string_view badbr_patterns[] = {"a{1a}", "a{1,x}", "a{1,0}",
+ "a{5,2}", "a{256}", "a{1,256}",
+ "a{1,2,3}", "a{9876543210}"};
+ for (string_view pat : badbr_patterns) {
+ ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_BADBR);
+ }
+
const string_view ebrack_patterns[] = {
"[", "[^", "[abc", "[]", "a[[.x", "a[[.x.", "a[[:digit:", "a[[:digit"};
for (string_view pat : ebrack_patterns) {
@@ -294,8 +311,6 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
}
const string_view bad_patterns[] = {
- // Unimplemented quantifiers.
- "a{1}", "{",
// Unimplemented word boundaries.
"[[:<:]]", "[[:>:]]",
// Unimplemented backreferences.
@@ -448,6 +463,7 @@ TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
// D_a((a . b) | (a . c)) = b | c, and D_a((a . b)*) = b . (a . b)*.
const ExprId need_bc = *pool.make_alt(ab, ac);
const ExprId star_ab = *pool.make_star(ab);
+ const ExprId rep_ab = *pool.make_repeat(ab, 2, 3);
const ExprId concat_left_fail = *pool.make_concat(need_bc, lit_c);
const ExprId concat_right_fail = *pool.make_concat(opt_a, need_bc);
const ExprId alt_right_fail = *pool.make_alt(lit_a, need_bc);
@@ -459,9 +475,9 @@ TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
while (pool.size() < 10000)
chain = *pool.make_concat(lit_a, chain);
- const ExprId exhausted_exprs[] = {need_bc, star_ab,
- concat_left_fail, concat_right_fail,
- alt_left_fail, alt_right_fail};
+ const ExprId exhausted_exprs[] = {
+ need_bc, star_ab, rep_ab, concat_left_fail,
+ concat_right_fail, alt_left_fail, alt_right_fail};
for (ExprId expr : exhausted_exprs) {
ErrorOr<ExprId> err = derivative(expr, 'a', pool);
ASSERT_FALSE(err.has_value());
@@ -521,8 +537,9 @@ TEST(LlvmLibcRegexMatcherTest, AlternationAndGroupingStabilization) {
TEST(LlvmLibcRegexMatcherTest, PlusAndQuestionDesugaringAndStabilization) {
ExprPool pool;
- // r+ desugars to Concat(r, Star(r)) and r? desugars to Alt(EmptyStr, r),
- // interning to identical ExprId handles.
+ // r+ pushes r and Star(r) separately onto ParsedAtomStack and r? lowers to
+ // Alt(EmptyStr, r), interning to identical ExprId handles without orphaned
+ // Concat spines.
ErrorOr<ExprId> plus_a = parse_ere("a+", REG_EXTENDED, pool);
ErrorOr<ExprId> concat_star_a = parse_ere("aa*", REG_EXTENDED, pool);
ASSERT_TRUE(plus_a.has_value());
@@ -541,4 +558,116 @@ TEST(LlvmLibcRegexMatcherTest, PlusAndQuestionDesugaringAndStabilization) {
size_t warm_nodes = pool.size();
EXPECT_EQ(match(pool, *ere, "aaabc"), MatchResult::Match);
EXPECT_EQ(pool.size(), warm_nodes);
+
+ // Pushing r and Star(r) as separate stack entries builds r . (r* . rest)
+ // once, giving (ab)+c and (ab)(ab)*c identical 10-node pools and allowing
+ // ^(a...2500_as...)+$ to compile in 5,007 nodes and match 2,500 'a's.
+ ExprPool plus_pool;
+ ExprPool star_pool;
+ ErrorOr<ExprId> group_plus = parse_ere("(ab)+c", REG_EXTENDED, plus_pool);
+ ErrorOr<ExprId> group_star = parse_ere("(ab)(ab)*c", REG_EXTENDED, star_pool);
+ ASSERT_TRUE(group_plus.has_value());
+ ASSERT_TRUE(group_star.has_value());
+ EXPECT_EQ(plus_pool.size(), size_t(10));
+ EXPECT_EQ(plus_pool.size(), star_pool.size());
+
+ char pat2500[2506];
+ pat2500[0] = '^';
+ pat2500[1] = '(';
+ for (size_t i = 0; i < 2500; ++i)
+ pat2500[2 + i] = 'a';
+ pat2500[2502] = ')';
+ pat2500[2503] = '+';
+ pat2500[2504] = '$';
+ pat2500[2505] = '\0';
+ ExprPool long_plus_pool;
+ ErrorOr<ExprId> long_plus =
+ parse_ere(string_view(pat2500, 2505), REG_EXTENDED, long_plus_pool);
+ ASSERT_TRUE(long_plus.has_value());
+ EXPECT_EQ(long_plus_pool.size(), size_t(5007));
+ EXPECT_EQ(match(long_plus_pool, *long_plus, string_view(pat2500 + 2, 2500)),
+ MatchResult::Match);
+}
+
+TEST(LlvmLibcRegexMatcherTest, RepeatStabilizationAndRfc2822Grammar) {
+ // Native Repeat(body, 255, 255) compiles into 4 nodes inside 1 KiB; both
+ // unanchored a{255} / a{255}b (via make_alt interval coalescing) and anchored
+ // ^a{255}$ match and stabilize on warm runs.
+ ExprPool rep_pool;
+ ErrorOr<ExprId> a255 = parse_ere("a{255}", REG_EXTENDED, rep_pool);
+ ASSERT_TRUE(a255.has_value());
+ EXPECT_EQ(rep_pool.size(), size_t(4));
+ EXPECT_LE(rep_pool.allocated_bytes(), size_t(1024));
+
+ ErrorOr<ExprId> a255b = parse_ere("a{255}b", REG_EXTENDED, rep_pool);
+ ErrorOr<ExprId> anchored_a255 = parse_ere("^a{255}$", REG_EXTENDED, rep_pool);
+ ASSERT_TRUE(a255b.has_value());
+ ASSERT_TRUE(anchored_a255.has_value());
+ char buf256[256];
+ for (char &ch : buf256)
+ ch = 'a';
+ EXPECT_EQ(match(rep_pool, *a255, string_view(buf256, 255)),
+ MatchResult::Match);
+ buf256[255] = 'b';
+ EXPECT_EQ(match(rep_pool, *a255b, string_view(buf256, 256)),
+ MatchResult::Match);
+ buf256[255] = 'a';
+ EXPECT_EQ(match(rep_pool, *anchored_a255, string_view(buf256, 255)),
+ MatchResult::Match);
+ size_t warm_nodes = rep_pool.size();
+ EXPECT_EQ(match(rep_pool, *a255, string_view(buf256, 255)),
+ MatchResult::Match);
+ EXPECT_EQ(match(rep_pool, *anchored_a255, string_view(buf256, 255)),
+ MatchResult::Match);
+ EXPECT_EQ(rep_pool.size(), warm_nodes);
+ EXPECT_EQ(match(rep_pool, *anchored_a255, string_view(buf256, 254)),
+ MatchResult::NoMatch);
+ EXPECT_EQ(match(rep_pool, *anchored_a255, string_view(buf256, 256)),
+ MatchResult::NoMatch);
+
+ // Build a >7 KiB ERE from RFC-2822 Section 3.4.1 addr-spec EBNF productions
+ // (F = FWS, C = CFWS, T = atext, W = word, S = addr-spec) and verify
+ // hash-consed DAG deduplication keeps the pool under 32 KiB.
+ char rfc_buf[16384];
+ size_t rfc_len = 0;
+ auto expand = [&](auto &self, string_view str) -> void {
+ for (char ch : str) {
+ if (ch == 'F')
+ self(self, "(([ \t]*\r\n)?[ \t]+)");
+ else if (ch == 'C')
+ self(self, R"(((F?\((F?([!-'*-[^-~]|\\([ -~]|[ ])))*F?\))+F?|F)?)");
+ else if (ch == 'T')
+ self(self, R"([[:alnum:]!#$%&'*+/=?^_`{|}~-])");
+ else if (ch == 'W')
+ self(self, R"(((CT+C)|(C"(F?([!#-[^-~]|\\([ -~]|[ ])))*F?"C)))");
+ else if (ch == 'S')
+ self(self, R"((((CT+(\.T+)*C)|W|(W(\.W{1,1}){0,31}))@)"
+ R"(((CT+(\.T+)*C)|(C\[(([0-9]{1,3}\.){3}[0-9]{1,3}|)"
+ R"((F?([!-Z^-~]|\\([ -~]|[ ])))*F?)\]C)|)"
+ R"(((CT+C)(\.(CT+C){1,1}){1,31}))))");
+ else
+ rfc_buf[rfc_len++] = ch;
+ }
+ };
+ expand(expand, "^(S|W{0,8}<S>)$");
+ string_view rfc2822_ere(rfc_buf, rfc_len);
+ EXPECT_GT(rfc2822_ere.size(), size_t(7000));
+
+ ExprPool rfc_pool;
+ ErrorOr<ExprId> addr_spec = parse_ere(rfc2822_ere, REG_EXTENDED, rfc_pool);
+ ASSERT_TRUE(addr_spec.has_value());
+ EXPECT_LE(rfc_pool.allocated_bytes(), size_t(32768));
+
+ const string_view good_addrs[] = {"user.name+tag at example.com",
+ R"("quoted..user"@[192.168.1.1])",
+ "(comment) alice at sub.domain.org (ok)",
+ "(\r\n folded comment )alice at example.com",
+ "Alice Smith <alice at example.com>"};
+ for (string_view addr : good_addrs)
+ EXPECT_EQ(match(rfc_pool, *addr_spec, addr), MatchResult::Match);
+ const string_view bad_addrs[] = {"user..name at example.com", "@example.com",
+ "user@", "user at .example.com",
+ "user at example..com"};
+ for (string_view addr : bad_addrs)
+ EXPECT_EQ(match(rfc_pool, *addr_spec, addr), MatchResult::NoMatch);
}
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index d30c957ba316ac..fe2a077c3c6da5 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -334,8 +334,7 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
LIBC_NAMESPACE::regfree(&preg);
// Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
- const char *const bad_patterns[] = {"a{1}", R"(\1)", R"(\b)", "[[:<:]]",
- "[[:>:]]"};
+ const char *const bad_patterns[] = {R"(\1)", R"(\b)", "[[:<:]]", "[[:>:]]"};
for (const char *pat : bad_patterns) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
@@ -589,6 +588,18 @@ TEST(LlvmLibcRegexTest, PlusAndQuestionQuantifiers) {
check(R"(^\d+-\w?$)", "123-a", "7-", "-a", "7-ab");
check(R"(a\+b\?c)", "a+b?c", "xx a+b?c yy", "abc", "aabbc");
+ // Framed near-miss boundary count checks ({n}, {n,m}, {n,}, {0}), anchored
+ // nullable bound, adjacent repeats, and literal '{' fallbacks.
+ check("xa{3}y", "xaaay", "xx xaaay yy", "xaay", "xaaaay");
+ check("xa{2,4}y", "xaay", "xaaaay", "xay", "xaaaaay");
+ check("xa{2,}y", "xaay", "xaaaaay", "xy", "xay");
+ check("xa{0}y", "xy", "xx xy yy", "xay", "xaay");
+ check("^a{0,3}$", "", "aaa", "aaaa", "b");
+ check("^a{2,3}b{1,2}$", "aab", "aaabb", "ab", "aaaabb");
+ check("^(a*$){2}$", "", "aaa", "b", "ab");
+ check("((a{1,10}){1,10}){1,10}", "a", "aaaa", "b", "");
+ check("^{abc|a{b|a{,2}$", "{abc", "a{,2}", "abc", "aa");
+
// Invalid repetition errors (REG_BADRPT).
const char *const bad_rpt[] = {"^+", "$+", "^?", "$?", "a++", "a??",
"a*+", "a+?", "a?*", "(+a)", "(?a)"};
@@ -597,6 +608,16 @@ TEST(LlvmLibcRegexTest, PlusAndQuestionQuantifiers) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADRPT);
LIBC_NAMESPACE::regfree(&preg);
}
+ auto check_err = [](const char *pat, int expected_err) {
+ regex_t preg{};
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), expected_err);
+ LIBC_NAMESPACE::regfree(&preg);
+ };
+ check_err("a{2}{2}", REG_BADRPT);
+ check_err("a{1,2", REG_EBRACE);
+ check_err("a{5,2}", REG_BADBR);
+ check_err("a{256}", REG_BADBR);
+ check_err("(a{255}){255}", REG_ESPACE);
}
#if defined(LIBC_ADD_NULL_CHECKS)
>From c9c9418d0c76b17001b138163d20ea3b12e02d83 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Sat, 31 Oct 2026 17:59:59 +0000
Subject: [PATCH 2/2] [libc][regex] Fix anchored repeat derivatives and bounds
When a bounded repetition body is nullable only under a specific anchor
context, such as a beginning-of-line branch in ^(^|b){2}$, evaluating
the derivative of the repetition without checking context-sensitive
nullability requires every copy of the body to consume at least one
character. In addition, computing worst-case repeat expansion
recursively on the DAG re-traverses shared subtrees exponentially on
deep alternations, and interval coalescing in make_alt only checked
adjacent spine items with shared suffixes, missing shared-prefix
concatenations and re-sorting when a merged interval received a larger
ExprId.
We collapse next_min to zero during Repeat derivative evaluation
whenever the body is nullable at the pre-byte context so leading copies
can match empty at the start of the line. To keep repeat-bound
validation and make_alt fast without growing Expr beyond 16 bytes, Expr
moves ch into the payload union alongside named Binary and RepeatBounds
structs, freeing two bytes to cache a has_repeat bit and a saturating
16-bit rep_expansion factor. We also pack hash_expr into a single
16-byte HashKey struct round and extend interval coalescing to
shared-prefix concatenations and full Alt spines while preserving
ascending ExprId order.
Assisted-by: Automated tooling, human reviewed.
---
libc/hdr/limits_macros.h | 8 +
libc/include/llvm-libc-macros/limits-macros.h | 5 +-
libc/src/__support/regex/CMakeLists.txt | 1 +
libc/src/__support/regex/regex_ast.h | 79 ++++---
libc/src/__support/regex/regex_expr_pool.cpp | 197 ++++++++++++++----
libc/src/__support/regex/regex_matcher.cpp | 13 +-
libc/src/__support/regex/regex_parser.cpp | 51 ++---
libc/test/src/__support/regex/expr_test.cpp | 30 ++-
.../test/src/__support/regex/matcher_test.cpp | 58 +++++-
libc/test/src/regex/regex_basic_test.cpp | 6 +-
10 files changed, 316 insertions(+), 132 deletions(-)
diff --git a/libc/hdr/limits_macros.h b/libc/hdr/limits_macros.h
index 7ed79685f7c3ff..fc63f35898f9b9 100644
--- a/libc/hdr/limits_macros.h
+++ b/libc/hdr/limits_macros.h
@@ -17,6 +17,14 @@
#include <limits.h>
+#ifndef _POSIX2_RE_DUP_MAX
+#define _POSIX2_RE_DUP_MAX 255
+#endif // _POSIX2_RE_DUP_MAX
+
+#ifndef RE_DUP_MAX
+#define RE_DUP_MAX _POSIX2_RE_DUP_MAX
+#endif // RE_DUP_MAX
+
#endif // LIBC_FULL_BUILD
#endif // LLVM_LIBC_HDR_LIMITS_MACROS_H
diff --git a/libc/include/llvm-libc-macros/limits-macros.h b/libc/include/llvm-libc-macros/limits-macros.h
index c7aa6087c32c49..66db0746145c2e 100644
--- a/libc/include/llvm-libc-macros/limits-macros.h
+++ b/libc/include/llvm-libc-macros/limits-macros.h
@@ -300,9 +300,10 @@
#ifndef _POSIX2_RE_DUP_MAX
#define _POSIX2_RE_DUP_MAX 255
-#endif
+#endif // _POSIX2_RE_DUP_MAX
+
#ifndef RE_DUP_MAX
#define RE_DUP_MAX _POSIX2_RE_DUP_MAX
-#endif
+#endif // RE_DUP_MAX
#endif // LLVM_LIBC_MACROS_LIMITS_MACROS_H
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index 6d9db1eef5a253..e866ce23526559 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -10,6 +10,7 @@ add_object_library(
libc.hdr.regex_macros
libc.hdr.stdint_proxy
libc.src.__support.alloc_checker
+ libc.src.__support.CPP.algorithm
libc.src.__support.CPP.array
libc.src.__support.CPP.bitset
libc.src.__support.CPP.new
diff --git a/libc/src/__support/regex/regex_ast.h b/libc/src/__support/regex/regex_ast.h
index c87146a350f4e5..83af439fe97e1a 100644
--- a/libc/src/__support/regex/regex_ast.h
+++ b/libc/src/__support/regex/regex_ast.h
@@ -29,9 +29,13 @@ inline constexpr ExprId EMPTY_SET_ID = 1;
inline constexpr ExprId EMPTY_STR_ID = 2;
/// Sentinel upper bound representing unbounded repetition in Repeat(body, m,
-/// ∞).
+/// REPEAT_INF); never collides with parsed bounds (<= RE_DUP_MAX = 255).
inline constexpr uint16_t REPEAT_INF = 0xFFFF;
+/// Compile-time guard against nested repetition bombs (e.g. (a{255}){255});
+/// ExprPool::MAX_NODE_LIMIT bounds lazy runtime state growth.
+inline constexpr uint16_t MAX_REPEAT_EXPANSION = 32768;
+
/// One bit per possible byte value (2^CHAR_BIT = 256), allowing O(1)
/// membership tests for wildcards and bracket expressions.
inline constexpr size_t BYTE_SET_BITS = size_t(1) << CHAR_BIT;
@@ -71,63 +75,75 @@ enum class ExprKind : uint8_t {
/// derivative-based matching. This structure is intended to be managed by
/// an ExprPool.
struct Expr {
+ struct Binary {
+ ExprId left;
+ ExprId right;
+ };
+ struct RepeatBounds {
+ ExprId body;
+ uint16_t min;
+ uint16_t max;
+ };
+
/// The type of this expression node.
ExprKind kind = ExprKind::EmptySet;
/// Whether this expression can match the empty string unconditionally.
bool nullable : 1;
/// Whether this expression or any child contains a context-sensitive Anchor.
bool has_anchors : 1;
- /// The character for Literal nodes.
- char ch = '\0';
+ /// Whether this expression or any child contains a Repeat node.
+ bool has_repeat : 1;
+ /// Saturated cumulative repetition multiplier for O(1) DAG expansion checks.
+ uint16_t rep_expansion = 1;
/// Precomputed 32-bit structural hash for O(1) lookup and rehashing.
uint32_t hash = 0;
union {
/// Sub-expressions for Concat and Alt nodes.
- struct {
- ExprId left;
- ExprId right;
- } bin = {INVALID_EXPR_ID, INVALID_EXPR_ID};
+ Binary bin = {INVALID_EXPR_ID, INVALID_EXPR_ID};
/// Sub-expression and bounds for Repeat nodes.
- struct {
- ExprId body;
- uint16_t min;
- uint16_t max;
- } rep;
+ RepeatBounds rep;
/// Sub-expression for Star nodes.
ExprId body;
/// Index into ExprPool::byte_sets for ByteSet nodes.
uint32_t byte_set_id;
/// Bitmask of AnchorKind flags for Anchor nodes.
uint8_t anchor_mask;
+ /// The character for Literal nodes.
+ char ch;
};
/// Default constructor creates an EmptySet node.
/// Public to allow array allocation in ExprPool::Block.
- constexpr Expr() : nullable(false), has_anchors(false) {}
+ constexpr Expr() : nullable(false), has_anchors(false), has_repeat(false) {}
private:
/// Create a leaf node with a specific nullability (and optional character).
constexpr Expr(ExprKind k, bool is_null, char c = '\0')
- : kind(k), nullable(is_null), has_anchors(false), ch(c) {}
+ : kind(k), nullable(is_null), has_anchors(false), has_repeat(false),
+ ch(c) {}
/// Create an Anchor node.
constexpr Expr(uint8_t mask)
: kind(ExprKind::Anchor), nullable(false), has_anchors(true),
- anchor_mask(mask) {}
+ has_repeat(false), anchor_mask(mask) {}
/// Create a ByteSet node.
constexpr Expr(uint32_t bs_id)
: kind(ExprKind::ByteSet), nullable(false), has_anchors(false),
- byte_set_id(bs_id) {}
+ has_repeat(false), byte_set_id(bs_id) {}
/// Create a Star node.
- constexpr Expr(ExprId b, bool anchors)
- : kind(ExprKind::Star), nullable(true), has_anchors(anchors), body(b) {}
+ constexpr Expr(ExprId b, bool anchors, bool repeats, uint16_t exp)
+ : kind(ExprKind::Star), nullable(true), has_anchors(anchors),
+ has_repeat(repeats), rep_expansion(exp), body(b) {}
/// Create a Repeat node.
constexpr Expr(ExprId b, uint16_t min_rep, uint16_t max_rep, bool is_null,
- bool anchors)
- : kind(ExprKind::Repeat), nullable(is_null), has_anchors(anchors),
+ bool anchors, uint16_t exp)
+ : kind(ExprKind::Repeat), nullable((min_rep == 0) || is_null),
+ has_anchors(anchors), has_repeat(true), rep_expansion(exp),
rep{b, min_rep, max_rep} {}
/// Create a binary node (Concat or Alt).
- constexpr Expr(ExprKind k, bool is_null, bool anchors, ExprId l, ExprId r)
- : kind(k), nullable(is_null), has_anchors(anchors), bin{l, r} {}
+ constexpr Expr(ExprKind k, bool is_null, bool anchors, bool repeats,
+ uint16_t exp, ExprId l, ExprId r)
+ : kind(k), nullable(is_null), has_anchors(anchors), has_repeat(repeats),
+ rep_expansion(exp), bin{l, r} {}
public:
static constexpr Expr make_empty_set() {
@@ -141,21 +157,24 @@ struct Expr {
}
static constexpr Expr make_anchor(uint8_t mask) { return Expr(mask); }
static constexpr Expr make_byte_set(uint32_t bs_id) { return Expr(bs_id); }
- static constexpr Expr make_star(ExprId b, bool anchors = false) {
- return Expr(b, anchors);
+ static constexpr Expr make_star(ExprId b, bool anchors = false,
+ bool repeats = false, uint16_t exp = 1) {
+ return Expr(b, anchors, repeats, exp);
}
static constexpr Expr make_repeat(ExprId b, uint16_t min_rep,
uint16_t max_rep, bool is_null = false,
- bool anchors = false) {
- return Expr(b, min_rep, max_rep, is_null, anchors);
+ bool anchors = false, uint16_t exp = 1) {
+ return Expr(b, min_rep, max_rep, is_null, anchors, exp);
}
static constexpr Expr make_concat(ExprId l, ExprId r, bool is_null = false,
- bool anchors = false) {
- return Expr(ExprKind::Concat, is_null, anchors, l, r);
+ bool anchors = false, bool repeats = false,
+ uint16_t exp = 1) {
+ return Expr(ExprKind::Concat, is_null, anchors, repeats, exp, l, r);
}
static constexpr Expr make_alt(ExprId l, ExprId r, bool is_null = false,
- bool anchors = false) {
- return Expr(ExprKind::Alt, is_null, anchors, l, r);
+ bool anchors = false, bool repeats = false,
+ uint16_t exp = 1) {
+ return Expr(ExprKind::Alt, is_null, anchors, repeats, exp, l, r);
}
/// Equivalence check for hash-consing.
diff --git a/libc/src/__support/regex/regex_expr_pool.cpp b/libc/src/__support/regex/regex_expr_pool.cpp
index 47083f77ddf01b..25e07dba10c596 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -13,6 +13,7 @@
#include "src/__support/regex/regex_expr_pool.h"
#include "hdr/regex_macros.h"
+#include "src/__support/CPP/algorithm.h"
#include "src/__support/CPP/new.h"
#include "src/__support/CPP/scope.h"
#include "src/__support/CPP/utility.h"
@@ -33,34 +34,36 @@ uint32_t hash_expr(const Expr &e) {
// high-entropy constants (derived from aHash) to produce a strong hash, while
// the constant value guarantees deterministic hashing for hash-consing.
internal::HashState hasher(0x12345678);
- uint64_t kind = static_cast<uint64_t>(e.kind);
- hasher.update(&kind, sizeof(kind));
+ struct HashKey {
+ uint64_t kind;
+ uint64_t payload;
+ } key = {static_cast<uint64_t>(e.kind), 0};
+ static_assert(sizeof(HashKey) == 2 * sizeof(uint64_t));
switch (e.kind) {
case ExprKind::Literal:
- hasher.update(&e.ch, sizeof(e.ch));
+ key.payload = static_cast<uint8_t>(e.ch);
break;
case ExprKind::Anchor:
- hasher.update(&e.anchor_mask, sizeof(e.anchor_mask));
+ key.payload = e.anchor_mask;
break;
case ExprKind::ByteSet:
- hasher.update(&e.byte_set_id, sizeof(e.byte_set_id));
+ key.payload = e.byte_set_id;
break;
case ExprKind::Star:
- hasher.update(&e.body, sizeof(e.body));
+ key.payload = e.body;
break;
case ExprKind::Repeat:
- hasher.update(&e.rep.body, sizeof(e.rep.body));
- hasher.update(&e.rep.min, sizeof(e.rep.min));
- hasher.update(&e.rep.max, sizeof(e.rep.max));
+ key.payload = (static_cast<uint64_t>(e.rep.body) << 32) |
+ (static_cast<uint64_t>(e.rep.min) << 16) | e.rep.max;
break;
case ExprKind::Concat:
case ExprKind::Alt:
- hasher.update(&e.bin.left, sizeof(e.bin.left));
- hasher.update(&e.bin.right, sizeof(e.bin.right));
+ key.payload = (static_cast<uint64_t>(e.bin.left) << 32) | e.bin.right;
break;
default:
break;
}
+ hasher.update(&key, sizeof(key));
uint64_t h = hasher.finish();
return static_cast<uint32_t>(h ^ (h >> 32));
}
@@ -286,39 +289,61 @@ ErrorOr<ExprId> ExprPool::make_byte_set(const ByteSet &bs) {
return id;
}
+namespace {
+
+// Multiplies a subexpression's repeat expansion factor by an enclosing repeat
+// bound, saturating at MAX_REPEAT_EXPANSION + 1 so nested bounds checks never
+// overflow uint16_t.
+uint16_t scale_exp(uint16_t base, uint16_t factor) {
+ if (factor <= 1)
+ return base;
+ uint32_t prod = static_cast<uint32_t>(base) * static_cast<uint32_t>(factor);
+ return static_cast<uint16_t>(
+ cpp::min<uint32_t>(prod, MAX_REPEAT_EXPANSION + 1));
+}
+
+} // namespace
+
ErrorOr<ExprId> ExprPool::make_star(ExprId body) {
if (body == INVALID_EXPR_ID)
return Error(REG_BADPAT);
// Ø* = ε and ε* = ε
if (body == EMPTY_SET_ID || body == EMPTY_STR_ID)
return empty_str();
- // (R*)* = R* and (R{m,n})* = R* for m <= 1.
+ // (R*)* = R* and (R{m,n})* = R* iff m <= 1 (since R^1 is in R{m,n} iff
+ // m <= 1, whereas (a{2,3})* excludes "a").
const Expr be = get(body);
if (be.kind == ExprKind::Star)
return body;
if (be.kind == ExprKind::Repeat && be.rep.min <= 1)
return make_star(be.rep.body);
- return intern(Expr::make_star(body, has_anchors(body)));
+ return intern(
+ Expr::make_star(body, be.has_anchors, be.has_repeat, be.rep_expansion));
}
ErrorOr<ExprId> ExprPool::make_repeat(ExprId body, uint16_t min, uint16_t max) {
- if (body == INVALID_EXPR_ID || min > max)
+ if (body == INVALID_EXPR_ID || min > max || min == REPEAT_INF)
return Error(REG_BADPAT);
- // Normalize R{0,0} = ε, ε{m,n} = ε, Ø{m,n}, R{1,1} = R, and R{0,∞} = R*.
+ // Normalize R{0,0} = ε, ε{m,n} = ε, Ø{m,n}, R{1,1} = R, and
+ // R{0,REPEAT_INF} = R*.
if (max == 0 || body == EMPTY_STR_ID)
return empty_str();
if (body == EMPTY_SET_ID)
return (min == 0) ? empty_str() : empty_set();
if (min == 1 && max == 1)
return body;
- const Expr be = get(body);
- if ((min == 0 && max == REPEAT_INF) || be.kind == ExprKind::Star)
+ if (min == 0 && max == REPEAT_INF)
return make_star(body);
+ const Expr be = get(body);
+ if (be.kind == ExprKind::Star)
+ return body;
if (be.kind == ExprKind::Repeat && be.rep.min == 1 &&
be.rep.max == REPEAT_INF && min == 1 && max == REPEAT_INF)
return body;
- return intern(Expr::make_repeat(
- body, min, max, (min == 0) || is_nullable(body), has_anchors(body)));
+ uint16_t factor = (max == REPEAT_INF) ? min : max;
+ return intern(Expr::make_repeat(body, min, max, (min == 0) || be.nullable,
+ be.has_anchors,
+ scale_exp(be.rep_expansion, factor)));
}
ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
@@ -341,28 +366,60 @@ ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
return tail;
return make_concat(le.bin.left, *tail);
}
- return intern(Expr::make_concat(l, r, is_nullable(l) && is_nullable(r),
- has_anchors(l) || has_anchors(r)));
+ const Expr re = get(r);
+ return intern(Expr::make_concat(
+ l, r, le.nullable && re.nullable, le.has_anchors || re.has_anchors,
+ le.has_repeat || re.has_repeat,
+ cpp::max(le.rep_expansion, re.rep_expansion)));
}
namespace {
-// Coalesces R{m1,n1} | R{m2,n2} (and shared-suffix Concat nodes) when their
-// repetition intervals overlap or are adjacent. Returns INVALID_EXPR_ID when
-// the pair cannot be coalesced.
+// Coalesces R{m1,n1} | R{m2,n2} (and shared-prefix/shared-suffix Concat nodes)
+// when their repetition intervals overlap or are adjacent. Returns
+// INVALID_EXPR_ID when the pair cannot be coalesced.
ErrorOr<ExprId> coalesce_repeat_pair(ExprId a, ExprId b, ExprPool &pool) {
const Expr ae = pool.get(a);
const Expr be = pool.get(b);
- if (ae.kind == ExprKind::Concat && be.kind == ExprKind::Concat &&
- ae.bin.right == be.bin.right) {
- ErrorOr<ExprId> left = coalesce_repeat_pair(ae.bin.left, be.bin.left, pool);
+ if (!ae.has_repeat && !be.has_repeat)
+ return INVALID_EXPR_ID;
+ if (ae.kind == ExprKind::Concat && be.kind == ExprKind::Concat) {
+ if (ae.bin.right == be.bin.right) {
+ ErrorOr<ExprId> left =
+ coalesce_repeat_pair(ae.bin.left, be.bin.left, pool);
+ if (!left.has_value() || *left == INVALID_EXPR_ID)
+ return left;
+ return pool.make_concat(*left, ae.bin.right);
+ }
+ if (ae.bin.left == be.bin.left) {
+ ErrorOr<ExprId> right =
+ coalesce_repeat_pair(ae.bin.right, be.bin.right, pool);
+ if (!right.has_value() || *right == INVALID_EXPR_ID)
+ return right;
+ return pool.make_concat(ae.bin.left, *right);
+ }
+ }
+ // Handle Concat(X, S) | S and Concat(P, X) | P where R{0,0} normalized to
+ // EmptyStr on one side.
+ if (ae.kind == ExprKind::Concat && ae.bin.right == b) {
+ ErrorOr<ExprId> left =
+ coalesce_repeat_pair(ae.bin.left, EMPTY_STR_ID, pool);
+ if (!left.has_value() || *left == INVALID_EXPR_ID)
+ return left;
+ return pool.make_concat(*left, b);
+ }
+ if (be.kind == ExprKind::Concat && be.bin.right == a) {
+ ErrorOr<ExprId> left =
+ coalesce_repeat_pair(EMPTY_STR_ID, be.bin.left, pool);
if (!left.has_value() || *left == INVALID_EXPR_ID)
return left;
- return pool.make_concat(*left, ae.bin.right);
+ return pool.make_concat(*left, a);
}
if (ae.kind != ExprKind::Repeat && be.kind != ExprKind::Repeat)
return INVALID_EXPR_ID;
ExprId body = (ae.kind == ExprKind::Repeat) ? ae.rep.body : be.rep.body;
+ // Undo make_repeat normalizations: R -> [1,1], EmptyStr -> [0,0], and
+ // Star(R) -> [0,REPEAT_INF].
auto bounds = [&](ExprId id, const Expr &e, uint16_t &lo,
uint16_t &hi) -> bool {
if (e.kind == ExprKind::Repeat && e.rep.body == body) {
@@ -370,6 +427,11 @@ ErrorOr<ExprId> coalesce_repeat_pair(ExprId a, ExprId b, ExprPool &pool) {
hi = e.rep.max;
return true;
}
+ if (e.kind == ExprKind::Star && e.body == body) {
+ lo = 0;
+ hi = REPEAT_INF;
+ return true;
+ }
if (id == body) {
lo = 1;
hi = 1;
@@ -385,12 +447,35 @@ ErrorOr<ExprId> coalesce_repeat_pair(ExprId a, ExprId b, ExprPool &pool) {
uint16_t lo1 = 0, hi1 = 0, lo2 = 0, hi2 = 0;
if (!bounds(a, ae, lo1, hi1) || !bounds(b, be, lo2, hi2))
return INVALID_EXPR_ID;
- uint16_t max_lo = (lo1 > lo2) ? lo1 : lo2;
- uint16_t min_hi = (hi1 < hi2) ? hi1 : hi2;
+ // Integer intervals [lo1,hi1] and [lo2,hi2] coalesce iff they overlap or are
+ // adjacent (max_lo <= min_hi + 1, e.g. [2,3] U [4,5] = [2,5]).
+ uint16_t max_lo = cpp::max(lo1, lo2);
+ uint16_t min_hi = cpp::min(hi1, hi2);
if (min_hi != REPEAT_INF && max_lo > min_hi + 1)
return INVALID_EXPR_ID;
- return pool.make_repeat(body, (lo1 < lo2) ? lo1 : lo2,
- (hi1 > hi2) ? hi1 : hi2);
+ return pool.make_repeat(body, cpp::min(lo1, lo2), cpp::max(hi1, hi2));
+}
+
+// Attempts to coalesce a single non-Alt branch `item` with any branch in
+// `spine`, returning INVALID_EXPR_ID if no branch coalesces.
+ErrorOr<ExprId> coalesce_item_into_spine(ExprId item, ExprId spine,
+ ExprPool &pool) {
+ const Expr item_e = pool.get(item);
+ const Expr spine_e = pool.get(spine);
+ if (!item_e.has_repeat && !spine_e.has_repeat)
+ return INVALID_EXPR_ID;
+ if (spine_e.kind != ExprKind::Alt)
+ return coalesce_repeat_pair(item, spine, pool);
+ ErrorOr<ExprId> head_c = coalesce_repeat_pair(item, spine_e.bin.left, pool);
+ if (!head_c.has_value())
+ return head_c;
+ if (*head_c != INVALID_EXPR_ID)
+ return pool.make_alt(*head_c, spine_e.bin.right);
+ ErrorOr<ExprId> tail_c =
+ coalesce_item_into_spine(item, spine_e.bin.right, pool);
+ if (!tail_c.has_value() || *tail_c == INVALID_EXPR_ID)
+ return tail_c;
+ return pool.make_alt(spine_e.bin.left, *tail_c);
}
} // namespace
@@ -416,8 +501,28 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
return coalesced;
if (l > r)
cpp::swap(l, r);
- return intern(Expr::make_alt(l, r, is_nullable(l) || is_nullable(r),
- has_anchors(l) || has_anchors(r)));
+ return intern(Expr::make_alt(l, r, le.nullable || re.nullable,
+ le.has_anchors || re.has_anchors,
+ le.has_repeat || re.has_repeat,
+ cpp::max(le.rep_expansion, re.rep_expansion)));
+ }
+
+ if (le.has_repeat || re.has_repeat) {
+ if (le.kind != ExprKind::Alt) {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(l, r, *this);
+ if (!c.has_value() || *c != INVALID_EXPR_ID)
+ return c;
+ } else if (re.kind != ExprKind::Alt) {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(r, l, *this);
+ if (!c.has_value() || *c != INVALID_EXPR_ID)
+ return c;
+ } else {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(le.bin.left, r, *this);
+ if (!c.has_value())
+ return c;
+ if (*c != INVALID_EXPR_ID)
+ return make_alt(le.bin.right, *c);
+ }
}
// Merge two right-associated sorted Alt spines in ascending ExprId order so
@@ -427,15 +532,6 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
ExprId tl = (le.kind == ExprKind::Alt) ? le.bin.right : EMPTY_SET_ID;
ExprId hr = (re.kind == ExprKind::Alt) ? re.bin.left : r;
ExprId tr = (re.kind == ExprKind::Alt) ? re.bin.right : EMPTY_SET_ID;
- ErrorOr<ExprId> coalesced = coalesce_repeat_pair(hl, hr, *this);
- if (!coalesced.has_value())
- return coalesced;
- if (*coalesced != INVALID_EXPR_ID) {
- ErrorOr<ExprId> tails = make_alt(tl, tr);
- if (!tails.has_value())
- return tails;
- return make_alt(*coalesced, *tails);
- }
ExprId head = hl;
ErrorOr<ExprId> rest = EMPTY_SET_ID;
if (hl == hr) {
@@ -448,9 +544,18 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
}
if (!rest.has_value())
return rest;
- return intern(Expr::make_alt(head, *rest,
- is_nullable(head) || is_nullable(*rest),
- has_anchors(head) || has_anchors(*rest)));
+ // Tail coalescing may produce an already-interned ExprId <= head; re-merge
+ // when needed to preserve ascending ExprId order along the Alt spine.
+ const Expr re_rest = get(*rest);
+ ExprId rest_head = (re_rest.kind == ExprKind::Alt) ? re_rest.bin.left : *rest;
+ if (head >= rest_head)
+ return make_alt(head, *rest);
+ const Expr he = get(head);
+ return intern(
+ Expr::make_alt(head, *rest, he.nullable || re_rest.nullable,
+ he.has_anchors || re_rest.has_anchors,
+ he.has_repeat || re_rest.has_repeat,
+ cpp::max(he.rep_expansion, re_rest.rep_expansion)));
}
} // namespace regex
diff --git a/libc/src/__support/regex/regex_matcher.cpp b/libc/src/__support/regex/regex_matcher.cpp
index 003735ffc5121f..1a7746c06f0238 100644
--- a/libc/src/__support/regex/regex_matcher.cpp
+++ b/libc/src/__support/regex/regex_matcher.cpp
@@ -44,7 +44,7 @@ uint8_t ctx_at(cpp::string_view input, size_t pos, int cflags, int eflags) {
bool nullable(ExprId id, uint8_t anchor_ctx, const ExprPool &pool) {
const Expr &e = pool.get(id);
- if (!e.has_anchors)
+ if (e.nullable || !e.has_anchors)
return e.nullable;
switch (e.kind) {
case ExprKind::Anchor:
@@ -56,7 +56,7 @@ bool nullable(ExprId id, uint8_t anchor_ctx, const ExprPool &pool) {
return nullable(e.bin.left, anchor_ctx, pool) ||
nullable(e.bin.right, anchor_ctx, pool);
case ExprKind::Repeat:
- return (e.rep.min == 0) || nullable(e.rep.body, anchor_ctx, pool);
+ return nullable(e.rep.body, anchor_ctx, pool);
default:
return e.nullable;
}
@@ -110,12 +110,17 @@ ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool,
return pool.make_concat(*d_body, id);
}
case ExprKind::Repeat: {
- // Bounded Repetition Rule: D_c(R{m,n}) = D_c(R) . R{max(0,m-1), n-1}
+ // Bounded Repetition Rule:
+ // D_c(R{m,n}) = D_c(R) . R{nullable(R, ctx_before) ? 0 : max(0,m-1), n-1}.
+ // When R is nullable at ctx_before (e.g. via '^'), up to m - 1 leading
+ // copies may match empty at ctx_before before the copy that consumes c.
ErrorOr<ExprId> d_body = derivative(e.rep.body, c, pool, ctx_before);
if (!d_body.has_value() || *d_body == EMPTY_SET_ID)
return d_body;
uint16_t next_min =
- (e.rep.min > 0) ? static_cast<uint16_t>(e.rep.min - 1) : uint16_t(0);
+ (e.rep.min > 0 && !nullable(e.rep.body, ctx_before, pool))
+ ? static_cast<uint16_t>(e.rep.min - 1)
+ : static_cast<uint16_t>(0);
uint16_t next_max = (e.rep.max == REPEAT_INF)
? REPEAT_INF
: static_cast<uint16_t>(e.rep.max - 1);
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 80c3ce37ac855d..34257646a4134a 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -168,38 +168,17 @@ ErrorOr<ExprId> parse_bracket_expr(cpp::string_view &rem, int cflags,
// Maximum parenthesized group nesting depth to prevent parser stack overflow.
constexpr size_t MAX_GROUP_DEPTH = 128;
-// Maximum cumulative nested repetition multiplier to bound state expansion.
-constexpr size_t MAX_REPEAT_EXPANSION = 32768;
-
-size_t repeat_expansion(ExprId id, const ExprPool &pool) {
- const Expr &e = pool.get(id);
- if (e.kind == ExprKind::Repeat) {
- uint16_t bound = (e.rep.max == REPEAT_INF) ? e.rep.min : e.rep.max;
- return (bound > 1 ? size_t(bound) : size_t(1)) *
- repeat_expansion(e.rep.body, pool);
- }
- if (e.kind == ExprKind::Star)
- return repeat_expansion(e.body, pool);
- if (e.kind == ExprKind::Concat || e.kind == ExprKind::Alt) {
- size_t l = repeat_expansion(e.bin.left, pool);
- size_t r =
- (e.bin.right == e.bin.left) ? l : repeat_expansion(e.bin.right, pool);
- return (l > r) ? l : r;
- }
- return 1;
-}
-
// Parses a decimal repetition bound in [0, RE_DUP_MAX] from the prefix of spec.
cpp::optional<uint16_t> consume_bound(cpp::string_view &spec) {
- if (!internal::isdigit(spec.front()))
+ if (spec.empty() || !internal::isdigit(spec.front()))
return cpp::nullopt;
uint32_t val = 0;
- while (!spec.empty() && internal::isdigit(spec.front())) {
+ do {
val = val * 10 + static_cast<uint32_t>(spec.front() - '0');
if (val > RE_DUP_MAX)
return cpp::nullopt;
spec.remove_prefix(1);
- }
+ } while (!spec.empty() && internal::isdigit(spec.front()));
return static_cast<uint16_t>(val);
}
@@ -208,10 +187,9 @@ cpp::optional<uint16_t> consume_bound(cpp::string_view &spec) {
ErrorOr<ExprId> parse_repeat_bounds(cpp::string_view &rem, ExprId atom,
ExprPool &pool) {
rem.remove_prefix(1);
- size_t close_pos = 0;
- while (close_pos < rem.size() && rem[close_pos] != '}')
- ++close_pos;
- if (close_pos == rem.size())
+ // Scan for '}' before parsing bounds so unclosed "a{1,x" reports REG_EBRACE.
+ size_t close_pos = rem.find_first_of('}');
+ if (close_pos == cpp::string_view::npos)
return Error(REG_EBRACE);
cpp::string_view spec = rem.substr(0, close_pos);
rem.remove_prefix(close_pos + 1);
@@ -236,7 +214,7 @@ ErrorOr<ExprId> parse_repeat_bounds(cpp::string_view &rem, ExprId atom,
}
uint16_t bound = (max_val == REPEAT_INF) ? min_val : max_val;
- if (bound > 1 && repeat_expansion(atom, pool) * bound > MAX_REPEAT_EXPANSION)
+ if (bound > 1 && pool.get(atom).rep_expansion > MAX_REPEAT_EXPANSION / bound)
return Error(REG_ESPACE);
return pool.make_repeat(atom, min_val, max_val);
}
@@ -313,9 +291,9 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
atom = pool.make_anchor(ANCHOR_EOL);
can_repeat = false;
break;
- // Valid postfix '*', '+', or '?' is consumed after a repeatable atom below;
- // reaching a quantifier at the start of an atom means it has no preceding
- // repeatable atom (e.g. "*a", "^*", or doubled "a**").
+ // Valid postfix '*', '+', '?', or '{m,n}' is consumed after a repeatable
+ // atom below; reaching a quantifier at the start of an atom means it has no
+ // preceding repeatable atom (e.g. "*a", "^*", or doubled "a**").
case '*':
case '+':
case '?':
@@ -358,10 +336,15 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
rem.remove_prefix(1);
return pool.make_star(*atom);
}
+ // Push r and Star(r) separately rather than Repeat(r, 1, REPEAT_INF) so
+ // right-to-left folding builds r . (r* . rest) once and regexec peels off
+ // long Concat spines without re-associating r on each repetition.
if (rem.starts_with('+')) {
rem.remove_prefix(1);
- prefix_atom = *atom;
- return pool.make_star(*atom);
+ ErrorOr<ExprId> star = pool.make_star(*atom);
+ if (star.has_value())
+ prefix_atom = *atom;
+ return star;
}
if (rem.starts_with('?')) {
rem.remove_prefix(1);
diff --git a/libc/test/src/__support/regex/expr_test.cpp b/libc/test/src/__support/regex/expr_test.cpp
index 26074c4767461f..7f8d46291599ea 100644
--- a/libc/test/src/__support/regex/expr_test.cpp
+++ b/libc/test/src/__support/regex/expr_test.cpp
@@ -227,8 +227,15 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAltAndStar) {
// Repeat identities and interval coalescing in make_alt.
const ExprId plus_a = *pool.make_repeat(lit_a, 1, REPEAT_INF);
- EXPECT_EQ(pool.make_repeat(INVALID_EXPR_ID, 1, 2).error(), REG_BADPAT);
- EXPECT_EQ(pool.make_repeat(lit_a, 3, 2).error(), REG_BADPAT);
+ auto bad_rep_id = pool.make_repeat(INVALID_EXPR_ID, 1, 2);
+ auto bad_rep_range = pool.make_repeat(lit_a, 3, 2);
+ auto bad_rep_inf = pool.make_repeat(lit_a, REPEAT_INF, REPEAT_INF);
+ ASSERT_FALSE(bad_rep_id.has_value());
+ ASSERT_FALSE(bad_rep_range.has_value());
+ ASSERT_FALSE(bad_rep_inf.has_value());
+ EXPECT_EQ(bad_rep_id.error(), REG_BADPAT);
+ EXPECT_EQ(bad_rep_range.error(), REG_BADPAT);
+ EXPECT_EQ(bad_rep_inf.error(), REG_BADPAT);
EXPECT_EQ(*pool.make_repeat(lit_a, 0, 0), empty_str);
EXPECT_EQ(*pool.make_repeat(empty_str, 2, 4), empty_str);
EXPECT_EQ(*pool.make_repeat(empty_set, 0, 3), empty_str);
@@ -239,14 +246,27 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAltAndStar) {
EXPECT_EQ(*pool.make_star(plus_a), star_a);
EXPECT_EQ(*pool.make_repeat(plus_a, 1, REPEAT_INF), plus_a);
+ const ExprId rep_1_3 = *pool.make_repeat(lit_a, 1, 3);
const ExprId rep_2_3 = *pool.make_repeat(lit_a, 2, 3);
const ExprId rep_4_5 = *pool.make_repeat(lit_a, 4, 5);
- EXPECT_EQ(*pool.make_alt(rep_2_3, rep_4_5), *pool.make_repeat(lit_a, 2, 5));
- EXPECT_EQ(*pool.make_alt(lit_a, rep_2_3), *pool.make_repeat(lit_a, 1, 3));
+ const ExprId rep_1_2 = *pool.make_repeat(lit_a, 1, 2);
+ const ExprId rep_2_5 = *pool.make_repeat(lit_a, 2, 5);
+ EXPECT_EQ(*pool.make_alt(rep_2_3, rep_4_5), rep_2_5);
+ EXPECT_EQ(*pool.make_alt(lit_a, rep_2_3), rep_1_3);
EXPECT_EQ(*pool.make_alt(empty_str, plus_a), star_a);
+ EXPECT_EQ(*pool.make_alt(*pool.make_repeat(lit_a, 2, REPEAT_INF), star_a),
+ star_a);
EXPECT_EQ(*pool.make_alt(*pool.make_concat(rep_2_3, lit_b),
*pool.make_concat(rep_4_5, lit_b)),
- *pool.make_concat(*pool.make_repeat(lit_a, 2, 5), lit_b));
+ *pool.make_concat(rep_2_5, lit_b));
+ EXPECT_EQ(*pool.make_alt(*pool.make_concat(lit_b, rep_2_3),
+ *pool.make_concat(lit_b, rep_4_5)),
+ *pool.make_concat(lit_b, rep_2_5));
+ EXPECT_EQ(*pool.make_alt(*pool.make_alt(rep_2_3, lit_c), rep_4_5),
+ *pool.make_alt(rep_2_3, *pool.make_alt(lit_c, rep_4_5)));
+ const ExprId merged = *pool.make_alt(*pool.make_alt(lit_c, rep_2_3), rep_1_2);
+ EXPECT_EQ(merged, *pool.make_alt(lit_c, rep_1_3));
+ EXPECT_LT(pool.get(merged).bin.left, pool.get(merged).bin.right);
EXPECT_TRUE(pool.is_nullable(*pool.make_repeat(lit_a, 0, 2)));
EXPECT_FALSE(pool.is_nullable(rep_2_3));
EXPECT_TRUE(
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index ee001da472162a..0130ddfd28bd43 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -625,6 +625,36 @@ TEST(LlvmLibcRegexMatcherTest, RepeatStabilizationAndRfc2822Grammar) {
EXPECT_EQ(match(rep_pool, *anchored_a255, string_view(buf256, 256)),
MatchResult::NoMatch);
+ // Shared-prefix Concat derivatives (^(a+){255}$, (ab?){255}b) and interleaved
+ // multi-branch spines (x?.{255}a|.{255}b) coalesce without hitting
+ // MAX_NODE_LIMIT, and cached Expr::rep_expansion parses 30-deep nested '+'
+ // inside '{2}' in O(1) per node.
+ ExprPool prefix_pool;
+ ErrorOr<ExprId> plus_255 =
+ parse_ere("^(a+){255}$", REG_EXTENDED, prefix_pool);
+ ErrorOr<ExprId> opt_255b =
+ parse_ere("(ab?){255}b", REG_EXTENDED, prefix_pool);
+ ErrorOr<ExprId> inter_255 =
+ parse_ere("x?.{255}a|.{255}b", REG_EXTENDED, prefix_pool);
+ ASSERT_TRUE(plus_255.has_value());
+ ASSERT_TRUE(opt_255b.has_value());
+ ASSERT_TRUE(inter_255.has_value());
+ EXPECT_EQ(match(prefix_pool, *plus_255, string_view(buf256, 255)),
+ MatchResult::Match);
+ buf256[255] = 'b';
+ EXPECT_EQ(match(prefix_pool, *opt_255b, string_view(buf256, 256)),
+ MatchResult::Match);
+ EXPECT_EQ(match(prefix_pool, *inter_255, string_view(buf256, 256)),
+ MatchResult::Match);
+
+ ExprPool dag_exp_pool;
+ ErrorOr<ExprId> deep_plus_rep = parse_ere(
+ "(((((((((((((((((((((((((((((((a)+)+)+)+)+)+)+)+)+)+)+)+)+)+)+)+"
+ ")+)+)+)+)+)+)+)+)+)+)+)+)+)+){2}",
+ REG_EXTENDED, dag_exp_pool);
+ ASSERT_TRUE(deep_plus_rep.has_value());
+ EXPECT_EQ(match(dag_exp_pool, *deep_plus_rep, "aa"), MatchResult::Match);
+
// Build a >7 KiB ERE from RFC-2822 Section 3.4.1 addr-spec EBNF productions
// (F = FWS, C = CFWS, T = atext, W = word, S = addr-spec) and verify
// hash-consed DAG deduplication keeps the pool under 32 KiB.
@@ -632,21 +662,29 @@ TEST(LlvmLibcRegexMatcherTest, RepeatStabilizationAndRfc2822Grammar) {
size_t rfc_len = 0;
auto expand = [&](auto &self, string_view str) -> void {
for (char ch : str) {
- if (ch == 'F')
+ if (ch == 'F') {
self(self, "(([ \t]*\r\n)?[ \t]+)");
- else if (ch == 'C')
- self(self, R"(((F?\((F?([!-'*-[^-~]|\\([ -~]|[ ])))*F?\))+F?|F)?)");
- else if (ch == 'T')
+ } else if (ch == 'C') {
+ self(self, R"(((F?\((F?([!-'*-[^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?\))+F?|F)?)");
+ } else if (ch == 'T') {
self(self, R"([[:alnum:]!#$%&'*+/=?^_`{|}~-])");
- else if (ch == 'W')
- self(self, R"(((CT+C)|(C"(F?([!#-[^-~]|\\([ -~]|[ ])))*F?"C)))");
- else if (ch == 'S')
+ } else if (ch == 'W') {
+ self(self, R"(((CT+C)|(C"(F?([!#-[^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?"C)))");
+ } else if (ch == 'S') {
self(self, R"((((CT+(\.T+)*C)|W|(W(\.W{1,1}){0,31}))@)"
R"(((CT+(\.T+)*C)|(C\[(([0-9]{1,3}\.){3}[0-9]{1,3}|)"
- R"((F?([!-Z^-~]|\\([ -~]|[ ])))*F?)\]C)|)"
+ R"((F?([!-Z^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?)\]C)|)"
R"(((CT+C)(\.(CT+C){1,1}){1,31}))))");
- else
- rfc_buf[rfc_len++] = ch;
+ } else if (rfc_len < sizeof(rfc_buf)) {
+ rfc_buf[rfc_len] = ch;
+ ++rfc_len;
+ }
}
};
expand(expand, "^(S|W{0,8}<S>)$");
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index fe2a077c3c6da5..6eeb79f75bcab3 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -333,7 +333,8 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
- // Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
+ // Unimplemented ERE backreferences and word-boundary extensions return
+ // REG_BADPAT.
const char *const bad_patterns[] = {R"(\1)", R"(\b)", "[[:<:]]", "[[:>:]]"};
for (const char *pat : bad_patterns) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADPAT);
@@ -597,6 +598,9 @@ TEST(LlvmLibcRegexTest, PlusAndQuestionQuantifiers) {
check("^a{0,3}$", "", "aaa", "aaaa", "b");
check("^a{2,3}b{1,2}$", "aab", "aaabb", "ab", "aaaabb");
check("^(a*$){2}$", "", "aaa", "b", "ab");
+ check("^(^|b){2}$", "b", "bb", "bbb", "a");
+ check("^(a{1,2}|a{4,5})$", "aa", "aaaa", "aaa", "aaaaaa");
+ check("^(a{2})*$", "", "aaaa", "a", "aaa");
check("((a{1,10}){1,10}){1,10}", "a", "aaaa", "b", "");
check("^{abc|a{b|a{,2}$", "{abc", "a{,2}", "abc", "aa");
More information about the libc-commits
mailing list