[libc-commits] [libc] [libc][regex] Add bracket expressions and character ranges (PR #234895)
via libc-commits
libc-commits at lists.llvm.org
Fri Oct 23 03:20:38 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libc
Author: Jeff Bailey (kaladron)
<details>
<summary>Changes</summary>
Implement POSIX bracket expressions in parse_ere, compiling character lists, character ranges, and negated sets into canonical ByteSet nodes. In POSIX regular expressions, bracket expressions support literal right brackets when placed first, literal hyphens when placed at the start or end of a list, and character range endpoints. Negated bracket expressions must exclude the NUL byte and exclude newlines when REG_NEWLINE is specified.
Add parse_bracket_expr and parse_coll_elem to parse single bytes, ranges, and collating elements into a 256-bit bitset. This implementation intentionally covers 8-bit byte values and US-ASCII ranges, with multi-byte UTF-8 range compilation scheduled for a subsequent patch. Enforce POSIX error reporting for unclosed brackets (REG_EBRACK), reversed or chained ranges (REG_ERANGE), and unknown multi-character collating symbols outside the POSIX locale (REG_ECOLLATE). Guard POSIX character class prefixes with REG_BADPAT until subsequent class support lands.
External compliance suites: Henry Spencer suite passes 116/456 (+45) boolean/error cases and 46/456 (+17) full-position cases; AT&T testregex passes 71/395 (+17) ERE boolean/error cases and 71/465 (+17) overall boolean/error cases with 0 regressions.
Assisted-by: Automated tooling, human reviewed.
---
Full diff: https://github.com/llvm/llvm-project/pull/234895.diff
4 Files Affected:
- (modified) libc/src/__support/CPP/bitset.h (+4)
- (modified) libc/src/__support/regex/regex_parser.cpp (+88-1)
- (modified) libc/test/src/__support/regex/matcher_test.cpp (+89-4)
- (modified) libc/test/src/regex/regex_basic_test.cpp (+95-1)
``````````diff
diff --git a/libc/src/__support/CPP/bitset.h b/libc/src/__support/CPP/bitset.h
index db193f414abd2..e264a363b9dad 100644
--- a/libc/src/__support/CPP/bitset.h
+++ b/libc/src/__support/CPP/bitset.h
@@ -29,6 +29,10 @@ template <size_t NumberOfBits> struct bitset {
Data[i] = 0;
}
+ LIBC_INLINE constexpr void reset(size_t Index) {
+ Data[Index / BITS_PER_UNIT] &= ~mask(Index);
+ }
+
LIBC_INLINE constexpr bool test(size_t Index) const {
return Data[Index / BITS_PER_UNIT] & mask(Index);
}
diff --git a/libc/src/__support/regex/regex_parser.cpp b/libc/src/__support/regex/regex_parser.cpp
index 818d3564da99e..9b22a4b9df3fc 100644
--- a/libc/src/__support/regex/regex_parser.cpp
+++ b/libc/src/__support/regex/regex_parser.cpp
@@ -64,6 +64,91 @@ constexpr bool is_unimplemented_ere_escape(char c) {
}
}
+// Parses a single collating element [[.c.]] or equivalence class [[=c=]].
+ErrorOr<uint8_t> parse_coll_elem(cpp::string_view &rem) {
+ cpp::string_view close_delim = rem.starts_with("[.") ? ".]" : "=]";
+ rem.remove_prefix(2);
+ size_t delim_pos = 0;
+ while (delim_pos + 2 <= rem.size() &&
+ !rem.substr(delim_pos).starts_with(close_delim))
+ ++delim_pos;
+ if (delim_pos + 2 > rem.size())
+ return Error(REG_EBRACK);
+ cpp::string_view elem = rem.substr(0, delim_pos);
+ rem.remove_prefix(delim_pos + 2);
+ if (elem.size() == 1)
+ return static_cast<uint8_t>(elem.front());
+ if (elem == "one")
+ return static_cast<uint8_t>('1');
+ return Error(REG_ECOLLATE);
+}
+
+// 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());
+ rem.remove_prefix(1);
+ return c;
+}
+
+// Parses a POSIX bracket expression [...], returning a ByteSet node.
+ErrorOr<ExprId> parse_bracket_expr(cpp::string_view &rem, int cflags,
+ ExprPool &pool) {
+ if (rem.empty())
+ return Error(REG_EBRACK);
+
+ bool negate = false;
+ if (rem.starts_with('^')) {
+ negate = true;
+ rem.remove_prefix(1);
+ if (rem.empty())
+ return Error(REG_EBRACK);
+ }
+
+ ByteSet bs;
+ bool first = true;
+ while (!rem.empty()) {
+ if (rem.starts_with(']') && !first) {
+ rem.remove_prefix(1);
+ if (negate) {
+ bs.flip();
+ bs.reset(0);
+ if (cflags & REG_NEWLINE)
+ bs.reset('\n');
+ }
+ return pool.make_byte_set(bs);
+ }
+ first = false;
+
+ ErrorOr<uint8_t> c1 = parse_bracket_elem(rem);
+ if (!c1.has_value())
+ return Error(c1.error());
+
+ if (rem.starts_with('-') && !rem.starts_with("-]")) {
+ rem.remove_prefix(1);
+ if (rem.empty())
+ return Error(REG_ERANGE);
+ if (rem.starts_with("[:"))
+ return Error(REG_ERANGE);
+ ErrorOr<uint8_t> c2 = parse_bracket_elem(rem);
+ if (!c2.has_value())
+ return Error(c2.error());
+ if (*c1 > *c2)
+ return Error(REG_ERANGE);
+ bs.set_range(*c1, *c2);
+ if (rem.starts_with('-') && !rem.starts_with("-]"))
+ return Error(REG_ERANGE);
+ } else {
+ bs.set(*c1);
+ }
+ }
+
+ return Error(REG_EBRACK);
+}
+
// Parses a single ERE atom and an optional trailing '*' quantifier.
ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
ExprPool &pool) {
@@ -112,11 +197,13 @@ ErrorOr<ExprId> parse_quantified_atom(cpp::string_view &rem, int cflags,
if (!rem.empty() && rem.front() >= '0' && rem.front() <= '9')
return Error(REG_BADRPT);
return Error(REG_BADPAT);
+ case '[':
+ atom = parse_bracket_expr(rem, cflags, pool);
+ break;
// TODO: Wire ERE metacharacters as each operator is implemented in
// subsequent patches.
case '(':
case ')':
- case '[':
case '|':
return Error(REG_BADPAT);
default:
diff --git a/libc/test/src/__support/regex/matcher_test.cpp b/libc/test/src/__support/regex/matcher_test.cpp
index cbda7a16e19ef..ec0c8668c7f58 100644
--- a/libc/test/src/__support/regex/matcher_test.cpp
+++ b/libc/test/src/__support/regex/matcher_test.cpp
@@ -261,11 +261,35 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
EXPECT_EQ(err.error(), REG_BADRPT);
}
+ const string_view ebrack_patterns[] = {"[abc", "[]", "a[[.x", "a[[.x."};
+ for (string_view pat : ebrack_patterns) {
+ ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_EBRACK);
+ }
+
+ const string_view erange_patterns[] = {"a[3-1]c", "a[1-3-5]c", "a[1-",
+ "a[a-[:digit:]]c"};
+ for (string_view pat : erange_patterns) {
+ ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_ERANGE);
+ }
+
+ const string_view ecollate_patterns[] = {"a[[.notdef.]]b", "a[[.x,.]]",
+ "a[[=b,=]]"};
+ for (string_view pat : ecollate_patterns) {
+ ErrorOr<ExprId> err = parse_ere(pat, REG_EXTENDED, pool);
+ ASSERT_FALSE(err.has_value());
+ EXPECT_EQ(err.error(), REG_ECOLLATE);
+ }
+
const string_view bad_patterns[] = {
- "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"(\')", R"(\w)", R"(\W)", R"(\s)", R"(\S)", R"(\d)", R"(\D)"};
+ "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)"};
for (string_view bad : bad_patterns) {
ErrorOr<ExprId> err = parse_ere(bad, REG_EXTENDED, pool);
ASSERT_FALSE(err.has_value());
@@ -273,6 +297,67 @@ TEST(LlvmLibcRegexMatcherTest, ParseEreEscapesAndErrors) {
}
}
+TEST(LlvmLibcRegexMatcherTest, BracketMatchingAndRanges) {
+ ExprPool pool;
+ ErrorOr<ExprId> range = parse_ere("x[c-f]y", REG_EXTENDED, pool);
+ ASSERT_TRUE(range.has_value());
+ EXPECT_EQ(match(pool, *range, "xcy"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *range, "xdy"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *range, "xey"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *range, "xfy"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *range, "xby"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *range, "xgy"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *range, "xy"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *range, "xccy"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *range, "x-y"), MatchResult::NoMatch);
+
+ ErrorOr<ExprId> negated = parse_ere("x[^c-f]y", REG_EXTENDED, pool);
+ ASSERT_TRUE(negated.has_value());
+ EXPECT_EQ(match(pool, *negated, "xby"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *negated, "xgy"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *negated, "xcy"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *negated, "xfy"), MatchResult::NoMatch);
+ EXPECT_EQ(match(pool, *negated, "xy"), MatchResult::NoMatch);
+
+ ErrorOr<ExprId> nl_neg =
+ parse_ere("x[^c-f]y", REG_EXTENDED | REG_NEWLINE, pool);
+ ASSERT_TRUE(nl_neg.has_value());
+ EXPECT_EQ(match(pool, *nl_neg, "x\ny", REG_NEWLINE, 0), MatchResult::NoMatch);
+
+ ErrorOr<ExprId> first_rb = parse_ere("a[]b]c", REG_EXTENDED, pool);
+ ASSERT_TRUE(first_rb.has_value());
+ EXPECT_EQ(match(pool, *first_rb, "a]c"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *first_rb, "abc"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *first_rb, "ac"), MatchResult::NoMatch);
+
+ ErrorOr<ExprId> lead_dash = parse_ere("a[-b]c", REG_EXTENDED, pool);
+ ASSERT_TRUE(lead_dash.has_value());
+ EXPECT_EQ(match(pool, *lead_dash, "a-c"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *lead_dash, "abc"), MatchResult::Match);
+
+ ErrorOr<ExprId> trail_dash = parse_ere("a[b-]c", REG_EXTENDED, pool);
+ ASSERT_TRUE(trail_dash.has_value());
+ EXPECT_EQ(match(pool, *trail_dash, "a-c"), MatchResult::Match);
+ EXPECT_EQ(match(pool, *trail_dash, "abc"), MatchResult::Match);
+
+ ErrorOr<ExprId> coll_sym = parse_ere("a[[.x.]]b", REG_EXTENDED, pool);
+ ASSERT_TRUE(coll_sym.has_value());
+ EXPECT_EQ(match(pool, *coll_sym, "axb"), MatchResult::Match);
+
+ ErrorOr<ExprId> coll_rb = parse_ere("a[[.].]]b", REG_EXTENDED, pool);
+ ASSERT_TRUE(coll_rb.has_value());
+ EXPECT_EQ(match(pool, *coll_rb, "a]b"), MatchResult::Match);
+
+ ErrorOr<ExprId> coll_one = parse_ere("a[[.one.]]b", REG_EXTENDED, pool);
+ ASSERT_TRUE(coll_one.has_value());
+ EXPECT_EQ(match(pool, *coll_one, "a1b"), MatchResult::Match);
+
+ ErrorOr<ExprId> star_range = parse_ere("[a-c]*", REG_EXTENDED, pool);
+ ASSERT_TRUE(star_range.has_value());
+ EXPECT_EQ(match(pool, *star_range, ""), MatchResult::Match);
+ EXPECT_EQ(match(pool, *star_range, "abcba"), MatchResult::Match);
+}
+
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 a15cc6aca1499..28958aa543a14 100644
--- a/libc/test/src/regex/regex_basic_test.cpp
+++ b/libc/test/src/regex/regex_basic_test.cpp
@@ -200,6 +200,100 @@ TEST(LlvmLibcRegexTest, EscapedMetacharacters) {
LIBC_NAMESPACE::regfree(&preg);
}
+TEST(LlvmLibcRegexTest, BracketExpressionsAndRanges) {
+ regex_t preg;
+ // Matching and range checks with near-miss boundary tests.
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "x[c-f]y", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xcy", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xdy", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xey", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xfy", 0, nullptr, 0), 0);
+ // Near-misses: adjacent ASCII values 'b' (c - 1) and 'g' (f + 1), empty,
+ // multi-char, and literal '-'.
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xby", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xgy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xccy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "x-y", 0, nullptr, 0), REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ // Negated brackets and REG_NEWLINE interaction.
+ ASSERT_EQ(
+ LIBC_NAMESPACE::regcomp(&preg, "x[^c-f]y", REG_EXTENDED | REG_NOSUB), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xby", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xgy", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xcy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xfy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xy", 0, nullptr, 0), REG_NOMATCH);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "xbbby", 0, nullptr, 0),
+ REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "x[^c-f]y",
+ REG_EXTENDED | REG_NEWLINE | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "x\ny", 0, nullptr, 0), REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ // First-position ']', leading '-', trailing '-', and collating elements.
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a[]b]c", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a]c", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abc", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "ac", 0, nullptr, 0), REG_NOMATCH);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a[-b]c", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a-c", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abc", 0, nullptr, 0), 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "a[b-]c", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a-c", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abc", 0, nullptr, 0), 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(
+ LIBC_NAMESPACE::regcomp(&preg, "a[[.x.]]b", REG_EXTENDED | REG_NOSUB), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "axb", 0, nullptr, 0), 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(
+ LIBC_NAMESPACE::regcomp(&preg, "a[[.].]]b", REG_EXTENDED | REG_NOSUB), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "a]b", 0, nullptr, 0), 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ ASSERT_EQ(LIBC_NAMESPACE::regcomp(&preg, "[a-c]*", REG_EXTENDED | REG_NOSUB),
+ 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "", 0, nullptr, 0), 0);
+ EXPECT_EQ(LIBC_NAMESPACE::regexec(&preg, "abcba", 0, nullptr, 0), 0);
+ LIBC_NAMESPACE::regfree(&preg);
+
+ // Bracket error conditions.
+ const char *const ebrack_patterns[] = {"[abc", "[]", "a[[.x", "a[[.x."};
+ 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"};
+ for (const char *pat : erange_patterns) {
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_ERANGE);
+ LIBC_NAMESPACE::regfree(&preg);
+ }
+
+ const char *const ecollate_patterns[] = {"a[[.notdef.]]b", "a[[.x,.]]",
+ "a[[=b,=]]"};
+ for (const char *pat : ecollate_patterns) {
+ EXPECT_EQ(LIBC_NAMESPACE::regcomp(&preg, pat, REG_EXTENDED), REG_ECOLLATE);
+ LIBC_NAMESPACE::regfree(&preg);
+ }
+}
+
TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
regex_t preg;
// Missing REG_EXTENDED (BRE mode) and unsupported cflags return REG_BADPAT.
@@ -213,7 +307,7 @@ TEST(LlvmLibcRegexTest, UnimplementedSyntaxAndFlags) {
LIBC_NAMESPACE::regfree(&preg);
// Unimplemented ERE metacharacters and special escapes return REG_BADPAT.
- const char *const bad_patterns[] = {"a+", "a?", "(a)", "[a]",
+ const char *const bad_patterns[] = {"a+", "a?", "(a)", "[[:alpha:]]",
"a{1}", "a|b", R"(\1)", R"(\b)",
R"(\w)", R"(\s)", R"(\d)"};
for (const char *pat : bad_patterns) {
``````````
</details>
https://github.com/llvm/llvm-project/pull/234895
More information about the libc-commits
mailing list