[libc-commits] [libc] [libc][regex] Add POSIX character classes and shorthands (PR #234991)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Mon Oct 26 11:55:37 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/234991

>From dfaaa65174f86e8f48035184be9a8a2dcb3334d2 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 23 Oct 2026 16:16:42 +0100
Subject: [PATCH 1/2] [libc][regex] Add POSIX character classes and shorthands

POSIX ERE character class expressions like [:alpha:] and [:digit:] allow
portable character classifications inside bracket expressions, while
common escape shorthands (\d, \D, \s, \S, \w, \W) provide concise
alternatives for character classes.  In POSIX regular expressions,
character classes inside brackets must correctly reject unknown class
names with REG_ECTYPE, reject range endpoints involving classes with
REG_ERANGE, and integrate with negated bracket sets and REG_NEWLINE.

Add regex_charclass.h containing lookup_posix_class to map all 12 POSIX
classes (alnum, alpha, blank, cntrl, digit, graph, lower, print, punct,
space, upper, xdigit) to constexpr 256-bit bitsets in the POSIX/C
locale.  Support operator|= in bitset.h for bitset composition.  In
parse_bracket_expr, recognize [:name:] sequences, enforce REG_ECTYPE on
invalid names, reject class endpoints with REG_ERANGE, and reserve word
boundary anchors [[:<:]] and [[:>:]] with REG_BADPAT until word anchor
support lands.  Support shorthands \d, \D, \s, \S, \w, and \W in
parse_quantified_atom, clearing newlines on negated shorthands when
REG_NEWLINE is set.

External compliance suites: Henry Spencer suite passes 126/456 (+10)
boolean/error cases and 55/456 (+9) full-position cases; AT&T testregex
passes 74/400 (+3) ERE boolean/error cases and 76/473 (+5) overall
boolean/error cases with 0 regressions.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/__support/CPP/bitset.h               |  7 ++
 libc/src/__support/regex/CMakeLists.txt       |  4 +
 libc/src/__support/regex/regex_charclass.h    | 88 +++++++++++++++++++
 libc/src/__support/regex/regex_parser.cpp     | 62 +++++++++++--
 libc/test/src/__support/CPP/bitset_test.cpp   | 15 ++++
 .../test/src/__support/regex/matcher_test.cpp | 52 +++++++++--
 libc/test/src/regex/regex_basic_test.cpp      | 75 ++++++++++++++--
 7 files changed, 280 insertions(+), 23 deletions(-)
 create mode 100644 libc/src/__support/regex/regex_charclass.h

diff --git a/libc/src/__support/CPP/bitset.h b/libc/src/__support/CPP/bitset.h
index e264a363b9dad1..ab492ab1f549d5 100644
--- a/libc/src/__support/CPP/bitset.h
+++ b/libc/src/__support/CPP/bitset.h
@@ -80,6 +80,13 @@ template <size_t NumberOfBits> struct bitset {
     return true;
   }
 
+  LIBC_INLINE constexpr bitset<NumberOfBits> &
+  operator|=(const bitset<NumberOfBits> &other) {
+    for (size_t i = 0; i < NUMBER_OF_UNITS; ++i)
+      Data[i] |= other.Data[i];
+    return *this;
+  }
+
 private:
   static constexpr size_t BITS_PER_BYTE = 8;
   static constexpr size_t BITS_PER_UNIT = BITS_PER_BYTE * sizeof(size_t);
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index 870ec56993eca0..1191e79b1773be 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -54,16 +54,20 @@ add_object_library(
   SRCS
     regex_parser.cpp
   HDRS
+    regex_charclass.h
     regex_parser.h
   DEPENDS
     .regex_expr_pool
     libc.hdr.regex_macros
     libc.src.__support.alloc_checker
     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.string_view
+    libc.src.__support.ctype_utils
     libc.src.__support.error_or
     libc.src.__support.macros.config
 )
diff --git a/libc/src/__support/regex/regex_charclass.h b/libc/src/__support/regex/regex_charclass.h
new file mode 100644
index 00000000000000..cb01bc33fe2736
--- /dev/null
+++ b/libc/src/__support/regex/regex_charclass.h
@@ -0,0 +1,88 @@
+//===-- POSIX character class tables for regex ------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_CHARCLASS_H
+#define LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_CHARCLASS_H
+
+#include "src/__support/CPP/array.h"
+#include "src/__support/CPP/bitset.h"
+#include "src/__support/CPP/optional.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/ctype_utils.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/regex/regex_ast.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace regex {
+
+struct PosixClassEntry {
+  cpp::string_view name;
+  ByteSet set;
+};
+
+template <typename Pred>
+LIBC_INLINE constexpr ByteSet make_class_set(Pred pred) {
+  ByteSet bs;
+  for (size_t c = 0; c < 256; ++c)
+    if (pred(static_cast<char>(c)))
+      bs.set(c);
+  return bs;
+}
+
+LIBC_INLINE constexpr ByteSet digit_class_set() {
+  return make_class_set([](char c) { return internal::isdigit(c); });
+}
+
+LIBC_INLINE constexpr ByteSet space_class_set() {
+  return make_class_set([](char c) { return internal::isspace(c); });
+}
+
+LIBC_INLINE constexpr ByteSet word_class_set() {
+  return make_class_set(
+      [](char c) { return internal::isalnum(c) || c == '_'; });
+}
+
+LIBC_INLINE constexpr cpp::array<PosixClassEntry, 12> POSIX_CLASSES = {{
+    {"alnum", make_class_set([](char c) { return internal::isalnum(c); })},
+    {"alpha", make_class_set([](char c) { return internal::isalpha(c); })},
+    {"blank", make_class_set([](char c) { return c == ' ' || c == '\t'; })},
+    {"cntrl", make_class_set([](char c) {
+       const unsigned ch = static_cast<unsigned char>(c);
+       return ch < 0x20 || ch == 0x7F;
+     })},
+    {"digit", digit_class_set()},
+    {"graph", make_class_set([](char c) { return internal::isgraph(c); })},
+    {"lower", make_class_set([](char c) { return internal::islower(c); })},
+    {"print", make_class_set([](char c) {
+       const unsigned ch = static_cast<unsigned char>(c);
+       return (ch - ' ') < 95;
+     })},
+    {"punct", make_class_set([](char c) {
+       return !internal::isalnum(c) && internal::isgraph(c);
+     })},
+    {"space", space_class_set()},
+    {"upper", make_class_set([](char c) { return internal::isupper(c); })},
+    {"xdigit", make_class_set([](char c) {
+       return internal::isdigit(c) || (c >= 'a' && c <= 'f') ||
+              (c >= 'A' && c <= 'F');
+     })},
+}};
+
+LIBC_INLINE constexpr cpp::optional<ByteSet>
+lookup_posix_class(cpp::string_view name) {
+  for (const auto &entry : POSIX_CLASSES)
+    if (entry.name == name)
+      return entry.set;
+  return cpp::nullopt;
+}
+
+} // namespace regex
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_CHARCLASS_H
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 7637183fb55aaa..14adb0f0e7bb8f 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -23,6 +23,7 @@
 #include "src/__support/error_or.h"
 #include "src/__support/macros/config.h"
 #include "src/__support/regex/regex_ast.h"
