[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 06:40:00 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/3] [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 741593cccc4695674fb403e3ab1c1754331cd5a8 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Sat, 31 Oct 2026 13:29:45 +0000
Subject: [PATCH 2/3] [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
now caches a has_repeat bit and a saturating 16-bit rep_expansion factor
in existing padding, packs hash_expr into a single 16-byte round, and
extends 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/regex_ast.h          |  79 ++++---
 libc/src/__support/regex/regex_expr_pool.cpp  | 195 +++++++++++++-----
 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 +-
 9 files changed, 312 insertions(+), 133 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/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..62abbdca647536 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -33,34 +33,32 @@ 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));
+  uint64_t words[2] = {static_cast<uint64_t>(e.kind), 0};
   switch (e.kind) {
   case ExprKind::Literal:
-    hasher.update(&e.ch, sizeof(e.ch));
+    words[1] = static_cast<uint8_t>(e.ch);
     break;
   case ExprKind::Anchor:
-    hasher.update(&e.anchor_mask, sizeof(e.anchor_mask));
+    words[1] = e.anchor_mask;
     break;
   case ExprKind::ByteSet:
-    hasher.update(&e.byte_set_id, sizeof(e.byte_set_id));
+    words[1] = e.byte_set_id;
     break;
   case ExprKind::Star:
-    hasher.update(&e.body, sizeof(e.body));
+    words[1] = 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));
+    words[1] = (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));
+    words[1] = (static_cast<uint64_t>(e.bin.left) << 32) | e.bin.right;
     break;
   default:
     break;
   }
+  hasher.update(words, sizeof(words));
   uint64_t h = hasher.finish();
   return static_cast<uint32_t>(h ^ (h >> 32));
 }
@@ -286,54 +284,76 @@ ErrorOr<ExprId> ExprPool::make_byte_set(const ByteSet &bs) {
   return id;
 }
 
+namespace {
+
+uint16_t max_exp(uint16_t a, uint16_t b) { return (a > b) ? a : b; }
+
+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 (prod > MAX_REPEAT_EXPANSION)
+             ? static_cast<uint16_t>(MAX_REPEAT_EXPANSION + 1)
+             : static_cast<uint16_t>(prod);
+}
+
+} // namespace
+
 ErrorOr<ExprId> ExprPool::make_star(ExprId body) {
   if (body == INVALID_EXPR_ID)
     return Error(REG_BADPAT);
-  // Ø* = ε and ε* = ε
+  // EmptySet* = EmptyStr and EmptyStr* = EmptyStr.
   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} = EmptyStr, EmptyStr{m,n} = EmptyStr, EmptySet{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) {
   if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
     return Error(REG_BADPAT);
   // Apply basic algebraic identities for concatenation:
-  // Ø · R = R · Ø = Ø (Identity: null set)
+  // EmptySet . R = R . EmptySet = EmptySet (Identity: null set)
   if (l == EMPTY_SET_ID || r == EMPTY_SET_ID)
     return empty_set();
-  // ε · R = R · ε = R (Identity: empty string)
+  // EmptyStr . R = R . EmptyStr = R (Identity: empty string)
   if (l == EMPTY_STR_ID)
     return r;
   if (r == EMPTY_STR_ID)
     return l;
-  // (A · B) · C = A · (B · C) (Right-associative canonical form)
+  // (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);
@@ -341,28 +361,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,
+                                  max_exp(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 +422,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,6 +442,8 @@ 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;
+  // 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 = (lo1 > lo2) ? lo1 : lo2;
   uint16_t min_hi = (hi1 < hi2) ? hi1 : hi2;
   if (min_hi != REPEAT_INF && max_lo > min_hi + 1)
@@ -393,13 +452,35 @@ ErrorOr<ExprId> coalesce_repeat_pair(ExprId a, ExprId b, ExprPool &pool) {
                           (hi1 > hi2) ? 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
 
 ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
   if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
     return Error(REG_BADPAT);
   // Apply basic algebraic identities for alternation:
-  // Ø | R = R | Ø = R (Identity: null set)
+  // EmptySet | R = R | EmptySet = R (Identity: null set)
   if (l == EMPTY_SET_ID)
     return r;
   if (r == EMPTY_SET_ID)
@@ -416,8 +497,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,
+                                 max_exp(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 +528,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 +540,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,
+                     max_exp(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");
 

>From a0627f71c0208e9f5e8853deeaca7c6ac2192348 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Sat, 31 Oct 2026 13:39:49 +0000
Subject: [PATCH 3/3] [libc][regex][NFC] Use named struct in hash_expr

Replaced the two-element uint64_t array and raw subscript in hash_expr
with a two-field HashKey struct verified by static_assert so both 64-bit
halves are addressed by name without array indexing.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/__support/regex/regex_expr_pool.cpp | 22 ++++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/libc/src/__support/regex/regex_expr_pool.cpp b/libc/src/__support/regex/regex_expr_pool.cpp
index 62abbdca647536..35e09c0af17100 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -33,32 +33,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 words[2] = {static_cast<uint64_t>(e.kind), 0};
+  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:
-    words[1] = static_cast<uint8_t>(e.ch);
+    key.payload = static_cast<uint8_t>(e.ch);
     break;
   case ExprKind::Anchor:
-    words[1] = e.anchor_mask;
+    key.payload = e.anchor_mask;
     break;
   case ExprKind::ByteSet:
-    words[1] = e.byte_set_id;
+    key.payload = e.byte_set_id;
     break;
   case ExprKind::Star:
-    words[1] = e.body;
+    key.payload = e.body;
     break;
   case ExprKind::Repeat:
-    words[1] = (static_cast<uint64_t>(e.rep.body) << 32) |
-               (static_cast<uint64_t>(e.rep.min) << 16) | 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:
-    words[1] = (static_cast<uint64_t>(e.bin.left) << 32) | e.bin.right;
+    key.payload = (static_cast<uint64_t>(e.bin.left) << 32) | e.bin.right;
     break;
   default:
     break;
   }
-  hasher.update(words, sizeof(words));
+  hasher.update(&key, sizeof(key));
   uint64_t h = hasher.finish();
   return static_cast<uint32_t>(h ^ (h >> 32));
 }



More information about the libc-commits mailing list