[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