[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