Formatted src/services/xeger

This commit is contained in:
Aaron Po
2026-07-19 04:10:33 -04:00
parent a238db1d37
commit 3df657dc54
3 changed files with 306 additions and 305 deletions

View File

@@ -16,8 +16,8 @@
std::string GenerateStringFromRegex(std::string_view pattern, std::mt19937& rng, std::string GenerateStringFromRegex(std::string_view pattern, std::mt19937& rng,
const int unbounded_repeat_cap) { const int unbounded_repeat_cap) {
xeger_internal::Parser parser(pattern); xeger_internal::Parser parser(pattern);
const xeger_internal::NodePtr ast = parser.Parse(); const xeger_internal::NodePtr ast = parser.Parse();
xeger_internal::XegerGenerator generator(rng, unbounded_repeat_cap); xeger_internal::XegerGenerator generator(rng, unbounded_repeat_cap);
return generator.Run(*ast); return generator.Run(*ast);
} }

View File

@@ -3,108 +3,108 @@
* @brief Visitor that walks a regex AST and emits a random matching string. * @brief Visitor that walks a regex AST and emits a random matching string.
*/ */
#include "services/xeger/xeger_internal.h"
#include <cassert> #include <cassert>
#include <limits> #include <limits>
#include <random> #include <random>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include "services/xeger/xeger_internal.h"
namespace xeger_internal { namespace xeger_internal {
XegerGenerator::XegerGenerator(std::mt19937& rng, const int unbounded_cap) XegerGenerator::XegerGenerator(std::mt19937& rng, const int unbounded_cap)
: rng_(rng), unbounded_cap_(unbounded_cap) {} : rng_(rng), unbounded_cap_(unbounded_cap) {}
std::string XegerGenerator::Run(const Node& root) { std::string XegerGenerator::Run(const Node& root) {
out_.clear(); out_.clear();
root.Accept(*this); root.Accept(*this);
return out_; return out_;
} }
void XegerGenerator::Visit(const Literal& node) { out_ += node.ch; } void XegerGenerator::Visit(const Literal& node) { out_ += node.ch; }
void XegerGenerator::Visit(const AnyChar&) { void XegerGenerator::Visit(const AnyChar&) {
static constexpr int kPrintableLow = ' '; // empty space - character 32 static constexpr int kPrintableLow = ' '; // empty space - character 32
static constexpr int kPrintableHigh = '~'; // tilde - character 126 static constexpr int kPrintableHigh = '~'; // tilde - character 126
std::uniform_int_distribution<int> dist(kPrintableLow, kPrintableHigh); std::uniform_int_distribution<int> dist(kPrintableLow, kPrintableHigh);
// assert that the rand number is in the char range // assert that the rand number is in the char range
bool inCharRange = dist.min() >= std::numeric_limits<char>::min() && bool inCharRange = dist.min() >= std::numeric_limits<char>::min() &&
dist.max() <= std::numeric_limits<char>::max(); dist.max() <= std::numeric_limits<char>::max();
assert(inCharRange && "Random number is out of char range"); assert(inCharRange && "Random number is out of char range");
out_ += static_cast<char>(dist(rng_)); out_ += static_cast<char>(dist(rng_));
} }
void XegerGenerator::Visit(const CharClass& node) { void XegerGenerator::Visit(const CharClass& node) {
out_ += PickFromClass(node); out_ += PickFromClass(node);
} }
void XegerGenerator::Visit(const Concat& node) { void XegerGenerator::Visit(const Concat& node) {
for (const NodePtr& part : node.parts) { for (const NodePtr& part : node.parts) {
part->Accept(*this); part->Accept(*this);
} }
} }
void XegerGenerator::Visit(const Alternation& node) { void XegerGenerator::Visit(const Alternation& node) {
std::uniform_int_distribution<size_t> dist(0, node.options.size() - 1); std::uniform_int_distribution<size_t> dist(0, node.options.size() - 1);
node.options[dist(rng_)]->Accept(*this); node.options[dist(rng_)]->Accept(*this);
} }
void XegerGenerator::Visit(const Repeat& node) { void XegerGenerator::Visit(const Repeat& node) {
const int lo = node.min; const int lo = node.min;
const int hi = (node.max < 0) ? node.min + unbounded_cap_ : node.max; const int hi = (node.max < 0) ? node.min + unbounded_cap_ : node.max;
std::uniform_int_distribution<int> dist(lo, hi); std::uniform_int_distribution<int> dist(lo, hi);
const int count = dist(rng_); const int count = dist(rng_);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
node.child->Accept(*this); node.child->Accept(*this);
} }
} }
bool XegerGenerator::InRanges(const CharClass& node, const char c) { bool XegerGenerator::InRanges(const CharClass& node, const char c) {
for (const auto& [lo, hi] : node.ranges) { for (const auto& [lo, hi] : node.ranges) {
if (c >= lo && c <= hi) { if (c >= lo && c <= hi) {
return true; return true;
} }
} }
return false; return false;
} }
char XegerGenerator::PickFromClass(const CharClass& node) { char XegerGenerator::PickFromClass(const CharClass& node) {
if (!node.negated) { if (!node.negated) {
int total = 0; int total = 0;
for (const auto& [lo, hi] : node.ranges) { for (const auto& [lo, hi] : node.ranges) {
total += hi - lo + 1; total += hi - lo + 1;
}
std::uniform_int_distribution<int> dist(0, total - 1);
int k = dist(rng_);
for (const auto& [lo, hi] : node.ranges) {
const int width = hi - lo + 1;
if (k < width) {
return static_cast<char>(lo + k);
} }
k -= width; std::uniform_int_distribution<int> dist(0, total - 1);
} int k = dist(rng_);
return node.ranges.back().second; // unreachable for (const auto& [lo, hi] : node.ranges) {
} const int width = hi - lo + 1;
if (k < width) {
return static_cast<char>(lo + k);
}
k -= width;
}
return node.ranges.back().second; // unreachable
}
// Negated class: draw random printable characters until one falls // Negated class: draw random printable characters until one falls
// outside the forbidden set. // outside the forbidden set.
static constexpr int kPrintableLow = 32; static constexpr int kPrintableLow = 32;
static constexpr int kPrintableHigh = 126; static constexpr int kPrintableHigh = 126;
static constexpr int kMaxAttempts = 1000; static constexpr int kMaxAttempts = 1000;
std::uniform_int_distribution<int> dist(kPrintableLow, kPrintableHigh); std::uniform_int_distribution<int> dist(kPrintableLow, kPrintableHigh);
for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
const char c = static_cast<char>(dist(rng_)); const char c = static_cast<char>(dist(rng_));
if (!InRanges(node, c)) { if (!InRanges(node, c)) {
return c; return c;
} }
} }
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: negated class rules out every printable " "GenerateStringFromRegex: negated class rules out every printable "
"character"); "character");
} }
} // namespace xeger_internal } // namespace xeger_internal