+#include "src/__support/regex/regex_charclass.h"
 #include "src/__support/regex/regex_expr_pool.h"
 
 namespace LIBC_NAMESPACE_DECL {
@@ -52,12 +53,6 @@ constexpr bool is_unimplemented_ere_escape(char c) {
   case 'Z':
   case '`':
   case '\'':
-  case 'w':
-  case 'W':
-  case 's':
-  case 'S':
-  case 'd':
-  case 'D':
     return true;
   default:
     return false;
@@ -120,6 +115,29 @@ ErrorOr<ExprId> parse_bracket_expr(cpp::string_view &rem, int cflags,
     }
     first = false;
 
+    if (rem.starts_with("[:")) {
+      rem.remove_prefix(2);
+      // TODO: Adopt cpp::string_view::find once implemented in
+      // src/__support/CPP/string_view.h.
+      size_t delim_pos = 0;
+      while (delim_pos + 2 <= rem.size() &&
+             !rem.substr(delim_pos).starts_with(":]"))
+        ++delim_pos;
+      if (delim_pos + 2 > rem.size())
+        return Error(REG_EBRACK);
+      cpp::string_view name = rem.substr(0, delim_pos);
+      rem.remove_prefix(delim_pos + 2);
+      if (name == "<" || name == ">")
+        return Error(REG_BADPAT);
+      cpp::optional<ByteSet> opt = lookup_posix_class(name);
+      if (!opt.has_value())
+        return Error(REG_ECTYPE);
+      bs |= *opt;
+      if (rem.starts_with('-') && !rem.starts_with("-]"))
+        return Error(REG_ERANGE);
+      continue;
+    }
+
     ErrorOr<uint8_t> c1 = parse_bracket_elem(rem);
     if (!c1.has_value())
       return Error(c1.error());
@@ -159,9 +177,35 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
       return Error(REG_EESCAPE);
     c = rem.front();
     rem.remove_prefix(1);
-    if (is_unimplemented_ere_escape(c))
-      return Error(REG_BADPAT);
-    atom = pool.make_lit(c);
+    switch (c) {
+    case 'd':
+    case 's':
+    case 'w': {
+      const ByteSet bs = (c == 'd')   ? digit_class_set()
+                         : (c == 's') ? space_class_set()
+                                      : word_class_set();
+      atom = pool.make_byte_set(bs);
+      break;
+    }
+    case 'D':
+    case 'S':
+    case 'W': {
+      ByteSet bs = (c == 'D')   ? digit_class_set()
+                   : (c == 'S') ? space_class_set()
+                                : word_class_set();
+      bs.flip();
+      bs.reset(0);
+      if (cflags & REG_NEWLINE)
+        bs.reset('\n');
+      atom = pool.make_byte_set(bs);
+      break;
+    }
+    default:
+      if (is_unimplemented_ere_escape(c))
+        return Error(REG_BADPAT);
+      atom = pool.make_lit(c);
+      break;
+    }
     break;
   }
   case '.': {
diff --git a/libc/test/src/__support/CPP/bitset_test.cpp b/libc/test/src/__support/CPP/bitset_test.cpp
index 3632e54907ceb4..777d6b8ab7be7a 100644
--- a/libc/test/src/__support/CPP/bitset_test.cpp
+++ b/libc/test/src/__support/CPP/bitset_test.cpp
@@ -221,3 +221,18 @@ TEST(LlvmLibcBitsetTest, SetRangeTest) {
     EXPECT_TRUE(bitset.test(j));
   bitset.reset();
 }
+
+TEST(LlvmLibcBitsetTest, BitwiseOrAssign) {
+  LIBC_NAMESPACE::cpp::bitset<64> b1;
+  LIBC_NAMESPACE::cpp::bitset<64> b2;
+  b1.set(1);
+  b1.set(5);
+  b2.set(5);
+  b2.set(10);
+  b1 |= b2;
+  EXPECT_TRUE(b1.test(1));
+  EXPECT_TRUE(b1.test(5));
+  EXPECT_TRUE(b1.test(10));
+  EXPECT_FALSE(b1.test(0));
+  EXPECT_FALSE(b1.test(2));
+}
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 891789434e2ab2..9c9a5e7e3eca36 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -261,8 +261,8 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
     EXPECT_EQ(err.error(), REG_BADRPT);
   }
 
-  const string_view ebrack_patterns[] = {"[",  "[^",    "[abc",
-                                         "[]", "a[[.x", "a[[.x."};
+  const string_view ebrack_patterns[] = {
+      "[", "[^", "[abc", "[]", "a[[.x", "a[[.x.", "a[[:digit:", "a[[:digit"};
   for (string_view pat : ebrack_patterns) {
     ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
     ASSERT_FALSE(err.has_value());
@@ -270,7 +270,7 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
   }
 
   const string_view erange_patterns[] = {"a[3-1]c", "a[1-3-5]c", "a[1-",
-                                         "a[a-[:digit:]]c"};
+                                         "a[a-[:digit:]]c", "a[[:alpha:]-z]c"};
   for (string_view pat : erange_patterns) {
     ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
     ASSERT_FALSE(err.has_value());
@@ -285,12 +285,19 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
     EXPECT_EQ(err.error(), REG_ECOLLATE);
   }
 
