[libc-commits] [libc] adb8031 - [libc][regex] Add '+' and '?' ERE quantifiers (#236364)

via libc-commits libc-commits at lists.llvm.org
Wed Oct 28 06:52:06 PDT 2026


Author: Jeff Bailey
Date: 2026-10-28T13:52:00Z
New Revision: adb80317aa28758c25207de48962af704e2e0cac

URL: https://github.com/llvm/llvm-project/commit/adb80317aa28758c25207de48962af704e2e0cac
DIFF: https://github.com/llvm/llvm-project/commit/adb80317aa28758c25207de48962af704e2e0cac.diff

LOG: [libc][regex] Add '+' and '?' ERE quantifiers (#236364)

In POSIX Extended Regular Expressions (ERE), the '+' quantifier matches
one or more occurrences of the preceding atom and '?' matches zero or
one occurrence. Quantifiers with no preceding repeatable atom, following
an anchor, or chained directly after another quantifier without grouping
must be rejected with REG_BADRPT.

Lower 'r+' to Concat(r, Star(r)) and 'r?' to Alt(EmptyStr, r) in
parse_quantified_atom. Because both references to 'r' in Concat(r,
Star(r)) share the same interned ExprId handle, '+' adds at most two
ExprPool nodes and '?' adds one node regardless of subexpression
complexity, without introducing new AST node kinds.

External compliance suites: Henry Spencer suite passes 218/456 (+50)
boolean/error cases and 73/456 (+8) full-position cases; AT&T testregex
passes 333/400 (+81) ERE boolean/error cases and 335/473 (+81) overall
boolean/error cases with 0 regressions.

Assisted-by: Automated tooling, human reviewed.

Added: 
    

Modified: 
    libc/src/__support/regex/regex_parser.cpp
    libc/test/src/__support/regex/matcher_test.cpp
    libc/test/src/regex/regex_basic_test.cpp

Removed: 
    


################################################################################
diff  --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index bbbb249c2ab211..f1864afd41171a 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -171,7 +171,8 @@ ErrorOr<ExprId> parse_alt(cpp::string_view &rem, int cflags, ExprPool &pool,
                           size_t depth, size_t &re_nsub,
                           ParsedAtomStack &stack);
 
-// Parses a single ERE atom and an optional trailing '*' quantifier.
+// Parses a single ERE atom and an optional trailing '*', '+', or '?'
+// quantifier.
 ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
                                       ExprPool &pool, size_t depth,
                                       size_t &re_nsub, ParsedAtomStack &stack) {
@@ -235,9 +236,9 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
     atom = pool.make_anchor(ANCHOR_EOL);
     can_repeat = false;
     break;
-  // Valid postfix '*' is consumed after a repeatable atom below; reaching a
-  // quantifier at the start of an atom means it has no preceding repeatable
-  // atom (e.g. "*a", "^*", or doubled "a**").
+  // Valid postfix '*', '+', or '?' is consumed after a repeatable atom below;
+  // reaching a quantifier at the start of an atom means it has no preceding
+  // repeatable atom (e.g. "*a", "^*", or doubled "a**").
   case '*':
   case '+':
   case '?':
@@ -279,7 +280,18 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
     rem.remove_prefix(1);
     return pool.make_star(*atom);
   }
-  if (rem.starts_with('+') || rem.starts_with('?') || rem.starts_with('{'))
+  if (rem.starts_with('+')) {
+    rem.remove_prefix(1);
+    ErrorOr<ExprId> star = pool.make_star(*atom);
+    if (!star.has_value())
+      return star;
+    return pool.make_concat(*atom, *star);
+  }
+  if (rem.starts_with('?')) {
+    rem.remove_prefix(1);
+    return pool.make_alt(EMPTY_STR_ID, *atom);
+  }
+  if (rem.starts_with('{'))
     return Error(REG_BADPAT);
   return atom;
 }