View File

@@ -3,8 +3,6 @@
* @brief Recursive-descent parser that turns a regex pattern into an AST. * @brief Recursive-descent parser that turns a regex pattern into an AST.
*/ */
#include "services/xeger/xeger_internal.h"
#include <cctype> #include <cctype>
#include <format> #include <format>
#include <memory> #include <memory>
@@ -14,31 +12,33 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "services/xeger/xeger_internal.h"
namespace xeger_internal { namespace xeger_internal {
Parser::Parser(std::string_view pattern) : src_(StripAnchors(pattern)) {} Parser::Parser(std::string_view pattern) : src_(StripAnchors(pattern)) {}
NodePtr Parser::Parse() { NodePtr Parser::Parse() {
NodePtr node = ParseAlternation(); NodePtr node = ParseAlternation();
if (!AtEnd()) { if (!AtEnd()) {
throw std::runtime_error( throw std::runtime_error(
std::format("GenerateStringFromRegex: unexpected '{}' at position {}", std::format("GenerateStringFromRegex: unexpected '{}' at position {}",
Peek(), pos_)); Peek(), pos_));
} }
return node; return node;
} }
std::string Parser::StripAnchors(std::string_view pattern) { std::string Parser::StripAnchors(std::string_view pattern) {
if (!pattern.empty() && pattern.front() == kAnchorStart) { if (!pattern.empty() && pattern.front() == kAnchorStart) {
pattern.remove_prefix(1); pattern.remove_prefix(1);
} }
const bool has_trailing_dollar = const bool has_trailing_dollar =
!pattern.empty() && pattern.back() == kAnchorEnd && !pattern.empty() && pattern.back() == kAnchorEnd &&
(pattern.size() < 2 || pattern[pattern.size() - 2] != kEscape); (pattern.size() < 2 || pattern[pattern.size() - 2] != kEscape);
if (has_trailing_dollar) { if (has_trailing_dollar) {
pattern.remove_suffix(1); pattern.remove_suffix(1);
} }
return std::string(pattern); return std::string(pattern);
} }
bool Parser::AtEnd() const { return pos_ >= src_.size(); } bool Parser::AtEnd() const { return pos_ >= src_.size(); }
@@ -46,262 +46,263 @@ char Parser::Peek() const { return src_[pos_]; }
char Parser::Advance() { return src_[pos_++]; } char Parser::Advance() { return src_[pos_++]; }
bool Parser::Match(const char c) { bool Parser::Match(const char c) {
if (!AtEnd() && Peek() == c) { if (!AtEnd() && Peek() == c) {
++pos_; ++pos_;
return true; return true;
} }
return false; return false;
} }
void Parser::Expect(const char c) { void Parser::Expect(const char c) {
if (!Match(c)) { if (!Match(c)) {
throw std::runtime_error(std::format( throw std::runtime_error(std::format(
"GenerateStringFromRegex: expected '{}' at position {}", c, pos_)); "GenerateStringFromRegex: expected '{}' at position {}", c, pos_));
} }
} }
NodePtr Parser::ParseAlternation() { NodePtr Parser::ParseAlternation() {
std::vector<NodePtr> options; std::vector<NodePtr> options;
options.push_back(ParseConcat()); options.push_back(ParseConcat());
while (Match(kAlternation)) { while (Match(kAlternation)) {
options.push_back(ParseConcat()); options.push_back(ParseConcat());
} }
if (options.size() == 1) { if (options.size() == 1) {
return std::move(options.front()); return std::move(options.front());
} }
auto alternation = std::make_unique<Alternation>(); auto alternation = std::make_unique<Alternation>();
alternation->options = std::move(options); alternation->options = std::move(options);
return alternation; return alternation;
} }
NodePtr Parser::ParseConcat() { NodePtr Parser::ParseConcat() {
auto concat = std::make_unique<Concat>(); auto concat = std::make_unique<Concat>();
while (!AtEnd() && Peek() != kAlternation && Peek() != kGroupClose) { while (!AtEnd() && Peek() != kAlternation && Peek() != kGroupClose) {
concat->parts.push_back(ParseRepeat()); concat->parts.push_back(ParseRepeat());
} }
if (concat->parts.size() == 1) { if (concat->parts.size() == 1) {
return std::move(concat->parts.front()); return std::move(concat->parts.front());
} }
return concat; return concat;
} }
NodePtr Parser::ParseRepeat() { NodePtr Parser::ParseRepeat() {
NodePtr atom = ParseAtom(); NodePtr atom = ParseAtom();
if (AtEnd()) { if (AtEnd()) {
return atom;
}
int lo = 0;
int hi = -1;
switch (Peek()) {
case kStar:
Advance();
lo = 0;
hi = -1;
break;
case kPlus:
Advance();
lo = 1;
hi = -1;
break;
case kOptional:
Advance();
lo = 0;
hi = 1;
break;
case kBraceOpen:
return ParseBrace(std::move(atom));
default:
return atom; return atom;
} }
auto repeat = std::make_unique<Repeat>();
repeat->child = std::move(atom); int lo = 0;
repeat->min = lo; int hi = -1;
repeat->max = hi; switch (Peek()) {
return repeat; case kStar:
Advance();
lo = 0;
hi = -1;
break;
case kPlus:
Advance();
lo = 1;
hi = -1;
break;
case kOptional:
Advance();
lo = 0;
hi = 1;
break;
case kBraceOpen:
return ParseBrace(std::move(atom));
default:
return atom;
}
auto repeat = std::make_unique<Repeat>();
repeat->child = std::move(atom);
repeat->min = lo;
repeat->max = hi;
return repeat;
} }
NodePtr Parser::ParseBrace(NodePtr atom) { NodePtr Parser::ParseBrace(NodePtr atom) {
Expect(kBraceOpen); Expect(kBraceOpen);
std::string lower_digits; std::string lower_digits;
std::string upper_digits; std::string upper_digits;
bool has_comma = false; bool has_comma = false;
while (!AtEnd() && std::isdigit(static_cast<unsigned char>(Peek())) != 0) { while (!AtEnd() && std::isdigit(static_cast<unsigned char>(Peek())) != 0) {
lower_digits += Advance(); lower_digits += Advance();
} }
if (Match(kBraceComma)) { if (Match(kBraceComma)) {
has_comma = true; has_comma = true;
while (!AtEnd() && while (!AtEnd() &&
std::isdigit(static_cast<unsigned char>(Peek())) != 0) { std::isdigit(static_cast<unsigned char>(Peek())) != 0) {
upper_digits += Advance(); upper_digits += Advance();
} }
} }
Expect(kBraceClose); Expect(kBraceClose);
auto repeat = std::make_unique<Repeat>(); auto repeat = std::make_unique<Repeat>();
repeat->child = std::move(atom); repeat->child = std::move(atom);
repeat->min = lower_digits.empty() ? 0 : std::stoi(lower_digits); repeat->min = lower_digits.empty() ? 0 : std::stoi(lower_digits);
if (!has_comma) { if (!has_comma) {
repeat->max = repeat->min; repeat->max = repeat->min;
} else if (upper_digits.empty()) { } else if (upper_digits.empty()) {
repeat->max = -1; repeat->max = -1;
} else { } else {
repeat->max = std::stoi(upper_digits); repeat->max = std::stoi(upper_digits);
} }
if (repeat->max != -1 && repeat->max < repeat->min) { if (repeat->max != -1 && repeat->max < repeat->min) {
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: repetition max is smaller than min"); "GenerateStringFromRegex: repetition max is smaller than min");
} }
return repeat; return repeat;
} }
NodePtr Parser::ParseAtom() { NodePtr Parser::ParseAtom() {
if (AtEnd()) { if (AtEnd()) {
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: pattern ended unexpectedly"); "GenerateStringFromRegex: pattern ended unexpectedly");
} }
const char c = Peek(); const char c = Peek();
switch (c) { switch (c) {
case kGroupOpen: { case kGroupOpen: {
Advance(); Advance();
// Non-capturing group "(?:...)" -- capture semantics don't apply // Non-capturing group "(?:...)" -- capture semantics don't apply
// here (nothing back-references a group), so it's treated exactly // here (nothing back-references a group), so it's treated exactly
// like a plain group once the "?:" marker is consumed. // like a plain group once the "?:" marker is consumed.
if (!AtEnd() && Peek() == kNonCapturingMarker) { if (!AtEnd() && Peek() == kNonCapturingMarker) {
if (pos_ + 1 < src_.size() && src_[pos_ + 1] == kNonCapturingColon) { if (pos_ + 1 < src_.size() &&
pos_ += 2; src_[pos_ + 1] == kNonCapturingColon) {
} else { pos_ += 2;
throw std::runtime_error(std::format( } else {
"GenerateStringFromRegex: unsupported group syntax at " throw std::runtime_error(std::format(
"position {} (only plain '(...)' and non-capturing " "GenerateStringFromRegex: unsupported group syntax at "
"'(?:...)' groups are supported)", "position {} (only plain '(...)' and non-capturing "
pos_)); "'(?:...)' groups are supported)",
} pos_));
}
}
NodePtr inner = ParseAlternation();
Expect(kGroupClose);
return inner;
} }
NodePtr inner = ParseAlternation(); case kClassOpen:
Expect(kGroupClose); return ParseCharClass();
return inner; case kAnyChar:
} Advance();
case kClassOpen: return std::make_unique<AnyChar>();
return ParseCharClass(); case kEscape:
case kAnyChar: Advance();
Advance(); return ParseEscape();
return std::make_unique<AnyChar>(); case kStar:
case kEscape: case kPlus:
Advance(); case kOptional:
return ParseEscape(); case kBraceOpen:
case kStar: throw std::runtime_error(std::format(
case kPlus: "GenerateStringFromRegex: quantifier with no preceding item at "
case kOptional: "position {}",
case kBraceOpen: pos_));
throw std::runtime_error(std::format( default:
"GenerateStringFromRegex: quantifier with no preceding item at " Advance();
"position {}", return std::make_unique<Literal>(c);
pos_)); }
default:
Advance();
return std::make_unique<Literal>(c);
}
} }
NodePtr Parser::MakeClass(std::vector<std::pair<char, char>> ranges, NodePtr Parser::MakeClass(std::vector<std::pair<char, char>> ranges,
const bool negated) { const bool negated) {
auto char_class = std::make_unique<CharClass>(); auto char_class = std::make_unique<CharClass>();
char_class->ranges = std::move(ranges); char_class->ranges = std::move(ranges);
char_class->negated = negated; char_class->negated = negated;
return char_class; return char_class;
} }
NodePtr Parser::ParseEscape() { NodePtr Parser::ParseEscape() {
if (AtEnd()) { if (AtEnd()) {
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: a backslash at the very end has nothing " "GenerateStringFromRegex: a backslash at the very end has nothing "
"to escape"); "to escape");
} }
const char c = Advance(); const char c = Advance();
switch (c) { switch (c) {
case 'd': case 'd':
return MakeClass({{'0', '9'}}, false); return MakeClass({{'0', '9'}}, false);
case 'D': case 'D':
return MakeClass({{'0', '9'}}, true); return MakeClass({{'0', '9'}}, true);
case 'w': case 'w':
return MakeClass({{'a', 'z'}, {'A', 'Z'}, {'0', '9'}, {'_', '_'}}, return MakeClass({{'a', 'z'}, {'A', 'Z'}, {'0', '9'}, {'_', '_'}},
false); false);
case 'W': case 'W':
return MakeClass({{'a', 'z'}, {'A', 'Z'}, {'0', '9'}, {'_', '_'}}, return MakeClass({{'a', 'z'}, {'A', 'Z'}, {'0', '9'}, {'_', '_'}},
true); true);
case 's': case 's':
return MakeClass({{' ', ' '}, {'\t', '\t'}, {'\n', '\n'}, {'\r', '\r'}}, return MakeClass(
false); {{' ', ' '}, {'\t', '\t'}, {'\n', '\n'}, {'\r', '\r'}}, false);
case 'S': case 'S':
return MakeClass({{' ', ' '}, {'\t', '\t'}, {'\n', '\n'}, {'\r', '\r'}}, return MakeClass(
true); {{' ', ' '}, {'\t', '\t'}, {'\n', '\n'}, {'\r', '\r'}}, true);
case 'n': case 'n':
return std::make_unique<Literal>('\n'); return std::make_unique<Literal>('\n');
case 't': case 't':
return std::make_unique<Literal>('\t'); return std::make_unique<Literal>('\t');
case 'r': case 'r':
return std::make_unique<Literal>('\r'); return std::make_unique<Literal>('\r');
default: default:
// \. \* \( \\ etc. -- the literal character itself. // \. \* \( \\ etc. -- the literal character itself.
return std::make_unique<Literal>(c); return std::make_unique<Literal>(c);
} }
} }
NodePtr Parser::ParseCharClass() { NodePtr Parser::ParseCharClass() {
Expect(kClassOpen); Expect(kClassOpen);
auto char_class = std::make_unique<CharClass>(); auto char_class = std::make_unique<CharClass>();
if (Match(kClassNegate)) { if (Match(kClassNegate)) {
char_class->negated = true; char_class->negated = true;
} }
if (AtEnd()) { if (AtEnd()) {
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: character class '[' is never closed"); "GenerateStringFromRegex: character class '[' is never closed");
} }
while (!AtEnd() && Peek() != kClassClose) { while (!AtEnd() && Peek() != kClassClose) {
char lo = ClassChar(); char lo = ClassChar();
const bool is_range = Peek() == kClassRange && pos_ + 1 < src_.size() && const bool is_range = Peek() == kClassRange && pos_ + 1 < src_.size() &&
src_[pos_ + 1] != kClassClose; src_[pos_ + 1] != kClassClose;
if (is_range) { if (is_range) {
Advance(); Advance();
char hi = ClassChar(); char hi = ClassChar();
if (hi < lo) { if (hi < lo) {
std::swap(lo, hi); std::swap(lo, hi);
}
char_class->ranges.emplace_back(lo, hi);
} else {
char_class->ranges.emplace_back(lo, lo);
} }
char_class->ranges.emplace_back(lo, hi); }
} else { Expect(kClassClose);
char_class->ranges.emplace_back(lo, lo); if (char_class->ranges.empty()) {
} throw std::runtime_error(
} "GenerateStringFromRegex: empty character class []");
Expect(kClassClose); }
if (char_class->ranges.empty()) { return char_class;
throw std::runtime_error(
"GenerateStringFromRegex: empty character class []");
}
return char_class;
} }
char Parser::ClassChar() { char Parser::ClassChar() {
const char c = Advance(); const char c = Advance();
if (c != kEscape) { if (c != kEscape) {
return c; return c;
} }
if (AtEnd()) { if (AtEnd()) {
throw std::runtime_error( throw std::runtime_error(
"GenerateStringFromRegex: backslash at end of character class"); "GenerateStringFromRegex: backslash at end of character class");
} }
const char escaped = Advance(); const char escaped = Advance();
switch (escaped) { switch (escaped) {
case 'n': case 'n':
return '\n'; return '\n';
case 't': case 't':
return '\t'; return '\t';
case 'r': case 'r':
return '\r'; return '\r';
default: default:
return escaped; return escaped;
} }
} }
} // namespace xeger_internal } // namespace xeger_internal