+  const string_view ectype_patterns[] = {"a[[:notdef:]]b", "a[[:alph:]]b",
+                                         "a[[:]:]]b", "a[[:-:]]b"};
+  for (string_view pat : ectype_patterns) {
+    ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+    ASSERT_FALSE(err.has_value());
+    EXPECT_EQ(err.error(), REG_ECTYPE);
+  }
+
   const string_view bad_patterns[] = {
-      "a+",    "a?",    "a{1}",  "{",     "(",     ")",     "[[:alpha:]]",
-      "|",     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)"};
+      "a+",      "a?",    "a{1}",  "{",     "(",     ")",     "[[:<:]]",
+      "[[:>:]]", "|",     R"(\1)", R"(\2)", R"(\3)", R"(\4)", R"(\5)",
+      R"(\6)",   R"(\7)", R"(\8)", R"(\9)", R"(\b)", R"(\B)", R"(\<)",
+      R"(\>)",   R"(\A)", R"(\Z)", R"(\`)", R"(\')"};
   for (string_view bad : bad_patterns) {
     ErrorOr<ExprId> err = parse_ere(bad, REG_EXTENDED, pool);
     ASSERT_FALSE(err.has_value());
@@ -373,6 +380,35 @@ TEST(LlvmLibcRegexMatcherTest, BracketMatchingAndRanges) {
   EXPECT_EQ(match(pool, *star_range, ""), MatchResult::Match);
   EXPECT_EQ(match(pool, *star_range, "abcba"), MatchResult::Match);
   EXPECT_EQ(match(pool, *star_range, "abzcba"), MatchResult::NoMatch);
+
+  ErrorOr<ExprId> alpha = parse_ere("^[[:alpha:]]*$", REG_EXTENDED, pool);
+  ASSERT_TRUE(alpha.has_value());
+  EXPECT_EQ(match(pool, *alpha, ""), MatchResult::Match);
+  EXPECT_EQ(match(pool, *alpha, "HelloWorld"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *alpha, "Hello1World"), MatchResult::NoMatch);
+
+  ErrorOr<ExprId> sh_w = parse_ere(R"(^\w*$)", REG_EXTENDED, pool);
+  ASSERT_TRUE(sh_w.has_value());
+  EXPECT_EQ(match(pool, *sh_w, ""), MatchResult::Match);
+  EXPECT_EQ(match(pool, *sh_w, "Hello_123"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *sh_w, "Hello-123"), MatchResult::NoMatch);
+
+  ErrorOr<ExprId> sh_d = parse_ere(R"(^\d*$)", REG_EXTENDED, pool);
+  ASSERT_TRUE(sh_d.has_value());
+  EXPECT_EQ(match(pool, *sh_d, ""), MatchResult::Match);
+  EXPECT_EQ(match(pool, *sh_d, "12345"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *sh_d, "123a45"), MatchResult::NoMatch);
+
+  ErrorOr<ExprId> sh_neg = parse_ere(R"(x\Wy)", REG_EXTENDED, pool);
+  ASSERT_TRUE(sh_neg.has_value());
+  EXPECT_EQ(match(pool, *sh_neg, "x y"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *sh_neg, "x_y"), MatchResult::NoMatch);
+
+  ErrorOr<ExprId> sh_neg_nl =
+      parse_ere(R"(x\Wy)", REG_EXTENDED | REG_NEWLINE, pool);
+  ASSERT_TRUE(sh_neg_nl.has_value());
+  EXPECT_EQ(match(pool, *sh_neg_nl, "x\ny", REG_NEWLINE, 0),
+            MatchResult::NoMatch);
 }
 
 TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index dc575549f3563d..557b51a39f827e 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -292,15 +292,15 @@ TEST(LlvmLibcRegexTest, BracketExpressionsAndRanges) {
   LIBC_NAMESPACE::regfree(&preg);
 
   // Bracket error conditions.
-  const char *const ebrack_patterns[] = {"[",  "[^",    "[abc",
-                                         "[]", "a[[.x", "a[[.x."};
+  const char *const ebrack_patterns[] = {
+      "[", "[^", "[abc", "[]", "a[[.x", "a[[.x.", "a[[:digit:", "a[[:digit"};
   for (const char *pat : ebrack_patterns) {
     EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_EBRACK);
     LIBC_NAMESPACE::regfree(&preg);
   }
 
   const char *const erange_patterns[] = {"a[3-1]c", "a[1-3-5]c", "a[1-",
-                                         "a[a-[:digit:]]c"};
+                                         "a[a-[:digit:]]c", "a[[:alpha:]-z]c"};
   for (const char *pat : erange_patterns) {
     EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_ERANGE);
     LIBC_NAMESPACE::regfree(&preg);
