[libc-commits] [libc] [libc][regex] Add '^' and '$' anchors and line flags (PR #233780)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Tue Oct 20 03:38:32 PDT 2026
https://github.com/kaladron created https://github.com/llvm/llvm-project/pull/233780
Beginning-of-line ('^') and end-of-line ('$') assertions depend on the surrounding fencepost context (string boundaries, newlines under REG_NEWLINE, and execution flags REG_NOTBOL and REG_NOTEOL) rather than consuming input characters.
Add ExprKind::Anchor with AnchorKind bitmasks (ANCHOR_BOL and ANCHOR_EOL) stored in Expr::anchor_mask, and pack a has_anchors bitfield alongside nullable so sizeof(Expr) remains 16 bytes and unanchored subtrees keep O(1) nullability checks. Evaluate context-aware nullability at each fencepost in derivative and match, enable REG_NEWLINE in regcomp and REG_NOTBOL and REG_NOTEOL in regexec, and guard RegexInternal cleanup with cpp::scope_exit.
Against the external compliance suites in REG_NOSUB mode, this passes 52/456 tests (+31) in the Henry Spencer suite and 40/202 ERE tests (+15) in Glenn Fowler's AT&T testregex basic.dat suite.
Assisted-by: Automated tooling, human reviewed.
>From 51ac50f17cf8da71f01ef93b4078867deca76662 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Tue, 20 Oct 2026 08:18:14 +0100
Subject: [PATCH] [libc][regex] Add '^' and '$' anchors and line flags
Beginning-of-line ('^') and end-of-line ('$') assertions depend on the
surrounding fencepost context (string boundaries, newlines under
REG_NEWLINE, and execution flags REG_NOTBOL and REG_NOTEOL) rather than
consuming input characters.
Add ExprKind::Anchor with AnchorKind bitmasks (ANCHOR_BOL and
ANCHOR_EOL) stored in Expr::anchor_mask, and pack a has_anchors bitfield
alongside nullable so sizeof(Expr) remains 16 bytes and unanchored
subtrees keep O(1) nullability checks. Evaluate context-aware
nullability at each fencepost in derivative and match, enable
REG_NEWLINE in regcomp and REG_NOTBOL and REG_NOTEOL in regexec, and
guard RegexInternal cleanup with cpp::scope_exit.
Against the external compliance suites in REG_NOSUB mode, this passes
52/456 tests (+31) in the Henry Spencer suite and 40/202 ERE tests (+15)
in Glenn Fowler's AT&T testregex basic.dat suite.
Assisted-by: Automated tooling, human reviewed.
---
libc/src/__support/regex/CMakeLists.txt | 4 +
libc/src/__support/regex/regex_ast.h | 55 ++++++++++----
libc/src/__support/regex/regex_expr_pool.cpp | 12 ++-
libc/src/__support/regex/regex_expr_pool.h | 7 +-
libc/src/__support/regex/regex_matcher.cpp | 73 ++++++++++++++----
libc/src/__support/regex/regex_matcher.h | 21 +++++-
libc/src/__support/regex/regex_parser.cpp | 8 +-
libc/src/regex/CMakeLists.txt | 1 +
libc/src/regex/regcomp.cpp | 16 ++--
libc/src/regex/regexec.cpp | 9 +--
libc/test/src/__support/regex/expr_test.cpp | 17 +++++
.../test/src/__support/regex/matcher_test.cpp | 65 ++++++++++++++--
libc/test/src/regex/CMakeLists.txt | 1 +
libc/test/src/regex/regex_basic_test.cpp | 75 ++++++++++++++-----
14 files changed, 288 insertions(+), 76 deletions(-)
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index b4be2eb3a211bf..95857681ed51d4 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -41,8 +41,12 @@ add_object_library(
regex_matcher.h
DEPENDS
.regex_expr_pool
+ .regex_internal
+ libc.hdr.regex_macros
+ libc.src.__support.common
libc.src.__support.CPP.string_view
libc.src.__support.error_or
+ libc.src.__support.libc_assert
)
add_object_library(
diff --git a/libc/src/__support/regex/regex_ast.h b/libc/src/__support/regex/regex_ast.h
index c3e6b527166955..a0c9354346bb50 100644
--- a/libc/src/__support/regex/regex_ast.h
+++ b/libc/src/__support/regex/regex_ast.h
@@ -33,6 +33,12 @@ inline constexpr ExprId EMPTY_STR_ID = 2;
inline constexpr size_t BYTE_SET_BITS = size_t(1) << CHAR_BIT;
using ByteSet = cpp::bitset<BYTE_SET_BITS>;
+/// Bitmask flags identifying zero-width boundary assertions.
+enum AnchorKind : uint16_t {
+ ANCHOR_BOL = 1 << 0,
+ ANCHOR_EOL = 1 << 1,
+};
+
/// Enumeration of Regular Expression AST node types.
enum class ExprKind : uint8_t {
/// Represents the empty set (matches nothing).
@@ -41,6 +47,8 @@ enum class ExprKind : uint8_t {
EmptyStr,
/// A literal character match.
Literal,
+ /// A zero-width boundary assertion (^ or $).
+ Anchor,
/// A 256-bit character class or wildcard match.
ByteSet,
/// Concatenation of two expressions (left followed by right).
@@ -57,9 +65,11 @@ enum class ExprKind : uint8_t {
struct Expr {
/// The type of this expression node.
ExprKind kind = ExprKind::EmptySet;
- /// Whether this expression can match the empty string.
- bool nullable = false;
- /// Character value for Literal nodes.
+ /// 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';
/// Precomputed 32-bit structural hash for O(1) lookup and rehashing.
uint32_t hash = 0;
@@ -71,23 +81,29 @@ struct Expr {
} bin = {INVALID_EXPR_ID, INVALID_EXPR_ID};
/// Index into ExprPool::byte_sets for ByteSet nodes.
uint32_t byte_set_id;
+ /// Bitmask of AnchorKind flags for Anchor nodes.
+ uint16_t anchor_mask;
};
/// Default constructor creates an EmptySet node.
/// Public to allow array allocation in ExprPool::Block.
- constexpr Expr() = default;
+ constexpr Expr() : nullable(false), has_anchors(false) {}
private:
- /// Create a node of a specific kind with nullability.
- constexpr Expr(ExprKind k, bool is_null) : kind(k), nullable(is_null) {}
- /// Create a Literal node.
- constexpr Expr(char c) : kind(ExprKind::Literal), ch(c) {}
+ /// 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) {}
+ /// Create an Anchor node.
+ constexpr Expr(uint16_t mask)
+ : kind(ExprKind::Anchor), nullable(false), has_anchors(true),
+ anchor_mask(mask) {}
/// Create a ByteSet node.
constexpr Expr(uint32_t bs_id)
- : kind(ExprKind::ByteSet), byte_set_id(bs_id) {}
+ : kind(ExprKind::ByteSet), nullable(false), has_anchors(false),
+ byte_set_id(bs_id) {}
/// Create a binary node (Concat or Alt).
- constexpr Expr(ExprKind k, bool is_null, ExprId l, ExprId r)
- : kind(k), nullable(is_null), bin{l, r} {}
+ constexpr Expr(ExprKind k, bool is_null, bool anchors, ExprId l, ExprId r)
+ : kind(k), nullable(is_null), has_anchors(anchors), bin{l, r} {}
public:
static constexpr Expr make_empty_set() {
@@ -96,13 +112,18 @@ struct Expr {
static constexpr Expr make_empty_str() {
return Expr(ExprKind::EmptyStr, true);
}
- static constexpr Expr make_literal(char c) { return Expr(c); }
+ static constexpr Expr make_literal(char c) {
+ return Expr(ExprKind::Literal, false, c);
+ }
+ static constexpr Expr make_anchor(uint16_t mask) { return Expr(mask); }
static constexpr Expr make_byte_set(uint32_t bs_id) { return Expr(bs_id); }
- static constexpr Expr make_concat(ExprId l, ExprId r, bool is_null = false) {
- return Expr(ExprKind::Concat, is_null, l, r);
+ 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);
}
- static constexpr Expr make_alt(ExprId l, ExprId r, bool is_null = false) {
- return Expr(ExprKind::Alt, is_null, 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);
}
/// Equivalence check for hash-consing.
@@ -115,6 +136,8 @@ struct Expr {
return true;
case ExprKind::Literal:
return ch == other.ch;
+ case ExprKind::Anchor:
+ return anchor_mask == other.anchor_mask;
case ExprKind::ByteSet:
return byte_set_id == other.byte_set_id;
case ExprKind::Concat:
diff --git a/libc/src/__support/regex/regex_expr_pool.cpp b/libc/src/__support/regex/regex_expr_pool.cpp
index 3f8c4266782a19..3cae0f8f0d495c 100644
--- a/libc/src/__support/regex/regex_expr_pool.cpp
+++ b/libc/src/__support/regex/regex_expr_pool.cpp
@@ -39,6 +39,9 @@ uint32_t hash_expr(const Expr &e) {
case ExprKind::Literal:
hasher.update(&e.ch, sizeof(e.ch));
break;
+ case ExprKind::Anchor:
+ hasher.update(&e.anchor_mask, sizeof(e.anchor_mask));
+ break;
case ExprKind::ByteSet:
hasher.update(&e.byte_set_id, sizeof(e.byte_set_id));
break;
@@ -226,6 +229,9 @@ ErrorOr<ExprId> ExprPool::empty_str() {
ErrorOr<ExprId> ExprPool::make_lit(char c) {
return intern(Expr::make_literal(c));
}
+ErrorOr<ExprId> ExprPool::make_anchor(uint16_t mask) {
+ return intern(Expr::make_anchor(mask));
+}
ErrorOr<ExprId> ExprPool::make_byte_set(const ByteSet &bs) {
size_t count = 0;
@@ -284,7 +290,8 @@ ErrorOr<ExprId> ExprPool::make_concat(ExprId l, ExprId r) {
return r;
if (r == EMPTY_STR_ID)
return l;
- return intern(Expr::make_concat(l, r, is_nullable(l) && is_nullable(r)));
+ return intern(Expr::make_concat(l, r, is_nullable(l) && is_nullable(r),
+ has_anchors(l) || has_anchors(r)));
}
ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
if (l == INVALID_EXPR_ID || r == INVALID_EXPR_ID)
@@ -300,7 +307,8 @@ ErrorOr<ExprId> ExprPool::make_alt(ExprId l, ExprId r) {
return l;
if (l > r)
cpp::swap(l, r);
- return intern(Expr::make_alt(l, r, is_nullable(l) || is_nullable(r)));
+ return intern(Expr::make_alt(l, r, is_nullable(l) || is_nullable(r),
+ has_anchors(l) || has_anchors(r)));
}
} // namespace regex
diff --git a/libc/src/__support/regex/regex_expr_pool.h b/libc/src/__support/regex/regex_expr_pool.h
index 145f92493bc826..e45e31ed61d97a 100644
--- a/libc/src/__support/regex/regex_expr_pool.h
+++ b/libc/src/__support/regex/regex_expr_pool.h
@@ -134,9 +134,12 @@ class ExprPool {
return byte_sets[id];
}
- /// O(1) nullability query for a valid ExprId.
+ /// O(1) unconditional nullability query for a valid ExprId.
bool is_nullable(ExprId id) const { return get(id).nullable; }
+ /// O(1) query indicating whether the subtree at id contains an Anchor node.
+ bool has_anchors(ExprId id) const { return get(id).has_anchors; }
+
/// Returns the number of unique AST nodes currently interned in this pool.
size_t size() const { return node_count; }
@@ -158,6 +161,8 @@ class ExprPool {
ErrorOr<ExprId> empty_str();
/// Creates or returns an existing Literal node for the given character.
ErrorOr<ExprId> make_lit(char c);
+ /// Creates or returns an existing Anchor node for the given AnchorKind mask.
+ ErrorOr<ExprId> make_anchor(uint16_t mask);
/// Creates or returns an existing node for the given 256-bit character set.
ErrorOr<ExprId> make_byte_set(const ByteSet &bs);
/// Normalizing factory for Concatenation (L ยท R).
diff --git a/libc/src/__support/regex/regex_matcher.cpp b/libc/src/__support/regex/regex_matcher.cpp
index 13d0346645f2f6..4e4480099d83c0 100644
--- a/libc/src/__support/regex/regex_matcher.cpp
+++ b/libc/src/__support/regex/regex_matcher.cpp
@@ -14,8 +14,10 @@
#include "src/__support/regex/regex_matcher.h"
+#include "hdr/regex_macros.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/error_or.h"
+#include "src/__support/libc_assert.h"
#include "src/__support/macros/config.h"
#include "src/__support/regex/regex_ast.h"
#include "src/__support/regex/regex_expr_pool.h"
@@ -23,12 +25,49 @@
namespace LIBC_NAMESPACE_DECL {
namespace regex {
-ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool) {
+namespace {
+
+// Computes the active AnchorKind bitmask at fencepost position pos in input.
+uint8_t ctx_at(cpp::string_view input, size_t pos, int cflags, int eflags) {
+ LIBC_ASSERT(pos <= input.size());
+ uint8_t ctx = 0;
+ if ((pos == 0 && !(eflags & REG_NOTBOL)) ||
+ (pos > 0 && (cflags & REG_NEWLINE) && input[pos - 1] == '\n'))
+ ctx |= ANCHOR_BOL;
+ if ((pos == input.size() && !(eflags & REG_NOTEOL)) ||
+ (pos < input.size() && (cflags & REG_NEWLINE) && input[pos] == '\n'))
+ ctx |= ANCHOR_EOL;
+ return ctx;
+}
+
+} // namespace
+
+bool nullable(ExprId id, uint8_t anchor_ctx, const ExprPool &pool) {
+ const Expr &e = pool.get(id);
+ if (!e.has_anchors)
+ return e.nullable;
+ switch (e.kind) {
+ case ExprKind::Anchor:
+ return (anchor_ctx & e.anchor_mask) == e.anchor_mask;
+ case ExprKind::Concat:
+ return nullable(e.bin.left, anchor_ctx, pool) &&
+ nullable(e.bin.right, anchor_ctx, pool);
+ case ExprKind::Alt:
+ return nullable(e.bin.left, anchor_ctx, pool) ||
+ nullable(e.bin.right, anchor_ctx, pool);
+ default:
+ return e.nullable;
+ }
+}
+
+ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool,
+ uint8_t ctx_before) {
// Copy the 16-byte node by value as recursive calls below mutate pool.
const Expr e = pool.get(id);
switch (e.kind) {
case ExprKind::EmptySet:
case ExprKind::EmptyStr:
+ case ExprKind::Anchor:
return EMPTY_SET_ID;
case ExprKind::Literal:
return (c == e.ch) ? EMPTY_STR_ID : EMPTY_SET_ID;
@@ -38,25 +77,25 @@ ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool) {
: EMPTY_SET_ID;
case ExprKind::Concat: {
// Product Rule:
- // D_c(L . R) = (D_c(L) . R) | (nullable(L) ? D_c(R) : EmptySet)
- const bool left_nullable = pool.is_nullable(e.bin.left);
- ErrorOr<ExprId> d_l = derivative(e.bin.left, c, pool);
+ // D_c(L . R) = (D_c(L) . R) | (nullable(L, ctx_before) ? D_c(R) : EmptySet)
+ const bool left_nullable = nullable(e.bin.left, ctx_before, pool);
+ ErrorOr<ExprId> d_l = derivative(e.bin.left, c, pool, ctx_before);
if (!d_l.has_value())
return d_l;
ErrorOr<ExprId> lhs = pool.make_concat(*d_l, e.bin.right);
if (!lhs.has_value() || !left_nullable)
return lhs;
- ErrorOr<ExprId> d_r = derivative(e.bin.right, c, pool);
+ ErrorOr<ExprId> d_r = derivative(e.bin.right, c, pool, ctx_before);
if (!d_r.has_value())
return d_r;
return pool.make_alt(*lhs, *d_r);
}
case ExprKind::Alt: {
// Sum Rule: D_c(L | R) = D_c(L) | D_c(R)
- ErrorOr<ExprId> d_l = derivative(e.bin.left, c, pool);
+ ErrorOr<ExprId> d_l = derivative(e.bin.left, c, pool, ctx_before);
if (!d_l.has_value())
return d_l;
- ErrorOr<ExprId> d_r = derivative(e.bin.right, c, pool);
+ ErrorOr<ExprId> d_r = derivative(e.bin.right, c, pool, ctx_before);
if (!d_r.has_value())
return d_r;
return pool.make_alt(*d_l, *d_r);
@@ -65,21 +104,23 @@ ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool) {
__builtin_unreachable();
}
-MatchResult match(ExprPool &pool, const ExprId pattern,
- cpp::string_view input) {
- if (pool.is_nullable(pattern))
+MatchResult match(ExprPool &pool, const ExprId pattern, cpp::string_view input,
+ int cflags, int eflags) {
+ uint8_t ctx = ctx_at(input, 0, cflags, eflags);
+ if (nullable(pattern, ctx, pool))
return MatchResult::Match;
// Unanchored substring search: at each byte, step active partial matches via
- // derivative(state, c) and union with pattern so a new match can begin after
- // the current byte (equivalent to matching .* . pattern without constructing
- // a prefix wildcard node).
+ // derivative(state, c, pool, ctx) and union with pattern so a new match can
+ // begin at the next fencepost (equivalent to matching .* . pattern without
+ // constructing a prefix wildcard node).
ExprId state = pattern;
- for (char c : input) {
- ErrorOr<ExprId> d = derivative(state, c, pool);
+ for (size_t pos = 0; pos < input.size(); ++pos) {
+ ErrorOr<ExprId> d = derivative(state, input[pos], pool, ctx);
if (!d.has_value())
return MatchResult::OutOfMemory;
- if (pool.is_nullable(*d))
+ ctx = ctx_at(input, pos + 1, cflags, eflags);
+ if (nullable(*d, ctx, pool) || nullable(pattern, ctx, pool))
return MatchResult::Match;
ErrorOr<ExprId> next = pool.make_alt(pattern, *d);
if (!next.has_value())
diff --git a/libc/src/__support/regex/regex_matcher.h b/libc/src/__support/regex/regex_matcher.h
index 759c7ebdafb258..110633c66ef65c 100644
--- a/libc/src/__support/regex/regex_matcher.h
+++ b/libc/src/__support/regex/regex_matcher.h
@@ -15,10 +15,12 @@
#define LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_MATCHER_H
#include "src/__support/CPP/string_view.h"
+#include "src/__support/common.h"
#include "src/__support/error_or.h"
#include "src/__support/macros/config.h"
#include "src/__support/regex/regex_ast.h"
#include "src/__support/regex/regex_expr_pool.h"
+#include "src/__support/regex/regex_internal.h"
namespace LIBC_NAMESPACE_DECL {
namespace regex {
@@ -30,14 +32,27 @@ enum class MatchResult {
OutOfMemory,
};
-/// Computes the Brzozowski derivative D_c(id) with respect to character c.
+/// Evaluates whether id can match the empty string at a fencepost with the
+/// given active AnchorKind bitmask.
+bool nullable(ExprId id, uint8_t anchor_ctx, const ExprPool &pool);
+
+/// Computes the Brzozowski derivative D_c(id) with respect to character c
+/// when stepping across c from a fencepost with active anchor mask ctx_before.
///
/// Formally, D_c(L) = { w | c . w in L }.
-ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool);
+ErrorOr<ExprId> derivative(ExprId id, char c, ExprPool &pool,
+ uint8_t ctx_before = 0);
/// Evaluates whether pattern matches any substring of input using lazy
/// Brzozowski derivatives.
-MatchResult match(ExprPool &pool, const ExprId pattern, cpp::string_view input);
+MatchResult match(ExprPool &pool, const ExprId pattern, cpp::string_view input,
+ int cflags = 0, int eflags = 0);
+
+/// Evaluates a compiled RegexInternal against input.
+LIBC_INLINE MatchResult match(RegexInternal &internal, cpp::string_view input,
+ int eflags = 0) {
+ return match(internal.pool, internal.root, input, internal.cflags, eflags);
+}
} // namespace regex
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 0406e598bb5770..862085fcdb8d38 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -94,10 +94,14 @@ ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags,
atom = pool.make_byte_set(bs);
break;
}
- // TODO: Wire ERE metacharacters as each operator is implemented in
- // subsequent patches.
case '^':
+ atom = pool.make_anchor(ANCHOR_BOL);
+ break;
case '$':
+ atom = pool.make_anchor(ANCHOR_EOL);
+ break;
+ // TODO: Wire ERE metacharacters as each operator is implemented in
+ // subsequent patches.
case '*':
case '+':
case '?':
diff --git a/libc/src/regex/CMakeLists.txt b/libc/src/regex/CMakeLists.txt
index f5c9bdfff3ab2c..74859f7d52c2f3 100644
--- a/libc/src/regex/CMakeLists.txt
+++ b/libc/src/regex/CMakeLists.txt
@@ -9,6 +9,7 @@ add_entrypoint_object(
libc.hdr.types.regex_t
libc.src.__support.alloc_checker
libc.src.__support.CPP.new
+ libc.src.__support.CPP.scope
libc.src.__support.CPP.string_view
libc.src.__support.error_or
libc.src.__support.macros.null_check
diff --git a/libc/src/regex/regcomp.cpp b/libc/src/regex/regcomp.cpp
index 505c31065f1792..7bb362a592ea3a 100644
--- a/libc/src/regex/regcomp.cpp
+++ b/libc/src/regex/regcomp.cpp
@@ -15,6 +15,7 @@
#include "hdr/regex_macros.h"
#include "src/__support/CPP/new.h"
+#include "src/__support/CPP/scope.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/alloc-checker.h"
#include "src/__support/common.h"
@@ -41,26 +42,27 @@ LLVM_LIBC_FUNCTION(int, regcomp,
preg->__internal = nullptr;
preg->re_nsub = 0;
- // TODO: Enable BRE (!(cflags & REG_EXTENDED)), REG_ICASE, REG_NEWLINE, and
- // REG_NOSPEC as each feature is implemented in subsequent patches.
- if (!(cflags & REG_EXTENDED) || (cflags & ~(REG_EXTENDED | REG_NOSUB)) != 0)
+ // TODO: Enable BRE (!(cflags & REG_EXTENDED)), REG_ICASE, and REG_NOSPEC as
+ // each feature is implemented in subsequent patches.
+ if (!(cflags & REG_EXTENDED) ||
+ (cflags & ~(REG_EXTENDED | REG_NOSUB | REG_NEWLINE)) != 0)
return REG_BADPAT;
// TODO: Once cpp::unique_ptr and AllocChecker-backed cpp::make_unique are
// available in src/__support/CPP/, manage RegexInternal via unique_ptr so
- // error paths do not require manual delete.
+ // error paths do not require a scope guard.
AllocChecker ac;
regex::RegexInternal *internal = new (ac) regex::RegexInternal();
if (!ac)
return REG_ESPACE;
+ cpp::scope_exit internal_guard([internal] { delete internal; });
ErrorOr<regex::ExprId> root =
regex::parse_ere(cpp::string_view(pattern), cflags, internal->pool);
- if (!root.has_value()) {
- delete internal;
+ if (!root.has_value())
return root.error();
- }
+ internal_guard.release();
internal->root = *root;
internal->cflags = cflags;
preg->__internal = internal;
diff --git a/libc/src/regex/regexec.cpp b/libc/src/regex/regexec.cpp
index 4da121afd0aab4..c9d06837d329ae 100644
--- a/libc/src/regex/regexec.cpp
+++ b/libc/src/regex/regexec.cpp
@@ -31,11 +31,11 @@ LLVM_LIBC_FUNCTION(int, regexec,
LIBC_CRASH_ON_NULLPTR(preg);
LIBC_CRASH_ON_NULLPTR(string);
- // TODO: Submatch extraction (nmatch/pmatch) and execution flags (eflags)
- // will be wired in subsequent patches.
+ // TODO: Submatch extraction (nmatch/pmatch) and REG_STARTEND will be wired
+ // in subsequent patches.
(void)nmatch;
(void)pmatch;
- if (eflags != 0)
+ if ((eflags & ~(REG_NOTBOL | REG_NOTEOL)) != 0)
return REG_BADPAT;
// Guard against a null internal pointer (e.g. called after regfree).
@@ -47,8 +47,7 @@ LLVM_LIBC_FUNCTION(int, regexec,
// const regex_t*. Serialise access to internal->pool during lazy derivative
// evaluation.
cpp::lock_guard guard(internal->lock);
- switch (
- regex::match(internal->pool, internal->root, cpp::string_view(string))) {
+ switch (regex::match(*internal, cpp::string_view(string), eflags)) {
case regex::MatchResult::Match:
return 0;
case regex::MatchResult::NoMatch:
diff --git a/libc/test/src/__support/regex/expr_test.cpp b/libc/test/src/__support/regex/expr_test.cpp
index 40f64767b8a635..7bf990a049c8f1 100644
--- a/libc/test/src/__support/regex/expr_test.cpp
+++ b/libc/test/src/__support/regex/expr_test.cpp
@@ -15,6 +15,8 @@
#include "src/__support/regex/regex_expr_pool.h"
#include "test/UnitTest/Test.h"
+using LIBC_NAMESPACE::regex::ANCHOR_BOL;
+using LIBC_NAMESPACE::regex::ANCHOR_EOL;
using LIBC_NAMESPACE::regex::ByteSet;
using LIBC_NAMESPACE::regex::EMPTY_SET_ID;
using LIBC_NAMESPACE::regex::Expr;
@@ -33,11 +35,26 @@ TEST(LlvmLibcRegexExprTest, Interning) {
ASSERT_TRUE(lit_a_2.has_value());
// Hash-consing: same literal should return the same ExprId.
EXPECT_EQ(*lit_a, *lit_a_2);
+ EXPECT_FALSE(pool.has_anchors(*lit_a));
auto lit_b = pool.make_lit('b');
ASSERT_TRUE(lit_b.has_value());
EXPECT_NE(*lit_a, *lit_b);
+ auto bol_1 = pool.make_anchor(ANCHOR_BOL);
+ auto bol_2 = pool.make_anchor(ANCHOR_BOL);
+ auto eol = pool.make_anchor(ANCHOR_EOL);
+ ASSERT_TRUE(bol_1.has_value());
+ ASSERT_TRUE(bol_2.has_value());
+ ASSERT_TRUE(eol.has_value());
+ EXPECT_EQ(*bol_1, *bol_2);
+ EXPECT_NE(*bol_1, *eol);
+ EXPECT_FALSE(pool.is_nullable(*bol_1));
+ EXPECT_TRUE(pool.has_anchors(*bol_1));
+ EXPECT_TRUE(pool.has_anchors(*pool.make_concat(*bol_1, *lit_a)));
+ EXPECT_TRUE(pool.has_anchors(*pool.make_alt(*lit_a, *eol)));
+ EXPECT_FALSE(pool.has_anchors(*pool.make_concat(*lit_a, *lit_b)));
+
auto empty_set_1 = pool.empty_set();
auto empty_set_2 = pool.empty_set();
ASSERT_TRUE(empty_set_1.has_value());
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 028d9cd8328b97..3e79443ba3efb9 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -139,30 +139,85 @@ TEST(LlvmLibcRegexMatcherTest, WildcardMatchingAndNewlineFlag) {
EXPECT_EQ(match(pool, *nl_pat, "a\nc"), MatchResult::NoMatch);
}
+TEST(LlvmLibcRegexMatcherTest, AnchorMatchingAndFlags) {
+ ExprPool pool;
+ ErrorOr<ExprId> pat = parse_ere("^foo$", REG_EXTENDED, pool);
+ ASSERT_TRUE(pat.has_value());
+ EXPECT_LE(pool.size(), size_t(12));
+ EXPECT_LE(pool.allocated_bytes(), size_t(1024));
+
+ EXPECT_EQ(match(pool, *pat, "foo"), MatchResult::Match);
+ const size_t warm_nodes = pool.size();
+ const size_t warm_bytes = pool.allocated_bytes();
+ EXPECT_EQ(match(pool, *pat, "foo"), MatchResult::Match);
+ EXPECT_EQ(pool.size(), warm_nodes);
+ EXPECT_EQ(pool.allocated_bytes(), warm_bytes);
+
+ // Near-miss negative checks for ^foo$.
+ EXPECT_EQ(match(pool, *pat, "xfoo"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "foox"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "foo\nbar"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, ""), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "foo", 0, REG_NOTBOL), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "foo", 0, REG_NOTEOL), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *pat, "bar\nfoo\nbaz", REG_NEWLINE, 0),
+ MatchResult::Match);
+
+ // Empty-line and multi-anchor patterns.
+ ErrorOr<ExprId> empty_line = parse_ere("^$", REG_EXTENDED, pool);
+ ErrorOr<ExprId> multi_bol = parse_ere("^^ab$$", REG_EXTENDED, pool);
+ ErrorOr<ExprId> mid_bol = parse_ere("a^b", REG_EXTENDED, pool);
+ ErrorOr<ExprId> nl_bol = parse_ere("\n^b", REG_EXTENDED, pool);
+ ASSERT_TRUE(empty_line.has_value());
+ ASSERT_TRUE(multi_bol.has_value());
+ ASSERT_TRUE(mid_bol.has_value());
+ ASSERT_TRUE(nl_bol.has_value());
+
+ EXPECT_EQ(match(pool, *empty_line, ""), MatchResult::Match);
+ EXPECT_EQ(match(pool, *empty_line, "a"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *empty_line, "\n"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *empty_line, "a\n\nb", REG_NEWLINE, 0),
+ MatchResult::Match);
+ EXPECT_EQ(match(pool, *empty_line, "a\nb", REG_NEWLINE, 0),
+ MatchResult::NoMatch);
+
+ EXPECT_EQ(match(pool, *multi_bol, "ab"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *multi_bol, "xab"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *multi_bol, "abx"), MatchResult::NoMatch);
+
+ EXPECT_EQ(match(pool, *mid_bol, "ab"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *mid_bol, "a^b"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *nl_bol, "\nb"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *nl_bol, "\nb", REG_NEWLINE, 0), MatchResult::Match);
+}
+
TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
ExprPool pool;
ErrorOr<ExprId> escaped_dot = parse_ere(R"(a\.b)", REG_EXTENDED, pool);
ErrorOr<ExprId> escaped_star = parse_ere(R"(a\*b)", REG_EXTENDED, pool);
ErrorOr<ExprId> escaped_slash = parse_ere(R"(a\\b)", REG_EXTENDED, pool);
+ ErrorOr<ExprId> escaped_anchors = parse_ere(R"(a\^b\$c)", REG_EXTENDED, pool);
ASSERT_TRUE(escaped_dot.has_value());
ASSERT_TRUE(escaped_star.has_value());
ASSERT_TRUE(escaped_slash.has_value());
+ ASSERT_TRUE(escaped_anchors.has_value());
EXPECT_EQ(match(pool, *escaped_dot, "a.b"), MatchResult::Match);
EXPECT_EQ(match(pool, *escaped_dot, "axb"), MatchResult::NoMatch);
EXPECT_EQ(match(pool, *escaped_star, "a*b"), MatchResult::Match);
EXPECT_EQ(match(pool, *escaped_slash, R"(a\b)"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *escaped_anchors, "a^b$c"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *escaped_anchors, "axb$c"), MatchResult::NoMatch);
ErrorOr<ExprId> trailing_escape = parse_ere(R"(abc\)", REG_EXTENDED, pool);
ASSERT_FALSE(trailing_escape.has_value());
EXPECT_EQ(trailing_escape.error(), REG_EESCAPE);
const string_view bad_patterns[] = {
- "^", "$", "*", "+", "?", "(", ")",
- "[", "{", "|", R"(\1)", R"(\2)", R"(\3)", R"(\4)",
- R"(\5)", R"(\6)", R"(\7)", R"(\8)", R"(\9)", R"(\b)", R"(\B)",
- R"(\<)", R"(\>)", R"(\A)", R"(\Z)", R"(\`)", R"(\')", R"(\w)",
- R"(\W)", R"(\s)", R"(\S)", R"(\d)", R"(\D)"};
+ "*", "+", "?", "(", ")", "[", "{", "|",
+ R"(\1)", R"(\2)", R"(\3)", R"(\4)", R"(\5)", R"(\6)", R"(\7)", R"(\8)",
+ R"(\9)", R"(\b)", R"(\B)", R"(\<)", R"(\>)", R"(\A)", R"(\Z)", R"(\`)",
+ R"(\')", R"(\w)", R"(\W)", R"(\s)", R"(\S)", R"(\d)", R"(\D)"};
for (string_view bad : bad_patterns) {
ErrorOr<ExprId> err = parse_ere(bad, REG_EXTENDED, pool);
ASSERT_FALSE(err.has_value());
diff --git a/libc/test/src/regex/CMakeLists.txt b/libc/test/src/regex/CMakeLists.txt
index 165dcb3b066a9c..73207994179c44 100644
--- a/libc/test/src/regex/CMakeLists.txt
+++ b/libc/test/src/regex/CMakeLists.txt
@@ -21,6 +21,7 @@ if(LLVM_LIBC_FULL_BUILD AND LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS)
DEPENDS
libc.hdr.regex_macros
libc.hdr.types.regex_t
+ libc.src.__support.CPP.scope
libc.src.regex.regcomp
libc.src.regex.regexec
libc.src.regex.regfree
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index 4b0614afcbca64..05444b49e1dd79 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -18,16 +18,19 @@
#include "hdr/regex_macros.h"
#include "hdr/types/regex_t.h"
+#include "src/__support/CPP/scope.h"
+
+using LIBC_NAMESPACE::cpp::scope_exit;
TEST(LlvmLibcRegexTest, BasicLiteralRoundTrip) {
regex_t preg;
ASSERT_EQ(0,
LIBC_NAMESPACE::regcomp(&preg, "hello", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(0,
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(0,
LIBC_NAMESPACE::regexec(&preg, "say hello world", 0, nullptr, 0));
- ASSERT_EQ(REG_NOMATCH,
+ EXPECT_EQ(REG_NOMATCH,
LIBC_NAMESPACE::regexec(&preg, "goodbye", 0, nullptr, 0));
- LIBC_NAMESPACE::regfree(&preg);
}
TEST(LlvmLibcRegexTest, MismatchCases) {
@@ -35,45 +38,45 @@ TEST(LlvmLibcRegexTest, MismatchCases) {
// Partial match
ASSERT_EQ(0,
LIBC_NAMESPACE::regcomp(&preg, "hello", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(REG_NOMATCH, LIBC_NAMESPACE::regexec(&preg, "hell", 0, nullptr, 0));
+ EXPECT_EQ(REG_NOMATCH, LIBC_NAMESPACE::regexec(&preg, "hell", 0, nullptr, 0));
LIBC_NAMESPACE::regfree(&preg);
// Case sensitivity
ASSERT_EQ(0,
LIBC_NAMESPACE::regcomp(&preg, "Hello", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(REG_NOMATCH,
+ EXPECT_EQ(REG_NOMATCH,
LIBC_NAMESPACE::regexec(&preg, "hello", 0, nullptr, 0));
LIBC_NAMESPACE::regfree(&preg);
// Empty string vs non-empty pattern
ASSERT_EQ(0, LIBC_NAMESPACE::regcomp(&preg, "a", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(REG_NOMATCH, LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0));
+ EXPECT_EQ(REG_NOMATCH, LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0));
LIBC_NAMESPACE::regfree(&preg);
}
TEST(LlvmLibcRegexTest, EmptyString) {
regex_t preg;
ASSERT_EQ(0, LIBC_NAMESPACE::regcomp(&preg, "", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "anything", 0, nullptr, 0));
- ASSERT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0));
- LIBC_NAMESPACE::regfree(&preg);
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "anything", 0, nullptr, 0));
+ EXPECT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0));
}
TEST(LlvmLibcRegexTest, ExactMatch) {
regex_t preg;
ASSERT_EQ(0,
LIBC_NAMESPACE::regcomp(&preg, "test", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "test", 0, nullptr, 0));
- LIBC_NAMESPACE::regfree(&preg);
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(0, LIBC_NAMESPACE::regexec(&preg, "test", 0, nullptr, 0));
}
TEST(LlvmLibcRegexTest, NullByteStopsParsing) {
regex_t preg;
ASSERT_EQ(0,
LIBC_NAMESPACE::regcomp(&preg, "match", REG_EXTENDED | REG_NOSUB));
- ASSERT_EQ(REG_NOMATCH,
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(REG_NOMATCH,
LIBC_NAMESPACE::regexec(&preg, "doesn't \0 match", 0, nullptr, 0));
- LIBC_NAMESPACE::regfree(&preg);
}
TEST(LlvmLibcRegexTest, OverlappingPrefixAndPostFreeSafety) {
@@ -129,6 +132,39 @@ TEST(LlvmLibcRegexTest, WildcardDot) {
LIBC_NAMESPACE::regfree(&preg);
}
+TEST(LlvmLibcRegexTest, AnchorsAndFlags) {
+ regex_t preg;
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "^foo",
+ REG_EXTENDED | REG_NOSUB | REG_NEWLINE),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "foobar", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xfoo", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "foo", 0, nullptr, REG_NOTBOL),
+ REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "bar\nfoo", 0, nullptr, REG_NOTBOL),
+ 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "foo$",
+ REG_EXTENDED | REG_NOSUB | REG_NEWLINE),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "barfoo", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "foox", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "foo", 0, nullptr, REG_NOTEOL),
+ REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "foo\nbar", 0, nullptr, REG_NOTEOL),
+ 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "^$",
+ REG_EXTENDED | REG_NOSUB | REG_NEWLINE),
+ 0);
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a\n\nb", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a\nb", 0, nullptr, 0), REG_NOMATCH);
+}
+
TEST(LlvmLibcRegexTest, EscapedMetacharacters) {
regex_t preg;
ASSERT_EQ(
@@ -153,14 +189,14 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a", REG_EXTENDED | REG_ICASE),
REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
- EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a", REG_EXTENDED | REG_NEWLINE),
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a", ~(REG_NOSUB | REG_NEWLINE)),
REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
// Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
- const char *const bad_patterns[] = {
- "^a", "a$", "a*", "a+", "a?", "(a)", "[a]",
- "a{1}", "a|b", R"(\1)", R"(\b)", R"(\w)", R"(\s)", R"(\d)"};
+ const char *const bad_patterns[] = {"a*", "a+", "a?", "(a)",
+ "[a]", "a{1}", "a|b", R"(\1)",
+ R"(\b)", R"(\w)", R"(\s)", R"(\d)"};
for (const char *pat : bad_patterns) {
EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADPAT);
LIBC_NAMESPACE::regfree(&preg);
@@ -168,9 +204,10 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
// Unsupported eflags in regexec return REG_BADPAT.
ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a", REG_EXTENDED), 0);
- EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a", 0, nullptr, REG_NOTBOL),
+ scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a", 0, nullptr,
+ ~(REG_NOTBOL | REG_NOTEOL)),
REG_BADPAT);
- LIBC_NAMESPACE::regfree(&preg);
}
#if defined(LIBC_ADD_NULL_CHECKS)
More information about the libc-commits
mailing list