[libc-commits] [libc] [libc][regex] Add ERE {m, n} interval quantifiers (PR #236569)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Sun Nov 1 00:35:21 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/236569
>From 871067bfb35fc615760856aa6a6b2dafb858e3a5 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Sat, 31 Oct 2026 20:49:23 +0000
Subject: [PATCH] [libc][regex] Add ERE {m,n} interval quantifiers (#236569)
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); although POSIX leaves an unescaped '{' not
followed by an ASCII digit undefined, real-world patterns rely on it
matching a literal '{'. Unrolling counted repetitions at parse time
duplicates subexpressions up to 255 times per quantifier, while
unanchored derivative stepping across high-count intervals builds
quadratic alternation chains as active repetition states accumulate at
each input byte.
We represent interval quantifiers directly in Expr by moving ch into
the 8-byte payload union alongside named Binary and RepeatBounds
structs, freeing two bytes to cache a has_repeat bit and a saturating
16-bit rep_expansion factor within the 16-byte node size, and packing
hash_expr into a single 16-byte HashKey struct round. During Repeat
derivative evaluation, next_min collapses to zero whenever the body is
nullable at the pre-byte anchor context so leading copies can match
empty at the start of a line, while ExprPool::make_alt coalesces
overlapping or adjacent intervals across bare nodes, shared-prefix or
shared-suffix Concat nodes, and full Alt spines in ascending ExprId
order. We 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/hdr/limits_macros.h | 8 +
libc/include/limits.yaml | 4 +
libc/include/llvm-libc-macros/limits-macros.h | 8 +
libc/src/__support/regex/CMakeLists.txt | 3 +
libc/src/__support/regex/regex_ast.h | 84 +++++--
libc/src/__support/regex/regex_expr_pool.cpp | 229 ++++++++++++++++--
libc/src/__support/regex/regex_expr_pool.h | 2 +
libc/src/__support/regex/regex_matcher.cpp | 24 +-
libc/src/__support/regex/regex_parser.cpp | 96 ++++++--
libc/test/src/__support/regex/expr_test.cpp | 51 ++++
.../test/src/__support/regex/matcher_test.cpp | 196 ++++++++++++++-
libc/test/src/regex/regex_basic_test.cpp | 31 ++-
12 files changed, 667 insertions(+), 69 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/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..66db0746145c2e 100644
--- a/libc/include/llvm-libc-macros/limits-macros.h
+++ b/libc/include/llvm-libc-macros/limits-macros.h
@@ -298,4 +298,12 @@
#define NZERO 20
#endif // NZERO
+#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 // LLVM_LIBC_MACROS_LIMITS_MACROS_H
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index a9113005c08fe7..f66fe3efe4dc41 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -10,9 +10,11 @@ add_object_library(
libc.hdr.regex_macros
libc.hdr.stdint_proxy
libc.src.__support.alloc_checker
+ libc.src.__support.CPP.algorithm
libc.src.__support.CPP.array
libc.src.__support.CPP.bitset
libc.src.__support.CPP.new
+ libc.src.__support.CPP.optional
libc.src.__support.CPP.scope
libc.src.__support.CPP.span
libc.src.__support.CPP.utility
@@ -58,6 +60,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..83af439fe97e1a 100644
--- a/libc/src/__support/regex/regex_ast.h
+++ b/libc/src/__support/regex/regex_ast.h
@@ -28,6 +28,14 @@ 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,
+/// 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;
@@ -57,6 +65,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.
@@ -65,52 +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.
+ 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, 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() {
@@ -124,16 +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, 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.
@@ -152,6 +193,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..6f40b6f721c099 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -13,7 +13,9 @@
#include "src/__support/regex/regex_expr_pool.h"
#include "hdr/regex_macros.h"
+#include "src/__support/CPP/algorithm.h"
#include "src/__support/CPP/new.h"
+#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/scope.h"
#include "src/__support/CPP/utility.h"
#include "src/__support/alloc-checker.h"
@@ -33,29 +35,36 @@ uint32_t hash_expr(const Expr &e) {
// high-entropy constants (derived from aHash) to produce a strong hash, while
// the constant value guarantees deterministic hashing for hash-consing.
internal::HashState hasher(0x12345678);
- uint64_t kind = static_cast<uint64_t>(e.kind);
- hasher.update(&kind, sizeof(kind));
+ struct HashKey {
+ uint64_t kind;
+ uint64_t payload;
+ } key = {static_cast<uint64_t>(e.kind), 0};
+ static_assert(sizeof(HashKey) == 2 * sizeof(uint64_t));
switch (e.kind) {
case ExprKind::Literal:
- hasher.update(&e.ch, sizeof(e.ch));
+ key.payload = static_cast<uint8_t>(e.ch);
break;
case ExprKind::Anchor:
- hasher.update(&e.anchor_mask, sizeof(e.anchor_mask));
+ key.payload = e.anchor_mask;
break;
case ExprKind::ByteSet:
- hasher.update(&e.byte_set_id, sizeof(e.byte_set_id));
+ key.payload = e.byte_set_id;
break;
case ExprKind::Star:
- hasher.update(&e.body, sizeof(e.body));
+ key.payload = e.body;
+ break;
+ case ExprKind::Repeat:
+ key.payload = (static_cast<uint64_t>(e.rep.body) << 32) |
+ (static_cast<uint64_t>(e.rep.min) << 16) | e.rep.max;
break;
case ExprKind::Concat:
case ExprKind::Alt:
- hasher.update(&e.bin.left, sizeof(e.bin.left));
- hasher.update(&e.bin.right, sizeof(e.bin.right));
+ key.payload = (static_cast<uint64_t>(e.bin.left) << 32) | e.bin.right;
break;
default:
break;
}
+ hasher.update(&key, sizeof(key));
uint64_t h = hasher.finish();
return static_cast<uint32_t>(h ^ (h >> 32));
}
@@ -281,16 +290,61 @@ ErrorOr<ExprId> ExprPool::make_byte_set(const ByteSet &bs) {
return id;
}
+namespace {
+
+// Multiplies a subexpression's repeat expansion factor by an enclosing repeat
+// bound, saturating at MAX_REPEAT_EXPANSION + 1 so nested bounds checks never
+// overflow uint16_t.
+uint16_t scale_exp(uint16_t base, uint16_t factor) {
+ if (factor <= 1)
+ return base;
+ uint32_t prod = static_cast<uint32_t>(base) * static_cast<uint32_t>(factor);
+ return static_cast<uint16_t>(
+ cpp::min<uint32_t>(prod, MAX_REPEAT_EXPANSION + 1));
+}
+
+} // namespace
+
ErrorOr<ExprId> ExprPool::make_star(ExprId body) {
if (body == INVALID_EXPR_ID)
return Error(REG_BADPAT);
// Ø* = ε and ε* = ε
if (body == EMPTY_SET_ID || body == EMPTY_STR_ID)
return empty_str();
- // (R*)* = R* (Idempotency)
- if (get(body).kind == ExprKind::Star)
+ // (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;
- return intern(Expr::make_star(body, has_anchors(body)));
+ if (be.kind == ExprKind::Repeat && be.rep.min <= 1)
+ return make_star(be.rep.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 || min == REPEAT_INF)
+ return Error(REG_BADPAT);
+ // Normalize R{0,0} = ε, ε{m,n} = ε, Ø{m,n}, R{1,1} = R, and
+ // R{0,REPEAT_INF} = R*.
+ if (max == 0 || body == EMPTY_STR_ID)
+ return empty_str();
+ if (body == EMPTY_SET_ID)
+ return (min == 0) ? empty_str() : empty_set();
+ if (min == 1 && max == 1)
+ return body;
+ 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;
+ 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) {
@@ -313,10 +367,115 @@ ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
return tail;
return make_concat(le.bin.left, *tail);
}
- return intern(Expr::make_concat(l, r, is_nullable(l) && is_nullable(r),
- has_anchors(l) || has_anchors(r)));
+ const Expr re = get(r);
+ return intern(Expr::make_concat(
+ l, r, le.nullable && re.nullable, le.has_anchors || re.has_anchors,
+ le.has_repeat || re.has_repeat,
+ cpp::max(le.rep_expansion, re.rep_expansion)));
}
+namespace {
+
+// Coalesces R{m1,n1} | R{m2,n2} (and shared-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.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, 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;
+ struct RepeatInterval {
+ uint16_t min;
+ uint16_t max;
+ };
+ // Undo make_repeat normalizations: R -> [1,1], EmptyStr -> [0,0], and
+ // Star(R) -> [0,REPEAT_INF].
+ auto get_interval = [&](ExprId id,
+ const Expr &e) -> cpp::optional<RepeatInterval> {
+ if (e.kind == ExprKind::Repeat && e.rep.body == body)
+ return RepeatInterval{e.rep.min, e.rep.max};
+ if (e.kind == ExprKind::Star && e.body == body)
+ return RepeatInterval{0, REPEAT_INF};
+ if (id == body)
+ return RepeatInterval{1, 1};
+ if (id == EMPTY_STR_ID)
+ return RepeatInterval{0, 0};
+ return cpp::nullopt;
+ };
+ cpp::optional<RepeatInterval> lhs = get_interval(a, ae);
+ cpp::optional<RepeatInterval> rhs = get_interval(b, be);
+ if (!lhs.has_value() || !rhs.has_value())
+ return INVALID_EXPR_ID;
+ // Integer intervals [lhs->min, lhs->max] and [rhs->min, rhs->max] coalesce
+ // iff they overlap or are adjacent (later_min <= earlier_max + 1, e.g.
+ // [2,3] U [4,5] = [2,5]).
+ uint16_t later_min = cpp::max(lhs->min, rhs->min);
+ uint16_t earlier_max = cpp::min(lhs->max, rhs->max);
+ if (earlier_max != REPEAT_INF && later_min > earlier_max + 1)
+ return INVALID_EXPR_ID;
+ return pool.make_repeat(body, cpp::min(lhs->min, rhs->min),
+ cpp::max(lhs->max, rhs->max));
+}
+
+// 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);
@@ -333,15 +492,38 @@ 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),
- has_anchors(l) || has_anchors(r)));
+ return intern(Expr::make_alt(l, r, le.nullable || re.nullable,
+ le.has_anchors || re.has_anchors,
+ le.has_repeat || re.has_repeat,
+ cpp::max(le.rep_expansion, re.rep_expansion)));
+ }
+
+ if (le.has_repeat || re.has_repeat) {
+ if (le.kind != ExprKind::Alt) {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(l, r, *this);
+ if (!c.has_value() || *c != INVALID_EXPR_ID)
+ return c;
+ } else if (re.kind != ExprKind::Alt) {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(r, l, *this);
+ if (!c.has_value() || *c != INVALID_EXPR_ID)
+ return c;
+ } else {
+ ErrorOr<ExprId> c = coalesce_item_into_spine(le.bin.left, r, *this);
+ if (!c.has_value())
+ return c;
+ if (*c != INVALID_EXPR_ID)
+ return make_alt(le.bin.right, *c);
+ }
}
// Merge two right-associated sorted Alt spines in ascending ExprId order so
// 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;
@@ -358,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,
+ cpp::max(he.rep_expansion, re_rest.rep_expansion)));
}
} // namespace regex
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..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:
@@ -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 nullable(e.rep.body, anchor_ctx, pool);
default:
return e.nullable;
}
@@ -107,6 +109,26 @@ 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{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 && !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);
+ 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..34257646a4134a 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,69 @@ 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;
+// 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 (spec.empty() || !internal::isdigit(spec.front()))
+ return cpp::nullopt;
+ uint32_t val = 0;
+ 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);
+}
+
+// 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);
+ // 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);
+
+ 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 && pool.get(atom).rep_expansion > MAX_REPEAT_EXPANSION / bound)
+ 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;
@@ -236,17 +291,18 @@ 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 '?':
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;
@@ -280,19 +336,22 @@ 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);
ErrorOr<ExprId> star = pool.make_star(*atom);
- if (!star.has_value())
- return star;
- return pool.make_concat(*atom, *star);
+ if (star.has_value())
+ prefix_atom = *atom;
+ return star;
}
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 +401,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..7f8d46291599ea 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,56 @@ 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);
+ 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);
+ 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_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);
+ 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(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(
+ 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..d35fa207178d9f 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,163 @@ 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...200_as...)+$ to compile in 407 nodes (2 * 200 + 7, rather than
+ // 3 * 200 + 8 = 608 with an orphaned Concat spine) while keeping the stack
+ // buffer <= 256 B for GPU targets.
+ 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 pat200[206];
+ pat200[0] = '^';
+ pat200[1] = '(';
+ for (size_t i = 0; i < 200; ++i)
+ pat200[2 + i] = 'a';
+ pat200[202] = ')';
+ pat200[203] = '+';
+ pat200[204] = '$';
+ pat200[205] = '\0';
+ ExprPool long_plus_pool;
+ ErrorOr<ExprId> long_plus =
+ parse_ere(string_view(pat200, 205), REG_EXTENDED, long_plus_pool);
+ ASSERT_TRUE(long_plus.has_value());
+ EXPECT_EQ(long_plus_pool.size(), size_t(407));
+ EXPECT_EQ(match(long_plus_pool, *long_plus, string_view(pat200 + 2, 200)),
+ 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);
+
+ // 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) at compile time in
+ // .rodata (avoiding GPU stack pressure) and verify hash-consed DAG
+ // deduplication keeps the pool under 32 KiB.
+ struct Rfc2822Pattern {
+ char buf[8192]{};
+ size_t len = 0;
+
+ constexpr void expand(string_view str) {
+ for (char ch : str) {
+ if (ch == 'F') {
+ expand("(([ \t]*\r\n)?[ \t]+)");
+ } else if (ch == 'C') {
+ expand(R"(((F?\((F?([!-'*-[^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?\))+F?|F)?)");
+ } else if (ch == 'T') {
+ expand(R"([[:alnum:]!#$%&'*+/=?^_`{|}~-])");
+ } else if (ch == 'W') {
+ expand(R"(((CT+C)|(C"(F?([!#-[^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?"C)))");
+ } else if (ch == 'S') {
+ expand(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^-~]|\\([ -~]|[ )"
+ "\t"
+ R"(])))*F?)\]C)|)"
+ R"(((CT+C)(\.(CT+C){1,1}){1,31}))))");
+ } else {
+ buf[len] = ch;
+ ++len;
+ }
+ }
+ }
+
+ constexpr Rfc2822Pattern() { expand("^(S|W{0,8}<S>)$"); }
+ };
+ static constexpr Rfc2822Pattern RFC2822_PATTERN{};
+ static_assert(RFC2822_PATTERN.len > 7000);
+ string_view rfc2822_ere(RFC2822_PATTERN.buf, RFC2822_PATTERN.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..6eeb79f75bcab3 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -333,9 +333,9 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
- // Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
- const char *const bad_patterns[] = {"a{1}", R"(\1)", R"(\b)", "[[:<:]]",
- "[[:>:]]"};
+ // 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);
LIBC_NAMESPACE::regfree(&preg);
@@ -589,6 +589,21 @@ 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("^(^|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");
+
// Invalid repetition errors (REG_BADRPT).
const char *const bad_rpt[] = {"^+", "$+", "^?", "$?", "a++", "a??",
"a*+", "a+?", "a?*", "(+a)", "(?a)"};
@@ -597,6 +612,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)
More information about the libc-commits
mailing list