[libc-commits] [libc] [libc][regex] Add ERE literal parser and flag guards (PR #233045)
via libc-commits
libc-commits at lists.llvm.org
Sat Oct 17 00:47:25 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libc
Author: Jeff Bailey (kaladron)
<details>
<summary>Changes</summary>
Introduce regex::parse_ere in libc/src/__support/regex/regex_parser.{h,cpp} to parse Extended Regular Expression (ERE) literal sequences and backslash escape sequences into ExprPool, returning REG_EESCAPE on a trailing backslash.
To avoid silently mis-matching patterns that use syntax or flags not yet wired up, return REG_BADPAT when regcomp or regexec receives unimplemented flags (BRE mode, REG_ICASE, REG_NEWLINE, REG_NOSPEC, or non-zero eflags) or when parse_ere encounters unimplemented ERE metacharacters or special escapes. Each metacharacter and flag will be enabled as its implementation lands.
Against the external compliance suites in REG_NOSUB mode, this passes 20/456 tests in the Henry Spencer suite (+8 new ERE escape tests; -55 false-positive NOMATCH/BRE/flag passes removed by the REG_BADPAT guards) and 22/202 ERE tests in Glenn Fowler's AT&T testregex basic.dat suite (+10 new ERE tests).
Assisted-by: Automated tooling, human reviewed.
---
Full diff: https://github.com/llvm/llvm-project/pull/233045.diff
9 Files Affected:
- (modified) libc/src/__support/regex/CMakeLists.txt (+14)
- (added) libc/src/__support/regex/regex_parser.cpp (+113)
- (added) libc/src/__support/regex/regex_parser.h (+32)
- (modified) libc/src/regex/CMakeLists.txt (+1)
- (modified) libc/src/regex/regcomp.cpp (+11-20)
- (modified) libc/src/regex/regexec.cpp (+2-1)
- (modified) libc/test/src/__support/regex/CMakeLists.txt (+1)
- (modified) libc/test/src/__support/regex/matcher_test.cpp (+35-21)
- (modified) libc/test/src/regex/regex_basic_test.cpp (+44)
``````````diff
diff --git a/libc/src/__support/regex/CMakeLists.txt b/libc/src/__support/regex/CMakeLists.txt
index 4b1e772212c933..7b6ed6becca37a 100644
--- a/libc/src/__support/regex/CMakeLists.txt
+++ b/libc/src/__support/regex/CMakeLists.txt
@@ -41,3 +41,17 @@ add_object_library(
libc.src.__support.CPP.string_view
libc.src.__support.error_or
)
+
+add_object_library(
+ regex_parser
+ SRCS
+ regex_parser.cpp
+ HDRS
+ regex_parser.h
+ DEPENDS
+ .regex_expr_pool
+ libc.hdr.regex_macros
+ libc.src.__support.CPP.string_view
+ libc.src.__support.error_or
+ libc.src.__support.macros.config
+)
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
new file mode 100644
index 00000000000000..2a3f3d2c73dae4
--- /dev/null
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -0,0 +1,113 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Regular expression parser implementation.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/regex/regex_parser.h"
+
+#include "hdr/regex_macros.h"
+#include "src/__support/CPP/string_view.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"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace regex {
+
+namespace {
+
+// Returns true for backslash escape sequences that represent backreferences,
+// boundary assertions, or character-class shorthands rather than escaped
+// literal characters.
+constexpr bool is_unimplemented_ere_escape(char c) {
+ switch (c) {
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case 'b':
+ case 'B':
+ case '<':
+ case '>':
+ case 'A':
+ case 'Z':
+ case '`':
+ case '\'':
+ case 'w':
+ case 'W':
+ case 's':
+ case 'S':
+ case 'd':
+ case 'D':
+ return true;
+ default:
+ return false;
+ }
+}
+
+} // namespace
+
+ErrorOr<ExprId> parse_ere(cpp::string_view pattern, [[maybe_unused]] int cflags,
+ ExprPool &pool) {
+ ErrorOr<ExprId> cur = pool.empty_str();
+ if (!cur.has_value())
+ return cur;
+
+ cpp::string_view rem = pattern;
+ while (!rem.empty()) {
+ char c = rem.front();
+ rem.remove_prefix(1);
+ switch (c) {
+ case '\\': {
+ if (rem.empty())
+ return Error(REG_EESCAPE);
+ c = rem.front();
+ rem.remove_prefix(1);
+ if (is_unimplemented_ere_escape(c))
+ return Error(REG_BADPAT);
+ break;
+ }
+ // TODO: Wire ERE metacharacters as each operator is implemented in
+ // subsequent patches.
+ case '.':
+ case '^':
+ case '$':
+ case '*':
+ case '+':
+ case '?':
+ case '(':
+ case ')':
+ case '[':
+ case '{':
+ case '|':
+ return Error(REG_BADPAT);
+ default:
+ break;
+ }
+
+ ErrorOr<ExprId> lit = pool.make_lit(c);
+ if (!lit.has_value())
+ return lit;
+ cur = pool.make_concat(*cur, *lit);
+ if (!cur.has_value())
+ return cur;
+ }
+ return cur;
+}
+
+} // namespace regex
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/regex/regex_parser.h b/libc/src/__support/regex/regex_parser.h
new file mode 100644
index 00000000000000..2bf42cfdf2d8a6
--- /dev/null
+++ b/libc/src/__support/regex/regex_parser.h
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Regular expression parser interface.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_PARSER_H
+#define LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_PARSER_H
+
+#include "src/__support/CPP/string_view.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"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace regex {
+
+// Parses an Extended Regular Expression (ERE) into the given ExprPool.
+ErrorOr<ExprId> parse_ere(cpp::string_view pattern, int cflags, ExprPool &pool);
+
+} // namespace regex
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_REGEX_REGEX_PARSER_H
diff --git a/libc/src/regex/CMakeLists.txt b/libc/src/regex/CMakeLists.txt
index 4c984802554e59..f5c9bdfff3ab2c 100644
--- a/libc/src/regex/CMakeLists.txt
+++ b/libc/src/regex/CMakeLists.txt
@@ -13,6 +13,7 @@ add_entrypoint_object(
libc.src.__support.error_or
libc.src.__support.macros.null_check
libc.src.__support.regex.regex_internal
+ libc.src.__support.regex.regex_parser
)
add_entrypoint_object(
diff --git a/libc/src/regex/regcomp.cpp b/libc/src/regex/regcomp.cpp
index c4854be0535c75..505c31065f1792 100644
--- a/libc/src/regex/regcomp.cpp
+++ b/libc/src/regex/regcomp.cpp
@@ -23,6 +23,7 @@
#include "src/__support/macros/null_check.h"
#include "src/__support/regex/regex_ast.h"
#include "src/__support/regex/regex_internal.h"
+#include "src/__support/regex/regex_parser.h"
namespace LIBC_NAMESPACE_DECL {
@@ -40,6 +41,11 @@ 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)
+ 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.
@@ -48,29 +54,14 @@ LLVM_LIBC_FUNCTION(int, regcomp,
if (!ac)
return REG_ESPACE;
- ErrorOr<regex::ExprId> cur = internal->pool.empty_str();
- if (!cur.has_value()) {
+ ErrorOr<regex::ExprId> root =
+ regex::parse_ere(cpp::string_view(pattern), cflags, internal->pool);
+ if (!root.has_value()) {
delete internal;
- return cur.error();
- }
-
- // TODO: Replace direct literal concatenation with the ERE/BRE parser as
- // regular expression syntax support is wired up.
- cpp::string_view pattern_view(pattern);
- for (char c : pattern_view) {
- ErrorOr<regex::ExprId> lit = internal->pool.make_lit(c);
- if (!lit.has_value()) {
- delete internal;
- return lit.error();
- }
- cur = internal->pool.make_concat(*cur, *lit);
- if (!cur.has_value()) {
- delete internal;
- return cur.error();
- }
+ return root.error();
}
- internal->root = *cur;
+ internal->root = *root;
internal->cflags = cflags;
preg->__internal = internal;
return 0;
diff --git a/libc/src/regex/regexec.cpp b/libc/src/regex/regexec.cpp
index fdeab64029b092..4da121afd0aab4 100644
--- a/libc/src/regex/regexec.cpp
+++ b/libc/src/regex/regexec.cpp
@@ -35,7 +35,8 @@ LLVM_LIBC_FUNCTION(int, regexec,
// will be wired in subsequent patches.
(void)nmatch;
(void)pmatch;
- (void)eflags;
+ if (eflags != 0)
+ return REG_BADPAT;
// Guard against a null internal pointer (e.g. called after regfree).
auto *internal = static_cast<regex::RegexInternal *>(preg->__internal);
diff --git a/libc/test/src/__support/regex/CMakeLists.txt b/libc/test/src/__support/regex/CMakeLists.txt
index 772626c4055130..156b99c761901c 100644
--- a/libc/test/src/__support/regex/CMakeLists.txt
+++ b/libc/test/src/__support/regex/CMakeLists.txt
@@ -21,4 +21,5 @@ add_libc_test(
libc.src.__support.error_or
libc.src.__support.regex.regex_expr_pool
libc.src.__support.regex.regex_matcher
+ libc.src.__support.regex.regex_parser
)
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index 9cad1e30ba2148..53f178a0fc4c3d 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -17,6 +17,7 @@
#include "src/__support/regex/regex_ast.h"
#include "src/__support/regex/regex_expr_pool.h"
#include "src/__support/regex/regex_matcher.h"
+#include "src/__support/regex/regex_parser.h"
#include "test/UnitTest/Test.h"
using LIBC_NAMESPACE::ErrorOr;
@@ -28,25 +29,7 @@ using LIBC_NAMESPACE::regex::ExprId;
using LIBC_NAMESPACE::regex::ExprPool;
using LIBC_NAMESPACE::regex::match;
using LIBC_NAMESPACE::regex::MatchResult;
-
-namespace {
-
-ErrorOr<ExprId> make_literal_string(ExprPool &pool, string_view str) {
- ErrorOr<ExprId> cur = pool.empty_str();
- if (!cur.has_value())
- return cur;
- for (char c : str) {
- ErrorOr<ExprId> lit = pool.make_lit(c);
- if (!lit.has_value())
- return lit;
- cur = pool.make_concat(*cur, *lit);
- if (!cur.has_value())
- return cur;
- }
- return cur;
-}
-
-} // namespace
+using LIBC_NAMESPACE::regex::parse_ere;
TEST(LlvmLibcRegexMatcherTest, DerivativeBaseCases) {
ExprPool pool;
@@ -90,7 +73,7 @@ TEST(LlvmLibcRegexMatcherTest, DerivativeConcatAndAltRules) {
TEST(LlvmLibcRegexMatcherTest, MatchSubstringAndMemoization) {
ExprPool pool;
- ErrorOr<ExprId> hello = make_literal_string(pool, "hello");
+ ErrorOr<ExprId> hello = parse_ere("hello", REG_EXTENDED, pool);
ASSERT_TRUE(hello.has_value());
EXPECT_EQ(pool.size(), size_t(10));
EXPECT_LE(pool.allocated_bytes(), size_t(1024));
@@ -111,7 +94,7 @@ TEST(LlvmLibcRegexMatcherTest, MatchSubstringAndMemoization) {
TEST(LlvmLibcRegexMatcherTest, OverlappingPrefixMatch) {
ExprPool pool;
- ErrorOr<ExprId> pat = make_literal_string(pool, "ababac");
+ ErrorOr<ExprId> pat = parse_ere("ababac", REG_EXTENDED, pool);
ASSERT_TRUE(pat.has_value());
// At index 4 ('a'), the partial match "ababa" fails on 'b' at index 5 while
@@ -120,6 +103,37 @@ TEST(LlvmLibcRegexMatcherTest, OverlappingPrefixMatch) {
EXPECT_EQ(match(pool, *pat, "abababab"), MatchResult::NoMatch);
}
+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);
+ ASSERT_TRUE(escaped_dot.has_value());
+ ASSERT_TRUE(escaped_star.has_value());
+ ASSERT_TRUE(escaped_slash.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);
+
+ 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)"};
+ for (string_view bad : bad_patterns) {
+ ErrorOr<ExprId> err = parse_ere(bad, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_BADPAT);
+ }
+}
+
TEST(LlvmLibcRegexMatcherTest, PoolExhaustionPropagation) {
ExprPool pool;
const ExprId empty_str = *pool.empty_str();
diff --git a/libc/test/src/regex/regex_basic_test.cpp b/libc/test/src/regex/regex_basic_test.cpp
index 0d8e367a23fe25..10aea4bb5d42bc 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -118,6 +118,50 @@ TEST(LlvmLibcRegexTest, PoolExhaustionReturnsEspace) {
LIBC_NAMESPACE::regfree(&preg);
}
+TEST(LlvmLibcRegexTest, EscapedMetacharacters) {
+ regex_t preg;
+ ASSERT_EQ(
+ LIBC_NAMESPACE::regcomp(&preg, R"(a\.b\*c\\d)", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, R"(xx a.b*c\d yy)", 0, nullptr, 0),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, R"(aXb*c\d)", 0, nullptr, 0),
+ REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, R"(trailing\)", REG_EXTENDED),
+ REG_EESCAPE);
+ LIBC_NAMESPACE::regfree(&preg);
+}
+
+TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
+ regex_t preg;
+ // Missing REG_EXTENDED (BRE mode) and unsupported cflags return REG_BADPAT.
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a", 0), REG_BADPAT);
+ LIBC_NAMESPACE::regfree(&preg);
+ 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),
+ REG_BADPAT);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ // Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
+ const char *const bad_patterns[] = {
+ "a.b", "^a", "a$", "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);
+ }
+
+ // 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),
+ REG_BADPAT);
+ LIBC_NAMESPACE::regfree(&preg);
+}
+
#if defined(LIBC_ADD_NULL_CHECKS)
TEST(LlvmLibcRegexTest, NullPointerCrash) {
regex_t preg{};
``````````
</details>
https://github.com/llvm/llvm-project/pull/233045
More information about the libc-commits
mailing list