xiiregexbuilder

FPGA-Accelerated Regular Expression Matching Engine
commit 471ae799ee654084667d3635fd959ca9f4e0b452
parent 68de20aebb7b522651dd591789127470eaeaeefc
Author: Achuthan TM <achuthantm05@gmail.com>
Date:   Fri, 24 Apr 2026 07:47:08 +0530

Week 6: C++ compiler core updates, emitter logic, and PII FPGA wrapper (Part 3: Finalizing)

Diffstat:
Msrc/nfa.h | 6+-----
Msrc/parser.cpp | 34++++++++++++++++++++++------------
Msrc/parser.h | 23+++++++++++++++--------
Msrc/parser_tester.cpp | 58++++++++++++++++++++++++++++++++++++++--------------------
4 files changed, 76 insertions(+), 45 deletions(-)

diff --git a/src/nfa.h b/src/nfa.h @@ -10,9 +10,7 @@ struct NFAState { int id; bool isAccept; - // Map from character to set of destination state IDs std::map<unsigned char, std::set<int>> transitions; - explicit NFAState(int id, bool isAccept = false) : id(id), isAccept(isAccept) {} }; @@ -30,13 +28,11 @@ public: class NFABuilder { public: - // Global state counter to ensure unique IDs across all NFAs static int globalStateCounter; - static std::unique_ptr<NFA> build(ASTNode* root, int regexIdx); private: - static void linearize(ASTNode* node, int& posCounter, std::map<int, unsigned char>& posToChar, std::set<int>& dotPositions); + static void linearize(ASTNode* node, int& posCounter, std::map<int, std::set<unsigned char>>& posToChars, std::set<int>& dotPositions); static void computeNullableFirstLast(ASTNode* node); static void computeFollowpos(ASTNode* node, std::map<int, std::set<int>>& followpos); }; diff --git a/src/parser.cpp b/src/parser.cpp @@ -10,6 +10,7 @@ std::string nodeTypeToString(ASTNodeType type) { case ASTNodeType::STAR: return "STAR"; case ASTNodeType::PLUS: return "PLUS"; case ASTNodeType::OPTIONAL: return "OPTIONAL"; + case ASTNodeType::CHAR_CLASS: return "CHAR_CLASS"; default: return "UNKNOWN"; } } @@ -26,19 +27,12 @@ std::unique_ptr<ASTNode> Parser::parse() { // Expression -> Term ( '|' Term )* std::unique_ptr<ASTNode> Parser::parseExpression() { - if (check(TokenType::PIPE)) { - error("Empty alternation branch (missing left side)"); - } + if (check(TokenType::PIPE)) error("Empty alternation branch"); auto left = parseTerm(); - while (match(TokenType::PIPE)) { - if (isAtEnd() || peek().type == TokenType::RPAREN || peek().type == TokenType::PIPE || peek().type == TokenType::END_OF_INPUT) { - error("Empty alternation branch"); - } auto right = parseTerm(); left = std::make_unique<UnionNode>(std::move(left), std::move(right)); } - return left; } @@ -84,18 +78,34 @@ std::unique_ptr<ASTNode> Parser::parseAtom() { if (match(TokenType::LPAREN)) { auto node = parseExpression(); - if (!match(TokenType::RPAREN)) { - error("Unmatched parenthesis: expected ')'"); + if (!match(TokenType::RPAREN)) error("Unmatched parenthesis"); + return node; + } + + if (match(TokenType::LBRACKET)) { + auto node = std::make_unique<CharClassNode>(); + while (!check(TokenType::RBRACKET) && !isAtEnd()) { + if (peek().type == TokenType::END_OF_INPUT) error("Unterminated char class"); + char start = advance().value; + if (peek().value == '-' && pos + 1 < tokens.size() && + (tokens[pos+1].type == TokenType::LITERAL || tokens[pos+1].type == TokenType::DOT)) { + advance(); // consume '-' + char end = advance().value; + for (int c = start; c <= end; ++c) node->characters.insert(static_cast<unsigned char>(c)); + } else { + node->characters.insert(static_cast<unsigned char>(start)); + } } + if (!match(TokenType::RBRACKET)) error("Expected ']' after char class"); return node; } if (peek().type == TokenType::STAR || peek().type == TokenType::PLUS || peek().type == TokenType::QUESTION) { - error("Quantifier '" + tokenTypeToString(peek().type) + "' applied to nothing"); + error("Quantifier applied to nothing"); } error("Unexpected token: " + tokenTypeToString(peek().type)); - return nullptr; // Should not reach here + return nullptr; } const Token& Parser::peek() const { diff --git a/src/parser.h b/src/parser.h @@ -13,7 +13,8 @@ enum class ASTNodeType { UNION, STAR, PLUS, - OPTIONAL + OPTIONAL, + CHAR_CLASS }; class ASTNode { @@ -21,7 +22,6 @@ public: ASTNodeType type; virtual ~ASTNode() = default; - // The following fields are populated by the NFA builder (Stage 2), NOT the parser. bool nullable = false; std::set<int> firstpos; std::set<int> lastpos; @@ -34,13 +34,20 @@ std::string nodeTypeToString(ASTNodeType type); class LiteralNode : public ASTNode { public: char value; - int position; // Used for Glushkov's construction + int position; explicit LiteralNode(char v) : ASTNode(ASTNodeType::LITERAL), value(v), position(-1) {} }; +class CharClassNode : public ASTNode { +public: + std::set<unsigned char> characters; + int position; + CharClassNode() : ASTNode(ASTNodeType::CHAR_CLASS), position(-1) {} +}; + class DotNode : public ASTNode { public: - int position; // Used for Glushkov's construction + int position; DotNode() : ASTNode(ASTNodeType::DOT), position(-1) {} }; @@ -90,10 +97,10 @@ private: const std::vector<Token>& tokens; size_t pos; - std::unique_ptr<ASTNode> parseExpression(); // Union - std::unique_ptr<ASTNode> parseTerm(); // Concatenation - std::unique_ptr<ASTNode> parseFactor(); // Quantifiers - std::unique_ptr<ASTNode> parseAtom(); // Literals, Dot, Parens + std::unique_ptr<ASTNode> parseExpression(); + std::unique_ptr<ASTNode> parseTerm(); + std::unique_ptr<ASTNode> parseFactor(); + std::unique_ptr<ASTNode> parseAtom(); const Token& peek() const; const Token& advance(); diff --git a/src/parser_tester.cpp b/src/parser_tester.cpp @@ -7,22 +7,27 @@ #include "parser.h" #include "nfa.h" -std::string trim(const std::string& str) { +std::string trim(const std::string &str) +{ size_t first = str.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) return ""; + if (first == std::string::npos) + return ""; size_t last = str.find_last_not_of(" \t\r\n"); return str.substr(first, (last - first + 1)); } -int main(int argc, char* argv[]) { - if (argc < 2) { +int main(int argc, char *argv[]) +{ + if (argc < 2) + { std::cerr << "Usage: " << argv[0] << " <regex_file> [test_strings_file]" << std::endl; return 1; } std::string regexFilename = argv[1]; std::ifstream regexFile(regexFilename); - if (!regexFile.is_open()) { + if (!regexFile.is_open()) + { std::cerr << "Failed to open " << regexFilename << std::endl; return 1; } @@ -30,13 +35,17 @@ int main(int argc, char* argv[]) { // Optional: Test strings for validation std::string testFilename = (argc > 2) ? argv[2] : ""; std::vector<std::string> testStrings; - if (!testFilename.empty()) { + if (!testFilename.empty()) + { std::ifstream testFile(testFilename); - if (testFile.is_open()) { + if (testFile.is_open()) + { std::string testLine; - while (std::getline(testFile, testLine)) { + while (std::getline(testFile, testLine)) + { std::string trimmed = trim(testLine); - if (trimmed.empty() || trimmed[0] == '#') continue; + if (trimmed.empty() || trimmed[0] == '#') + continue; testStrings.push_back(trimmed); } } @@ -49,24 +58,30 @@ int main(int argc, char* argv[]) { std::vector<std::string> originalRegexes; // Core Pipeline: Regex -> Tokens -> AST -> NFA - while (std::getline(regexFile, line)) { + while (std::getline(regexFile, line)) + { lineNum++; std::string trimmedLine = trim(line); - if (trimmedLine.empty() || trimmedLine[0] == '#') continue; + if (trimmedLine.empty() || trimmedLine[0] == '#') + continue; originalRegexes.push_back(trimmedLine); Lexer lexer(trimmedLine, lineNum); - try { + try + { std::vector<Token> tokens = lexer.tokenize(); Parser parser(tokens); auto ast = parser.parse(); - + // Glushkov NFA construction auto nfa = NFABuilder::build(ast.get(), regexIdx++); - if (nfa) { + if (nfa) + { nfas.push_back(std::move(nfa)); } - } catch (const std::exception& e) { + } + catch (const std::exception &e) + { std::cerr << "Error processing regex '" << trimmedLine << "': " << e.what() << std::endl; } } @@ -74,20 +89,23 @@ int main(int argc, char* argv[]) { std::cout << "Successfully built " << nfas.size() << " NFAs." << std::endl; // Optional Validation: NFA Simulation - if (!testStrings.empty()) { + if (!testStrings.empty()) + { std::cout << "\n--- NFA Simulation Results ---" << std::endl; - for (const auto& testStr : testStrings) { + for (const auto &testStr : testStrings) + { std::cout << "String: \"" << testStr << "\"" << std::endl; - for (size_t i = 0; i < nfas.size(); ++i) { + for (size_t i = 0; i < nfas.size(); ++i) + { bool matches = nfas[i]->simulate(testStr); - std::cout << " Regex [" << i << "] (" << originalRegexes[i] << "): " + std::cout << " Regex [" << i << "] (" << originalRegexes[i] << "): " << (matches ? "MATCH" : "NO MATCH") << std::endl; } std::cout << std::endl; } } - // Future: Stage 3 — Verilog Emitter + // Future: Stage 3 - Verilog Emitter // if (!nfas.empty()) { // Emitter emitter(nfas); // emitter.emit("output/");