diff  --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index d535d57b4966c6..d0d3c4c39c4739 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -295,7 +295,7 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
 
   const string_view bad_patterns[] = {
       // Unimplemented quantifiers.
-      "a+", "a?", "a{1}", "{",
+      "a{1}", "{",
       // Unimplemented word boundaries.
       "[[:<:]]", "[[:>:]]",
       // Unimplemented backreferences.
@@ -518,3 +518,27 @@ TEST(LlvmLibcRegexMatcherTest, AlternationAndGroupingStabilization) {
   ASSERT_TRUE(multi_alt.has_value());
   EXPECT_EQ(alt_pool.size(), size_t(9));
 }
+
+TEST(LlvmLibcRegexMatcherTest, PlusAndQuestionDesugaringAndStabilization) {
+  ExprPool pool;
+  // r+ desugars to Concat(r, Star(r)) and r? desugars to Alt(EmptyStr, r),
+  // interning to identical ExprId handles.
+  ErrorOr<ExprId> plus_a = parse_ere("a+", REG_EXTENDED, pool);
+  ErrorOr<ExprId> concat_star_a = parse_ere("aa*", REG_EXTENDED, pool);
+  ASSERT_TRUE(plus_a.has_value());
+  ASSERT_TRUE(concat_star_a.has_value());
+  EXPECT_EQ(*plus_a, *concat_star_a);
+
+  ErrorOr<ExprId> qmark_a = parse_ere("a?", REG_EXTENDED, pool);
+  ErrorOr<ExprId> alt_empty_a = parse_ere("()|a", REG_EXTENDED, pool);
+  ASSERT_TRUE(qmark_a.has_value());
+  ASSERT_TRUE(alt_empty_a.has_value());
+  EXPECT_EQ(*qmark_a, *alt_empty_a);
+
+  ErrorOr<ExprId> ere = parse_ere("a+b?c", REG_EXTENDED, pool);
+  ASSERT_TRUE(ere.has_value());
+  EXPECT_EQ(match(pool, *ere, "aaabc"), MatchResult::Match);
+  size_t warm_nodes = pool.size();
+  EXPECT_EQ(match(pool, *ere, "aaabc"), MatchResult::Match);
+  EXPECT_EQ(pool.size(), warm_nodes);
+}

diff  --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index b36cbe9e965680..d30c957ba316ac 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -334,8 +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{1}",   R"(\1)",
-                                      R"(\b)", "[[:<:]]", "[[:>:]]"};
+  const char *const bad_patterns[] = {"a{1}", 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);
@@ -553,6 +553,52 @@ TEST(LlvmLibcRegexTest, AlternationAndGrouping) {
   check_err(deep_150, REG_ESPACE);
 }
 
+TEST(LlvmLibcRegexTest, PlusAndQuestionQuantifiers) {
+  auto check = [](const char *pat, const char *match1, const char *match2,
+                  const char *nomatch1, const char *nomatch2) {
+    regex_t preg{};
+    ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED | REG_NOSUB), 0);
+    scope_exit free_guard([&preg] { LIBC_NAMESPACE::regfree(&preg); });
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, match1, 0, nullptr, 0), 0);
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, match2, 0, nullptr, 0), 0);
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, nomatch1, 0, nullptr, 0),
+              REG_NOMATCH);
+    EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, nomatch2, 0, nullptr, 0),
+              REG_NOMATCH);
+  };
+
+  // Framed '+' and '?' near-miss checks (distinguishing '+', '?', and '*').
+  check("xa+y", "xay", "xaaay", "xy", "xaby");
+  check("xa?y", "xy", "xay", "xaay", "xby");
+
+  // Non-nullable '+' enforcement and anchored nullable '?' rule.
+  check("a+", "a", "aaa", "", "bbb");
+  check("^a?$", "", "a", "aa", "b");
+
+  // Adjacent and mixed quantifiers.
+  check("^a+b+c+$", "abc", "aaabbbccc", "ab", "ac");
+  check("^a?b?c?$", "", "ac", "aabc", "d");
+  check("^a+b?c*$", "a", "aaabccc", "bc", "abbc");
+
+  // Grouping, alternation, bracket classes, and escaped '+' / '?'.
+  check("^(ab)+$", "ab", "ababab", "", "aba");
+  check("^(ab)?$", "", "ab", "abab", "a");
+  check("^(a|b)+$", "a", "abba", "", "abc");
+  check("^a()+b?$", "a", "ab", "", "abb");
+  check("^a()?b+$", "ab", "abbb", "a", "");
+  check(R"(^\d+-\w?$)", "123-a", "7-", "-a", "7-ab");
+  check(R"(a\+b\?c)", "a+b?c", "xx a+b?c yy", "abc", "aabbc");
+
+  // Invalid repetition errors (REG_BADRPT).
+  const char *const bad_rpt[] = {"^+",  "$+",  "^?",  "$?",   "a++", "a??",
+                                 "a*+", "a+?", "a?*", "(+a)", "(?a)"};
+  for (const char *pat : bad_rpt) {
+    regex_t preg{};
+    EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_BADRPT);
+    LIBC_NAMESPACE::regfree(&preg);
+  }
+}
+
 #if defined(LIBC_ADD_NULL_CHECKS)
 TEST(LlvmLibcRegexTest, NullPointerCrash) {
   regex_t preg{};


        


More information about the libc-commits mailing list