@@ -312,6 +312,13 @@ TEST(LlvmLibcRegexTest, BracketExpressionsAndRanges) {
     EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_ECOLLATE);
     LIBC_NAMESPACE::regfree(&preg);
   }
+
+  const char *const ectype_patterns[] = {"a[[:notdef:]]b", "a[[:alph:]]b",
+                                         "a[[:]:]]b", "a[[:-:]]b"};
+  for (const char *pat : ectype_patterns) {
+    EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_ECTYPE);
+    LIBC_NAMESPACE::regfree(&preg);
+  }
 }
 
 TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
@@ -327,9 +334,8 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
   LIBC_NAMESPACE::regfree(&preg);
 
   // Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
-  const char *const bad_patterns[] = {"a+",    "a?",    "(a)",   "[[:alpha:]]",
-                                      "a{1}",  "a|b",   R"(\1)", R"(\b)",
-                                      R"(\w)", R"(\s)", R"(\d)"};
+  const char *const bad_patterns[] = {
+      "a+", "a?", "(a)", "a{1}", "a|b", 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);
@@ -343,6 +349,63 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
             REG_BADPAT);
 }
 
+TEST(LlvmLibcRegexTest, PosixCharacterClassesAndShorthands) {
+  auto check = [](const char *pat, const char *str, int expected = 0) {
+    regex_t preg;
+    ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED | REG_NOSUB), 0);
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, str, 0, nullptr, 0), expected);
+    LIBC_NAMESPACE::regfree(&preg);
+  };
+
+  // Near-miss ASCII neighbours for [:digit:] and \d.
+  check("^x[[:digit:]]y$", "x0y");
+  check("^x[[:digit:]]y$", "x9y");
+  check("^x[[:digit:]]y$", "x/y", REG_NOMATCH);
+  check("^x[[:digit:]]y$", "x:y", REG_NOMATCH);
+  check("^x[[:digit:]]y$", "x12y", REG_NOMATCH);
+
+  check(R"(^x\dy$)", "x5y");
+  check(R"(^x\dy$)", "x/y", REG_NOMATCH);
+  check(R"(^x\dy$)", "x:y", REG_NOMATCH);
+
+  // Near-miss ASCII neighbours for [:xdigit:].
+  check("^x[[:xdigit:]]y$", "xfy");
+  check("^x[[:xdigit:]]y$", "xFy");
+  check("^x[[:xdigit:]]y$", "xgy", REG_NOMATCH);
+  check("^x[[:xdigit:]]y$", "xGy", REG_NOMATCH);
+
+  // [:blank:] matches only space and tab, not newline.
+  check("^x[[:blank:]]y$", "x y");
+  check("^x[[:blank:]]y$", "x\ty");
+  check("^x[[:blank:]]y$", "x\ny", REG_NOMATCH);
+
+  // \w and \W: underscore matches \w; hyphen does not.
+  check(R"(^x\wy$)", "x_y");
+  check(R"(^x\wy$)", "x-y", REG_NOMATCH);
+  check(R"(^x\Wy$)", "x-y");
+  check(R"(^x\Wy$)", "x_y", REG_NOMATCH);
+
+  // Hyphen endpoint combinations with character classes.
+  check("^[--0[:alpha:]]$", "-");
+  check("^[--0[:alpha:]]$", ".");
+  check("^[--0[:alpha:]]$", "a");
+  check("^[--0[:alpha:]]$", "1", REG_NOMATCH);
+
+  check("^[[:alpha:]!--]$", "z");
+  check("^[[:alpha:]!--]$", "!");
+  check("^[[:alpha:]!--]$", "-");
+  check("^[[:alpha:]!--]$", ".", REG_NOMATCH);
+
+  // Anchored repetitions with negative rejection checks.
+  check(R"(^\w*$)", "");
+  check(R"(^\w*$)", "valid_word1");
+  check(R"(^\w*$)", "ab!ba", REG_NOMATCH);
+
+  check(R"(^\d*$)", "");
+  check(R"(^\d*$)", "12345");
+  check(R"(^\d*$)", "12a45", REG_NOMATCH);
+}
+
 #if defined(LIBC_ADD_NULL_CHECKS)
 TEST(LlvmLibcRegexTest, NullPointerCrash) {
   regex_t preg{};

>From 671c695ce68083cfa6e64fda65eb904f6be6f598 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 26 Oct 2026 18:29:50 +0000
Subject: [PATCH 2/2] [libc][regex] Address review on character classes
 (#234991)

Update regex_charclass.h to use the modern three-section file header
with Doxygen documentation.  Remove the unreachable leading bracket
class guard from parse_bracket_elem, as adjacent and endpoint class
checks are handled by dominating branch conditions in the bracket
parser.  Add the missing macros.attributes dependency to the
regex_parser CMake target.

Add test coverage for adjacent back-to-back POSIX character classes,
multi-class bracket expressions with literals, and multi-word bitset
composition in operator|= across word boundaries.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/__support/regex/CMakeLists.txt       |  1 +
 libc/src/__support/regex/regex_charclass.h    |  7 ++++++-
 libc/src/__support/regex/regex_parser.cpp     |  2 --
 libc/test/src/__support/CPP/bitset_test.cpp   | 17 +++++++++++++++
 .../test/src/__support/regex/matcher_test.cpp |  6 ++++++
 libc/test/src/regex/regex_basic_test.cpp      | 21 +++++++++++++++++++
 6 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index 1191e79b1773be..a9113005c08fe7 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -69,5 +69,6 @@ add_object_library(
     libc.src.__support.CPP.string_view
     libc.src.__support.ctype_utils
     libc.src.__support.error_or
+    libc.src.__support.macros.attributes
     libc.src.__support.macros.config
 )
diff --git a/libc/src/__support/regex/regex_charclass.h b/libc/src/__support/regex/regex_charclass.h
index cb01bc33fe2736..3563df1d3a4c2e 100644
--- a/libc/src/__support/regex/regex_charclass.h
+++ b/libc/src/__support/regex/regex_charclass.h
@@ -1,10 +1,15 @@
-//===-- POSIX character class tables for regex ------------------*- C++ -*-===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Defines POSIX character class bitset tables and lookup functions for regex.
+///
+//===----------------------------------------------------------------------===//
 
 #ifndef LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_CHARCLASS_H
 #define LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_CHARCLASS_H
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 14adb0f0e7bb8f..c178f5621432db 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -82,8 +82,6 @@ ErrorOr<uint8_t> parse_coll_elem(cpp::string_view &rem) {
 
 // Parses a single bracket expression item (collating element or single byte).
 ErrorOr<uint8_t> parse_bracket_elem(cpp::string_view &rem) {
-  if (rem.starts_with("[:"))
-    return Error(REG_BADPAT);
   if (rem.starts_with("[.") || rem.starts_with("[="))
     return parse_coll_elem(rem);
   uint8_t c = static_cast<uint8_t>(rem.front());
diff --git a/libc/test/src/__support/CPP/bitset_test.cpp b/libc/test/src/__support/CPP/bitset_test.cpp
index 777d6b8ab7be7a..9062d7812cd99c 100644
--- a/libc/test/src/__support/CPP/bitset_test.cpp
+++ b/libc/test/src/__support/CPP/bitset_test.cpp
@@ -236,3 +236,20 @@ TEST(LlvmLibcBitsetTest, BitwiseOrAssign) {
   EXPECT_FALSE(b1.test(0));
   EXPECT_FALSE(b1.test(2));
 }
+
+TEST(LlvmLibcBitsetTest, BitwiseOrAssignMultiWord) {
+  LIBC_NAMESPACE::cpp::bitset<256> b1;
+  LIBC_NAMESPACE::cpp::bitset<256> b2;
+  b1.set(5);
+  b1.set(70);
+  b2.set(70);
+  b2.set(200);
+  b1 |= b2;
+  EXPECT_TRUE(b1.test(5));
+  EXPECT_TRUE(b1.test(70));
+  EXPECT_TRUE(b1.test(200));
+  EXPECT_FALSE(b1.test(0));
+  EXPECT_FALSE(b1.test(69));
+  EXPECT_FALSE(b1.test(71));
+  EXPECT_FALSE(b1.test(255));
+}
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 9c9a5e7e3eca36..8c363ccf7cb596 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -409,6 +409,12 @@ TEST(LlvmLibcRegexMatcherTest, BracketMatchingAndRanges) {
   ASSERT_TRUE(sh_neg_nl.has_value());
   EXPECT_EQ(match(pool, *sh_neg_nl, "x\ny", REG_NEWLINE, 0),
             MatchResult::NoMatch);
+
+  ErrorOr<ExprId> multi_class =
+      parse_ere("^[[:alpha:][:blank:]]*$", REG_EXTENDED, pool);
+  ASSERT_TRUE(multi_class.has_value());
+  EXPECT_EQ(match(pool, *multi_class, "Hello World"), MatchResult::Match);
+  EXPECT_EQ(match(pool, *multi_class, "Hello1World"), MatchResult::NoMatch);
 }
 
 TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index 557b51a39f827e..76eacc993421b2 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -404,6 +404,27 @@ TEST(LlvmLibcRegexTest, PosixCharacterClassesAndShorthands) {
   check(R"(^\d*$)", "");
   check(R"(^\d*$)", "12345");
   check(R"(^\d*$)", "12a45", REG_NOMATCH);
+
+  // Back-to-back adjacent character classes in a single bracket expression.
+  check("^[[:alpha:][:blank:]]$", "a");
+  check("^[[:alpha:][:blank:]]$", " ");
+  check("^[[:alpha:][:blank:]]$", "\t");
+  check("^[[:alpha:][:blank:]]$", "1", REG_NOMATCH);
+  check("^[[:alpha:][:blank:]]$", "_", REG_NOMATCH);
+
+  check("^[[:alpha:][:blank:]]*$", "Hello World");
+  check("^[[:alpha:][:blank:]]*$", "Hello\tWorld");
+  check("^[[:alpha:][:blank:]]*$", "Hello1World", REG_NOMATCH);
+  check("^[[:alpha:][:blank:]]*$", "Hello_World", REG_NOMATCH);
+
+  // Multiple classes combined with literals.
+  check("^[[:digit:][:lower:]_]*$", "3abc_7");
+  check("^[[:digit:][:lower:]_]*$", "3ABC_7", REG_NOMATCH);
+
+  // Negated multi-class bracket expressions.
+  check("^[^[:digit:][:space:]]*$", "abcABC_!");
+  check("^[^[:digit:][:space:]]*$", "abc 123", REG_NOMATCH);
+  check("^[^[:digit:][:space:]]*$", "abc1", REG_NOMATCH);
 }
 
 #if defined(LIBC_ADD_NULL_CHECKS)



More information about the libc-commits mailing list