[libc-commits] [libc] 7550009 - [libc][regex] Add Kleene star and canonical AST normalisation (#233980)
via libc-commits
libc-commits at lists.llvm.org
Thu Oct 22 23:19:24 PDT 2026
Author: Jeff Bailey
Date: 2026-10-23T07:19:18+01:00
New Revision: 7550009a0c545bd052ca51ee0bb38b500dd05b24
URL: https://github.com/llvm/llvm-project/commit/7550009a0c545bd052ca51ee0bb38b500dd05b24
DIFF: https://github.com/llvm/llvm-project/commit/7550009a0c545bd052ca51ee0bb38b500dd05b24.diff
LOG: [libc][regex] Add Kleene star and canonical AST normalisation (#233980)
Add ExprKind::Star, ExprPool::make_star, and the Kleene star Brzozowski
derivative rule D_c(R*) = D_c(R) . R* to the internal regular expression
engine. To guarantee that derivative state spaces stabilise into a
finite set of canonical AST nodes during Kleene star evaluation, enforce
right-associative canonical form in ExprPool::make_concat and
right-associated, ascending-ExprId, deduplicated canonical form in
ExprPool::make_alt.
Update parse_ere to fold concatenated atoms right-to-left in fixed
64-atom stack chunks so that right-associative concatenation stays O(1)
per step without creating orphaned intermediate prefix nodes. Wire
trailing '*' quantifiers on repeatable atoms and return REG_BADRPT for
unpreceded, post-anchor, or doubled repetition operators.
External compliance suites: Henry Spencer suite passes 71/456 (+19)
boolean/error cases and 29/456 (+13) full-position cases; AT&T testregex
passes 54/395 (+14) ERE boolean/error cases and 54/465 (+14) overall
boolean/error cases with 0 regressions.
Assisted-by: Automated tooling, human reviewed.
Added:
Modified:
libc/src/__support/regex/CMakeLists.txt
libc/src/__support/regex/regex_ast.h
libc/src/__support/regex/regex_expr_pool.cpp
libc/src/__support/regex/regex_expr_pool.h
libc/src/__support/regex/regex_matcher.cpp
libc/src/__support/regex/regex_parser.cpp
libc/test/src/__support/regex/expr_test.cpp
libc/test/src/__support/regex/matcher_test.cpp
libc/test/src/regex/regex_basic_test.cpp
Removed:
################################################################################
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index 95857681ed51d4..870ec56993eca0 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -58,6 +58,11 @@ add_object_library(
DEPENDS
.regex_expr_pool
libc.hdr.regex_macros
+ libc.src.__support.alloc_checker
+ libc.src.__support.CPP.array
+ libc.src.__support.CPP.new
+ libc.src.__support.CPP.scope
+ libc.src.__support.CPP.span
libc.src.__support.CPP.string_view
libc.src.__support.error_or
libc.src.__support.macros.config
diff --git a/libc/src/__support/regex/regex_ast.h b/libc/src/__support/regex/regex_ast.h
index ffbde89b388014..5c5fa006bec76b 100644
--- a/libc/src/__support/regex/regex_ast.h
+++ b/libc/src/__support/regex/regex_ast.h
@@ -55,6 +55,8 @@ enum class ExprKind : uint8_t {
Concat,
/// Alternation between two expressions (left or right).
Alt,
+ /// Zero or more repetitions of a sub-expression (body*).
+ Star,
};
/// A node in the Regular Expression Abstract Syntax Tree.
@@ -79,6 +81,8 @@ struct Expr {
ExprId left;
ExprId right;
} bin = {INVALID_EXPR_ID, INVALID_EXPR_ID};
+ /// 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.
@@ -101,6 +105,9 @@ struct Expr {
constexpr Expr(uint32_t bs_id)
: kind(ExprKind::ByteSet), nullable(false), has_anchors(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) {}
/// 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} {}
@@ -117,6 +124,9 @@ 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_concat(ExprId l, ExprId r, bool is_null = false,
bool anchors = false) {
return Expr(ExprKind::Concat, is_null, anchors, l, r);
@@ -140,6 +150,8 @@ struct Expr {
return anchor_mask == other.anchor_mask;
case ExprKind::ByteSet:
return byte_set_id == other.byte_set_id;
+ case ExprKind::Star:
+ return body == other.body;
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 6c82b62b7def90..7f68619c4b272a 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -45,6 +45,9 @@ uint32_t hash_expr(const Expr &e) {
case ExprKind::ByteSet:
hasher.update(&e.byte_set_id, sizeof(e.byte_set_id));
break;
+ case ExprKind::Star:
+ hasher.update(&e.body, sizeof(e.body));
+ break;
case ExprKind::Concat:
case ExprKind::Alt:
hasher.update(&e.bin.left, sizeof(e.bin.left));
@@ -278,6 +281,18 @@ ErrorOr<ExprId> ExprPool::make_byte_set(const ByteSet &bs) {
return id;
}
+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* (Idempotency)
+ if (get(body).kind == ExprKind::Star)
+ return body;
+ return intern(Expr::make_star(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);
@@ -290,9 +305,18 @@ ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
return r;
if (r == EMPTY_STR_ID)
return l;
+ // (A · B) · C = A · (B · C) (Right-associative canonical form)
+ const Expr le = get(l);
+ if (le.kind == ExprKind::Concat) {
+ ErrorOr<ExprId> tail = make_concat(le.bin.right, r);
+ if (!tail.has_value())
+ 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)));
}
+
ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
return Error(REG_BADPAT);
@@ -305,10 +329,38 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
// R | R = R (Idempotency)
if (l == r)
return l;
- 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)));
+
+ const Expr le = get(l);
+ const Expr re = get(r);
+ if (le.kind != ExprKind::Alt && re.kind != ExprKind::Alt) {
+ 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)));
+ }
+
+ // 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.
+ 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;
+ ExprId head = hl;
+ ErrorOr<ExprId> rest = EMPTY_SET_ID;
+ if (hl == hr) {
+ rest = make_alt(tl, tr);
+ } else if (hl < hr) {
+ rest = make_alt(tl, r);
+ } else {
+ head = hr;
+ rest = make_alt(l, tr);
+ }
+ 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)));
}
} // namespace regex
diff --git a/libc/src/__support/regex/regex_expr_pool.h b/libc/src/__support/regex/regex_expr_pool.h
index 746ad5d9789d9c..37fc585e2e2387 100644
--- a/libc/src/__support/regex/regex_expr_pool.h
+++ b/libc/src/__support/regex/regex_expr_pool.h
@@ -60,11 +60,6 @@ class ExprPool {
/// chains short.
static constexpr double MAX_LOAD_FACTOR = 0.7;
- /// The maximum number of nodes allowed in the pool to prevent memory
- /// exhaustion during compilation of highly complex or maliciously crafted
- /// regular expressions.
- static constexpr size_t MAX_NODE_LIMIT = 10000;
-
// TODO: Once cpp::unique_ptr and AllocChecker-backed cpp::make_unique (and/or
// an owning unique_span) are available in src/__support/CPP/, replace raw
// Block* and span-backed manual new[]/delete[] ownership here so that Block
@@ -122,6 +117,11 @@ class ExprPool {
ErrorOr<ExprId> intern(Expr e);
public:
+ /// The maximum number of nodes allowed in the pool to prevent memory
+ /// exhaustion during compilation of highly complex or maliciously crafted
+ /// regular expressions.
+ static constexpr size_t MAX_NODE_LIMIT = 10000;
+
constexpr ExprPool() = default;
~ExprPool();
@@ -165,18 +165,11 @@ class ExprPool {
ErrorOr<ExprId> make_anchor(uint8_t mask);
/// Creates or returns an existing node for the given 256-bit character set.
ErrorOr<ExprId> make_byte_set(const ByteSet &bs);
- /// Normalizing factory for Concatenation (L · R).
- ///
- /// Applies algebraic simplifications before interning:
- /// - (Ø · R) or (R · Ø) => Ø
- /// - (ε · R) or (R · ε) => R
+ /// Creates or returns a normalized Kleene star node (body*).
+ ErrorOr<ExprId> make_star(ExprId body);
+ /// Creates or returns a right-associated Concatenation node (l . r).
ErrorOr<ExprId> make_concat(ExprId l, ExprId r);
-
- /// Normalizing factory for Alternation (L | R).
- ///
- /// Applies algebraic simplifications before interning:
- /// - (Ø | R) or (R | Ø) => R
- /// - (R | R) => R (Idempotency)
+ /// Creates or returns a canonical deduplicated Alternation node (l | r).
ErrorOr<ExprId> make_alt(ExprId l, ExprId r);
};
diff --git a/libc/src/__support/regex/regex_matcher.cpp b/libc/src/__support/regex/regex_matcher.cpp
index 4e4480099d83c0..c58daf83294f60 100644
--- a/libc/src/__support/regex/regex_matcher.cpp
+++ b/libc/src/__support/regex/regex_matcher.cpp
@@ -100,6 +100,13 @@ ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool,
return d_r;
return pool.make_alt(*d_l, *d_r);
}
+ case ExprKind::Star: {
+ // Kleene Rule: D_c(R*) = D_c(R) . R*
+ ErrorOr<ExprId> d_body = derivative(e.body, c, pool, ctx_before);
+ if (!d_body.has_value())
+ return d_body;
+ return pool.make_concat(*d_body, id);
+ }
}
__builtin_unreachable();
}
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 862085fcdb8d38..818d3564da99e3 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -14,7 +14,12 @@
#include "src/__support/regex/regex_parser.h"
#include "hdr/regex_macros.h"
+#include "src/__support/CPP/array.h"
+#include "src/__support/CPP/new.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/error_or.h"
#include "src/__support/macros/config.h"
#include "src/__support/regex/regex_ast.h"
@@ -59,70 +64,128 @@ constexpr bool is_unimplemented_ere_escape(char c) {
}
}
+// Parses a single ERE atom and an optional trailing '*' quantifier.
+ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
+ ExprPool &pool) {
+ char c = rem.front();
+ rem.remove_prefix(1);
+ ErrorOr<ExprId> atom = INVALID_EXPR_ID;
+ bool can_repeat = true;
+ switch (c) {
+ case '\\': {
+ if (rem.empty())
+ return Error(REG_EESCAPE);
+ c = rem.front();
+ rem.remove_prefix(1);
+ if (is_unimplemented_ere_escape(c))
+ return Error(REG_BADPAT);
+ atom = pool.make_lit(c);
+ break;
+ }
+ case '.': {
+ ByteSet bs;
+ if (cflags & REG_NEWLINE) {
+ bs.set_range(0, '\n' - 1);
+ bs.set_range('\n' + 1, BYTE_SET_BITS - 1);
+ } else {
+ bs.flip();
+ }
+ atom = pool.make_byte_set(bs);
+ break;
+ }
+ case '^':
+ atom = pool.make_anchor(ANCHOR_BOL);
+ can_repeat = false;
+ break;
+ case '$':
+ atom = pool.make_anchor(ANCHOR_EOL);
+ can_repeat = false;
+ break;
+ // Valid postfix '*' 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 '?':
+ return Error(REG_BADRPT);
+ case '{':
+ if (!rem.empty() && rem.front() >= '0' && rem.front() <= '9')
+ return Error(REG_BADRPT);
+ return Error(REG_BADPAT);
+ // TODO: Wire ERE metacharacters as each operator is implemented in
+ // subsequent patches.
+ case '(':
+ case ')':
+ case '[':
+ case '|':
+ return Error(REG_BADPAT);
+ default:
+ atom = pool.make_lit(c);
+ break;
+ }
+
+ if (!atom.has_value() || !can_repeat)
+ return atom;
+ if (rem.starts_with('*')) {
+ rem.remove_prefix(1);
+ return pool.make_star(*atom);
+ }
+ if (rem.starts_with('+') || rem.starts_with('?') || rem.starts_with('{'))
+ return Error(REG_BADPAT);
+ return atom;
+}
+
} // namespace
ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags,
ExprPool &pool) {
- ErrorOr<ExprId> cur = pool.empty_str();
- if (!cur.has_value())
- return cur;
+ // Collect quantified atoms in a 64-atom (256-byte) stack buffer, spilling to
+ // the heap only for longer chains, then fold right-to-left so
+ // right-associative make_concat stays O(1) without orphaned prefixes.
+ // TODO: Replace inline_atoms + heap_atoms with an AllocChecker-backed
+ // SmallVector / unique_ptr once available in src/__support/CPP/.
+ constexpr size_t INLINE_ATOM_COUNT = 64;
+ cpp::array<ExprId, INLINE_ATOM_COUNT> inline_atoms{};
+ ExprId *heap_atoms = nullptr;
+ cpp::scope_exit cleanup([&heap_atoms] { delete[] heap_atoms; });
+ cpp::span<ExprId> atoms(inline_atoms);
+ size_t atom_count = 0;
cpp::string_view rem = pattern;
while (!rem.empty()) {
- char c = rem.front();
- rem.remove_prefix(1);
- ErrorOr<ExprId> atom = INVALID_EXPR_ID;
- switch (c) {
- case '\\': {
- if (rem.empty())
- return Error(REG_EESCAPE);
- c = rem.front();
- rem.remove_prefix(1);
- if (is_unimplemented_ere_escape(c))
- return Error(REG_BADPAT);
- atom = pool.make_lit(c);
- break;
- }
- case '.': {
- ByteSet bs;
- if (cflags & REG_NEWLINE) {
- bs.set_range(0, '\n' - 1);
- bs.set_range('\n' + 1, BYTE_SET_BITS - 1);
- } else {
- bs.flip();
+ if (atom_count >= ExprPool::MAX_NODE_LIMIT)
+ return Error(REG_ESPACE);
+ if (atom_count == atoms.size()) {
+ size_t new_cap = atoms.size() * 2;
+ AllocChecker ac;
+ ExprId *new_atoms = new (ac) ExprId[new_cap];
+ if (!ac)
+ return Error(REG_ESPACE);
+ ExprId *dst = new_atoms;
+ for (ExprId existing : atoms) {
+ *dst = existing;
+ ++dst;
}
- atom = pool.make_byte_set(bs);
- break;
+ delete[] heap_atoms;
+ heap_atoms = new_atoms;
+ atoms = cpp::span<ExprId>(heap_atoms, new_cap);
}
- case '^':
- atom = pool.make_anchor(ANCHOR_BOL);
- break;
- case '$':
- atom = pool.make_anchor(ANCHOR_EOL);
- break;
- // TODO: Wire ERE metacharacters as each operator is implemented in
- // subsequent patches.
- case '*':
- case '+':
- case '?':
- case '(':
- case ')':
- case '[':
- case '{':
- case '|':
- return Error(REG_BADPAT);
- default:
- atom = pool.make_lit(c);
- break;
- }
-
+ ErrorOr<ExprId> atom = parse_quantified_atom(rem, cflags, pool);
if (!atom.has_value())
return atom;
- cur = pool.make_concat(*cur, *atom);
- if (!cur.has_value())
- return cur;
+ atoms[atom_count] = *atom;
+ ++atom_count;
+ }
+
+ ErrorOr<ExprId> acc = pool.empty_str();
+ if (!acc.has_value())
+ return acc;
+ for (size_t i = atom_count; i > 0; --i) {
+ acc = pool.make_concat(atoms[i - 1], *acc);
+ if (!acc.has_value())
+ return acc;
}
- return cur;
+ return acc;
}
} // namespace regex
diff --git a/libc/test/src/__support/regex/expr_test.cpp b/libc/test/src/__support/regex/expr_test.cpp
index 7bf990a049c8f1..990a67d0cc87c3 100644
--- a/libc/test/src/__support/regex/expr_test.cpp
+++ b/libc/test/src/__support/regex/expr_test.cpp
@@ -143,6 +143,12 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesConcat) {
EXPECT_EQ(*pool.make_concat(empty_str, lit_a), lit_a);
EXPECT_EQ(*pool.make_concat(lit_a, empty_str), lit_a);
+ // Right-associativity: (a · b) · c == a · (b · c).
+ const ExprId lit_c = *pool.make_lit('c');
+ const ExprId ab = *pool.make_concat(lit_a, lit_b);
+ const ExprId bc = *pool.make_concat(lit_b, lit_c);
+ EXPECT_EQ(*pool.make_concat(ab, lit_c), *pool.make_concat(lit_a, bc));
+
// Nullability of (L · R) requires both L and R to be nullable.
const ExprId opt_a = *pool.make_alt(lit_a, empty_str);
const ExprId opt_b = *pool.make_alt(lit_b, empty_str);
@@ -160,10 +166,11 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesConcat) {
Expr::make_concat(lit_b, lit_b));
}
-TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAlt) {
+TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAltAndStar) {
ExprPool pool;
const ExprId lit_a = *pool.make_lit('a');
const ExprId lit_b = *pool.make_lit('b');
+ const ExprId lit_c = *pool.make_lit('c');
const ExprId empty_set = *pool.empty_set();
const ExprId empty_str = *pool.empty_str();
@@ -183,13 +190,39 @@ TEST(LlvmLibcRegexExprTest, AlgebraicIdentitiesAlt) {
EXPECT_EQ(*pool.make_alt(lit_a, lit_a), lit_a);
// (A | B) == (B | A) (Commutativity)
- EXPECT_EQ(*pool.make_alt(lit_a, lit_b), *pool.make_alt(lit_b, lit_a));
+ const ExprId ab = *pool.make_alt(lit_a, lit_b);
+ EXPECT_EQ(ab, *pool.make_alt(lit_b, lit_a));
+
+ // Canonical right-associated sorted deduplication across 3-way alternations.
+ const ExprId bc = *pool.make_alt(lit_b, lit_c);
+ const ExprId ac = *pool.make_alt(lit_a, lit_c);
+ const ExprId abc = *pool.make_alt(lit_a, bc);
+ EXPECT_EQ(*pool.make_alt(ab, lit_c), abc);
+ EXPECT_EQ(*pool.make_alt(lit_c, ab), abc);
+ EXPECT_EQ(*pool.make_alt(ac, lit_b), abc);
+ EXPECT_EQ(*pool.make_alt(ab, bc), abc);
+ EXPECT_EQ(*pool.make_alt(abc, lit_b), abc);
// Nullability of (L | R) when L is non-nullable and R (with higher ExprId) is
// nullable.
const ExprId opt_b = *pool.make_alt(lit_b, empty_str);
const ExprId opt_bb = *pool.make_concat(opt_b, opt_b);
EXPECT_TRUE(pool.is_nullable(*pool.make_alt(lit_a, opt_bb)));
+
+ // Star identities: INVALID_EXPR_ID, Ø* => ε, ε* => ε, and (R*)* => R*.
+ auto bad_star = pool.make_star(INVALID_EXPR_ID);
+ ASSERT_FALSE(bad_star.has_value());
+ EXPECT_EQ(bad_star.error(), REG_BADPAT);
+ EXPECT_EQ(*pool.make_star(empty_set), empty_str);
+ EXPECT_EQ(*pool.make_star(empty_str), empty_str);
+
+ const ExprId star_a = *pool.make_star(lit_a);
+ EXPECT_EQ(*pool.make_star(star_a), star_a);
+ EXPECT_TRUE(pool.is_nullable(star_a));
+ EXPECT_FALSE(pool.has_anchors(star_a));
+ 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));
}
TEST(LlvmLibcRegexExprTest, HermeticHundredPools) {
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 3e79443ba3efb9..cbda7a16e19ef2 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -191,6 +191,45 @@ TEST(LlvmLibcRegexMatcherTest, AnchorMatchingAndFlags) {
EXPECT_EQ(match(pool, *nl_bol, "\nb", REG_NEWLINE, 0), MatchResult::Match);
}
+TEST(LlvmLibcRegexMatcherTest, StarMatchingAndStabilization) {
+ ExprPool pool;
+ ErrorOr<ExprId> pat = parse_ere("ab*c", REG_EXTENDED, pool);
+ ASSERT_TRUE(pat.has_value());
+ EXPECT_LE(pool.allocated_bytes(), size_t(1024));
+
+ EXPECT_EQ(match(pool, *pat, "ac"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *pat, "abc"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *pat, "abbbc"), MatchResult::Match);
+ // Near-miss negative checks for ab*c.
+ EXPECT_EQ(match(pool, *pat, "abbd"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "bbbc"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "abXc"), MatchResult::NoMatch);
+
+ // State-space stabilization canary: scanning "a.*c" across a 10,000-byte
+ // input must converge to a small fixed set of canonical derivative states
+ // within the initial 1 KiB pool.
+ ExprPool star_pool;
+ ErrorOr<ExprId> dot_star = parse_ere("a.*c", REG_EXTENDED, star_pool);
+ ASSERT_TRUE(dot_star.has_value());
+ char long_input[10000];
+ long_input[0] = 'a';
+ for (size_t i = 1; i < 9999; ++i)
+ long_input[i] = 'b';
+ long_input[9999] = 'c';
+ EXPECT_EQ(match(star_pool, *dot_star, string_view(long_input, 10000)),
+ MatchResult::Match);
+ EXPECT_LE(star_pool.size(), size_t(16));
+ EXPECT_LE(star_pool.allocated_bytes(), size_t(1024));
+
+ ErrorOr<ExprId> nl_dot_star =
+ parse_ere("a.*c", REG_EXTENDED | REG_NEWLINE, star_pool);
+ ASSERT_TRUE(nl_dot_star.has_value());
+ EXPECT_EQ(match(star_pool, *nl_dot_star, "abbc", REG_NEWLINE, 0),
+ MatchResult::Match);
+ EXPECT_EQ(match(star_pool, *nl_dot_star, "a\nc", REG_NEWLINE, 0),
+ MatchResult::NoMatch);
+}
+
TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
ExprPool pool;
ErrorOr<ExprId> escaped_dot = parse_ere(R"(a\.b)", REG_EXTENDED, pool);
@@ -213,8 +252,17 @@ 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}"};
+ 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 bad_patterns[] = {
- "*", "+", "?", "(", ")", "[", "{", "|",
+ "a+", "a?", "a{1}", "{", "(", ")", "[", "|",
R"(\1)", R"(\2)", R"(\3)", R"(\4)", R"(\5)", R"(\6)", R"(\7)", R"(\8)",
R"(\9)", R"(\b)", R"(\B)", R"(\<)", R"(\>)", R"(\A)", R"(\Z)", R"(\`)",
R"(\')", R"(\w)", R"(\W)", R"(\s)", R"(\S)", R"(\d)", R"(\D)"};
@@ -233,11 +281,12 @@ TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
const ExprId lit_c = *pool.make_lit('c');
const ExprId ab = *pool.make_concat(lit_a, lit_b);
+ const ExprId ac = *pool.make_concat(lit_a, lit_c);
const ExprId opt_a = *pool.make_alt(lit_a, empty_str);
- const ExprId opt_ab = *pool.make_concat(opt_a, lit_b);
- // D_a(((a | "") . b) . c) = b . c, which requires interning Concat(b, c).
- const ExprId need_bc = *pool.make_concat(opt_ab, lit_c);
- const ExprId concat_left_fail = *pool.make_concat(need_bc, lit_a);
+ // 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 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);
const ExprId lit_d = *pool.make_lit('d');
@@ -248,9 +297,9 @@ TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
while (pool.size() < 10000)
chain = *pool.make_concat(lit_a, chain);
- const ExprId exhausted_exprs[] = {need_bc, concat_left_fail,
- concat_right_fail, alt_left_fail,
- alt_right_fail};
+ const ExprId exhausted_exprs[] = {need_bc, star_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());
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index 05444b49e1dd79..a15cc6aca1499a 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -165,6 +165,25 @@ TEST(LlvmLibcRegexTest, AnchorsAndFlags) {
EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a\nb", 0, nullptr, 0), REG_NOMATCH);
}
+TEST(LlvmLibcRegexTest, KleeneStarAndBadRpt) {
+ regex_t preg;
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "ab*c", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "ac", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abc", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abbbc", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abbd", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "bbbc", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abXc", 0, nullptr, 0), REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ const char *const bad_rpt[] = {"*", "+", "?", "{1}", "^*", "$*", "a**"};
+ for (const char *pat : bad_rpt) {
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADRPT);
+ LIBC_NAMESPACE::regfree(&preg);
+ }
+}
+
TEST(LlvmLibcRegexTest, EscapedMetacharacters) {
regex_t preg;
ASSERT_EQ(
@@ -194,9 +213,9 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
LIBC_NAMESPACE::regfree(&preg);
// Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
- const char *const bad_patterns[] = {"a*", "a+", "a?", "(a)",
- "[a]", "a{1}", "a|b", R"(\1)",
- R"(\b)", R"(\w)", R"(\s)", R"(\d)"};
+ const char *const bad_patterns[] = {"a+", "a?", "(a)", "[a]",
+ "a{1}", "a|b", R"(\1)", R"(\b)",
+ R"(\w)", R"(\s)", R"(\d)"};
for (const char *pat : bad_patterns) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
More information about the libc-commits
mailing list